commit 7c055f64f3dfb5fb256cab12d40e27de7c766f9a Author: Dmitry Date: Wed May 22 23:48:44 2024 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/.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/.metadata b/.metadata new file mode 100644 index 0000000..52213b5 --- /dev/null +++ b/.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: "6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e + base_revision: 6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e + - platform: macos + create_revision: 6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e + base_revision: 6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e + + # 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/README.md b/README.md new file mode 100644 index 0000000..02b65df --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# mnemo_cards + +Mnemo + +## 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/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0cdae21 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,9 @@ +analyzer: + errors: + todo: info # чтобы отображать тудушки +# unused_import: error # Лишние импорты в проекте не нужны +# unused_local_variable: error # Неиспользуемые переменные в проекте не нужны +# missing_required_param: error # Не пропускаем обязательные параметры +# prefer_relative_imports: error # Относительные импорты в проекте +# directives_ordering: error # Следим за порядком импортов в проекте + unawaited_futures: error # Всегда резолвим Future \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/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/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..b4cc5a6 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,83 @@ +plugins { + id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration + 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' +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + + +android { + namespace "com.cinnabarflower.mnemo_cards" + 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.cinnabarflower.mnemo_cards" + // 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 25 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { + release { + signingConfig signingConfigs.release + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..fa36c93 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,62 @@ +{ + "project_info": { + "project_number": "701767851968", + "project_id": "mnemo-cards", + "storage_bucket": "mnemo-cards.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:701767851968:android:6190df55346394732f7225", + "android_client_info": { + "package_name": "com.cinnabarflower.mnemo_cards" + } + }, + "oauth_client": [ + { + "client_id": "701767851968-3jgootslus3ie76t682j4v7glletloud.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.cinnabarflower.mnemo_cards", + "certificate_hash": "6df4f259b9c53ac01472c17df69e8c91494f3947" + } + }, + { + "client_id": "701767851968-vqvjgf1u79jg924inm25bfuv4dg41t2u.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.cinnabarflower.mnemo_cards", + "certificate_hash": "19f45c4abc59f7ce5f22e84376273ee1ee19bdc2" + } + }, + { + "client_id": "701767851968-bud97rud1d9qtqju96addn31nhm0oofu.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyBGn7PVVDX-o7WipivtuBjdoH5nYEPsHms" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "701767851968-bud97rud1d9qtqju96addn31nhm0oofu.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "701767851968-8dqcmk706p08gujqbl2m9s4sq1aljibs.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.cinnabarflower.mnemoCards" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..e9ee25c --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6abf7e0 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/mnemo_cards/MainActivity.kt b/android/app/src/main/kotlin/com/example/mnemo_cards/MainActivity.kt new file mode 100644 index 0000000..d2b73e8 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/mnemo_cards/MainActivity.kt @@ -0,0 +1,6 @@ +package com.cinnabarflower.mnemo_cards + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/drawable-hdpi/android12splash.png b/android/app/src/main/res/drawable-hdpi/android12splash.png new file mode 100644 index 0000000..a950af6 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000..a950af6 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/android12splash.png b/android/app/src/main/res/drawable-mdpi/android12splash.png new file mode 100644 index 0000000..b8e69a5 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000..b8e69a5 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/android/app/src/main/res/drawable-night-hdpi/android12splash.png new file mode 100644 index 0000000..a950af6 Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/android/app/src/main/res/drawable-night-mdpi/android12splash.png new file mode 100644 index 0000000..b8e69a5 Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png new file mode 100644 index 0000000..aa97e95 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png new file mode 100644 index 0000000..1d18d62 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png new file mode 100644 index 0000000..6a284fd Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000..3107d37 Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-xhdpi/android12splash.png b/android/app/src/main/res/drawable-xhdpi/android12splash.png new file mode 100644 index 0000000..aa97e95 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000..aa97e95 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxhdpi/android12splash.png new file mode 100644 index 0000000..1d18d62 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000..1d18d62 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png new file mode 100644 index 0000000..6a284fd Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 0000000..6a284fd Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..3107d37 Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..5d3ad53 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..926d850 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..c9a2d06 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..4a53f7f Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..dce1e97 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000..ba4b2ae --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..dbc9ea9 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 0000000..e437c38 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..0d1fa8f --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..8ec5168 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..1a07f32 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,35 @@ +buildscript { + ext.kotlin_version = '1.8.20' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + // START: FlutterFire Configuration + classpath 'com.google.gms:google-services:4.3.14' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' + // END: FlutterFire Configuration + 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/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..0961483 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4608M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e1ca574 --- /dev/null +++ b/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.6.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..8ad364a --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,38 @@ +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") + + plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.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.7.10" apply false + } + + // For yoo kassa + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.3.37") + } + } +} + +include ":app" + +apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/codegen.sh b/codegen.sh new file mode 100644 index 0000000..039619b --- /dev/null +++ b/codegen.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dart run build_runner build --delete-conflicting-outputs \ No newline at end of file diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/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/firebase.json b/firebase.json new file mode 100644 index 0000000..4d12b0d --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"mnemo-cards","appId":"1:701767851968:android:6190df55346394732f7225","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"mnemo-cards","appId":"1:701767851968:ios:5c9040634eac8c152f7225","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"mnemo-cards","configurations":{"android":"1:701767851968:android:6190df55346394732f7225","ios":"1:701767851968:ios:5c9040634eac8c152f7225"}}}}}} \ No newline at end of file diff --git a/icons/back.png b/icons/back.png new file mode 100644 index 0000000..078e97b Binary files /dev/null and b/icons/back.png differ diff --git a/icons/cards.png b/icons/cards.png new file mode 100644 index 0000000..6d826f1 Binary files /dev/null and b/icons/cards.png differ diff --git a/icons/chat.png b/icons/chat.png new file mode 100644 index 0000000..06990a2 Binary files /dev/null and b/icons/chat.png differ diff --git a/icons/clock.png b/icons/clock.png new file mode 100644 index 0000000..c57fc40 Binary files /dev/null and b/icons/clock.png differ diff --git a/icons/exit.png b/icons/exit.png new file mode 100644 index 0000000..915ad08 Binary files /dev/null and b/icons/exit.png differ diff --git a/icons/google.png b/icons/google.png new file mode 100644 index 0000000..d626325 Binary files /dev/null and b/icons/google.png differ diff --git a/icons/heart.png b/icons/heart.png new file mode 100644 index 0000000..89331b2 Binary files /dev/null and b/icons/heart.png differ diff --git a/icons/mic_off.png b/icons/mic_off.png new file mode 100644 index 0000000..e147ad5 Binary files /dev/null and b/icons/mic_off.png differ diff --git a/icons/mic_on.png b/icons/mic_on.png new file mode 100644 index 0000000..54150be Binary files /dev/null and b/icons/mic_on.png differ diff --git a/icons/profile.png b/icons/profile.png new file mode 100644 index 0000000..b4cd411 Binary files /dev/null and b/icons/profile.png differ diff --git a/icons/shuffle.png b/icons/shuffle.png new file mode 100644 index 0000000..7ec9a54 Binary files /dev/null and b/icons/shuffle.png differ diff --git a/icons/sound_off.png b/icons/sound_off.png new file mode 100644 index 0000000..40cd041 Binary files /dev/null and b/icons/sound_off.png differ diff --git a/icons/sound_on.png b/icons/sound_on.png new file mode 100644 index 0000000..263a078 Binary files /dev/null and b/icons/sound_on.png differ diff --git a/icons/view.png b/icons/view.png new file mode 100644 index 0000000..73be8f0 Binary files /dev/null and b/icons/view.png differ diff --git a/images/cerdo.jpg b/images/cerdo.jpg new file mode 100644 index 0000000..31c8c5f Binary files /dev/null and b/images/cerdo.jpg differ diff --git a/images/cerdo_big.jpg b/images/cerdo_big.jpg new file mode 100644 index 0000000..b4b80b1 Binary files /dev/null and b/images/cerdo_big.jpg differ diff --git a/images_gen.sh b/images_gen.sh new file mode 100644 index 0000000..03500e2 --- /dev/null +++ b/images_gen.sh @@ -0,0 +1,2 @@ +dart run flutter_native_splash:create +dart run flutter_launcher_icons \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/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/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..163000d --- /dev/null +++ b/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 + 14.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/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/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/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/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..003e055 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,49 @@ +# source 'https://github.com/CocoaPods/Specs.git' +# source 'https://git.yoomoney.ru/scm/sdk/cocoa-pod-specs.git' + + +# Uncomment this line to define a global platform for your project +platform :ios, '14.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! :linkage => :static +# 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/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..1893d20 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,1503 @@ +PODS: + - abseil/algorithm (1.20240116.2): + - abseil/algorithm/algorithm (= 1.20240116.2) + - abseil/algorithm/container (= 1.20240116.2) + - abseil/algorithm/algorithm (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/algorithm/container (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/nullability + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base (1.20240116.2): + - abseil/base/atomic_hook (= 1.20240116.2) + - abseil/base/base (= 1.20240116.2) + - abseil/base/base_internal (= 1.20240116.2) + - abseil/base/config (= 1.20240116.2) + - abseil/base/core_headers (= 1.20240116.2) + - abseil/base/cycleclock_internal (= 1.20240116.2) + - abseil/base/dynamic_annotations (= 1.20240116.2) + - abseil/base/endian (= 1.20240116.2) + - abseil/base/errno_saver (= 1.20240116.2) + - abseil/base/fast_type_id (= 1.20240116.2) + - abseil/base/log_severity (= 1.20240116.2) + - abseil/base/malloc_internal (= 1.20240116.2) + - abseil/base/no_destructor (= 1.20240116.2) + - abseil/base/nullability (= 1.20240116.2) + - abseil/base/prefetch (= 1.20240116.2) + - abseil/base/pretty_function (= 1.20240116.2) + - abseil/base/raw_logging_internal (= 1.20240116.2) + - abseil/base/spinlock_wait (= 1.20240116.2) + - abseil/base/strerror (= 1.20240116.2) + - abseil/base/throw_delegate (= 1.20240116.2) + - abseil/base/atomic_hook (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/base (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/cycleclock_internal + - abseil/base/dynamic_annotations + - abseil/base/log_severity + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/spinlock_wait + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/base_internal (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/config (1.20240116.2): + - abseil/xcprivacy + - abseil/base/core_headers (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/cycleclock_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/xcprivacy + - abseil/base/dynamic_annotations (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/endian (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/xcprivacy + - abseil/base/errno_saver (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/fast_type_id (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/log_severity (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/malloc_internal (1.20240116.2): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/base/no_destructor (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/nullability (1.20240116.2): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/prefetch (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/pretty_function (1.20240116.2): + - abseil/xcprivacy + - abseil/base/raw_logging_internal (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/xcprivacy + - abseil/base/spinlock_wait (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/strerror (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/throw_delegate (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/cleanup/cleanup_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/common (1.20240116.2): + - abseil/meta/type_traits + - abseil/types/optional + - abseil/xcprivacy + - abseil/container/common_policy_traits (1.20240116.2): + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/compressed_tuple (1.20240116.2): + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/container_memory (1.20240116.2): + - abseil/base/config + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/fixed_array (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/flat_hash_map (1.20240116.2): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_map + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/flat_hash_set (1.20240116.2): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_set + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/hash_function_defaults (1.20240116.2): + - abseil/base/config + - abseil/hash/hash + - abseil/strings/cord + - abseil/strings/strings + - abseil/xcprivacy + - abseil/container/hash_policy_traits (1.20240116.2): + - abseil/container/common_policy_traits + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/hashtable_debug_hooks (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/container/hashtablez_sampler (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/memory/memory + - abseil/profiling/exponential_biased + - abseil/profiling/sample_recorder + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/inlined_vector (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/inlined_vector_internal + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/inlined_vector_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/span + - abseil/xcprivacy + - abseil/container/layout (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/demangle_internal + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/raw_hash_map (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/container_memory + - abseil/container/raw_hash_set + - abseil/xcprivacy + - abseil/container/raw_hash_set (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/container/common + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/hash_policy_traits + - abseil/container/hashtable_debug_hooks + - abseil/container/hashtablez_sampler + - abseil/hash/hash + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/crc/cpu_detect (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/crc32c (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/crc/cpu_detect + - abseil/crc/crc_internal + - abseil/crc/non_temporal_memcpy + - abseil/strings/str_format + - abseil/strings/strings + - abseil/xcprivacy + - abseil/crc/crc_cord_state (1.20240116.2): + - abseil/base/config + - abseil/crc/crc32c + - abseil/numeric/bits + - abseil/strings/strings + - abseil/xcprivacy + - abseil/crc/crc_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/crc/cpu_detect + - abseil/memory/memory + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/crc/non_temporal_arm_intrinsics (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/non_temporal_memcpy (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/crc/non_temporal_arm_intrinsics + - abseil/xcprivacy + - abseil/debugging/debugging_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/errno_saver + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/debugging/demangle_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/debugging/stacktrace (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/xcprivacy + - abseil/debugging/symbolize (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/debugging/demangle_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/commandlineflag (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/commandlineflag_internal (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/xcprivacy + - abseil/flags/config (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/flags/program_name + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/flag (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/config + - abseil/flags/flag_internal + - abseil/flags/reflection + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/flag_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/marshalling + - abseil/flags/reflection + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/utility/utility + - abseil/xcprivacy + - abseil/flags/marshalling (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/numeric/int128 + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/path_util (1.20240116.2): + - abseil/base/config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/private_handle_accessor (1.20240116.2): + - abseil/base/config + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/program_name (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/reflection (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/container/flat_hash_map + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/private_handle_accessor + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/functional/any_invocable (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/bind_front (1.20240116.2): + - abseil/base/base_internal + - abseil/container/compressed_tuple + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/function_ref (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/functional/any_invocable + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/hash/city (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/xcprivacy + - abseil/hash/hash (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/container/fixed_array + - abseil/functional/function_ref + - abseil/hash/city + - abseil/hash/low_level_hash + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/types/optional + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/hash/low_level_hash (1.20240116.2): + - abseil/base/config + - abseil/base/endian + - abseil/base/prefetch + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/memory (1.20240116.2): + - abseil/memory/memory (= 1.20240116.2) + - abseil/memory/memory (1.20240116.2): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/meta (1.20240116.2): + - abseil/meta/type_traits (= 1.20240116.2) + - abseil/meta/type_traits (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/bits (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/int128 (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/numeric/representation (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/profiling/exponential_biased (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/profiling/sample_recorder (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/xcprivacy + - abseil/random/bit_gen_ref (1.20240116.2): + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/random + - abseil/xcprivacy + - abseil/random/distributions (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/internal/fastmath + - abseil/random/internal/generate_real + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/traits + - abseil/random/internal/uniform_helper + - abseil/random/internal/wide_multiply + - abseil/strings/strings + - abseil/xcprivacy + - abseil/random/internal/distribution_caller (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/utility/utility + - abseil/xcprivacy + - abseil/random/internal/fast_uniform_bits (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/fastmath (1.20240116.2): + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/random/internal/generate_real (1.20240116.2): + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/fastmath + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/iostream_state_saver (1.20240116.2): + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/nonsecure_base (1.20240116.2): + - abseil/base/core_headers + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/pcg_engine (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/fastmath + - abseil/random/internal/iostream_state_saver + - abseil/xcprivacy + - abseil/random/internal/platform (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/internal/pool_urbg (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/random/internal/randen + - abseil/random/internal/seed_material + - abseil/random/internal/traits + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/randen (1.20240116.2): + - abseil/base/raw_logging_internal + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes + - abseil/random/internal/randen_slow + - abseil/xcprivacy + - abseil/random/internal/randen_engine (1.20240116.2): + - abseil/base/endian + - abseil/meta/type_traits + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/randen + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes (1.20240116.2): + - abseil/base/config + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes_impl + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes_impl (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/randen_slow (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/salted_seed_seq (1.20240116.2): + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/seed_material + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/seed_material (1.20240116.2): + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/random/internal/fast_uniform_bits + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/traits (1.20240116.2): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/uniform_helper (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/wide_multiply (1.20240116.2): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/random (1.20240116.2): + - abseil/random/distributions + - abseil/random/internal/nonsecure_base + - abseil/random/internal/pcg_engine + - abseil/random/internal/pool_urbg + - abseil/random/internal/randen_engine + - abseil/random/seed_sequences + - abseil/xcprivacy + - abseil/random/seed_gen_exception (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/seed_sequences (1.20240116.2): + - abseil/base/config + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/xcprivacy + - abseil/status/status (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/functional/function_ref + - abseil/memory/memory + - abseil/strings/cord + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/status/statusor (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/status/status + - abseil/strings/has_ostream_operator + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/charset (1.20240116.2): + - abseil/base/core_headers + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/strings/cord (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/crc/crc32c + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_info + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_scope + - abseil/strings/cordz_update_tracker + - abseil/strings/internal + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cord_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/inlined_vector + - abseil/container/layout + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_functions (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/profiling/exponential_biased + - abseil/xcprivacy + - abseil/strings/cordz_handle (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/strings/cordz_info (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_handle + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_tracker + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_statistics (1.20240116.2): + - abseil/base/config + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_scope (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/cord_internal + - abseil/strings/cordz_info + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_tracker (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/has_ostream_operator (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/strings/str_format (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/strings/str_format_internal + - abseil/strings/string_view + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/str_format_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/container/fixed_array + - abseil/container/inlined_vector + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/numeric/representation + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/string_view (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/xcprivacy + - abseil/strings/strings (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/charset + - abseil/strings/internal + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/synchronization/graphcycles_internal (1.20240116.2): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/synchronization/kernel_timeout_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/synchronization/synchronization (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/synchronization/graphcycles_internal + - abseil/synchronization/kernel_timeout_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/time (1.20240116.2): + - abseil/time/internal (= 1.20240116.2) + - abseil/time/time (= 1.20240116.2) + - abseil/time/internal (1.20240116.2): + - abseil/time/internal/cctz (= 1.20240116.2) + - abseil/time/internal/cctz (1.20240116.2): + - abseil/time/internal/cctz/civil_time (= 1.20240116.2) + - abseil/time/internal/cctz/time_zone (= 1.20240116.2) + - abseil/time/internal/cctz/civil_time (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/time/internal/cctz/time_zone (1.20240116.2): + - abseil/base/config + - abseil/time/internal/cctz/civil_time + - abseil/xcprivacy + - abseil/time/time (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/time/internal/cctz/civil_time + - abseil/time/internal/cctz/time_zone + - abseil/types/optional + - abseil/xcprivacy + - abseil/types (1.20240116.2): + - abseil/types/any (= 1.20240116.2) + - abseil/types/bad_any_cast (= 1.20240116.2) + - abseil/types/bad_any_cast_impl (= 1.20240116.2) + - abseil/types/bad_optional_access (= 1.20240116.2) + - abseil/types/bad_variant_access (= 1.20240116.2) + - abseil/types/compare (= 1.20240116.2) + - abseil/types/optional (= 1.20240116.2) + - abseil/types/span (= 1.20240116.2) + - abseil/types/variant (= 1.20240116.2) + - abseil/types/any (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/types/bad_any_cast + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/bad_any_cast (1.20240116.2): + - abseil/base/config + - abseil/types/bad_any_cast_impl + - abseil/xcprivacy + - abseil/types/bad_any_cast_impl (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_optional_access (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_variant_access (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/compare (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/optional (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/bad_optional_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/span (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/variant (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/bad_variant_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/utility/utility (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/xcprivacy (1.20240116.2) + - AppAuth (1.7.5): + - AppAuth/Core (= 1.7.5) + - AppAuth/ExternalUserAgent (= 1.7.5) + - AppAuth/Core (1.7.5) + - AppAuth/ExternalUserAgent (1.7.5): + - AppAuth/Core + - AppMetrica_FMDB (5.2.0) + - AppMetrica_Protobuf (5.2.0) + - AppMetricaAdSupport (5.2.0): + - AppMetricaCore (= 5.2.0) + - AppMetricaCoreExtension (= 5.2.0) + - AppMetricaAnalytics (5.2.0): + - AppMetricaAdSupport (= 5.2.0) + - AppMetricaCore (= 5.2.0) + - AppMetricaCrashes (= 5.2.0) + - AppMetricaWebKit (= 5.2.0) + - AppMetricaCore (5.2.0): + - AppMetrica_FMDB (= 5.2.0) + - AppMetrica_Protobuf (= 5.2.0) + - AppMetricaCoreUtils (= 5.2.0) + - AppMetricaEncodingUtils (= 5.2.0) + - AppMetricaHostState (= 5.2.0) + - AppMetricaLog (= 5.2.0) + - AppMetricaNetwork (= 5.2.0) + - AppMetricaPlatform (= 5.2.0) + - AppMetricaProtobufUtils (= 5.2.0) + - AppMetricaStorageUtils (= 5.2.0) + - AppMetricaCoreExtension (5.2.0): + - AppMetricaCore (= 5.2.0) + - AppMetricaStorageUtils (= 5.2.0) + - AppMetricaCoreUtils (5.2.0): + - AppMetricaLog (= 5.2.0) + - AppMetricaCrashes (5.2.0): + - AppMetricaCore (= 5.2.0) + - AppMetricaCoreExtension (= 5.2.0) + - AppMetricaCoreUtils (= 5.2.0) + - AppMetricaEncodingUtils (= 5.2.0) + - AppMetricaHostState (= 5.2.0) + - AppMetricaLog (= 5.2.0) + - AppMetricaPlatform (= 5.2.0) + - AppMetricaProtobufUtils (= 5.2.0) + - AppMetricaStorageUtils (= 5.2.0) + - KSCrash/Recording (= 1.17.0) + - KSCrash/Recording/Tools + - AppMetricaEncodingUtils (5.2.0): + - AppMetricaCoreUtils (= 5.2.0) + - AppMetricaLog (= 5.2.0) + - AppMetricaPlatform (= 5.2.0) + - AppMetricaHostState (5.2.0): + - AppMetricaCoreUtils (= 5.2.0) + - AppMetricaLog (= 5.2.0) + - AppMetricaLog (5.2.0) + - AppMetricaNetwork (5.2.0): + - AppMetricaCoreUtils (= 5.2.0) + - AppMetricaLog (= 5.2.0) + - AppMetricaPlatform (= 5.2.0) + - AppMetricaPlatform (5.2.0): + - AppMetricaCoreUtils (= 5.2.0) + - AppMetricaLog (= 5.2.0) + - AppMetricaProtobufUtils (5.2.0): + - AppMetrica_Protobuf (= 5.2.0) + - AppMetricaStorageUtils (5.2.0): + - AppMetricaCoreUtils (= 5.2.0) + - AppMetricaLog (= 5.2.0) + - AppMetricaWebKit (5.2.0): + - AppMetricaCore (= 5.2.0) + - AppMetricaCoreUtils (= 5.2.0) + - AppMetricaLog (= 5.2.0) + - BoringSSL-GRPC (0.0.32): + - BoringSSL-GRPC/Implementation (= 0.0.32) + - BoringSSL-GRPC/Interface (= 0.0.32) + - BoringSSL-GRPC/Implementation (0.0.32): + - BoringSSL-GRPC/Interface (= 0.0.32) + - BoringSSL-GRPC/Interface (0.0.32) + - cloud_firestore (4.17.2): + - Firebase/Firestore (= 10.24.0) + - firebase_core + - Flutter + - device_info_plus (0.0.1): + - Flutter + - DivKit (28.13.0): + - DivKit_LayoutKit (= 28.13.0) + - DivKit_Serialization (= 28.13.0) + - VGSLCommonCore (~> 2.4) + - VGSLNetworking (~> 2.4) + - DivKit_LayoutKit (28.13.0): + - DivKit_LayoutKitInterface (= 28.13.0) + - VGSLCommonCore (~> 2.4) + - DivKit_LayoutKitInterface (28.13.0): + - VGSLBase (~> 2.4) + - VGSLBaseTiny (~> 2.4) + - VGSLBaseUI (~> 2.4) + - DivKit_Serialization (28.13.0): + - VGSLCommonCore (~> 2.4) + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Firebase/CoreOnly (10.24.0): + - FirebaseCore (= 10.24.0) + - Firebase/Firestore (10.24.0): + - Firebase/CoreOnly + - FirebaseFirestore (~> 10.24.0) + - firebase_core (2.30.1): + - Firebase/CoreOnly (= 10.24.0) + - Flutter + - FirebaseAppCheckInterop (10.25.0) + - FirebaseCore (10.24.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.12) + - GoogleUtilities/Logger (~> 7.12) + - FirebaseCoreExtension (10.25.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreInternal (10.25.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseFirestore (10.24.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreExtension (~> 10.0) + - FirebaseFirestoreInternal (= 10.24.0) + - FirebaseSharedSwift (~> 10.0) + - FirebaseFirestoreInternal (10.24.0): + - abseil/algorithm (~> 1.20240116.1) + - abseil/base (~> 1.20240116.1) + - abseil/container/flat_hash_map (~> 1.20240116.1) + - abseil/memory (~> 1.20240116.1) + - abseil/meta (~> 1.20240116.1) + - abseil/strings/strings (~> 1.20240116.1) + - abseil/time (~> 1.20240116.1) + - abseil/types (~> 1.20240116.1) + - FirebaseAppCheckInterop (~> 10.17) + - FirebaseCore (~> 10.0) + - "gRPC-C++ (~> 1.62.0)" + - gRPC-Core (~> 1.62.0) + - leveldb-library (~> 1.22) + - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseSharedSwift (10.25.0) + - Flutter (1.0.0) + - flutter_tts (0.0.1): + - Flutter + - FMobileSdk (2.0.0.1230) + - FunctionalSwift (2.0.7) + - google_sign_in_ios (0.0.1): + - AppAuth (>= 1.7.4) + - Flutter + - FlutterMacOS + - GoogleSignIn (~> 7.1) + - GTMSessionFetcher (>= 3.4.0) + - GoogleSignIn (7.1.0): + - AppAuth (< 2.0, >= 1.7.3) + - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/Environment (7.13.2): + - GoogleUtilities/Privacy + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.13.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - "GoogleUtilities/NSData+zlib (7.13.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.2) + - "gRPC-C++ (1.62.5)": + - "gRPC-C++/Implementation (= 1.62.5)" + - "gRPC-C++/Interface (= 1.62.5)" + - "gRPC-C++/Implementation (1.62.5)": + - abseil/algorithm/container (~> 1.20240116.2) + - abseil/base/base (~> 1.20240116.2) + - abseil/base/config (~> 1.20240116.2) + - abseil/base/core_headers (~> 1.20240116.2) + - abseil/cleanup/cleanup (~> 1.20240116.2) + - abseil/container/flat_hash_map (~> 1.20240116.2) + - abseil/container/flat_hash_set (~> 1.20240116.2) + - abseil/container/inlined_vector (~> 1.20240116.2) + - abseil/flags/flag (~> 1.20240116.2) + - abseil/flags/marshalling (~> 1.20240116.2) + - abseil/functional/any_invocable (~> 1.20240116.2) + - abseil/functional/bind_front (~> 1.20240116.2) + - abseil/functional/function_ref (~> 1.20240116.2) + - abseil/hash/hash (~> 1.20240116.2) + - abseil/memory/memory (~> 1.20240116.2) + - abseil/meta/type_traits (~> 1.20240116.2) + - abseil/random/bit_gen_ref (~> 1.20240116.2) + - abseil/random/distributions (~> 1.20240116.2) + - abseil/random/random (~> 1.20240116.2) + - abseil/status/status (~> 1.20240116.2) + - abseil/status/statusor (~> 1.20240116.2) + - abseil/strings/cord (~> 1.20240116.2) + - abseil/strings/str_format (~> 1.20240116.2) + - abseil/strings/strings (~> 1.20240116.2) + - abseil/synchronization/synchronization (~> 1.20240116.2) + - abseil/time/time (~> 1.20240116.2) + - abseil/types/optional (~> 1.20240116.2) + - abseil/types/span (~> 1.20240116.2) + - abseil/types/variant (~> 1.20240116.2) + - abseil/utility/utility (~> 1.20240116.2) + - "gRPC-C++/Interface (= 1.62.5)" + - "gRPC-C++/Privacy (= 1.62.5)" + - gRPC-Core (= 1.62.5) + - "gRPC-C++/Interface (1.62.5)" + - "gRPC-C++/Privacy (1.62.5)" + - gRPC-Core (1.62.5): + - gRPC-Core/Implementation (= 1.62.5) + - gRPC-Core/Interface (= 1.62.5) + - gRPC-Core/Implementation (1.62.5): + - abseil/algorithm/container (~> 1.20240116.2) + - abseil/base/base (~> 1.20240116.2) + - abseil/base/config (~> 1.20240116.2) + - abseil/base/core_headers (~> 1.20240116.2) + - abseil/cleanup/cleanup (~> 1.20240116.2) + - abseil/container/flat_hash_map (~> 1.20240116.2) + - abseil/container/flat_hash_set (~> 1.20240116.2) + - abseil/container/inlined_vector (~> 1.20240116.2) + - abseil/flags/flag (~> 1.20240116.2) + - abseil/flags/marshalling (~> 1.20240116.2) + - abseil/functional/any_invocable (~> 1.20240116.2) + - abseil/functional/bind_front (~> 1.20240116.2) + - abseil/functional/function_ref (~> 1.20240116.2) + - abseil/hash/hash (~> 1.20240116.2) + - abseil/memory/memory (~> 1.20240116.2) + - abseil/meta/type_traits (~> 1.20240116.2) + - abseil/random/bit_gen_ref (~> 1.20240116.2) + - abseil/random/distributions (~> 1.20240116.2) + - abseil/random/random (~> 1.20240116.2) + - abseil/status/status (~> 1.20240116.2) + - abseil/status/statusor (~> 1.20240116.2) + - abseil/strings/cord (~> 1.20240116.2) + - abseil/strings/str_format (~> 1.20240116.2) + - abseil/strings/strings (~> 1.20240116.2) + - abseil/synchronization/synchronization (~> 1.20240116.2) + - abseil/time/time (~> 1.20240116.2) + - abseil/types/optional (~> 1.20240116.2) + - abseil/types/span (~> 1.20240116.2) + - abseil/types/variant (~> 1.20240116.2) + - abseil/utility/utility (~> 1.20240116.2) + - BoringSSL-GRPC (= 0.0.32) + - gRPC-Core/Interface (= 1.62.5) + - gRPC-Core/Privacy (= 1.62.5) + - gRPC-Core/Interface (1.62.5) + - gRPC-Core/Privacy (1.62.5) + - GTMAppAuth (4.1.1): + - AppAuth/Core (~> 1.7) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.4.1): + - GTMSessionFetcher/Full (= 3.4.1) + - GTMSessionFetcher/Core (3.4.1) + - GTMSessionFetcher/Full (3.4.1): + - GTMSessionFetcher/Core + - in_app_purchase_storekit (0.0.1): + - Flutter + - FlutterMacOS + - KSCrash/Recording (1.17.0): + - KSCrash/Recording/Tools (= 1.17.0) + - KSCrash/Recording/Tools (1.17.0) + - leveldb-library (1.22.5) + - MoneyAuth (10.13.1): + - FMobileSdk + - FunctionalSwift (~> 2.0) + - YooMoneyCoreApi (~> 3.1) + - YooMoneySessionProfiler (~> 5.0) + - YooMoneyUI (~> 7.7) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - PromisesObjC (2.4.0) + - SDWebImage (5.19.1): + - SDWebImage/Core (= 5.19.1) + - SDWebImage/Core (5.19.1) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - SPaySDK (1.0.9) + - SwiftyGif (5.4.5) + - VGSL_Fundamentals (2.4.1): + - VGSL_Fundamentals_Tiny (= 2.4.1) + - VGSL_Fundamentals_Tiny (2.4.1) + - VGSLBase (2.4.1): + - VGSL_Fundamentals (= 2.4.1) + - VGSLBaseTiny (= 2.4.1) + - VGSLBaseUI (= 2.4.1) + - VGSLBaseTiny (2.4.1): + - VGSL_Fundamentals_Tiny (= 2.4.1) + - VGSLBaseUI (2.4.1): + - VGSLBaseTiny (= 2.4.1) + - VGSLCommonCore (2.4.1): + - VGSLBase (= 2.4.1) + - VGSLNetworking (2.4.1): + - VGSLBase (= 2.4.1) + - yandex_mobileads (6.3.0): + - Flutter + - YandexMobileAds (~> 6.4.0) + - YandexMobileAds (6.4.1): + - DivKit (= 28.13.0) + - YandexMobileMetrica (< 5.0.0, >= 4.0.0) + - YandexMobileMetrica (4.5.2): + - YandexMobileMetrica/Static (= 4.5.2) + - YandexMobileMetrica/Static (4.5.2): + - YandexMobileMetrica/Static/Core (= 4.5.2) + - YandexMobileMetrica/Static/Crashes (= 4.5.2) + - YandexMobileMetrica/Static/Core (4.5.2) + - YandexMobileMetrica/Static/Crashes (4.5.2): + - YandexMobileMetrica/Static/Core + - yookassa_payments_flutter (1.3.1): + - Flutter + - YooKassaPayments (= 6.16.0) + - YooKassaPayments (6.16.0): + - AppMetricaAnalytics (~> 5.2.0) + - MoneyAuth (~> 10.13.0) + - SPaySDK (~> 1.0.8) + - YooKassaPaymentsApi (~> 2.22.0) + - YooKassaWalletApi (~> 2.6.0) + - YooMoneyCoreApi (~> 3.1) + - YooMoneySessionProfiler (~> 5.0.4) + - YooMoneyUI (~> 7.7) + - YooKassaPaymentsApi (2.22.0): + - FunctionalSwift + - YooMoneyCoreApi (~> 3.1.1) + - YooKassaWalletApi (2.6.0): + - FunctionalSwift + - YooMoneyCoreApi + - YooMoneyCoreApi (3.1.1): + - FunctionalSwift (~> 2.0) + - YooMoneySessionProfiler (5.0.4): + - FMobileSdk + - YooMoneyUI (7.7.6): + - FunctionalSwift + +DEPENDENCIES: + - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - Flutter (from `Flutter`) + - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) + - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - yandex_mobileads (from `.symlinks/plugins/yandex_mobileads/ios`) + - yookassa_payments_flutter (from `.symlinks/plugins/yookassa_payments_flutter/ios`) + +SPEC REPOS: + https://git.yoomoney.ru/scm/sdk/cocoa-pod-specs.git: + - FMobileSdk + - FunctionalSwift + - MoneyAuth + - YooKassaPayments + - YooKassaPaymentsApi + - YooKassaWalletApi + - YooMoneyCoreApi + - YooMoneySessionProfiler + - YooMoneyUI + https://github.com/CocoaPods/Specs.git: + - abseil + - AppAuth + - AppMetrica_FMDB + - AppMetrica_Protobuf + - AppMetricaAdSupport + - AppMetricaAnalytics + - AppMetricaCore + - AppMetricaCoreExtension + - AppMetricaCoreUtils + - AppMetricaCrashes + - AppMetricaEncodingUtils + - AppMetricaHostState + - AppMetricaLog + - AppMetricaNetwork + - AppMetricaPlatform + - AppMetricaProtobufUtils + - AppMetricaStorageUtils + - AppMetricaWebKit + - BoringSSL-GRPC + - DivKit + - DivKit_LayoutKit + - DivKit_LayoutKitInterface + - DivKit_Serialization + - DKImagePickerController + - DKPhotoGallery + - Firebase + - FirebaseAppCheckInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseFirestore + - FirebaseFirestoreInternal + - FirebaseSharedSwift + - GoogleSignIn + - GoogleUtilities + - "gRPC-C++" + - gRPC-Core + - GTMAppAuth + - GTMSessionFetcher + - KSCrash + - leveldb-library + - nanopb + - PromisesObjC + - SDWebImage + - SPaySDK + - SwiftyGif + - VGSL_Fundamentals + - VGSL_Fundamentals_Tiny + - VGSLBase + - VGSLBaseTiny + - VGSLBaseUI + - VGSLCommonCore + - VGSLNetworking + - YandexMobileAds + - YandexMobileMetrica + +EXTERNAL SOURCES: + cloud_firestore: + :path: ".symlinks/plugins/cloud_firestore/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + Flutter: + :path: Flutter + flutter_tts: + :path: ".symlinks/plugins/flutter_tts/ios" + google_sign_in_ios: + :path: ".symlinks/plugins/google_sign_in_ios/darwin" + in_app_purchase_storekit: + :path: ".symlinks/plugins/in_app_purchase_storekit/darwin" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + yandex_mobileads: + :path: ".symlinks/plugins/yandex_mobileads/ios" + yookassa_payments_flutter: + :path: ".symlinks/plugins/yookassa_payments_flutter/ios" + +SPEC CHECKSUMS: + abseil: d121da9ef7e2ff4cab7666e76c5a3e0915ae08c3 + AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa + AppMetrica_FMDB: 86a7247cecf4b315735b119f8547779bffca535a + AppMetrica_Protobuf: 326de64e6b52ab2cddce121c780461ac9eedb6c4 + AppMetricaAdSupport: 1ebdf7d6f4555675aa776fb7fee5bd96ebef1dcf + AppMetricaAnalytics: bebf8c4f75c5015937b3e51f878b30693d293944 + AppMetricaCore: 647efed7efaa8fad9e38aa417d95aeccaf8b0926 + AppMetricaCoreExtension: 2b93264b869438d890df5bf6a69407fce5542e1c + AppMetricaCoreUtils: f6b3cfde963e1027e3044630e2fd7e1007422c1b + AppMetricaCrashes: f096e2cee83a46769685b850df846f5822c81b24 + AppMetricaEncodingUtils: 3b7d0aafefbc9a0ae84515a4b381d0a576f944f2 + AppMetricaHostState: 280370ecaf3096d4ff313bb7c13fb13252ae99e7 + AppMetricaLog: 7f5b21edad9e93e12d5d8e2aeafd17cbc2befb2b + AppMetricaNetwork: 070f7ce9fcad0e97d762d76ed7616236f9c73417 + AppMetricaPlatform: 36742fdd5e4290ab923cf5ede28612dae96a6671 + AppMetricaProtobufUtils: 37f172ca2fffacba2f0d564308c873bba4726b88 + AppMetricaStorageUtils: 4de179f2354946734cc7c407322398f5276e3305 + AppMetricaWebKit: 271bdf19ac5473df925213ec80d330805ed96818 + BoringSSL-GRPC: 1e2348957acdbcad360b80a264a90799984b2ba6 + cloud_firestore: a291c7ca0e498f9b08bbabd47a2499a5ae02c152 + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + DivKit: 65af423ea03c32b6ecaa63a64d4a2ea26c217f3b + DivKit_LayoutKit: 3f88fab9edefb4938b9ccb5359ecbce4e1eeba69 + DivKit_LayoutKitInterface: 90449756ba956a7fbf3b168c01f7a0b7fc6a9de0 + DivKit_Serialization: 1b536e30c49163548bb62120320097c8ab41936d + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + Firebase: 91fefd38712feb9186ea8996af6cbdef41473442 + firebase_core: 7f1e1156934d0da3be260174812842df9420e4ab + FirebaseAppCheckInterop: 5da5ce93e8797a215e3f677fb0654b74e736c8b8 + FirebaseCore: 11dc8a16dfb7c5e3c3f45ba0e191a33ac4f50894 + FirebaseCoreExtension: 8a47811d0b155501559ef05d089518152a0a1677 + FirebaseCoreInternal: 910a81992c33715fec9263ca7381d59ab3a750b7 + FirebaseFirestore: 6df1bc70a56c15921286ff2a3096fa2d350b8823 + FirebaseFirestoreInternal: d9a6e08e9bb4016ce7c0b3544f1cf7abcd7cf26f + FirebaseSharedSwift: 0274086954b1b2d5fd7e829eccc587044d72a4ba + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_tts: 0f492aab6accf87059b72354fcb4ba934304771d + FMobileSdk: 8c49b5c8207eb4b92df761bbf16486f03bdac6ac + FunctionalSwift: 89f68db7d8cf103a4dc0878fbaa007a2a5532485 + google_sign_in_ios: 07375bfbf2620bc93a602c0e27160d6afc6ead38 + GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db + GoogleUtilities: c56430aef51a1aa57b25da78c3f8397e522c67b7 + "gRPC-C++": e725ef63c4475d7cdb7e2cf16eb0fde84bd9ee51 + gRPC-Core: eee4be35df218649fe66d721a05a7f27a28f069b + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMSessionFetcher: 8000756fc1c19d2e5697b90311f7832d2e33f6cd + in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892 + KSCrash: 593ec373759e4c1bce381421a627326a20d2dc66 + leveldb-library: e8eadf9008a61f9e1dde3978c086d2b6d9b9dc28 + MoneyAuth: e577da32ec35b502043c3766daf99a0054ec86eb + nanopb: 438bc412db1928dac798aa6fd75726007be04262 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + SDWebImage: 40b0b4053e36c660a764958bff99eed16610acbb + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + SPaySDK: 1015f868b6e9255457704cd6a3d051829263661a + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + VGSL_Fundamentals: 4a965a687c76a5a435d7aaeb6b7182e82408bf4c + VGSL_Fundamentals_Tiny: feab274b20c5838fbe09b887db0f6cfa28e26a1c + VGSLBase: 37b95dd4ea137f73e8f9a8bd8ec474d8b4d75a68 + VGSLBaseTiny: 0a0a4992a348184e8580cf73efa8eb373d750f6b + VGSLBaseUI: 62f147155e7b7049a32a30ac87b5bd8b83e636c5 + VGSLCommonCore: dd33088af6633d1c09f0c9bb8027920b99e2e0c8 + VGSLNetworking: e12d616b447522bf524c689d28f55cfbb20473cd + yandex_mobileads: 410b1d02d86e07cb38d1c9eb2fbf9384410f9619 + YandexMobileAds: ed587c669f2deb078f39a070ee2466cc40eca5a9 + YandexMobileMetrica: f5368ee93f286c793d73b58da00929babfc897c1 + yookassa_payments_flutter: f36a2380714a41ecd238667db3768052ab1c2240 + YooKassaPayments: 1f1e5daab12dd05b11e56fe568c71b5a43b77b14 + YooKassaPaymentsApi: 88ecca54eaf785787eeb1682f352016757ec0cdc + YooKassaWalletApi: 034a9d4f122557377ae3b9284e6292603fb38e6b + YooMoneyCoreApi: f53a22898878aab2f07449351a8a08f4918a6100 + YooMoneySessionProfiler: f4ac772cb6675f1200efbde75d69126eb4c867e6 + YooMoneyUI: 7332c851831398441dc841bb6474322ee5b8883d + +PODFILE CHECKSUM: 84385eb1972ae5f3226ffd3e0f88ab4954cec863 + +COCOAPODS: 1.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e087fd2 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,747 @@ +// !$*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 */; }; + 88925E0304DA934B99CF07B5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 039AEB256EC09C6DAD0D33F6 /* GoogleService-Info.plist */; }; + 92F786B29EF12520764C9B34 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88D363FEAADB3317B3E14A0A /* 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 */; }; + CF53E9FF8298D562D675E324 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F30B1603BBE3F803AFE2385E /* 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 */ + 039AEB256EC09C6DAD0D33F6 /* 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 = ""; }; + 4CC79C7D41E157D878D889F0 /* 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 = ""; }; + 5B971DEAC48A015D192A8EAB /* 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 = ""; }; + 5DA2E070BDB961F389CD961D /* 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 = ""; }; + 5EFDFD0076C43FC4B256A2F9 /* 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 = ""; }; + 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 = ""; }; + 88D363FEAADB3317B3E14A0A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.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 = ""; }; + D56245B45AA4EC506A74FF4E /* 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 = ""; }; + F30B1603BBE3F803AFE2385E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F34F38B9F662A96F77B6E782 /* 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 */ + 7F8493084A5D32479BB9EA15 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CF53E9FF8298D562D675E324 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 92F786B29EF12520764C9B34 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 70EAEEDC0F0D76C181949289 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 88D363FEAADB3317B3E14A0A /* Pods_Runner.framework */, + F30B1603BBE3F803AFE2385E /* 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 */, + 9BC5FF8B20D1CEE670C66ACD /* Pods */, + 70EAEEDC0F0D76C181949289 /* Frameworks */, + 039AEB256EC09C6DAD0D33F6 /* 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 = ""; + }; + 9BC5FF8B20D1CEE670C66ACD /* Pods */ = { + isa = PBXGroup; + children = ( + 4CC79C7D41E157D878D889F0 /* Pods-Runner.debug.xcconfig */, + 5B971DEAC48A015D192A8EAB /* Pods-Runner.release.xcconfig */, + 5DA2E070BDB961F389CD961D /* Pods-Runner.profile.xcconfig */, + 5EFDFD0076C43FC4B256A2F9 /* Pods-RunnerTests.debug.xcconfig */, + F34F38B9F662A96F77B6E782 /* Pods-RunnerTests.release.xcconfig */, + D56245B45AA4EC506A74FF4E /* 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 = ( + FD56395D2DB589B058B3AF52 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 7F8493084A5D32479BB9EA15 /* 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 = ( + 11939B8737D0761B1D52BA09 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 10510DEBDDA8DED725BA2250 /* [CP] Copy Pods Resources */, + EAA1E3F7E6B26606E42D1048 /* [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 */, + 88925E0304DA934B99CF07B5 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 10510DEBDDA8DED725BA2250 /* [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; + }; + 11939B8737D0761B1D52BA09 /* [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"; + }; + EAA1E3F7E6B26606E42D1048 /* [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; + }; + FD56395D2DB589B058B3AF52 /* [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; + 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 = 14.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 = 6ZNY923QXG; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.cinnabarflower.mnemoCards; + 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 = 5EFDFD0076C43FC4B256A2F9 /* 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.cinnabarflower.mnemoCards.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 = F34F38B9F662A96F77B6E782 /* 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.cinnabarflower.mnemoCards.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 = D56245B45AA4EC506A74FF4E /* 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.cinnabarflower.mnemoCards.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 = 14.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 = 14.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 = 6ZNY923QXG; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.cinnabarflower.mnemoCards; + 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 = 6ZNY923QXG; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.cinnabarflower.mnemoCards; + 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/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/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/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..7d5ca83 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7dbaef4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..76a27b6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..477ca7e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..b32defa Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..ef06cca Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..f78b568 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..76a27b6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..0d37b70 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..4612e2f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..9f3e1c5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..80d87b6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..43c7efd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..a8f9625 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..4612e2f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..d5d13b7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..5d3ad53 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..4a53f7f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..744657b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..cf45e9d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..283d187 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json new file mode 100644 index 0000000..9f447e1 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "background.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png new file mode 100644 index 0000000..3107d37 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..00cabce --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "LaunchImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "LaunchImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "LaunchImage@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..b8e69a5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..aa97e95 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..1d18d62 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/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/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..8d2b7d5 --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..813a0db --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 701767851968-8dqcmk706p08gujqbl2m9s4sq1aljibs.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.701767851968-8dqcmk706p08gujqbl2m9s4sq1aljibs + ANDROID_CLIENT_ID + 701767851968-3jgootslus3ie76t682j4v7glletloud.apps.googleusercontent.com + API_KEY + AIzaSyAodY8s0ntALNeeGiUiTx6eg4g2Ar6C1to + GCM_SENDER_ID + 701767851968 + PLIST_VERSION + 1 + BUNDLE_ID + com.cinnabarflower.mnemoCards + PROJECT_ID + mnemo-cards + STORAGE_BUCKET + mnemo-cards.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:701767851968:ios:5c9040634eac8c152f7225 + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..b32e312 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,967 @@ + + + + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + yookassapaymentsflutter + + + + LSApplicationQueriesSchemes + + yoomoneyauth + sberpay + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Mnemo Cards + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + mnemo_cards + 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 + + SKAdNetworkItems + + + + SKAdNetworkIdentifier + zq492l623r.skadnetwork + + + + SKAdNetworkIdentifier + cstr6suwn9.skadnetwork + + + + SKAdNetworkIdentifier + n9x2a789qt.skadnetwork + + + + SKAdNetworkIdentifier + r26jy69rpl.skadnetwork + + + + SKAdNetworkIdentifier + 5l3tpt7t6e.skadnetwork + + + + SKAdNetworkIdentifier + 4dzt52r2t5.skadnetwork + + + + SKAdNetworkIdentifier + su67r6k2v3.skadnetwork + + + + SKAdNetworkIdentifier + ludvb6z3bs.skadnetwork + + + + SKAdNetworkIdentifier + kbd757ywx3.skadnetwork + + + SKAdNetworkIdentifier + 633vhxswh4.skadnetwork + + + SKAdNetworkIdentifier + tmhh9296z4.skadnetwork + + + SKAdNetworkIdentifier + vcra2ehyfk.skadnetwork + + + SKAdNetworkIdentifier + zh3b7bxvad.skadnetwork + + + SKAdNetworkIdentifier + xmn954pzmp.skadnetwork + + + SKAdNetworkIdentifier + 79w64w269u.skadnetwork + + + SKAdNetworkIdentifier + 488r3q3dtq.skadnetwork + + + SKAdNetworkIdentifier + d7g9azk84q.skadnetwork + + + SKAdNetworkIdentifier + nzq8sh4pbs.skadnetwork + + + SKAdNetworkIdentifier + 866k9ut3g3.skadnetwork + + + SKAdNetworkIdentifier + 2q884k2j68.skadnetwork + + + SKAdNetworkIdentifier + x8jxxk4ff5.skadnetwork + + + SKAdNetworkIdentifier + gfat3222tu.skadnetwork + + + SKAdNetworkIdentifier + pd25vrrwzn.skadnetwork + + + SKAdNetworkIdentifier + lr83yxwka7.skadnetwork + + + SKAdNetworkIdentifier + cp8zw746q7.skadnetwork + + + SKAdNetworkIdentifier + pwdxu55a5a.skadnetwork + + + SKAdNetworkIdentifier + c6k4g5qg8m.skadnetwork + + + SKAdNetworkIdentifier + s39g8k73mm.skadnetwork + + + SKAdNetworkIdentifier + wg4vff78zm.skadnetwork + + + SKAdNetworkIdentifier + g28c52eehv.skadnetwork + + + SKAdNetworkIdentifier + 523jb4fst2.skadnetwork + + + SKAdNetworkIdentifier + 294l99pt4k.skadnetwork + + + SKAdNetworkIdentifier + 3qy4746246.skadnetwork + + + SKAdNetworkIdentifier + a8cz6cu7e5.skadnetwork + + + SKAdNetworkIdentifier + ggvn48r87g.skadnetwork + + + SKAdNetworkIdentifier + y755zyxw56.skadnetwork + + + SKAdNetworkIdentifier + qlbq5gtkt8.skadnetwork + + + SKAdNetworkIdentifier + mls7yz5dvl.skadnetwork + + + SKAdNetworkIdentifier + 67369282zy.skadnetwork + + + SKAdNetworkIdentifier + 899vrgt9g8.skadnetwork + + + SKAdNetworkIdentifier + mj797d8u6f.skadnetwork + + + SKAdNetworkIdentifier + 3sh42y64q3.skadnetwork + + + SKAdNetworkIdentifier + f38h382jlk.skadnetwork + + + SKAdNetworkIdentifier + 24t9a8vw3c.skadnetwork + + + SKAdNetworkIdentifier + mp6xlyr22a.skadnetwork + + + SKAdNetworkIdentifier + x44k69ngh6.skadnetwork + + + SKAdNetworkIdentifier + 88k8774x49.skadnetwork + + + SKAdNetworkIdentifier + hs6bdukanm.skadnetwork + + + SKAdNetworkIdentifier + t3b3f7n3x8.skadnetwork + + + SKAdNetworkIdentifier + prcb7njmu6.skadnetwork + + + SKAdNetworkIdentifier + c7g47wypnu.skadnetwork + + + SKAdNetworkIdentifier + 52fl2v3hgk.skadnetwork + + + SKAdNetworkIdentifier + 9vvzujtq5s.skadnetwork + + + SKAdNetworkIdentifier + m8dbw4sv7c.skadnetwork + + + SKAdNetworkIdentifier + 9g2aggbj52.skadnetwork + + + SKAdNetworkIdentifier + m5mvw97r93.skadnetwork + + + SKAdNetworkIdentifier + z5b3gh5ugf.skadnetwork + + + SKAdNetworkIdentifier + dd3a75yxkv.skadnetwork + + + SKAdNetworkIdentifier + 9nlqeag3gk.skadnetwork + + + SKAdNetworkIdentifier + cj5566h2ga.skadnetwork + + + SKAdNetworkIdentifier + h5jmj969g5.skadnetwork + + + SKAdNetworkIdentifier + dr774724x4.skadnetwork + + + SKAdNetworkIdentifier + t7ky8fmwkd.skadnetwork + + + SKAdNetworkIdentifier + fz2k2k5tej.skadnetwork + + + SKAdNetworkIdentifier + u679fj5vs4.skadnetwork + + + SKAdNetworkIdentifier + cs644xg564.skadnetwork + + + SKAdNetworkIdentifier + 9b89h5y424.skadnetwork + + + SKAdNetworkIdentifier + w28pnjg2k4.skadnetwork + + + SKAdNetworkIdentifier + 2rq3zucswp.skadnetwork + + + SKAdNetworkIdentifier + a7xqa6mtl2.skadnetwork + + + SKAdNetworkIdentifier + g2y4y55b64.skadnetwork + + + SKAdNetworkIdentifier + vc83br9sjg.skadnetwork + + + SKAdNetworkIdentifier + eqhxz8m8av.skadnetwork + + + SKAdNetworkIdentifier + 7k3cvf297u.skadnetwork + + + SKAdNetworkIdentifier + w9q455wk68.skadnetwork + + + SKAdNetworkIdentifier + nu4557a4je.skadnetwork + + + SKAdNetworkIdentifier + v4nxqhlyqp.skadnetwork + + + SKAdNetworkIdentifier + wzmmz9fp6w.skadnetwork + + + SKAdNetworkIdentifier + 7fmhfwg9en.skadnetwork + + + SKAdNetworkIdentifier + yclnxrl5pm.skadnetwork + + + SKAdNetworkIdentifier + 7tnzynbdc7.skadnetwork + + + SKAdNetworkIdentifier + l6nv3x923s.skadnetwork + + + SKAdNetworkIdentifier + h8vml93bkz.skadnetwork + + + SKAdNetworkIdentifier + uzqba5354d.skadnetwork + + + SKAdNetworkIdentifier + 8qiegk9qfv.skadnetwork + + + SKAdNetworkIdentifier + v79kvwwj4g.skadnetwork + + + SKAdNetworkIdentifier + xx9sdjej2w.skadnetwork + + + SKAdNetworkIdentifier + au67k4efj4.skadnetwork + + + SKAdNetworkIdentifier + t38b2kh725.skadnetwork + + + SKAdNetworkIdentifier + 7ug5zh24hu.skadnetwork + + + SKAdNetworkIdentifier + rx5hdcabgc.skadnetwork + + + SKAdNetworkIdentifier + 5lm9lj6jb7.skadnetwork + + + SKAdNetworkIdentifier + qqp299437r.skadnetwork + + + SKAdNetworkIdentifier + zmvfpc5aq8.skadnetwork + + + SKAdNetworkIdentifier + 9rd848q2bz.skadnetwork + + + SKAdNetworkIdentifier + 79pbpufp6p.skadnetwork + + + SKAdNetworkIdentifier + dmv22haz9p.skadnetwork + + + SKAdNetworkIdentifier + y5ghdn5j9k.skadnetwork + + + SKAdNetworkIdentifier + n6fk4nfna4.skadnetwork + + + SKAdNetworkIdentifier + 7rz58n8ntl.skadnetwork + + + SKAdNetworkIdentifier + v9wttpbfk9.skadnetwork + + + SKAdNetworkIdentifier + n38lu8286q.skadnetwork + + + SKAdNetworkIdentifier + feyaarzu9v.skadnetwork + + + SKAdNetworkIdentifier + 7fbxrn65az.skadnetwork + + + SKAdNetworkIdentifier + 47vhws6wlr.skadnetwork + + + SKAdNetworkIdentifier + ejvt5qm6ak.skadnetwork + + + SKAdNetworkIdentifier + b55w3d8y8z.skadnetwork + + + SKAdNetworkIdentifier + v7896pgt74.skadnetwork + + + SKAdNetworkIdentifier + 5ghnmfs3dh.skadnetwork + + + SKAdNetworkIdentifier + 275upjj5gd.skadnetwork + + + SKAdNetworkIdentifier + 627r9wr2y5.skadnetwork + + + SKAdNetworkIdentifier + sczv5946wb.skadnetwork + + + SKAdNetworkIdentifier + 8w3np9l82g.skadnetwork + + + SKAdNetworkIdentifier + hb56zgv37p.skadnetwork + + + SKAdNetworkIdentifier + 9t245vhmpl.skadnetwork + + + SKAdNetworkIdentifier + nrt9jy4kw9.skadnetwork + + + SKAdNetworkIdentifier + 7953jerfzd.skadnetwork + + + SKAdNetworkIdentifier + dn942472g5.skadnetwork + + + SKAdNetworkIdentifier + 6v7lgmsu45.skadnetwork + + + SKAdNetworkIdentifier + cad8qz2s3j.skadnetwork + + + SKAdNetworkIdentifier + eh6m2bh4zr.skadnetwork + + + SKAdNetworkIdentifier + jb7bn6koa5.skadnetwork + + + SKAdNetworkIdentifier + fkak3gfpt6.skadnetwork + + + SKAdNetworkIdentifier + a2p9lx4jpn.skadnetwork + + + SKAdNetworkIdentifier + 97r2b46745.skadnetwork + + + SKAdNetworkIdentifier + 22mmun2rn5.skadnetwork + + + SKAdNetworkIdentifier + 238da6jt44.skadnetwork + + + SKAdNetworkIdentifier + 44jx6755aq.skadnetwork + + + SKAdNetworkIdentifier + b9bk5wbcq9.skadnetwork + + + SKAdNetworkIdentifier + k674qkevps.skadnetwork + + + SKAdNetworkIdentifier + tl55sbb4fm.skadnetwork + + + SKAdNetworkIdentifier + 24zw6aqk47.skadnetwork + + + SKAdNetworkIdentifier + 4468km3ulz.skadnetwork + + + SKAdNetworkIdentifier + 2tdux39lx8.skadnetwork + + + SKAdNetworkIdentifier + 2u9pt9hc89.skadnetwork + + + SKAdNetworkIdentifier + 8s468mfl3y.skadnetwork + + + SKAdNetworkIdentifier + 3cgn6rq224.skadnetwork + + + SKAdNetworkIdentifier + glqzh8vgby.skadnetwork + + + SKAdNetworkIdentifier + av6w8kgt66.skadnetwork + + + SKAdNetworkIdentifier + klf5c3l5u5.skadnetwork + + + SKAdNetworkIdentifier + nfqy3847ph.skadnetwork + + + SKAdNetworkIdentifier + dticjx1a9i.skadnetwork + + + SKAdNetworkIdentifier + ppxm28t8ap.skadnetwork + + + SKAdNetworkIdentifier + 9wsyqb3ku7.skadnetwork + + + SKAdNetworkIdentifier + 74b6s63p6l.skadnetwork + + + SKAdNetworkIdentifier + xy9t38ct57.skadnetwork + + + SKAdNetworkIdentifier + 424m5254lk.skadnetwork + + + SKAdNetworkIdentifier + qu637u8glc.skadnetwork + + + SKAdNetworkIdentifier + f73kdq92p3.skadnetwork + + + SKAdNetworkIdentifier + 44n7hlldy6.skadnetwork + + + SKAdNetworkIdentifier + kbmxgpxpgc.skadnetwork + + + SKAdNetworkIdentifier + ecpz2srf59.skadnetwork + + + SKAdNetworkIdentifier + x5854y7y24.skadnetwork + + + SKAdNetworkIdentifier + f7s53z58qe.skadnetwork + + + SKAdNetworkIdentifier + x8uqf25wch.skadnetwork + + + SKAdNetworkIdentifier + uw77j35x4d.skadnetwork + + + SKAdNetworkIdentifier + 6964rsfnh4.skadnetwork + + + SKAdNetworkIdentifier + gvmwg8q7h5.skadnetwork + + + SKAdNetworkIdentifier + 6yxyv74ff7.skadnetwork + + + SKAdNetworkIdentifier + 84993kbrcf.skadnetwork + + + SKAdNetworkIdentifier + 54nzkqm89y.skadnetwork + + + SKAdNetworkIdentifier + pwa73g5rt2.skadnetwork + + + SKAdNetworkIdentifier + mlmmfzh3r3.skadnetwork + + + SKAdNetworkIdentifier + 9yg77x724h.skadnetwork + + + SKAdNetworkIdentifier + n66cz3y3bx.skadnetwork + + + SKAdNetworkIdentifier + 578prtvx9j.skadnetwork + + + SKAdNetworkIdentifier + bvpn9ufa9b.skadnetwork + + + SKAdNetworkIdentifier + 6qx585k4p6.skadnetwork + + + SKAdNetworkIdentifier + mtkv5xtk9e.skadnetwork + + + SKAdNetworkIdentifier + l93v5h6a4m.skadnetwork + + + SKAdNetworkIdentifier + rvh3l7un93.skadnetwork + + + SKAdNetworkIdentifier + gta9lk7p23.skadnetwork + + + SKAdNetworkIdentifier + 5tjdwbrq8w.skadnetwork + + + SKAdNetworkIdentifier + r45fhb6rf7.skadnetwork + + + SKAdNetworkIdentifier + 32z4fx6l9h.skadnetwork + + + SKAdNetworkIdentifier + e5fvkxwrpn.skadnetwork + + + SKAdNetworkIdentifier + 8c4e2ghe7u.skadnetwork + + + SKAdNetworkIdentifier + axh5283zss.skadnetwork + + + SKAdNetworkIdentifier + 3rd42ekr43.skadnetwork + + + SKAdNetworkIdentifier + 5mv394q32t.skadnetwork + + + SKAdNetworkIdentifier + 3qcr597p9d.skadnetwork + + + SKAdNetworkIdentifier + v72qych5uu.skadnetwork + + + SKAdNetworkIdentifier + ydx93a7ass.skadnetwork + + + SKAdNetworkIdentifier + 4pfyvq9l8r.skadnetwork + + + SKAdNetworkIdentifier + 5a6flpkh64.skadnetwork + + + SKAdNetworkIdentifier + 4fzdc2evr5.skadnetwork + + + SKAdNetworkIdentifier + 4w7y6s5ca2.skadnetwork + + + SKAdNetworkIdentifier + 252b5q8x7y.skadnetwork + + + SKAdNetworkIdentifier + 2fnua5tdw4.skadnetwork + + + SKAdNetworkIdentifier + 3l6bd9hu43.skadnetwork + + + SKAdNetworkIdentifier + 4mn522wn87.skadnetwork + + + SKAdNetworkIdentifier + 6g9af3uyq4.skadnetwork + + + SKAdNetworkIdentifier + 6p4ks3rnbw.skadnetwork + + + SKAdNetworkIdentifier + 6xzpu9s2p8.skadnetwork + + + SKAdNetworkIdentifier + 737z793b9f.skadnetwork + + + SKAdNetworkIdentifier + 89z7zv988g.skadnetwork + + + SKAdNetworkIdentifier + 8m87ys6875.skadnetwork + + + SKAdNetworkIdentifier + 8r8llnkz5a.skadnetwork + + + SKAdNetworkIdentifier + bxvub5ada5.skadnetwork + + + SKAdNetworkIdentifier + c3frkrj4fj.skadnetwork + + + SKAdNetworkIdentifier + cg4yq2srnc.skadnetwork + + + SKAdNetworkIdentifier + dbu4b84rxf.skadnetwork + + + SKAdNetworkIdentifier + dkc879ngq3.skadnetwork + + + SKAdNetworkIdentifier + dzg6xy7pwj.skadnetwork + + + SKAdNetworkIdentifier + gta8lk7p23.skadnetwork + + + SKAdNetworkIdentifier + hdw39hrw9y.skadnetwork + + + SKAdNetworkIdentifier + hjevpa356n.skadnetwork + + + SKAdNetworkIdentifier + krvm3zuq6h.skadnetwork + + + SKAdNetworkIdentifier + ln5gz23vtd.skadnetwork + + + SKAdNetworkIdentifier + m297p6643m.skadnetwork + + + SKAdNetworkIdentifier + p78axxw29g.skadnetwork + + + SKAdNetworkIdentifier + pu4na253f3.skadnetwork + + + SKAdNetworkIdentifier + s69wq72ugq.skadnetwork + + + SKAdNetworkIdentifier + t6d3zquu66.skadnetwork + + + SKAdNetworkIdentifier + vutu7akeur.skadnetwork + + + SKAdNetworkIdentifier + x2jnk7ly8j.skadnetwork + + + SKAdNetworkIdentifier + x5l83yy675.skadnetwork + + + SKAdNetworkIdentifier + y45688jllp.skadnetwork + + + SKAdNetworkIdentifier + yrqqpx2mcb.skadnetwork + + + SKAdNetworkIdentifier + z4gj7hsk7h.skadnetwork + + + SKAdNetworkIdentifier + 33r6p7g8nc.skadnetwork + + + SKAdNetworkIdentifier + g69uk9uh2b.skadnetwork + + + UIStatusBarHidden + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/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/ios/firebase_app_id_file.json b/ios/firebase_app_id_file.json new file mode 100644 index 0000000..a81cdf7 --- /dev/null +++ b/ios/firebase_app_id_file.json @@ -0,0 +1,7 @@ +{ + "file_generated_by": "FlutterFire CLI", + "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", + "GOOGLE_APP_ID": "1:701767851968:ios:4148f84add5cc6732f7225", + "FIREBASE_PROJECT_ID": "mnemo-cards", + "GCM_SENDER_ID": "701767851968" +} \ No newline at end of file diff --git a/lib.zip b/lib.zip new file mode 100644 index 0000000..0a685b8 Binary files /dev/null and b/lib.zip differ diff --git a/lib/admin/add_card.dart b/lib/admin/add_card.dart new file mode 100644 index 0000000..45f2f06 --- /dev/null +++ b/lib/admin/add_card.dart @@ -0,0 +1,300 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; + +import 'package:file_picker/file_picker.dart'; +import 'package:mnemo_cards/admin/admin_api.dart'; +import 'package:mnemo_cards/domain/router/app_router.dart'; +import 'package:mnemo_cards/features/packs/images_holder.dart'; +import 'package:mnemo_cards/widgets/game_card_widget.dart'; +import 'package:mnemo_cards/widgets/header.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../di/locator.dart'; +import '../main.dart'; +import '../widgets/image_fade.dart'; + +class EditCard { + GameCardDto gameCardDto; + AdminApi adminApi; + bool imageChanged = false; + + EditCard(GameCardDto? cardDto, this.adminApi) + : gameCardDto = cardDto ?? + const GameCardDto( + id: -1, + image: null, + mnemo: null, + original: null, + translation: null, + transcription: null, + transcriptionMnemo: null, + imageBack: null, + back: null, + ); + + void _saveCard() async { + final dto = imageChanged ? gameCardDto : gameCardDto.copyWith.image(null); + final result = await adminApi.addCard(dto); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + appRouter.maybePop(); + } + } + + void _deleteCard() async { + final result = await adminApi.deleteCard(gameCardDto); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + appRouter.maybePop(); + } + } + + void addCard(BuildContext context) { + final theme = Theme.of(context).textTheme; + + final TextEditingController originalController = + TextEditingController(text: gameCardDto.original); + final TextEditingController translationController = + TextEditingController(text: gameCardDto.translation); + final TextEditingController transcriptionController = + TextEditingController(text: gameCardDto.transcription); + final TextEditingController transcriptionMnemoController = + TextEditingController(text: gameCardDto.transcriptionMnemo); + final TextEditingController mnemoController = + TextEditingController(text: gameCardDto.mnemo); + final TextEditingController backController = + TextEditingController(text: gameCardDto.back); + + showDialog( + context: context, + builder: (c) { + return Center( + child: Scaffold( + body: Material( + child: ListView( + children: [ + Header( + '${gameCardDto.id}', + hasPopButton: true, + trail: Row( + children: [ + IconButton( + onPressed: _saveCard, + icon: Icon(Icons.save), + ), + if (gameCardDto.id >= 0) + GestureDetector( + onLongPress: _deleteCard, + child: Icon(Icons.delete), + ) + ], + ), + ), + StatefulBuilder(builder: (context, setState) { + return Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + TextField( + controller: originalController, + onChanged: (v) { + gameCardDto = + gameCardDto.copyWith(original: v); + }, + decoration: InputDecoration( + hintText: 'Original (cerdo)'), + maxLines: 1, + style: theme.displayMedium?.copyWith( + fontSize: + theme.displayMedium!.fontSize!, + ), + ), + TextField( + controller: translationController, + onChanged: (v) { + gameCardDto = gameCardDto.copyWith( + translation: v); + }, + decoration: InputDecoration( + hintText: 'Translation (свинья)'), + maxLines: 1, + style: theme.headlineSmall?.copyWith( + color: theme.headlineSmall!.color! + .withOpacity(0.5), + fontSize: + theme.headlineSmall!.fontSize!, + ), + ), + Row( + children: [ + Flexible( + child: TextField( + controller: transcriptionController, + onChanged: (v) { + gameCardDto = gameCardDto + .copyWith(transcription: v); + }, + decoration: InputDecoration( + hintText: 'Transcription'), + maxLines: 1, + style: + theme.headlineSmall?.copyWith( + color: + theme.headlineSmall!.color!, + fontSize: theme + .headlineSmall!.fontSize!, + ), + ), + ), + SizedBox( + width: 4.0, + ), + Flexible( + child: TextField( + controller: + transcriptionMnemoController, + onChanged: (v) { + gameCardDto = + gameCardDto.copyWith( + transcriptionMnemo: v); + }, + decoration: InputDecoration( + hintText: + 'Transcription mnemo'), + maxLines: 1, + style: + theme.headlineSmall?.copyWith( + color: + theme.headlineSmall!.color!, + fontSize: theme + .headlineSmall!.fontSize!, + ), + ), + ), + ], + ), + ], + ), + SizedBox( + width: MediaQuery.of(context).size.width - 20, + height: + MediaQuery.of(context).size.width - 20, + child: GestureDetector( + onTap: () async { + FilePickerResult? result = + await FilePicker.platform.pickFiles(); + if (result != null && + result.files.isNotEmpty) { + imageChanged = true; + final file = result.files.first; + final encoded = base64.normalize( + base64.encode( + File(file.path!).readAsBytesSync(), + ), + ); + gameCardDto = gameCardDto.copyWith( + image: encoded, + ); + } + setState(() {}); + }, + child: gameCardDto.image == null + ? Container( + alignment: Alignment.center, + color: Colors.white, + child: Icon( + Icons.image, + color: Colors.grey, + ), + ) + : Stack( + children: [ + ImageFade( + key: ValueKey(gameCardDto.id), + image: MemoryImage( + base64.decode( + gameCardDto.image!)), + fit: BoxFit.contain, + syncDuration: Duration.zero, + ), + Positioned( + top: 8.0, + right: 8.0, + child: IconButton( + onPressed: () { + gameCardDto = + gameCardDto.copyWith( + image: null, + ); + setState(() {}); + }, + icon: Icon(Icons.delete)), + ), + ], + ), + ), + ), + TextField( + controller: mnemoController, + onChanged: (v) { + gameCardDto = + gameCardDto.copyWith(mnemo: v); + }, + decoration: InputDecoration( + hintText: 'Mnemo ([свинья] с сердечком)', + ), + maxLines: 1, + style: theme.headlineSmall?.copyWith( + fontSize: theme.headlineSmall!.fontSize!, + ), + ), + TextField( + controller: backController, + onChanged: (v) { + gameCardDto = gameCardDto.copyWith(back: v); + }, + decoration: InputDecoration( + hintText: 'Задняя сторона', + ), + maxLines: 1, + style: theme.headlineSmall?.copyWith( + fontSize: theme.headlineSmall!.fontSize!, + ), + ), + ], + ), + ), + ], + ); + }), + ], + ), + ), + ), + ); + }); + } +} diff --git a/lib/admin/add_package.dart b/lib/admin/add_package.dart new file mode 100644 index 0000000..b14be75 --- /dev/null +++ b/lib/admin/add_package.dart @@ -0,0 +1,295 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:mnemo_cards/admin/admin_api.dart'; +import 'package:mnemo_cards/di/injector.dart'; +import 'package:mnemo_cards/managers/repository/api.dart'; +import 'package:mnemo_cards/managers/repository/dio_provider.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards/widgets/header.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:file_picker/file_picker.dart'; + +import '../main.dart'; +import '../widgets/image_fade.dart'; + +class AddPackage with Api { + EditCardPackDto cardPackDto; + AdminApi adminApi; + bool palleteVisible = false; + + AddPackage(EditCardPackDto? cardPackDto, this.adminApi) + : cardPackDto = cardPackDto ?? EditCardPackDto.fromJson({}); + + void _deletePack() async { + final delete = await showDialog( + context: scaffoldKey.currentContext!, + builder: (c) => Center( + child: Container( + color: Colors.white, + width: 200, + child: IconButton( + icon: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Удалить '), + Icon(Icons.delete), + ], + ), + onPressed: () { + Navigator.of(c).pop(true); + }, + ), + ), + )); + if (!delete) return; + final result = await adminApi.deletePack(cardPackDto); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + appRouter.maybePop(); + } + } + + void _savePack() async { + final result = await adminApi.editPack(cardPackDto); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + appRouter.maybePop(); + } + } + + void addPack(BuildContext context) { + showDialog( + context: context, + builder: (c) { + return Center( + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ListView( + children: [ + Header( + '${cardPackDto.id}', + hasPopButton: true, + trail: Row( + children: [ + IconButton( + onPressed: _savePack, + icon: Icon(Icons.save), + ), + if (cardPackDto.id != null) + GestureDetector( + onLongPress: _deletePack, + child: Icon(Icons.delete), + ) + ], + ), + ), + _TextParam( + 'Title', + cardPackDto.title, + (v) { + cardPackDto = cardPackDto.copyWith.title(v); + }, + ), + _TextParam( + 'Subtitle', + cardPackDto.subtitle, + (v) { + cardPackDto = cardPackDto.copyWith.subtitle(v); + }, + ), + _TextParam( + 'Description', + cardPackDto.description, + (v) => cardPackDto = cardPackDto.copyWith.description(v), + ), + _TextParam( + 'Size', + cardPackDto.size?.toString(), + (v) { + cardPackDto = cardPackDto.copyWith.size( + int.tryParse(v), + ); + }, + ), + _TextParam( + 'Google play id', + cardPackDto.googlePlayId, + (v) { + cardPackDto = cardPackDto.copyWith.googlePlayId(v); + }, + ), + _TextParam( + 'Appstore id', + cardPackDto.appStoreId, + (v) { + cardPackDto = cardPackDto.copyWith.appStoreId(v); + }, + ), + _TextParam( + 'Price', + cardPackDto.price, + (v) { + cardPackDto = cardPackDto.copyWith.price(v); + }, + ), + Row( + children: [ + Expanded(child: Text('Enabled ')), + StatefulBuilder(builder: (context, setState) { + return Switch( + value: cardPackDto.enabled ?? false, + onChanged: (v) { + setState(() { + cardPackDto = cardPackDto.copyWith.enabled(v); + }); + }); + }), + ], + ), + StatefulBuilder(builder: (context, setState) { + return Column( + children: [ + Row( + children: [ + Expanded(child: Text('Поменять цвет ')), + Switch( + value: palleteVisible, + onChanged: (v) => setState(() { + palleteVisible = !palleteVisible; + }), + ), + ], + ), + if (palleteVisible) + ColorPicker( + pickerAreaHeightPercent: 0.5, + paletteType: PaletteType.hsvWithHue, + pickerColor: cardPackDto.color?.asColor ?? + Colors.transparent, + onColorChanged: (color) { + String? colorString; + if (color.alpha != 0) { + colorString = '#${color.toHexString()}'; + } + cardPackDto = + cardPackDto.copyWith.color(colorString); + }, + ), + ], + ); + }), + StatefulBuilder(builder: (context, setState) { + return SizedBox( + width: MediaQuery.of(context).size.width - 20, + height: MediaQuery.of(context).size.width - 20, + child: GestureDetector( + onTap: () async { + FilePickerResult? result = + await FilePicker.platform.pickFiles(); + if (result != null && result.files.isNotEmpty) { + final file = result.files.first; + final encoded = base64.normalize( + base64.encode( + File(file.path!).readAsBytesSync(), + ), + ); + cardPackDto = cardPackDto.copyWith.cover( + encoded, + ); + } + setState(() {}); + }, + child: cardPackDto.cover == null + ? Container( + alignment: Alignment.center, + color: Colors.white, + child: Icon( + Icons.image, + color: Colors.grey, + ), + ) + : Stack( + children: [ + ImageFade( + key: ValueKey(cardPackDto.id), + image: MemoryImage( + base64.decode(cardPackDto.cover!)), + fit: BoxFit.contain, + syncDuration: Duration.zero, + ), + Positioned( + top: 8.0, + right: 8.0, + child: IconButton( + onPressed: () { + cardPackDto = cardPackDto.copyWith( + cover: null, + ); + setState(() {}); + }, + icon: Icon(Icons.delete)), + ), + ], + ), + ), + ); + }), + _TextParam( + 'Preview cards', + cardPackDto.previewCards?.toString(), + (v) { + final cards = (jsonDecode(v) as List).cast(); + cardPackDto = cardPackDto.copyWith.previewCards(cards); + }, + ), + ], + ), + ), + ), + ); + }); + } +} + +class _TextParam extends StatelessWidget { + final String title; + final String? initial; + final Function(String v) onChanged; + + _TextParam(this.title, this.initial, this.onChanged); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Text(title), + Expanded( + child: TextField( + controller: TextEditingController(text: initial), + onChanged: onChanged, + ), + ), + ], + ), + ); + } +} diff --git a/lib/admin/admin_api.dart b/lib/admin/admin_api.dart new file mode 100644 index 0000000..0e2307f --- /dev/null +++ b/lib/admin/admin_api.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:mnemo_cards/di/injector.dart'; +import 'package:mnemo_cards/features/packs/packs_api.dart'; +import 'package:mnemo_cards/managers/repository/api.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../di/locator.dart'; + +class AdminApi with Api { + final Dio _dio; + + AdminApi(this._dio); + + Future addCard(GameCardDto dto) async { + final r = await _dio.post('$path/cards/add', + data: jsonEncode(dto.toJson())); + return r.statusCode == 200; + } + + Future deleteCard(GameCardDto dto) async { + final r = await _dio.post( + '$path/cards/delete', + data: jsonEncode({'id': dto.id.toString()}), + ); + return r.statusCode == 200; + } + + Future> getCardIds() async { + final r = await _dio.get( + '$path/cards/ids', + ); + return (jsonDecode(r.data!)['ids'] as String).split(',').cast(); + } + + Future> getCards(List ids) async { + final r = await _dio.get( + '$path/cards', + queryParameters: {'ids': ids.join(',')}, + ); + return (jsonDecode(r.data!) as List) + .map((e) => GameCardDto.fromJson(e as Map)) + .toList() + .cast(); + } + + Future getEditPack(String id) async { + final r = await _dio.get( + '$path/pack/edit/$id', + ); + return EditCardPackDto.fromJson( + jsonDecode(r.data as String) as Map, + ); + } + + Future deletePack(EditCardPackDto dto) async { + final r = await _dio.post( + '$path/pack/delete', + data: jsonEncode({'id': dto.id.toString()}), + ); + return r.statusCode == 200; + } + + Future editPack(EditCardPackDto dto) async { + final r = await _dio.post( + '$path/pack/edit', + data: jsonEncode(dto.toJson()), + ); + return r.statusCode == 200; + } + + Future> packs() => + getIt.get().getPacksPreviews(null); +} diff --git a/lib/admin/all_cards.dart b/lib/admin/all_cards.dart new file mode 100644 index 0000000..efb6599 --- /dev/null +++ b/lib/admin/all_cards.dart @@ -0,0 +1,367 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/admin/add_card.dart'; +import 'package:mnemo_cards/admin/admin_api.dart'; +import 'package:mnemo_cards/di/injector.dart'; +import 'package:mnemo_cards/managers/repository/dio_provider.dart'; +import 'package:mnemo_cards/theme/themes.dart'; +import 'package:mnemo_cards/widgets/header.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../main.dart'; +import 'add_package.dart'; + +class AllCards extends StatefulWidget { + @override + State createState() => _AllCardsState(); + + const AllCards(); +} + +class _AllCardsState extends State { + static List ids = []; + static Set cards = {}; + static String filter = ''; + static List _packPreviews = []; + static EditCardPackDto? _currentPack; + static String? _currentId; + static List _selectedCards = []; + static Map _cache = {}; + + bool get selectingMode => _selectedCards.isNotEmpty; + + Dio get dio => getIt.get().dio; + + AdminApi get api => getIt.get(); + + Future loadCards() async { + var loadingIds = ids.toList(); + cards.clear(); + _cache.clear(); + while (loadingIds.isNotEmpty) { + try { + final ids = loadingIds.take(10).toList(); + loadingIds.removeRange(0, ids.length); + final cardsPage = await api.getCards(ids); + for (final card in cardsPage) { + final imageString = + card.image?.substring(0, min(100, card.image!.length)) ?? ''; + _cache[card.id.toString()] = + '${card.id}${card.mnemo}${card.transcription}${card.transcriptionMnemo}${card.original}${card.translation}${imageString}'; + } + cards.addAll(cardsPage); + setState(() {}); + } catch (e) { + print(e); + } + } + } + + Future loadIds() async { + ids = await api.getCardIds(); + setState(() {}); + } + + Future loadPacks() async { + _packPreviews = await api.packs(); + clearCurrentPack(); + } + + Future loadCurrentPack() async { + if (_currentId != null) { + _currentPack = await api.getEditPack(_currentId!); + setState(() {}); + } + } + + void clearCurrentPack() { + _currentId = null; + _currentPack = null; + setState(() {}); + } + + Future editPack(EditCardPackDto pack) async { + final result = await api.editPack(pack); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + loadCurrentPack(); + _selectedCards.clear(); + } + } + + void addOrEditPack() => AddPackage(_currentPack, api).addPack(context); + + List buildCards() { + final filteredCards = cards.where((dto) { + final filterOk = + filter.isEmpty || _cache[dto.id.toString()]?.contains(filter) == true; + final packFilter = + _currentPack?.addCardIds?.contains(dto.id.toString()) ?? true; + return filterOk && packFilter; + }); + return filteredCards.toList(); + } + + @override + Widget build(BuildContext context) { + final filteredCards = buildCards(); + return Scaffold( + body: SafeArea( + child: Stack( + children: [ + ListView.builder( + itemExtentBuilder: (index, dims) => index >= 1 ? 160.h : 350, + itemBuilder: (context, index) { + if (index == 0) + return Column( + children: [ + Header( + _currentId == null + ? 'Все карточки' + : _currentPack?.title ?? + 'Пак ${_currentPack?.id}', + subtitle: filteredCards.isEmpty + ? 'Нет карточек' + : '${filteredCards.length} карточек', + hasPopButton: true, + trail: IconButton( + onPressed: () async { + setState(() {}); + await loadIds(); + await loadPacks(); + await loadCards(); + }, + icon: Icon(Icons.sync)), + ), + dropdownButton(context), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + decoration: InputDecoration(hintText: 'Поиск'), + onChanged: (v) { + if (filter != v.toLowerCase()) { + filter = v.toLowerCase(); + setState(() {}); + } + }, + ), + ), + Expanded( + child: GestureDetector( + onTap: () => EditCard(null, api).addCard(context), + child: Container( + color: Colors.green.withOpacity(0.1), + alignment: Alignment.center, + child: Icon(Icons.add, size: 30), + ), + ), + ), + ], + ); + if (filteredCards.length >= (index - 1) * 3) { + final cardIndex = (index - 1) * 3; + return Row( + children: [ + if (filteredCards.length > cardIndex) + cardWidget(filteredCards[cardIndex]), + if (filteredCards.length > cardIndex + 1) + cardWidget(filteredCards[cardIndex + 1]), + if (filteredCards.length > cardIndex + 2) + cardWidget(filteredCards[cardIndex + 2]), + ], + ); + } + return null; + }), + if (selectingMode) + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 100, + padding: EdgeInsets.all(10.0), + alignment: Alignment.center, + child: Row( + children: [ + GestureDetector( + onTap: () { + _selectedCards.clear(); + setState(() {}); + }, + child: Container( + color: Colors.blue, + padding: EdgeInsets.all(8.0), + child: Row( + children: [ + Text('${_selectedCards.length} '), + Icon(Icons.clear) + ], + ), + ), + ), + if (_currentPack != null) + if (_selectedCards.every((c) => + _currentPack!.addCardIds?.contains(c) ?? false)) + Row( + children: [ + SizedBox(width: 8.0), + GestureDetector( + onTap: () { + final updated = + _currentPack!.copyWith.removeCardIds( + _selectedCards.toList(), + ); + editPack(updated); + }, + child: Container( + padding: EdgeInsets.all(8.0), + color: Colors.blue, + child: Text('Убрать из пака'), + ), + ), + SizedBox(width: 8.0), + GestureDetector( + onTap: () { + final updated = + _currentPack!.copyWith.previewCards( + _selectedCards.toList(), + ); + editPack(updated); + }, + child: Container( + padding: EdgeInsets.all(8.0), + color: Colors.blue, + child: Text('Как превью'), + ), + ), + ], + ) + else + MaterialButton( + onPressed: () { + final updated = _currentPack!.copyWith.addCardIds( + _selectedCards.toList(), + ); + editPack(updated); + }, + child: Container( + padding: EdgeInsets.all(8.0), + color: Colors.blue, + child: Text('Добавить в пак'), + ), + ) + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget cardWidget(GameCardDto card) { + void toggleSelect() { + final id = card.id.toString(); + if (_selectedCards.contains(id)) { + _selectedCards.remove(id); + } else { + _selectedCards.add(card.id.toString()); + } + setState(() {}); + } + + return GestureDetector( + onTap: () { + if (selectingMode) { + toggleSelect(); + } else { + EditCard(card, api).addCard(context); + } + }, + onLongPress: toggleSelect, + child: _Card(card, api, _selectedCards.contains(card.id.toString())), + ); + } + + void _onPackChanged(String? id) { + if (id == null) { + clearCurrentPack(); + } else { + _currentId = id; + loadCurrentPack(); + } + } + + Widget dropdownButton(BuildContext context) { + return Row( + children: [ + Expanded( + child: DropdownButton( + items: [..._packPreviews, null] + .map((p) => DropdownMenuItem( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: p == null + ? Text('Все паки') + : Text('${p.title} ${p.cards}'), + ), + value: p?.id, + )) + .toList(), + value: _currentId, + onChanged: (v) => _onPackChanged(v), + ), + ), + IconButton( + onPressed: addOrEditPack, + icon: _currentPack == null ? Icon(Icons.add) : Icon(Icons.edit), + ), + ], + ); + } +} + +class _Card extends StatelessWidget { + final GameCardDto cardDto; + final AdminApi api; + final bool selected; + + _Card(this.cardDto, this.api, this.selected); + + @override + Widget build(BuildContext context) { + final image = cardDto.image != null ? base64.decode(cardDto.image!) : null; + return Container( + padding: EdgeInsets.all(8.0), + width: MediaQuery.of(context).size.width / 3, + height: 160.h, + decoration: BoxDecoration( + border: Border.all(color: selected ? Colors.blue : borderGray), + color: selected ? Colors.blue.withOpacity(0.2) : null, + ), + child: Column(children: [ + Text(cardDto.translation ?? ''), + Text(cardDto.original ?? ''), + if (image != null) + Expanded( + child: Image.memory( + key: ObjectKey(cardDto.image), + gaplessPlayback: true, + image, + fit: BoxFit.scaleDown, + ), + ), + ]), + ); + } +} diff --git a/lib/di/injector.dart b/lib/di/injector.dart new file mode 100644 index 0000000..8feea83 --- /dev/null +++ b/lib/di/injector.dart @@ -0,0 +1,99 @@ +import 'dart:developer'; + +import 'package:get_it/get_it.dart'; +import 'package:mnemo_cards/admin/admin_api.dart'; +import 'package:mnemo_cards/di/locator.dart'; +import 'package:mnemo_cards/features/packs/pack_cache_manager.dart'; +import 'package:mnemo_cards/features/packs/packs_api.dart'; +import 'package:mnemo_cards/flags.dart'; +import 'package:mnemo_cards/managers/favorite_cards.dart'; +import 'package:mnemo_cards/managers/repository/dio_provider.dart'; +import 'package:mnemo_cards/managers/repository/http_repository.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../features/packs/images_holder.dart'; +import '../features/packs/pack_updater.dart'; +import '../features/packs/preview_packs_poller.dart'; +import '../features/purchase/in_app_purchase.dart'; +import '../features/tests/test_manager.dart'; +import '../features/packs/pack_holder.dart'; +import '../features/packs/pack_manager.dart'; +import '../features/packs/preview_pack_holder.dart'; +import '../managers/user_manager.dart'; + +GetIt getIt = GetIt.I; + +Future setInjections() async { + getIt.registerSingletonAsync(() => DioProvider().init()); + getIt.registerLazySingleton( + () => HttpRepository(getIt.get().dio), + ); + getIt.registerLazySingleton( + () => PacksApi(getIt.get().dio), + ); + if (ADMIN_BUILD) + getIt.registerLazySingleton( + () => AdminApi(getIt.get().dio), + ); + getIt.registerLazySingleton( + () => PackHolder(), + ); + getIt.registerLazySingleton( + () => PreviewPackHolder(), + ); + getIt.registerLazySingleton( + () => PackCacheManager(), + ); + getIt.registerLazySingleton( + () => ImagesHolder(), + ); + + getIt.registerLazySingleton( + () => PreviewPacksPoller( + getIt.get(), + getIt.get(), + ), + ); + + getIt.registerLazySingleton( + () => PackManager( + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + ), + ); + + getIt.registerLazySingleton( + () => PackUpdater( + getIt.get(), + getIt.get(), + getIt.get(), + getIt.get(), + ), + ); + + getIt.registerLazySingleton( + () => UserManager( + getIt.get(), + ), + ); + + getIt.registerLazySingleton( + () => FavoriteCardsController(), + ); + + getIt.registerLazySingleton( + () => + TestManager(getIt.get(), getIt.get()), + ); + + getIt.registerLazySingleton( + () => InAppPurchaseService(), + ); + + getIt.registerLazySingleton( + () => PurchaseDetailsStreamSubscription(), + ); +} diff --git a/lib/di/locator.dart b/lib/di/locator.dart new file mode 100644 index 0000000..8d740ea --- /dev/null +++ b/lib/di/locator.dart @@ -0,0 +1,43 @@ +import 'package:get_it/get_it.dart'; +import 'package:mnemo_cards/features/packs/preview_packs_poller.dart'; +import 'package:mnemo_cards/managers/favorite_cards.dart'; +import 'package:mnemo_cards/managers/repository/http_repository.dart'; + +import '../features/packs/images_holder.dart'; +import '../features/packs/pack_cache_manager.dart'; +import '../features/packs/pack_updater.dart'; +import '../features/purchase/in_app_purchase.dart'; +import '../features/packs/pack_manager.dart'; +import '../features/tests/test_manager.dart'; +import '../managers/user_manager.dart'; +import 'injector.dart'; + +const locator = Locator(); + +class Locator { + GetIt get _getIt => getIt; + + const Locator(); + + HttpRepository get repository => _getIt.get(); + + PackManager get packManager => _getIt.get(); + + UserManager get userManager => _getIt.get(); + + FavoriteCardsController get favoriteCardsController => + _getIt.get(); + + PackCacheManager get packCacheManager => _getIt.get(); + + PackUpdater get packUpdater => _getIt.get(); + + PreviewPacksPoller get previewPackPoller => _getIt.get(); + + TestManager get testManager => _getIt.get(); + + ImagesHolder get imagesHolder => _getIt.get(); + + InAppPurchaseService get purchaseService => + _getIt.get(); +} diff --git a/lib/di/module.dart b/lib/di/module.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/domain/router/app_router.dart b/lib/domain/router/app_router.dart new file mode 100644 index 0000000..21ce789 --- /dev/null +++ b/lib/domain/router/app_router.dart @@ -0,0 +1,91 @@ +import 'package:auto_route/auto_route.dart'; + +import '../../di/locator.dart'; +import '../../main.dart' as main; +import 'app_router.gr.dart'; + +@AutoRouterConfig( + replaceInRouteName: 'Route,Screen', +) +class AppRouter extends $AppRouter { + final bool admin; + + AppRouter(this.admin); + + @override + List get routes => [ + CustomRoute( + // initial: !admin, + initial: true, + page: MainTabsPage.page, + transitionsBuilder: TransitionsBuilders.noTransition, + children: [ + CustomRoute( + page: HomePage.page, + transitionsBuilder: TransitionsBuilders.noTransition, + durationInMilliseconds: 200, + ), + CustomRoute( + page: ExplorePage.page, + transitionsBuilder: TransitionsBuilders.noTransition, + durationInMilliseconds: 200, + ), + // CustomRoute( + // page: ProfilePage.page, + // transitionsBuilder: TransitionsBuilders.noTransition, + // durationInMilliseconds: 200, + // ), + ]), + CustomRoute( + page: ProfilePage.page, + transitionsBuilder: TransitionsBuilders.slideLeftWithFade, + durationInMilliseconds: 200, + ), + CustomRoute( + // initial: !locator.userManager.hasUser, + page: AuthPage.page, + transitionsBuilder: TransitionsBuilders.slideLeftWithFade, + durationInMilliseconds: 200, + ), + CustomRoute( + page: TestPage.page, + transitionsBuilder: TransitionsBuilders.slideLeftWithFade, + durationInMilliseconds: 200, + ), + CustomRoute( + page: CardGamePage.page, + transitionsBuilder: TransitionsBuilders.fadeIn, + durationInMilliseconds: 400, + ), + CustomRoute( + page: CardPackPage.page, + transitionsBuilder: TransitionsBuilders.fadeIn, + durationInMilliseconds: 200, + ), + ]; + + static void openAuth() { + final router = main.appRouter; + if (!router.isRouteActive(AuthPage.name)) { + router.replace(PageRouteInfo(AuthPage.name)); + } + } + + static void openAuthOrProfile() { + if (locator.userManager.hasUser) { + openProfile(); + } else { + openAuth(); + } + } + + static void openProfile() { + final router = main.appRouter; + if (!router.isRouteActive(ProfilePage.name)) { + if (router.current.name == AuthPage.name) { + router.maybePop(); + } + router.push(PageRouteInfo(ProfilePage.name)); + } + } +} diff --git a/lib/domain/router/app_router.gr.dart b/lib/domain/router/app_router.gr.dart new file mode 100644 index 0000000..0b46b2d --- /dev/null +++ b/lib/domain/router/app_router.gr.dart @@ -0,0 +1,273 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// AutoRouterGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:auto_route/auto_route.dart' as _i9; +import 'package:flutter/cupertino.dart' as _i11; +import 'package:mnemo_cards/features/tests/test_page.dart' as _i8; +import 'package:mnemo_cards/main.dart' as _i6; +import 'package:mnemo_cards/pages/auth_page.dart' as _i1; +import 'package:mnemo_cards/pages/explore_page.dart' as _i4; +import 'package:mnemo_cards/pages/home_page.dart' as _i5; +import 'package:mnemo_cards/pages/profile_page.dart' as _i7; +import 'package:mnemo_cards/widgets/card_game/card_game_page.dart' as _i2; +import 'package:mnemo_cards/widgets/card_pack/card_pack_page.dart' as _i3; +import 'package:mnemo_cards_common/mnemo_cards_common.dart' as _i10; + +abstract class $AppRouter extends _i9.RootStackRouter { + $AppRouter({super.navigatorKey}); + + @override + final Map pagesMap = { + AuthPage.name: (routeData) { + return _i9.AutoRoutePage( + routeData: routeData, + child: _i1.AuthPage(), + ); + }, + CardGamePage.name: (routeData) { + final args = routeData.argsAs(); + return _i9.AutoRoutePage( + routeData: routeData, + child: _i2.CardGamePage( + cardPack: args.cardPack, + startCardId: args.startCardId, + infinite: args.infinite, + key: args.key, + ), + ); + }, + CardPackPage.name: (routeData) { + final args = routeData.argsAs(); + return _i9.AutoRoutePage( + routeData: routeData, + child: _i3.CardPackPage( + args.cardPackId, + key: args.key, + ), + ); + }, + ExplorePage.name: (routeData) { + return _i9.AutoRoutePage( + routeData: routeData, + child: _i4.ExplorePage(), + ); + }, + HomePage.name: (routeData) { + return _i9.AutoRoutePage( + routeData: routeData, + child: _i5.HomePage(), + ); + }, + MainTabsPage.name: (routeData) { + return _i9.AutoRoutePage( + routeData: routeData, + child: const _i6.MainTabsPage(), + ); + }, + ProfilePage.name: (routeData) { + return _i9.AutoRoutePage( + routeData: routeData, + child: _i7.ProfilePage(), + ); + }, + TestPage.name: (routeData) { + final args = routeData.argsAs(); + return _i9.AutoRoutePage( + routeData: routeData, + child: _i8.TestPage(args.testId), + ); + }, + }; +} + +/// generated route for +/// [_i1.AuthPage] +class AuthPage extends _i9.PageRouteInfo { + const AuthPage({List<_i9.PageRouteInfo>? children}) + : super( + AuthPage.name, + initialChildren: children, + ); + + static const String name = 'AuthPage'; + + static const _i9.PageInfo page = _i9.PageInfo(name); +} + +/// generated route for +/// [_i2.CardGamePage] +class CardGamePage extends _i9.PageRouteInfo { + CardGamePage({ + required _i10.CardPackDto cardPack, + int? startCardId, + bool infinite = false, + _i11.Key? key, + List<_i9.PageRouteInfo>? children, + }) : super( + CardGamePage.name, + args: CardGamePageArgs( + cardPack: cardPack, + startCardId: startCardId, + infinite: infinite, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'CardGamePage'; + + static const _i9.PageInfo page = + _i9.PageInfo(name); +} + +class CardGamePageArgs { + const CardGamePageArgs({ + required this.cardPack, + this.startCardId, + this.infinite = false, + this.key, + }); + + final _i10.CardPackDto cardPack; + + final int? startCardId; + + final bool infinite; + + final _i11.Key? key; + + @override + String toString() { + return 'CardGamePageArgs{cardPack: $cardPack, startCardId: $startCardId, infinite: $infinite, key: $key}'; + } +} + +/// generated route for +/// [_i3.CardPackPage] +class CardPackPage extends _i9.PageRouteInfo { + CardPackPage({ + required String cardPackId, + _i11.Key? key, + List<_i9.PageRouteInfo>? children, + }) : super( + CardPackPage.name, + args: CardPackPageArgs( + cardPackId: cardPackId, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'CardPackPage'; + + static const _i9.PageInfo page = + _i9.PageInfo(name); +} + +class CardPackPageArgs { + const CardPackPageArgs({ + required this.cardPackId, + this.key, + }); + + final String cardPackId; + + final _i11.Key? key; + + @override + String toString() { + return 'CardPackPageArgs{cardPackId: $cardPackId, key: $key}'; + } +} + +/// generated route for +/// [_i4.ExplorePage] +class ExplorePage extends _i9.PageRouteInfo { + const ExplorePage({List<_i9.PageRouteInfo>? children}) + : super( + ExplorePage.name, + initialChildren: children, + ); + + static const String name = 'ExplorePage'; + + static const _i9.PageInfo page = _i9.PageInfo(name); +} + +/// generated route for +/// [_i5.HomePage] +class HomePage extends _i9.PageRouteInfo { + const HomePage({List<_i9.PageRouteInfo>? children}) + : super( + HomePage.name, + initialChildren: children, + ); + + static const String name = 'HomePage'; + + static const _i9.PageInfo page = _i9.PageInfo(name); +} + +/// generated route for +/// [_i6.MainTabsPage] +class MainTabsPage extends _i9.PageRouteInfo { + const MainTabsPage({List<_i9.PageRouteInfo>? children}) + : super( + MainTabsPage.name, + initialChildren: children, + ); + + static const String name = 'MainTabsPage'; + + static const _i9.PageInfo page = _i9.PageInfo(name); +} + +/// generated route for +/// [_i7.ProfilePage] +class ProfilePage extends _i9.PageRouteInfo { + const ProfilePage({List<_i9.PageRouteInfo>? children}) + : super( + ProfilePage.name, + initialChildren: children, + ); + + static const String name = 'ProfilePage'; + + static const _i9.PageInfo page = _i9.PageInfo(name); +} + +/// generated route for +/// [_i8.TestPage] +class TestPage extends _i9.PageRouteInfo { + TestPage({ + required String testId, + List<_i9.PageRouteInfo>? children, + }) : super( + TestPage.name, + args: TestPageArgs(testId: testId), + initialChildren: children, + ); + + static const String name = 'TestPage'; + + static const _i9.PageInfo page = + _i9.PageInfo(name); +} + +class TestPageArgs { + const TestPageArgs({required this.testId}); + + final String testId; + + @override + String toString() { + return 'TestPageArgs{testId: $testId}'; + } +} diff --git a/lib/features/card_flipper/flip_card.dart b/lib/features/card_flipper/flip_card.dart new file mode 100644 index 0000000..9b5a87e --- /dev/null +++ b/lib/features/card_flipper/flip_card.dart @@ -0,0 +1,142 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'flip_side.dart'; +import 'flip_card_controllers.dart'; +import 'dart:math'; + +/// [FlipCard] A component that provides a flip card animation + +class FlipCard extends StatefulWidget { + /// [controller] used to ccontrol the flip + final FlipCardController controller; + + ///[frontWidget] The Front side widget of the card + final Widget frontWidget; + + ///[backWidget] The Back side widget of the card + final Widget backWidget; + + /// [onTapFlipping] When enabled, the card will flip automatically when touched. + final bool onTapFlipping; + + /// [axis] The flip axis [Horizontal] and [Vertical] + final FlipAxis axis; + + /// [rotateSide] The card rotate side + final RotateSide rotateSide; + + /// [animationDuration] The amount of milliseconds a turn animation will take. + final Duration animationDuration; + + /// [disableSplashEffect] The option for disable Inkwell widget's splash effect. + final bool disableSplashEffect; + + /// [splashColor] The option for Inkwell widget's splashColor. + final Color? splashColor; + + /// [focusColor] The option for Inkwell widget's focusColor. + final Color? focusColor; + + const FlipCard({ + Key? key, + this.focusColor, + this.splashColor, + this.onTapFlipping = false, + this.disableSplashEffect = false, + required this.frontWidget, + required this.backWidget, + required this.controller, + this.axis = FlipAxis.vertical, + required this.rotateSide, + this.animationDuration = const Duration(milliseconds: 800), + }) : super(key: key); + + @override + FlipCardState createState() => FlipCardState(); +} + +class FlipCardState extends State with TickerProviderStateMixin { + late AnimationController animationController; + double anglePlus = 0; + late bool _isFront; + StreamSubscription? _sub; + + @override + void initState() { + super.initState(); + _isFront = widget.controller.isFront; + animationController = + AnimationController(duration: widget.animationDuration, vsync: this); + if (!_isFront) { + animationController.value = 1.0; + anglePlus = pi; + } + _sub ??= widget.controller.asStream.distinct().listen((isFront) async { + if (this._isFront != isFront) { + if (animationController.isAnimating) return; + this._isFront = isFront; + await animationController + .forward(from: 0) + .then((value) => anglePlus = pi); + } + }); + } + + @override + void dispose() { + animationController.dispose(); + _sub?.cancel(); + _sub = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) => AnimatedBuilder( + animation: animationController, + builder: (context, child) { + double piValue = 0.0; + if (widget.rotateSide == RotateSide.top || + widget.rotateSide == RotateSide.left) { + piValue = pi; + } else { + piValue = -pi; + } + print('animation controller ${animationController.value}'); + double angle = animationController.value * piValue; + late Matrix4 transform; + late Matrix4 transformForBack; + if (_isFront) angle += anglePlus; + if (widget.axis == FlipAxis.horizontal) { + transform = Matrix4.identity() + ..setEntry(3, 2, 0.001) + ..rotateX(angle); + transformForBack = Matrix4.identity()..rotateX(pi); + } else { + transform = Matrix4.identity() + ..setEntry(3, 2, 0.001) + ..rotateY(angle); + transformForBack = Matrix4.identity()..rotateY(pi); + } + + final isFrontWidget = _isFrontWidget(angle.abs()); + + return Transform( + alignment: Alignment.center, + transform: transform, + child: isFrontWidget + ? widget.frontWidget + : Transform( + transform: transformForBack, + alignment: Alignment.center, + child: widget.backWidget, + ), + ); + }); + + bool _isFrontWidget(double angle) { + const degrees90 = pi / 2; + const degrees270 = 3 * pi / 2; + return angle <= degrees90 || angle >= degrees270; + } +} diff --git a/lib/features/card_flipper/flip_card_controllers.dart b/lib/features/card_flipper/flip_card_controllers.dart new file mode 100644 index 0000000..fc9215c --- /dev/null +++ b/lib/features/card_flipper/flip_card_controllers.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +import 'package:rxdart/rxdart.dart'; + +import 'flip_card.dart'; + +///This controller used to call Fliping +class FlipCardController { + final StreamController _controller = StreamController.broadcast(); + bool isFront = true; + + FlipCardController(); + + /// Flip the card + Future flipcard() async { + isFront = !isFront; + _controller.add(isFront); + } + + bool get state => isFront; + + Stream get asStream => + _controller.stream.asBroadcastStream().startWith(state); +} diff --git a/lib/features/card_flipper/flip_side.dart b/lib/features/card_flipper/flip_side.dart new file mode 100644 index 0000000..66dcde9 --- /dev/null +++ b/lib/features/card_flipper/flip_side.dart @@ -0,0 +1,13 @@ +/// [FlipAxis] The flip axis [Horizontal] and [Vertical] +enum FlipAxis { + horizontal, + vertical, +} + +/// [rotateSide] The card rotate side +enum RotateSide { + right, + left, + top, + bottom, +} diff --git a/lib/features/card_swiper/flutter_card_swiper.dart b/lib/features/card_swiper/flutter_card_swiper.dart new file mode 100644 index 0000000..fa2a0da --- /dev/null +++ b/lib/features/card_swiper/flutter_card_swiper.dart @@ -0,0 +1,9 @@ +/// A Tinder-like card swiper package. It allows you to swipe left, right, up, +/// and down and define your own business logic for each direction. Very smooth +/// animations supporting Android, iOS, Web & Desktop. +library flutter_card_swiper; + +export 'src/allowed_swipe_direction.dart'; +export 'src/card_swiper.dart'; +export 'src/card_swiper_controller.dart'; +export 'src/enums.dart'; diff --git a/lib/features/card_swiper/src/allowed_swipe_direction.dart b/lib/features/card_swiper/src/allowed_swipe_direction.dart new file mode 100644 index 0000000..69486f3 --- /dev/null +++ b/lib/features/card_swiper/src/allowed_swipe_direction.dart @@ -0,0 +1,62 @@ +/// Class to define the direction in which the card can be swiped +class AllowedSwipeDirection { + /// Set to true to allow the card to be swiped in the up direction + final bool up; + + /// Set to true to allow the card to be swiped in the down direction + final bool down; + + /// Set to true to allow the card to be swiped in the left direction + final bool left; + + /// Set to true to allow the card to be swiped in the right direction + final bool right; + + /// Define the direction in which the card can be swiped + const AllowedSwipeDirection._({ + required this.up, + required this.down, + required this.left, + required this.right, + }); + + /// Allow the card to be swiped in any direction + const AllowedSwipeDirection.all() + : up = true, + down = true, + right = true, + left = true; + + /// Does not allow the card to be swiped in any direction + const AllowedSwipeDirection.none() + : up = false, + down = false, + right = false, + left = false; + + /// Allow the card to be swiped in only the specified directions + factory AllowedSwipeDirection.only({ + up = false, + down = false, + left = false, + right = false, + }) => + AllowedSwipeDirection._( + up: up, + down: down, + left: left, + right: right, + ); + + /// Allow the card to be swiped symmetrically in horizontal or vertical directions + factory AllowedSwipeDirection.symmetric({ + horizontal = false, + vertical = false, + }) => + AllowedSwipeDirection._( + up: vertical, + down: vertical, + right: horizontal, + left: horizontal, + ); +} diff --git a/lib/features/card_swiper/src/card_animation.dart b/lib/features/card_swiper/src/card_animation.dart new file mode 100644 index 0000000..d4dc443 --- /dev/null +++ b/lib/features/card_swiper/src/card_animation.dart @@ -0,0 +1,276 @@ +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/widgets.dart'; + +import 'allowed_swipe_direction.dart'; +import 'enums.dart'; + +class CardAnimation { + CardAnimation({ + required this.animationController, + required this.maxAngle, + required this.initialScale, + required this.initialOffset, + this.isHorizontalSwipingEnabled = true, + this.isVerticalSwipingEnabled = true, + this.allowedSwipeDirection = const AllowedSwipeDirection.all(), + this.onSwipeDirectionChanged, + }) : scale = initialScale; + + final double maxAngle; + final double initialScale; + final Offset initialOffset; + final AnimationController animationController; + final bool isHorizontalSwipingEnabled; + final bool isVerticalSwipingEnabled; + final AllowedSwipeDirection allowedSwipeDirection; + final ValueChanged? onSwipeDirectionChanged; + + double left = 0; + double top = 0; + double total = 0; + double angle = 0; + double scale; + Offset difference = Offset.zero; + + late Animation _leftAnimation; + late Animation _topAnimation; + late Animation _scaleAnimation; + late Animation _differenceAnimation; + + double get _maxAngleInRadian => maxAngle * (math.pi / 180); + + void sync() { + left = _leftAnimation.value; + top = _topAnimation.value; + scale = _scaleAnimation.value; + difference = _differenceAnimation.value; + } + + void reset() { + animationController.reset(); + left = 0; + top = 0; + total = 0; + angle = 0; + scale = initialScale; + difference = Offset.zero; + } + + void update(double dx, double dy, bool inverseAngle) { + if (allowedSwipeDirection.right && allowedSwipeDirection.left) { + if (left > 0) { + onSwipeDirectionChanged?.call(CardSwiperDirection.right); + } else if (left < 0) { + onSwipeDirectionChanged?.call(CardSwiperDirection.left); + } + left += dx; + } else if (allowedSwipeDirection.right) { + if (left >= 0) { + onSwipeDirectionChanged?.call(CardSwiperDirection.right); + left += dx; + } + } else if (allowedSwipeDirection.left) { + if (left <= 0) { + onSwipeDirectionChanged?.call(CardSwiperDirection.left); + left += dx; + } + } + + if (allowedSwipeDirection.up && allowedSwipeDirection.down) { + if (top > 0) { + onSwipeDirectionChanged?.call(CardSwiperDirection.bottom); + } else if (top < 0) { + onSwipeDirectionChanged?.call(CardSwiperDirection.top); + } + top += dy; + } else if (allowedSwipeDirection.up) { + if (top <= 0) { + onSwipeDirectionChanged?.call(CardSwiperDirection.top); + top += dy; + } + } else if (allowedSwipeDirection.down) { + if (top >= 0) { + onSwipeDirectionChanged?.call(CardSwiperDirection.bottom); + top += dy; + } + } + + total = left + top; + updateAngle(inverseAngle); + updateScale(); + updateDifference(); + } + + void updateAngle(bool inverse) { + angle = clampDouble( + _maxAngleInRadian * left / 1000, + -_maxAngleInRadian, + _maxAngleInRadian, + ); + if (inverse) angle *= -1; + } + + void updateScale() { + scale = clampDouble(initialScale + (total.abs() / 5000), initialScale, 1.0); + } + + void updateDifference() { + final discrepancy = (total / 10).abs(); + + var diffX = 0.0, diffY = 0.0; + + if (initialOffset.dx > 0) { + diffX = discrepancy; + } else if (initialOffset.dx < 0) { + diffX = -discrepancy; + } + + if (initialOffset.dy < 0) { + diffY = -discrepancy; + } else if (initialOffset.dy > 0) { + diffY = discrepancy; + } + + difference = Offset(diffX, diffY); + } + + void animate(BuildContext context, CardSwiperDirection direction) { + switch (direction) { + case CardSwiperDirection.left: + return animateHorizontally(context, false); + case CardSwiperDirection.right: + return animateHorizontally(context, true); + case CardSwiperDirection.top: + return animateVertically(context, false); + case CardSwiperDirection.bottom: + return animateVertically(context, true); + default: + return; + } + } + + void animateHorizontally(BuildContext context, bool isToRight) { + final screenWidth = MediaQuery.of(context).size.width; + + _leftAnimation = Tween( + begin: left, + end: isToRight ? screenWidth : -screenWidth, + ).animate(animationController); + _topAnimation = Tween( + begin: top, + end: top + top, + ).animate(animationController); + _scaleAnimation = Tween( + begin: scale, + end: 1.0, + ).animate(animationController); + _differenceAnimation = Tween( + begin: difference, + end: initialOffset, + ).animate(animationController); + animationController.forward(); + } + + void animateVertically(BuildContext context, bool isToBottom) { + final screenHeight = MediaQuery.of(context).size.height; + + _leftAnimation = Tween( + begin: left, + end: left + left, + ).animate(animationController); + _topAnimation = Tween( + begin: top, + end: isToBottom ? screenHeight : -screenHeight, + ).animate(animationController); + _scaleAnimation = Tween( + begin: scale, + end: 1.0, + ).animate(animationController); + _differenceAnimation = Tween( + begin: difference, + end: initialOffset, + ).animate(animationController); + animationController.forward(); + } + + void animateBack(BuildContext context) { + _leftAnimation = Tween( + begin: left, + end: 0, + ).animate(animationController); + _topAnimation = Tween( + begin: top, + end: 0, + ).animate(animationController); + _scaleAnimation = Tween( + begin: scale, + end: initialScale, + ).animate(animationController); + _differenceAnimation = Tween( + begin: difference, + end: Offset.zero, + ).animate(animationController); + animationController.forward(); + } + + void animateUndo(BuildContext context, CardSwiperDirection direction) { + switch (direction) { + case CardSwiperDirection.left: + return animateUndoHorizontally(context, false); + case CardSwiperDirection.right: + return animateUndoHorizontally(context, true); + case CardSwiperDirection.top: + return animateUndoVertically(context, false); + case CardSwiperDirection.bottom: + return animateUndoVertically(context, true); + default: + return; + } + } + + void animateUndoHorizontally(BuildContext context, bool isToRight) { + final size = MediaQuery.of(context).size; + + _leftAnimation = Tween( + begin: isToRight ? size.width : -size.width, + end: 0, + ).animate(animationController); + _topAnimation = Tween( + begin: top, + end: top + top, + ).animate(animationController); + _scaleAnimation = Tween( + begin: 1.0, + end: scale, + ).animate(animationController); + _differenceAnimation = Tween( + begin: initialOffset, + end: difference, + ).animate(animationController); + animationController.forward(); + } + + void animateUndoVertically(BuildContext context, bool isToBottom) { + final size = MediaQuery.of(context).size; + + _leftAnimation = Tween( + begin: left, + end: left + left, + ).animate(animationController); + _topAnimation = Tween( + begin: isToBottom ? -size.height : size.height, + end: 0, + ).animate(animationController); + _scaleAnimation = Tween( + begin: 1.0, + end: scale, + ).animate(animationController); + _differenceAnimation = Tween( + begin: initialOffset, + end: difference, + ).animate(animationController); + animationController.forward(); + } +} diff --git a/lib/features/card_swiper/src/card_swiper.dart b/lib/features/card_swiper/src/card_swiper.dart new file mode 100644 index 0000000..002ecfa --- /dev/null +++ b/lib/features/card_swiper/src/card_swiper.dart @@ -0,0 +1,548 @@ +import 'dart:collection'; +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:yandex_mobileads/mobile_ads.dart'; + +import '../../yandex_ads/yandex_ads.dart'; +import 'allowed_swipe_direction.dart'; +import 'card_animation.dart'; +import 'card_swiper_controller.dart'; +import 'enums.dart'; +import 'typedefs.dart'; +import 'undoable.dart'; +import 'extensions.dart'; + +class CardSwiper extends StatefulWidget { + /// Function that builds each card in the stack. + /// + /// The function is called with the index of the card to be built, the build context, the ratio + /// of vertical drag to [threshold] as a percentage, and the ratio of horizontal drag to [threshold] + /// as a percentage. The function should return a widget that represents the card at the given index. + /// It can return `null`, which will result in an empty card being displayed. + final NullableCardBuilder cardBuilder; + + /// The number of cards in the stack. + /// + /// The [cardsCount] parameter specifies the number of cards that will be displayed in the stack. + /// + /// This parameter is required and must be greater than 0. + final int cardsCount; + + /// The index of the card to display initially. + /// + /// Defaults to 0, meaning the first card in the stack is displayed initially. + final int initialIndex; + + /// The [CardSwiperController] used to control the swiper externally. + /// + /// If `null`, the swiper can only be controlled by user input. + final CardSwiperController? controller; + + /// The duration of each swipe animation. + /// + /// Defaults to 200 milliseconds. + final Duration duration; + + /// The padding around the swiper. + /// + /// Defaults to `EdgeInsets.symmetric(horizontal: 20, vertical: 25)`. + final EdgeInsetsGeometry padding; + + /// The maximum angle the card reaches while swiping. + /// + /// Must be between 0 and 360 degrees. Defaults to 30 degrees. + final double maxAngle; + + /// The threshold from which the card is swiped away. + /// + /// Must be between 1 and 100 percent of the card width. Defaults to 50 percent. + final int threshold; + + /// The scale of the card that is behind the front card. + /// + /// The [scale] and [backCardOffset] both impact the positions of the back cards. + /// In order to keep the back card position same after changing the [scale], + /// the [backCardOffset] should also be adjusted. + /// * As a rough rule of thumb, 0.1 change in [scale] effects an + /// [backCardOffset] of ~35px. + /// + /// Must be between 0 and 1. Defaults to 0.9. + final double scale; + + /// Whether swiping is disabled. + /// + /// If `true`, swiping is disabled, except when triggered by the [controller]. + /// + /// Defaults to `false`. + final bool isDisabled; + + /// Callback function that is called when a swipe action is performed. + /// + /// The function is called with the oldIndex, the currentIndex and the direction of the swipe. + /// If the function returns `false`, the swipe action is canceled and the current card remains + /// on top of the stack. If the function returns `true`, the swipe action is performed as expected. + final CardSwiperOnSwipe? onSwipe; + + /// Callback function that is called when there are no more cards to swipe. + final CardSwiperOnEnd? onEnd; + + /// Callback function that is called when the swiper is disabled. + final CardSwiperOnTapDisabled? onTapDisabled; + + /// The direction in which the card is swiped when triggered by the [controller]. + /// + /// Defaults to [CardSwiperDirection.right]. + final CardSwiperDirection direction; + + /// Defined the directions in which the card is allowed to be swiped. + /// Defaults to [AllowedSwipeDirection.all] + final AllowedSwipeDirection allowedSwipeDirection; + + /// A boolean value that determines whether the card stack should loop. When the last card is swiped, + /// if isLoop is true, the first card will become the last card again. The default value is true. + final bool isLoop; + + /// An integer that determines the number of cards that are displayed at the same time. + /// The default value is 2. Note that you must display at least one card, and no more than the [cardsCount] parameter. + final int numberOfCardsDisplayed; + + /// Callback function that is called when a card is unswiped. + /// + /// The function is called with the oldIndex, the currentIndex and the direction of the previous swipe. + /// If the function returns `false`, the undo action is canceled and the current card remains + /// on top of the stack. If the function returns `true`, the undo action is performed as expected. + final CardSwiperOnUndo? onUndo; + + /// Callback function that is called when a card swipe direction changes. + /// + /// The function is called with the last detected horizontal direction and the last detected vertical direction + final CardSwiperDirectionChange? onSwipeDirectionChange; + + /// The offset of the back card from the front card. + /// + /// In order to keep the back card position same after changing the [backCardOffset], + /// the [scale] should also be adjusted. + /// * As a rough rule of thumb, 35px change in [backCardOffset] effects a + /// [scale] change of 0.1. + /// + /// Must be a positive value. Defaults to Offset(0, 40). + final Offset backCardOffset; + + final int adsCount; + + const CardSwiper({ + Key? key, + required this.cardBuilder, + required this.cardsCount, + this.controller, + this.adsCount = 0, + this.initialIndex = 0, + this.padding = const EdgeInsets.symmetric(horizontal: 20, vertical: 25), + this.duration = const Duration(milliseconds: 200), + this.maxAngle = 30, + this.threshold = 50, + this.scale = 0.9, + this.isDisabled = false, + this.onTapDisabled, + this.onSwipe, + this.onEnd, + this.direction = CardSwiperDirection.right, + this.onSwipeDirectionChange, + this.allowedSwipeDirection = const AllowedSwipeDirection.all(), + this.isLoop = true, + this.numberOfCardsDisplayed = 2, + this.onUndo, + this.backCardOffset = const Offset(0, 40), + }) : assert( + maxAngle >= 0 && maxAngle <= 360, + 'maxAngle must be between 0 and 360', + ), + assert( + threshold >= 1 && threshold <= 100, + 'threshold must be between 1 and 100', + ), + assert( + direction != CardSwiperDirection.none, + 'direction must not be none', + ), + assert( + scale >= 0 && scale <= 1, + 'scale must be between 0 and 1', + ), + assert( + numberOfCardsDisplayed >= 1 && numberOfCardsDisplayed <= cardsCount, + 'you must display at least one card, and no more than [cardsCount]', + ), + assert( + initialIndex >= 0 && initialIndex < cardsCount, + 'initialIndex must be between 0 and [cardsCount]', + ), + super(key: key); + + @override + State createState() => _CardSwiperState(); +} + +class _CardSwiperState extends State + with SingleTickerProviderStateMixin { + late CardAnimation _cardAnimation; + late AnimationController _animationController; + + SwipeType _swipeType = SwipeType.none; + CardSwiperDirection _detectedDirection = CardSwiperDirection.none; + CardSwiperDirection _detectedHorizontalDirection = CardSwiperDirection.none; + CardSwiperDirection _detectedVerticalDirection = CardSwiperDirection.none; + bool _tappedOnTop = false; + + final _undoableIndex = Undoable(null); + final Queue _directionHistory = Queue(); + + int? get _currentIndex => _undoableIndex.state; + + int? get _nextIndex => getValidIndexOffset(1); + + bool get _canSwipe => _currentIndex != null && !widget.isDisabled; + + @override + void initState() { + super.initState(); + + _undoableIndex.state = widget.initialIndex; + + widget.controller?.addListener(_controllerListener); + + _animationController = AnimationController( + duration: widget.duration, + vsync: this, + ) + ..addListener(_animationListener) + ..addStatusListener(_animationStatusListener); + + _cardAnimation = CardAnimation( + animationController: _animationController, + maxAngle: widget.maxAngle, + initialScale: widget.scale, + allowedSwipeDirection: widget.allowedSwipeDirection, + initialOffset: widget.backCardOffset, + onSwipeDirectionChanged: onSwipeDirectionChanged, + ); + } + + void onSwipeDirectionChanged(CardSwiperDirection direction) { + if (direction == CardSwiperDirection.none) { + _detectedVerticalDirection = direction; + _detectedHorizontalDirection = direction; + widget.onSwipeDirectionChange + ?.call(_detectedHorizontalDirection, _detectedVerticalDirection); + } else if (direction == CardSwiperDirection.right || + direction == CardSwiperDirection.left) { + if (_detectedHorizontalDirection != direction) { + _detectedHorizontalDirection = direction; + widget.onSwipeDirectionChange + ?.call(_detectedHorizontalDirection, _detectedVerticalDirection); + } + } else if (direction == CardSwiperDirection.top || + direction == CardSwiperDirection.bottom) { + if (_detectedVerticalDirection != direction) { + _detectedVerticalDirection = direction; + widget.onSwipeDirectionChange + ?.call(_detectedHorizontalDirection, _detectedVerticalDirection); + } + } + } + + @override + void dispose() { + super.dispose(); + _animationController.dispose(); + widget.controller?.removeListener(_controllerListener); + } + + @override + Widget build(BuildContext context) { + widget.controller?.setIndex(_currentIndex ?? -1); + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Padding( + padding: widget.padding, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + fit: StackFit.expand, + children: List.generate(numberOfCardsOnScreen(), (index) { + if (index == 0) return _frontItem(constraints); + return _backItem(constraints, index); + }).reversed.toList(), + ); + }, + ), + ); + }, + ); + } + + Widget _frontItem(BoxConstraints constraints) { + return Positioned( + left: _cardAnimation.left, + top: _cardAnimation.top, + child: GestureDetector( + child: Transform.rotate( + angle: _cardAnimation.angle, + child: ConstrainedBox( + constraints: constraints, + child: widget.cardBuilder( + context, + _currentIndex!, + (100 * _cardAnimation.left / widget.threshold).ceil(), + (100 * _cardAnimation.top / widget.threshold).ceil(), + _cardAnimation.scale, + ), + ), + ), + onTap: () async { + if (widget.isDisabled) { + await widget.onTapDisabled?.call(); + } + }, + onPanStart: (tapInfo) { + if (!widget.isDisabled) { + final renderBox = context.findRenderObject()! as RenderBox; + final position = renderBox.globalToLocal(tapInfo.globalPosition); + + if (position.dy < renderBox.size.height / 2) _tappedOnTop = true; + } + }, + onPanUpdate: (tapInfo) { + if (!widget.isDisabled && + tapInfo.delta.dx.abs() > 2 * tapInfo.delta.dy.abs()) { + setState( + () => _cardAnimation.update( + tapInfo.delta.dx, + tapInfo.delta.dy, + _tappedOnTop, + ), + ); + } + }, + onPanEnd: (tapInfo) { + if (_canSwipe) { + _tappedOnTop = false; + _onEndAnimation(); + } + }, + ), + ); + } + + Widget _backItem(BoxConstraints constraints, int index) { + final scale = _cardAnimation.scale - ((1 - widget.scale) * (index - 1)); + return Positioned( + top: (widget.backCardOffset.dy * index) - _cardAnimation.difference.dy, + left: (widget.backCardOffset.dx * index) - _cardAnimation.difference.dx, + child: Transform.scale( + scale: scale, + child: ConstrainedBox( + constraints: constraints, + child: widget.cardBuilder( + context, + getValidIndexOffset(index)!, + 0, + 0, + scale, + ), + ), + ), + ); + } + + void _controllerListener() { + switch (widget.controller?.state) { + case CardSwiperState.swipe: + return _swipe(widget.direction); + case CardSwiperState.swipeLeft: + return _swipe(CardSwiperDirection.left); + case CardSwiperState.swipeRight: + return _swipe(CardSwiperDirection.right); + case CardSwiperState.swipeTop: + return _swipe(CardSwiperDirection.top); + case CardSwiperState.swipeBottom: + return _swipe(CardSwiperDirection.bottom); + case CardSwiperState.undo: + return _undo(); + case CardSwiperState.restart: + return _restart(); + default: + return; + } + } + + void _animationListener() { + if (_animationController.status == AnimationStatus.forward) { + setState(_cardAnimation.sync); + } + } + + Future _animationStatusListener(AnimationStatus status) async { + if (status == AnimationStatus.completed) { + switch (_swipeType) { + case SwipeType.swipe: + await _handleCompleteSwipe(); + break; + default: + break; + } + + _reset(); + } + } + + Future _handleCompleteSwipe() async { + final isLastCard = _currentIndex! == widget.cardsCount - 1; + final shouldCancelSwipe = await widget.onSwipe + ?.call(_currentIndex!, _nextIndex, _detectedDirection) == + false; + + if (shouldCancelSwipe) { + return; + } + + _undoableIndex.state = _nextIndex; + _directionHistory.add(_detectedDirection); + + if (isLastCard) { + widget.onEnd?.call(); + } + } + + void _reset() { + onSwipeDirectionChanged(CardSwiperDirection.none); + _detectedDirection = CardSwiperDirection.none; + setState(() { + _animationController.reset(); + _cardAnimation.reset(); + _swipeType = SwipeType.none; + }); + } + + void _onEndAnimation() { + if (_cardAnimation.left.abs() > widget.threshold) { + final direction = _cardAnimation.left.isNegative + ? CardSwiperDirection.left + : CardSwiperDirection.right; + if (direction == CardSwiperDirection.left && + widget.allowedSwipeDirection.left || + direction == CardSwiperDirection.right && + widget.allowedSwipeDirection.right) { + _swipe(direction); + } else { + _goBack(); + } + } else if (_cardAnimation.top.abs() > widget.threshold) { + final direction = _cardAnimation.top.isNegative + ? CardSwiperDirection.top + : CardSwiperDirection.bottom; + if (direction == CardSwiperDirection.top && + widget.allowedSwipeDirection.up || + direction == CardSwiperDirection.bottom && + widget.allowedSwipeDirection.down) { + _swipe(direction); + } else { + _goBack(); + } + } else { + _goBack(); + } + } + + void _swipe(CardSwiperDirection direction) { + if (_currentIndex == null) return; + _swipeType = SwipeType.swipe; + _detectedDirection = direction; + _cardAnimation.animate(context, direction); + // widget.controller?.notifyListeners(); + if (direction == CardSwiperDirection.left) { + widget.controller?.swipedLeft(); + } else if (direction == CardSwiperDirection.right) { + widget.controller?.swipedRight(); + } + } + + void _goBack() { + _swipeType = SwipeType.back; + _cardAnimation.animateBack(context); + } + + void _undo({bool animate = true}) { + if (_directionHistory.isEmpty) return; + if (_undoableIndex.previousState == null) return; + + final direction = _directionHistory.last; + final shouldCancelUndo = widget.onUndo?.call( + _currentIndex, + _undoableIndex.previousState!, + direction, + ) == + false; + + if (shouldCancelUndo) { + return; + } + + _undoableIndex.undo(); + _directionHistory.removeLast(); + _swipeType = SwipeType.undo; + widget.controller?.directionHistory.removeLast(); + widget.controller?.setIndex(_currentIndex ?? 0); + if (animate) { + _cardAnimation.animateUndo(context, direction); + } + } + + void _restart() { + int undoQuan = 4; + while (undoQuan > 0 && + _directionHistory.isNotEmpty && + _undoableIndex.previousState != null) { + _undo(animate: true); + _swipeType = SwipeType.none; + undoQuan--; + } + while ( + _directionHistory.isNotEmpty && _undoableIndex.previousState != null) { + _undo(animate: false); + _swipeType = SwipeType.none; + } + _swipeType = SwipeType.none; + print('RESTARTED'); + } + + int numberOfCardsOnScreen() { + if (widget.isLoop) { + return widget.numberOfCardsDisplayed; + } + if (_currentIndex == null) { + return 0; + } + + return math.min( + widget.numberOfCardsDisplayed, + widget.cardsCount - _currentIndex!, + ); + } + + int? getValidIndexOffset(int offset) { + if (_currentIndex == null) { + return null; + } + + final index = _currentIndex! + offset; + if (!widget.isLoop && !index.isBetween(0, widget.cardsCount - 1)) { + return null; + } + return index % widget.cardsCount; + } +} diff --git a/lib/features/card_swiper/src/card_swiper_controller.dart b/lib/features/card_swiper/src/card_swiper_controller.dart new file mode 100644 index 0000000..8b07964 --- /dev/null +++ b/lib/features/card_swiper/src/card_swiper_controller.dart @@ -0,0 +1,80 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; + +import 'enums.dart'; + +/// A controller that can be used to trigger swipes on a CardSwiper widget. +class CardSwiperController extends ChangeNotifier { + CardSwiperState? state; + final StreamController _controller = StreamController.broadcast(); + final Queue<(CardSwiperDirection, String)> directionHistory = Queue(); + + int currentIndex = 0; + + CardSwiperController() {} + + Set get swipedLeftIds => directionHistory + .where((element) => element.$1 == CardSwiperDirection.left) + .map((e) => e.$2) + .toSet(); + + Set get swipedRightIds => directionHistory + .where((element) => element.$1 == CardSwiperDirection.right) + .map((e) => e.$2) + .toSet(); + + int get progress => directionHistory.length; + + void setIndex(int index) { + currentIndex = index; + _controller.add(index); + } + + @override + Future dispose() async { + super.dispose(); + } + + /// Swipe the card by changing the status of the controller + void swipe({CardSwiperState? direction}) { + state = direction ?? CardSwiperState.swipeLeft; + notifyListeners(); + } + + void swipedLeft() { + directionHistory.add((CardSwiperDirection.left, currentIndex.toString())); + // notifyListeners(); + } + + void swipedRight() { + directionHistory.add((CardSwiperDirection.right, currentIndex.toString())); + // notifyListeners(); + } + + /// Swipe the card to the top side by changing the status of the controller + void swipeTop() { + state = CardSwiperState.swipeTop; + notifyListeners(); + } + + /// Swipe the card to the bottom side by changing the status of the controller + void swipeBottom() { + state = CardSwiperState.swipeBottom; + notifyListeners(); + } + + // Undo the last swipe by changing the status of the controller + void undo() { + state = CardSwiperState.undo; + notifyListeners(); + } + + void restart() { + state = CardSwiperState.restart; + notifyListeners(); + } + + Stream get asStream => _controller.stream.distinct(); +} diff --git a/lib/features/card_swiper/src/enums.dart b/lib/features/card_swiper/src/enums.dart new file mode 100644 index 0000000..daba98f --- /dev/null +++ b/lib/features/card_swiper/src/enums.dart @@ -0,0 +1,13 @@ +enum CardSwiperState { + swipe, + swipeLeft, + swipeRight, + swipeTop, + swipeBottom, + undo, + restart, +} + +enum CardSwiperDirection { none, left, right, top, bottom } + +enum SwipeType { none, swipe, back, undo } diff --git a/lib/features/card_swiper/src/extensions.dart b/lib/features/card_swiper/src/extensions.dart new file mode 100644 index 0000000..9372911 --- /dev/null +++ b/lib/features/card_swiper/src/extensions.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; + +import 'enums.dart'; + +extension Range on num { + bool isBetween(num from, num to) { + return from <= this && this <= to; + } +} + +extension CardSwiperDirectionExtension on CardSwiperDirection { + Axis get axis { + switch (this) { + case CardSwiperDirection.left: + case CardSwiperDirection.right: + return Axis.horizontal; + case CardSwiperDirection.top: + case CardSwiperDirection.bottom: + return Axis.vertical; + case CardSwiperDirection.none: + throw Exception('Direction is none'); + } + } +} diff --git a/lib/features/card_swiper/src/typedefs.dart b/lib/features/card_swiper/src/typedefs.dart new file mode 100644 index 0000000..9ac326b --- /dev/null +++ b/lib/features/card_swiper/src/typedefs.dart @@ -0,0 +1,39 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'enums.dart'; + +typedef CardSwiperOnSwipe = FutureOr Function( + int previousIndex, + int? currentIndex, + CardSwiperDirection direction, +); + +typedef CardSwiperOnSwipeUpdate = Function( + int? currentIndex, + CardSwiperDirection direction, +); + +typedef NullableCardBuilder = Widget? Function( + BuildContext context, + int index, + int horizontalOffsetPercentage, + int verticalOffsetPercentage, + double scale, +); + +typedef CardSwiperDirectionChange = Function( + CardSwiperDirection horizontalDirection, + CardSwiperDirection verticalDirection, +); + +typedef CardSwiperOnEnd = FutureOr Function(); + +typedef CardSwiperOnTapDisabled = FutureOr Function(); + +typedef CardSwiperOnUndo = bool Function( + int? previousIndex, + int currentIndex, + CardSwiperDirection direction, +); diff --git a/lib/features/card_swiper/src/undoable.dart b/lib/features/card_swiper/src/undoable.dart new file mode 100644 index 0000000..f79f428 --- /dev/null +++ b/lib/features/card_swiper/src/undoable.dart @@ -0,0 +1,21 @@ +class Undoable { + Undoable(this._value, {Undoable? previousValue}) : _previous = previousValue; + + T _value; + Undoable? _previous; + + T get state => _value; + T? get previousState => _previous?.state; + + set state(T newValue) { + _previous = Undoable(_value, previousValue: _previous); + _value = newValue; + } + + void undo() { + if (_previous != null) { + _value = _previous!._value; + _previous = _previous?._previous; + } + } +} diff --git a/lib/features/packs/images_holder.dart b/lib/features/packs/images_holder.dart new file mode 100644 index 0000000..e2e6432 --- /dev/null +++ b/lib/features/packs/images_holder.dart @@ -0,0 +1,36 @@ +import 'dart:developer'; + +import 'package:flutter/cupertino.dart'; +import 'package:mnemo_cards/di/locator.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +class ImagesHolder { + Map _images = {}; + DateTime? unlockTime; + + ImagesHolder(); + + void setPackImages(Map images) { + _images = Map.from(images); + } + + void clear() => _images.clear(); + + MemoryImage? get(String id) { + final memoryImage = _images[id]; + + return memoryImage; + } +} + +extension ImagesHolderStringExt on String { + MemoryImage? get memoryImage => locator.imagesHolder.get(this); +} + +extension ImagesHolderCardExt on GameCardDto { + MemoryImage? get memoryImage => locator.imagesHolder.get(id.toString()); +} + +extension ImagesHolderIntExt on int { + MemoryImage? get memoryImage => locator.imagesHolder.get(toString()); +} diff --git a/lib/features/packs/pack_cache_manager.dart b/lib/features/packs/pack_cache_manager.dart new file mode 100644 index 0000000..dc7caac --- /dev/null +++ b/lib/features/packs/pack_cache_manager.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/widgets.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../di/locator.dart'; + +// persist +class PackCacheManager { + late final SharedPreferences _sharedPreferences; + + PackCacheManager(); + + Future init() async { + _sharedPreferences = await SharedPreferences.getInstance(); + } + + Future loadPackFromCache( + String packId, { + bool withCardImages = true, + }) async { + log('Loading from cache ${packId}'); + try { + final packDir = await _packDirectory(packId); + final packFile = File('${packDir.path}/pack.json'); + if (!packFile.existsSync()) { + log('Pack file not exist'); + return throw Exception('Pack file not exist'); + } + var pack = CardPackDto.fromJson( + jsonDecode(packFile.readAsStringSync()), + ); + if (withCardImages) { + Map images = {}; + log('Cards ${packId}: ${pack.cards.length}'); + if (pack.cards.isNotEmpty) { + log('Loading cards ${packId}'); + for (final dto in pack.cards) { + final file = File('${packDir.path}/images/${dto.id}'); + if (file.existsSync()) { + try { + images[dto.id.toString()] = + MemoryImage(await file.readAsBytes()); + } catch (e) { + throw Exception('Cant load pack $packId $e'); + } + } else { + log('Cant load card ${dto.id} from $packId, skiping'); + continue; + } + } + } + log('Pack loaded ${packId}'); + locator.imagesHolder.setPackImages(images); + } + return pack; + } catch (e, s) { + log('Cant load from cache ${packId} ${e} ${s}'); + deletePack(packId); + rethrow; + } + } + + Future> _updateSavedPacks() async { + final packsDir = + Directory('${(await getApplicationDocumentsDirectory()).path}/packs'); + List savedPackIds = []; + if (packsDir.existsSync()) { + for (final packDir in packsDir.listSync()) { + if (File('${packDir.path}/pack.json').existsSync()) { + savedPackIds.add(packDir.path.split('/').last); + } + } + } + _sharedPreferences.setStringList('packs', savedPackIds); + return savedPackIds; + } + + Future> loadAllPacks() async { + final ids = await _updateSavedPacks(); + final List packs = []; + for (final id in ids) { + try { + final pack = await loadPackFromCache(id, withCardImages: false); + packs.add(pack); + } catch (e) { + log('Pack $id doesnt exist anymore'); + } + } + return packs; + } + + Future deletePack(String packId) async { + try { + final dir = await _packDirectory(packId); + dir.deleteSync(recursive: true); + } catch (e) { + log('Deletion failed $e'); + } + _updateSavedPacks(); + } + + Future loadPackFile(String packId, String path) async { + final bytes = + File('${(await _packDirectory(packId)).path}/$path').readAsBytesSync(); + return bytes; + } + + Future clearCache() async { + final saved = await _updateSavedPacks(); + for (final p in saved) { + await deletePack(p); + } + await _sharedPreferences.remove('packs'); + } + + Future savePack(CardPackDto dto, + {Map? images}) async { + log('Saving pack ${dto.id}'); + final dir = await _packDirectory(dto.id); + final json = jsonEncode(dto); + log('Saving dto ${dto.id}'); + File('${dir.path}/pack.json') + ..createSync(recursive: true) + ..writeAsStringSync(json); + + if (images != null) { + log('Saving card images ${dto.id}'); + final imageDir = Directory('${dir.path}/images'); + if (imageDir.existsSync()) { + imageDir.deleteSync(recursive: true); + } + for (final card in dto.cards) { + final bytes = images[card.id.toString()]?.bytes; + if (bytes != null) { + File('${imageDir.path}/${card.id}') + ..createSync(recursive: true) + ..writeAsBytesSync(bytes); + } + } + } + + log('Saved ${dto.id}'); + } + + Future _packDirectory(String id) async => Directory( + '${(await getApplicationDocumentsDirectory()).path}/packs/$id', + ); +} diff --git a/lib/features/packs/pack_holder.dart b/lib/features/packs/pack_holder.dart new file mode 100644 index 0000000..cb8ba3a --- /dev/null +++ b/lib/features/packs/pack_holder.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; + +class PackHolder { + final StreamController _streamController = + StreamController.broadcast(); + PackState? _state; + + PackHolder(); + + void setPacks(List packs) { + _state = PackState( + packs, + ); + _streamController.add(_state); + } + + void clear() { + _state = null; + _streamController.add(_state); + } + + PackState? get state => _state; + + Stream get asStream => _streamController.stream.startWith(_state); +} + +class PackState { + final List packsWithCards; + + PackState(this.packsWithCards); +} diff --git a/lib/features/packs/pack_manager.dart b/lib/features/packs/pack_manager.dart new file mode 100644 index 0000000..61c8fae --- /dev/null +++ b/lib/features/packs/pack_manager.dart @@ -0,0 +1,95 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:mnemo_cards/features/packs/pack_cache_manager.dart'; +import 'package:mnemo_cards/features/packs/packs_api.dart'; +import 'package:mnemo_cards/features/packs/preview_pack_holder.dart'; +import 'package:mnemo_cards/managers/repository/repository.dart'; +import 'package:mnemo_cards/utils/iterable_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'pack_holder.dart'; + +class PackManager { + final Repository _repository; + final PacksApi _packsApi; + final PackHolder _packHolder; + final PreviewPackHolder _previewPackHolder; + final PackCacheManager _cacheManager; + + PackManager( + this._repository, + this._packsApi, + this._packHolder, + this._previewPackHolder, + this._cacheManager, + ); + + Future init() async { + _packHolder.setPacks(await _cacheManager.loadAllPacks()); + + // _repository.userStream.map((u) => u.packs).distinct().listen((packs) async { + // final savedPacks = await savedPacksIds; + // for (final p in packs) { + // if (savedPacks.contains(p)) { + // final loaded = await loadPackFromCache(p); + // if (loaded.cards.isEmpty) { + // await downloadPackById(p); + // } + // } else { + // await downloadPackById(p); + // } + // } + // }); + } + + Stream getCardPackStream(String id) async* { + try { + yield await _cacheManager.loadPackFromCache(id); + } catch (err) { + log(err.toString()); + log('Cant load pack from cache, fetching pack'); + try { + final dto = await _packsApi.getPackDto(id).catchError((_) => null); + if (dto != null) { + yield dto; + } else { + final buyDto = await _packsApi.getPackBuy(id); + if (buyDto != null) { + yield buyDto; + } + } + } catch (err) { + log(err.toString()); + } + } + yield* _packHolder.asStream + .map((event) => + event?.packsWithCards.firstWhereOrNull((p) => p.id == id)) + .whereNotNull(); + } + + Future clearCache() async { + _packHolder.clear(); + await _cacheManager.clearCache(); + } + + Stream?> get packsStream => + _packHolder.asStream.map((event) => event?.packsWithCards); + + Stream?> get packsPreviewStream => + _previewPackHolder.asStream + .map((event) => event?.previewPacks) + .whereNotNull() + .distinct((prev, next) { + if (prev.length != next.length) return false; + for (int i = 0; i < prev.length; i++) { + if (prev[i].isAvailable != next[i].isAvailable) return false; + if (prev[i].id != next[i].id) return false; + if (prev[i].version != next[i].version) return false; + if (prev[i].price != next[i].price) return false; + } + return true; + }); +} diff --git a/lib/features/packs/pack_updater.dart b/lib/features/packs/pack_updater.dart new file mode 100644 index 0000000..a44b86a --- /dev/null +++ b/lib/features/packs/pack_updater.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:typed_data'; + +import 'package:archive/archive.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:mnemo_cards/features/packs/packs_api.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; +import '../../features/packs/pack_cache_manager.dart'; +import '../../features/packs/pack_holder.dart'; +import 'preview_pack_holder.dart'; + +/// Updates cache and state holders for available packs +/// Does polling +class PackUpdater { + final PacksApi _packsApi; + final PackCacheManager _cacheManager; + final PackHolder _packHolder; + final PreviewPackHolder _previewPackHolder; + + Completer? _updateCompleter; + + PackUpdater( + this._packsApi, + this._cacheManager, + this._packHolder, + this._previewPackHolder, + ); + + Future init() async { + final pollStream = Stream.periodic(const Duration(seconds: 60)); + pollStream.startWith(-1).listen((event) async { + await updateAvailablePacks(); + try { + final r = await _packsApi.getPacksPreviews(null); + _previewPackHolder.setPreviewPacks(r); + } catch (e) { + log(e.toString()); + } + }); + } + + Future updateAvailablePacks() async { + if (_updateCompleter?.isCompleted ?? true) { + _updateCompleter = Completer(); + } + unawaited(_updateAvailablePacks().whenComplete(() { + if (_updateCompleter?.isCompleted == false) { + _updateCompleter?.complete(); + } + })); + return _updateCompleter!.future; + } + + Future _updateAvailablePacks() async { + final savedList = await _cacheManager.loadAllPacks(); + final packData = Map.fromEntries( + savedList.map( + (e) => MapEntry( + e.id.toString(), + e.version, + ), + ), + ); + final actions = await _packsApi.getPacksActions(packData); + Map updatedPacks = {}; + bool shouldUpdateHolder = false; + + for (final action in actions) { + switch (action.action) { + case PackAction.delete: + log('DELETE ${action.id}'); + shouldUpdateHolder = true; + await _cacheManager.deletePack(action.id); + savedList.removeWhere((p) => p.id == action.id); + break; + case PackAction.update: + log('UPDATE ${action.id}'); + shouldUpdateHolder = true; + try { + final updatedPack = await _updatePackAndSaveImages(action.id); + updatedPacks[action.id] = updatedPack; + } catch (e) { + log('Error while download cards for ${action.id} $e'); + } + break; + case PackAction.unknown: + log('Unknown action on update ${action.id}'); + break; + } + } + if (shouldUpdateHolder) { + List packs = []; + for (final saved in savedList) { + if (updatedPacks[saved.id] != null) { + packs.add(updatedPacks[saved.id]!); + updatedPacks.remove(saved.id); + } else { + packs.add(saved); + } + } + packs.addAll(updatedPacks.values); + _packHolder.setPacks(packs); + } + } + + Future updatePackInfoWithCardsAndSave(String packId) async { + try { + // todo + // final pack = await _updatePackInfoWithCards(packId); + // await _cacheManager.savePack(pack, updateCards: true); + return true; + } catch (e) { + log(e.toString()); + } + return false; + } + + Future _updatePackAndSaveImages(String packId) async { + try { + log('Updating with cards ${packId}', name: 'updatePackWithCards'); + final packDto = await _packsApi.getPackDto(packId); + if (packDto == null) { + throw Exception('Cant update pack $packId'); + } + final imagesData = await _packsApi.packImagesArchive(packId); + Archive imagesArchive = ZipDecoder().decodeBytes(imagesData); + Map images = {}; + for (final file in imagesArchive) { + final fileData = file.content as List; + images[file.name] = MemoryImage( + Uint8List.fromList(fileData), + ); + } + _cacheManager.savePack(packDto, images: images); + log('Pack ${packId} updated with cards'); + return packDto; + } catch (e, s) { + log('Cant update pack', name: 'updatePackWithCards'); + log(e.toString(), stackTrace: s); + rethrow; + } + } +} diff --git a/lib/features/packs/packs_api.dart b/lib/features/packs/packs_api.dart new file mode 100644 index 0000000..49e2516 --- /dev/null +++ b/lib/features/packs/packs_api.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../../managers/repository/api.dart'; + +class PacksApi with Api { + final Dio _dio; + + PacksApi(this._dio); + + // previews for the main page + Future> getPacksPreviews( + Map? packData, + ) async { + final r = await _dio.get( + '$path/packs/previews', + queryParameters: packData, + ); + final previews = (jsonDecode(r.data!) as List) + .cast>() + .map(CardPackPreviewDto.fromJson) + .toList(); + return previews; + } + + // previews for the main page + Future> getPacksActions( + Map? packData, + ) async { + final r = await _dio.get( + '$path/packs/actions', + queryParameters: packData, + ); + final actions = (jsonDecode(r.data!) as List) + .cast>() + .map(CardPackAction.fromJson) + .toList(); + return actions; + } + + // pack dto for available packs or null + Future getPackDto(String id) async { + final r = await _dio.get( + '$path/pack/$id', + options: Options( + sendTimeout: Duration(seconds: 5), + receiveTimeout: Duration(seconds: 5), + ), + ); + return CardPackDto.fromJson( + jsonDecode(r.data as String) as Map, + ); + } + + // buy page dto if not available + Future getPackBuy(String id) async { + final r = await _dio.get( + '$path/pack/buy/$id', + options: Options( + sendTimeout: Duration(seconds: 5), + receiveTimeout: Duration(seconds: 5), + ), + ); + return CardPackBuyDto.fromJson( + jsonDecode(r.data as String) as Map, + ); + } + + /// cards zip archive for available pack + /// unavailable packs transfer images by base64 + Future packImagesArchive(String packId) async { + final response = await _dio.get( + '$path/pack/cards/$packId', + options: Options( + responseType: ResponseType.bytes, + ), + ); + final data = response.data; + if (data == null) { + return Uint8List.fromList([]); + } + return data; + } +} diff --git a/lib/features/packs/preview_pack_holder.dart b/lib/features/packs/preview_pack_holder.dart new file mode 100644 index 0000000..635d25f --- /dev/null +++ b/lib/features/packs/preview_pack_holder.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; + +class PreviewPackHolder { + final StreamController _streamController = + StreamController.broadcast(); + PreviewPackState? _state; + + PreviewPackHolder(); + + void setPreviewPacks(List packs) { + _state = PreviewPackState(packs); + _streamController.add(_state); + } + + void clear() { + _state = null; + _streamController.add(_state); + } + + PreviewPackState? get state => _state; + + Stream get asStream => + _streamController.stream.startWith(_state); +} + +class PreviewPackState { + final List previewPacks; + + PreviewPackState(this.previewPacks); +} diff --git a/lib/features/packs/preview_packs_poller.dart b/lib/features/packs/preview_packs_poller.dart new file mode 100644 index 0000000..cb35265 --- /dev/null +++ b/lib/features/packs/preview_packs_poller.dart @@ -0,0 +1,32 @@ +import 'dart:developer'; + +import 'package:mnemo_cards/features/packs/packs_api.dart'; + +import 'preview_pack_holder.dart'; + +class PreviewPacksPoller { + PacksApi _api; + PreviewPackHolder _previewPackHolder; + bool polling = false; + + PreviewPacksPoller(this._api, this._previewPackHolder); + + Future init() async { + Stream.periodic(Duration(seconds: 60), (_) async { + poll(); + }); + } + + Future poll() async { + if (!polling) { + try { + polling = true; + final r = await _api.getPacksPreviews(null); + _previewPackHolder.setPreviewPacks(r); + } catch (e) { + log(e.toString()); + } + polling = false; + } + } +} diff --git a/lib/features/purchase/in_app_purchase.dart b/lib/features/purchase/in_app_purchase.dart new file mode 100644 index 0000000..9b44660 --- /dev/null +++ b/lib/features/purchase/in_app_purchase.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:mnemo_cards/utils/iterable_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import '../../di/injector.dart'; +import '../../di/locator.dart'; +import '../../main.dart'; + +enum StoreItemType { + ads, + subscription, + pack, +} + +class InAppPurchaseService { + final InAppPurchase _inAppPurchase = InAppPurchase.instance; + final Stream> storeSubscription = + InAppPurchase.instance.purchaseStream; + + InAppPurchase get instance => _inAppPurchase; + + Future buyPack(CardPackBuyDto dto) async { + final pack = await getPackStoreProduct(dto); + final result = await buyItemInStore(pack.first); + return result; + } + + Future buyPackWithYooMoney(CardPackBuyDto dto, BuildContext? context) async { + final paymentUrl = + await locator.repository.createPayment(dto.id, PaymentSystem.yookassa); + if (paymentUrl == null) { + return false; + } + final controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..loadRequest(Uri.parse(paymentUrl)); + return showModalBottomSheet( + context: context ?? scaffoldKey.currentContext!, + enableDrag: false, + showDragHandle: true, + useSafeArea: true, + isScrollControlled: true, + backgroundColor: Colors.white, + builder: (context) { + return Container( + child: WebViewWidget(controller: controller), + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + ); + }, + ).then((_) async { + final success = await locator.repository.checkUserPayment(); + if (success) { + locator.packUpdater.updateAvailablePacks(); + return true; + } + return false; + }); + } + + Future> getPackStoreProduct(CardPackBuyDto dto) async { + final bool isAvailable = await _inAppPurchase.isAvailable(); + if (!isAvailable) { + return []; + } + final ids = { + if (Platform.isAndroid) dto.googlePlayId, + if (Platform.isIOS) dto.appStoreId, + }.whereNotNull().toSet(); + if (ids.isEmpty) { + return []; + } + + final ProductDetailsResponse productDetailResponse = + await _inAppPurchase.queryProductDetails(ids); + + if (productDetailResponse.error != null || + productDetailResponse.productDetails.isEmpty) { + return []; + } + + return productDetailResponse.productDetails; + } + + Future buyItemInStore(ProductDetails product) async { + final PurchaseParam purchaseParam = PurchaseParam(productDetails: product); + return InAppPurchase.instance + .buyNonConsumable(purchaseParam: purchaseParam); + } + + Future completePurchase(PurchaseDetails purchaseDetails) async { + await InAppPurchase.instance.completePurchase(purchaseDetails); + } +} + +class PurchaseDetailsStreamSubscription { + final InAppPurchaseService inAppPurchaseService = getIt.get(); + + Future onPurchased(PurchaseDetails purchaseDetails) async { + try { + final result = await locator.repository.checkPayment( + purchaseDetails.productID, + purchaseDetails.verificationData.serverVerificationData, + PaymentSystem.google, + ); + if (result) { + log('Purchased'); + } else { + throw Exception('Cant verify purchase'); + } + } catch (e, s) { + log(e.toString(), stackTrace: s); + } + } + + StreamSubscription>? _streamSubscription; + + PurchaseDetailsStreamSubscription(); + + Future init() async { + _streamSubscription = inAppPurchaseService.storeSubscription.listen( + (List events) { + Future.forEach( + events, + (PurchaseDetails purchaseDetails) async { + if (purchaseDetails.pendingCompletePurchase || + purchaseDetails.status == PurchaseStatus.purchased) { + onPurchased(purchaseDetails); + } + }, + ); + }, + ); + } + + void close() { + _streamSubscription?.cancel(); + } +} diff --git a/lib/features/stories/stories_page.dart b/lib/features/stories/stories_page.dart new file mode 100644 index 0000000..b9fbbd7 --- /dev/null +++ b/lib/features/stories/stories_page.dart @@ -0,0 +1,51 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:story/story.dart'; + +class StoriesPage extends StatelessWidget { + StoriesPage(); + + @override + Widget build(BuildContext context) { + final topPadding = MediaQuery.of(context).padding.top; + return Scaffold( + body: StoryPageView( + indicatorHeight: 4.0, + indicatorPadding: EdgeInsets.symmetric( + vertical: topPadding + 8.0, + horizontal: 8.0, + ), + onPageLimitReached: () { + Navigator.pop(context); + }, + gestureItemBuilder: (context, story, page) => Stack( + children: [ + Positioned( + top: topPadding + 24.0, + left: 12.0, + child: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: Icon(Icons.clear), + ), + ), + ], + ), + itemBuilder: (c, pageIndex, storyIndex) { + return Container( + color: Colors.redAccent, + alignment: Alignment.center, + child: Text( + "Index of PageView: $pageIndex Index of story on each page: $storyIndex"), + ); + }, + storyLength: (pageIndex) { + return 1; + }, + pageLength: 2, + ), + ); + } +} diff --git a/lib/features/stories/stories_row.dart b/lib/features/stories/stories_row.dart new file mode 100644 index 0000000..630dd57 --- /dev/null +++ b/lib/features/stories/stories_row.dart @@ -0,0 +1,29 @@ +import 'package:flutter/cupertino.dart'; + +import 'stories_widget.dart'; + +class StoriesRow extends StatelessWidget { + final double height; + + StoriesRow(this.height); + + @override + Widget build(BuildContext context) { + return Container( + height: height, + alignment: Alignment.center, + child: ListView( + scrollDirection: Axis.horizontal, + children: [1, 2, 3, 4] + .map((e) => Padding( + padding: const EdgeInsets.all(4.0), + child: StoriesWidget( + width: height - 8, + height: height - 8, + ), + )) + .toList(), + ), + ); + } +} diff --git a/lib/features/stories/stories_widget.dart b/lib/features/stories/stories_widget.dart new file mode 100644 index 0000000..5e84bcb --- /dev/null +++ b/lib/features/stories/stories_widget.dart @@ -0,0 +1,41 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mnemo_cards/features/stories/stories_page.dart'; + +class StoriesWidget extends StatefulWidget { + final double width; + final double height; + + const StoriesWidget({required this.width, required this.height, super.key}); + + @override + State createState() => _StoriesWidgetState(); +} + +class _StoriesWidgetState extends State { + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + AutoRouter.of(context).pushWidget(StoriesPage()); + }, + child: SizedBox( + width: widget.width, + height: widget.height, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(32.0), + border: Border.all( + color: Colors.blue, + width: 4.0, + ), + ), + child: Center( + child: Text('Story'), + ), + ), + ), + ); + } +} diff --git a/lib/features/tests/progress_widget.dart b/lib/features/tests/progress_widget.dart new file mode 100644 index 0000000..9ef81c5 --- /dev/null +++ b/lib/features/tests/progress_widget.dart @@ -0,0 +1,233 @@ +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../../theme/themes.dart'; + +class ProgressWidget extends StatefulWidget { + final Color color; + final List? results; + final int length; + final Stream? timer; + final bool showMic; + final bool showSound; + final String? popText; + final ValueGetter? popValue; + + @override + State createState() => _ProgressWidgetState(); + + ProgressWidget( + this.results, + this.length, + this.color, { + this.timer, + this.popValue, + this.showMic = false, + this.showSound = false, + this.popText, + super.key, + }); +} + +enum Result { + correct, + wrong, + skiped, +} + +class _ProgressWidgetState extends State { + List get results => widget.results!; + + int get length => widget.length; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + TapRegion( + onTapInside: AutoRouter.of(context).maybePop, + behavior: HitTestBehavior.opaque, + child: Container( + height: 30.h, + child: Row( + children: [ + Image.asset( + 'icons/back.png', + color: Colors.black, + width: 17, + height: 23.h, + ), + if (widget.popText != null) + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + widget.popText!, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + ), + Expanded( + child: widget.results == null + ? SizedBox.shrink() + : Padding( + padding: EdgeInsets.only(left: 10.0.w), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.h), + child: Container( + height: 16.h, + color: white, + child: Stack( + alignment: Alignment.center, + children: [ + Row( + children: [ + Expanded( + child: AnimatedFractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: length == 0 + ? 0 + : (results + .where((r) => r != Result.skiped) + .length) / + length, + duration: Duration(milliseconds: 300), + child: Container( + color: widget.color, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + if (widget.showMic) + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: Image.asset( + 'icons/mic_on.png', + height: 18.h, + width: 13.w, + ), + ), + if (widget.showSound) + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: Image.asset( + 'icons/sound_on.png', + height: 18.h, + width: 24.w, + ), + ), + if (widget.timer != null) + Padding( + padding: EdgeInsets.only(left: 10.0.w), + child: _TimeWidget(widget.timer), + ), + ], + ); + } + + Widget _text() { + return Text( + results.where((r) => r != Result.skiped).length < length + ? '${results.where((r) => r != Result.skiped).length + 1} из ${length}' + : '$length', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w800, + ), + ); + } + + Widget correctAnswers() { + return Align( + alignment: Alignment.bottomLeft, + child: Container( + height: 6, + alignment: Alignment.bottomLeft, + child: Row( + children: [ + ...List.generate( + length, + (index) => Expanded( + child: AnimatedContainer( + curve: Curves.linear, + duration: Duration(milliseconds: 300), + color: index < results.length + ? results[index] == Result.correct + ? Colors.lightGreenAccent[200] + : results[index] == Result.wrong + ? Colors.redAccent[100] + : Colors.white + : Colors.transparent, + ), + ), + ), + ], + ), + ), + ); + } + + _ProgressWidgetState(); +} + +class _TimeWidget extends StatelessWidget { + final Stream? timer; + + _TimeWidget(Stream? timer) : this.timer = timer; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 68.w, + child: StreamBuilder( + stream: timer, + builder: (context, snapshot) { + var text = '-:-'; + if (snapshot.hasData) { + final minutes = snapshot.data?.inMinutes ?? 0; + final seconds = (snapshot.data?.inSeconds ?? 0) - 60 * minutes; + text = + "${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}"; + } + return Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'icons/clock.png', + width: 15.w, + height: 15.h, + ), + SizedBox( + width: 4.0.w, + ), + Text( + text, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ) + ], + ); + }), + ); + } +} diff --git a/lib/features/tests/question_states/input_buttons_test_state.dart b/lib/features/tests/question_states/input_buttons_test_state.dart new file mode 100644 index 0000000..ff05b5d --- /dev/null +++ b/lib/features/tests/question_states/input_buttons_test_state.dart @@ -0,0 +1,27 @@ +import 'dart:math'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:mnemo_cards/features/tests/question_states/test_question_state.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +part 'input_buttons_test_state.g.dart'; + +@JsonSerializable() +@CopyWith() +class InputButtonsTestState extends TestQuestionState { + final List answer; + final List answerIndexes; + final bool isCorrect; + final bool isAnswered; + final int seed; + + InputButtonsTestState({ + required this.seed, + this.answer = const [], + this.answerIndexes = const [], + this.isCorrect = false, + this.isAnswered = false, + super.testType = TestQuestionType.simple, + }) {} +} diff --git a/lib/features/tests/question_states/input_buttons_test_state.g.dart b/lib/features/tests/question_states/input_buttons_test_state.g.dart new file mode 100644 index 0000000..6311a53 --- /dev/null +++ b/lib/features/tests/question_states/input_buttons_test_state.g.dart @@ -0,0 +1,160 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'input_buttons_test_state.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$InputButtonsTestStateCWProxy { + InputButtonsTestState seed(int seed); + + InputButtonsTestState answer(List answer); + + InputButtonsTestState answerIndexes(List answerIndexes); + + InputButtonsTestState isCorrect(bool isCorrect); + + InputButtonsTestState isAnswered(bool isAnswered); + + InputButtonsTestState testType(TestQuestionType testType); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `InputButtonsTestState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// InputButtonsTestState(...).copyWith(id: 12, name: "My name") + /// ```` + InputButtonsTestState call({ + int? seed, + List? answer, + List? answerIndexes, + bool? isCorrect, + bool? isAnswered, + TestQuestionType? testType, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfInputButtonsTestState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfInputButtonsTestState.copyWith.fieldName(...)` +class _$InputButtonsTestStateCWProxyImpl + implements _$InputButtonsTestStateCWProxy { + const _$InputButtonsTestStateCWProxyImpl(this._value); + + final InputButtonsTestState _value; + + @override + InputButtonsTestState seed(int seed) => this(seed: seed); + + @override + InputButtonsTestState answer(List answer) => this(answer: answer); + + @override + InputButtonsTestState answerIndexes(List answerIndexes) => + this(answerIndexes: answerIndexes); + + @override + InputButtonsTestState isCorrect(bool isCorrect) => this(isCorrect: isCorrect); + + @override + InputButtonsTestState isAnswered(bool isAnswered) => + this(isAnswered: isAnswered); + + @override + InputButtonsTestState testType(TestQuestionType testType) => + this(testType: testType); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `InputButtonsTestState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// InputButtonsTestState(...).copyWith(id: 12, name: "My name") + /// ```` + InputButtonsTestState call({ + Object? seed = const $CopyWithPlaceholder(), + Object? answer = const $CopyWithPlaceholder(), + Object? answerIndexes = const $CopyWithPlaceholder(), + Object? isCorrect = const $CopyWithPlaceholder(), + Object? isAnswered = const $CopyWithPlaceholder(), + Object? testType = const $CopyWithPlaceholder(), + }) { + return InputButtonsTestState( + seed: seed == const $CopyWithPlaceholder() || seed == null + ? _value.seed + // ignore: cast_nullable_to_non_nullable + : seed as int, + answer: answer == const $CopyWithPlaceholder() || answer == null + ? _value.answer + // ignore: cast_nullable_to_non_nullable + : answer as List, + answerIndexes: + answerIndexes == const $CopyWithPlaceholder() || answerIndexes == null + ? _value.answerIndexes + // ignore: cast_nullable_to_non_nullable + : answerIndexes as List, + isCorrect: isCorrect == const $CopyWithPlaceholder() || isCorrect == null + ? _value.isCorrect + // ignore: cast_nullable_to_non_nullable + : isCorrect as bool, + isAnswered: + isAnswered == const $CopyWithPlaceholder() || isAnswered == null + ? _value.isAnswered + // ignore: cast_nullable_to_non_nullable + : isAnswered as bool, + testType: testType == const $CopyWithPlaceholder() || testType == null + ? _value.testType + // ignore: cast_nullable_to_non_nullable + : testType as TestQuestionType, + ); + } +} + +extension $InputButtonsTestStateCopyWith on InputButtonsTestState { + /// Returns a callable class that can be used as follows: `instanceOfInputButtonsTestState.copyWith(...)` or like so:`instanceOfInputButtonsTestState.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$InputButtonsTestStateCWProxy get copyWith => + _$InputButtonsTestStateCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +InputButtonsTestState _$InputButtonsTestStateFromJson( + Map json) => + InputButtonsTestState( + seed: (json['seed'] as num).toInt(), + answer: (json['answer'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + answerIndexes: (json['answerIndexes'] as List?) + ?.map((e) => (e as num).toInt()) + .toList() ?? + const [], + isCorrect: json['isCorrect'] as bool? ?? false, + isAnswered: json['isAnswered'] as bool? ?? false, + testType: + $enumDecodeNullable(_$TestQuestionTypeEnumMap, json['testType']) ?? + TestQuestionType.simple, + ); + +Map _$InputButtonsTestStateToJson( + InputButtonsTestState instance) => + { + 'testType': _$TestQuestionTypeEnumMap[instance.testType]!, + 'answer': instance.answer, + 'answerIndexes': instance.answerIndexes, + 'isCorrect': instance.isCorrect, + 'isAnswered': instance.isAnswered, + 'seed': instance.seed, + }; + +const _$TestQuestionTypeEnumMap = { + TestQuestionType.simple: 'simple', + TestQuestionType.input_buttons: 'input_buttons', + TestQuestionType.matrix: 'matrix', + TestQuestionType.match: 'match', + TestQuestionType.undefined: 'undefined', +}; diff --git a/lib/features/tests/question_states/simple_test_state.dart b/lib/features/tests/question_states/simple_test_state.dart new file mode 100644 index 0000000..e28871d --- /dev/null +++ b/lib/features/tests/question_states/simple_test_state.dart @@ -0,0 +1,25 @@ +import 'dart:math'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:mnemo_cards/features/tests/question_states/test_question_state.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +part 'simple_test_state.g.dart'; + +@JsonSerializable() +@CopyWith() +class SimpleTestState extends TestQuestionState { + final String answer; + final bool isCorrect; + final bool isAnswered; + final int seed; + + SimpleTestState({ + required this.seed, + this.answer = '', + this.isCorrect = false, + this.isAnswered = false, + super.testType = TestQuestionType.simple, + }); +} diff --git a/lib/features/tests/question_states/simple_test_state.g.dart b/lib/features/tests/question_states/simple_test_state.g.dart new file mode 100644 index 0000000..8e79c04 --- /dev/null +++ b/lib/features/tests/question_states/simple_test_state.g.dart @@ -0,0 +1,134 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'simple_test_state.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$SimpleTestStateCWProxy { + SimpleTestState seed(int seed); + + SimpleTestState answer(String answer); + + SimpleTestState isCorrect(bool isCorrect); + + SimpleTestState isAnswered(bool isAnswered); + + SimpleTestState testType(TestQuestionType testType); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `SimpleTestState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// SimpleTestState(...).copyWith(id: 12, name: "My name") + /// ```` + SimpleTestState call({ + int? seed, + String? answer, + bool? isCorrect, + bool? isAnswered, + TestQuestionType? testType, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfSimpleTestState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfSimpleTestState.copyWith.fieldName(...)` +class _$SimpleTestStateCWProxyImpl implements _$SimpleTestStateCWProxy { + const _$SimpleTestStateCWProxyImpl(this._value); + + final SimpleTestState _value; + + @override + SimpleTestState seed(int seed) => this(seed: seed); + + @override + SimpleTestState answer(String answer) => this(answer: answer); + + @override + SimpleTestState isCorrect(bool isCorrect) => this(isCorrect: isCorrect); + + @override + SimpleTestState isAnswered(bool isAnswered) => this(isAnswered: isAnswered); + + @override + SimpleTestState testType(TestQuestionType testType) => + this(testType: testType); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `SimpleTestState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// SimpleTestState(...).copyWith(id: 12, name: "My name") + /// ```` + SimpleTestState call({ + Object? seed = const $CopyWithPlaceholder(), + Object? answer = const $CopyWithPlaceholder(), + Object? isCorrect = const $CopyWithPlaceholder(), + Object? isAnswered = const $CopyWithPlaceholder(), + Object? testType = const $CopyWithPlaceholder(), + }) { + return SimpleTestState( + seed: seed == const $CopyWithPlaceholder() || seed == null + ? _value.seed + // ignore: cast_nullable_to_non_nullable + : seed as int, + answer: answer == const $CopyWithPlaceholder() || answer == null + ? _value.answer + // ignore: cast_nullable_to_non_nullable + : answer as String, + isCorrect: isCorrect == const $CopyWithPlaceholder() || isCorrect == null + ? _value.isCorrect + // ignore: cast_nullable_to_non_nullable + : isCorrect as bool, + isAnswered: + isAnswered == const $CopyWithPlaceholder() || isAnswered == null + ? _value.isAnswered + // ignore: cast_nullable_to_non_nullable + : isAnswered as bool, + testType: testType == const $CopyWithPlaceholder() || testType == null + ? _value.testType + // ignore: cast_nullable_to_non_nullable + : testType as TestQuestionType, + ); + } +} + +extension $SimpleTestStateCopyWith on SimpleTestState { + /// Returns a callable class that can be used as follows: `instanceOfSimpleTestState.copyWith(...)` or like so:`instanceOfSimpleTestState.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$SimpleTestStateCWProxy get copyWith => _$SimpleTestStateCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SimpleTestState _$SimpleTestStateFromJson(Map json) => + SimpleTestState( + seed: (json['seed'] as num).toInt(), + answer: json['answer'] as String? ?? '', + isCorrect: json['isCorrect'] as bool? ?? false, + isAnswered: json['isAnswered'] as bool? ?? false, + testType: + $enumDecodeNullable(_$TestQuestionTypeEnumMap, json['testType']) ?? + TestQuestionType.simple, + ); + +Map _$SimpleTestStateToJson(SimpleTestState instance) => + { + 'testType': _$TestQuestionTypeEnumMap[instance.testType]!, + 'answer': instance.answer, + 'isCorrect': instance.isCorrect, + 'isAnswered': instance.isAnswered, + 'seed': instance.seed, + }; + +const _$TestQuestionTypeEnumMap = { + TestQuestionType.simple: 'simple', + TestQuestionType.input_buttons: 'input_buttons', + TestQuestionType.matrix: 'matrix', + TestQuestionType.match: 'match', + TestQuestionType.undefined: 'undefined', +}; diff --git a/lib/features/tests/question_states/test_question_state.dart b/lib/features/tests/question_states/test_question_state.dart new file mode 100644 index 0000000..5ac7a5d --- /dev/null +++ b/lib/features/tests/question_states/test_question_state.dart @@ -0,0 +1,17 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +part 'test_question_state.g.dart'; + +@CopyWith() +class TestQuestionState { + final TestQuestionType testType; + + bool get isCorrect => false; + + bool get isAnswered => false; + + const TestQuestionState({ + required this.testType, + }); +} diff --git a/lib/features/tests/question_states/test_question_state.g.dart b/lib/features/tests/question_states/test_question_state.g.dart new file mode 100644 index 0000000..d2b0159 --- /dev/null +++ b/lib/features/tests/question_states/test_question_state.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_question_state.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$TestQuestionStateCWProxy { + TestQuestionState testType(TestQuestionType testType); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TestQuestionState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// TestQuestionState(...).copyWith(id: 12, name: "My name") + /// ```` + TestQuestionState call({ + TestQuestionType? testType, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTestQuestionState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTestQuestionState.copyWith.fieldName(...)` +class _$TestQuestionStateCWProxyImpl implements _$TestQuestionStateCWProxy { + const _$TestQuestionStateCWProxyImpl(this._value); + + final TestQuestionState _value; + + @override + TestQuestionState testType(TestQuestionType testType) => + this(testType: testType); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TestQuestionState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// TestQuestionState(...).copyWith(id: 12, name: "My name") + /// ```` + TestQuestionState call({ + Object? testType = const $CopyWithPlaceholder(), + }) { + return TestQuestionState( + testType: testType == const $CopyWithPlaceholder() || testType == null + ? _value.testType + // ignore: cast_nullable_to_non_nullable + : testType as TestQuestionType, + ); + } +} + +extension $TestQuestionStateCopyWith on TestQuestionState { + /// Returns a callable class that can be used as follows: `instanceOfTestQuestionState.copyWith(...)` or like so:`instanceOfTestQuestionState.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$TestQuestionStateCWProxy get copyWith => + _$TestQuestionStateCWProxyImpl(this); +} diff --git a/lib/features/tests/test_manager.dart b/lib/features/tests/test_manager.dart new file mode 100644 index 0000000..0229193 --- /dev/null +++ b/lib/features/tests/test_manager.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter/widgets.dart'; +import 'package:mnemo_cards/features/tests/test_state_holder.dart'; +import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart'; +import 'package:mnemo_cards/features/packs/pack_cache_manager.dart'; +import 'package:mnemo_cards/theme/themes.dart'; +import 'package:mnemo_cards/utils/iterable_helper.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../../managers/repository/http_repository.dart'; +import 'question_states/test_question_state.dart'; + +class TestManager extends ChangeNotifier { + final PackCacheManager packCacheManager; + final HttpRepository _repository; + final List questions = []; + Color color = borderGray; + final PageController pageController = PageController(); + final TestHolder testHolder = TestHolder(); + + Future> loadTests({String? packId}) async { + final tests = (await _repository.getTests(packId: packId ?? '')) + .map((e) => TestDto.fromJson(e)) + .toList(); + return tests; + } + + TestQuestionState? getState(int id) => testHolder.testQuestionState(id); + + Iterable get states => testHolder.state.questions.values; + + Future setActiveTest(String id, {String? packId}) async { + clearStates(); + if (packId != null) { + await packCacheManager.loadPackFromCache(packId); + } + final test = await _repository.getTest(id); + questions.clear(); + questions.addAll(test!.questions); + color = test.color?.asColor ?? borderGray; + testHolder.setQuestions(questions); + } + + void setState(int id, TestQuestionState state) { + testHolder.setQuestionState(id, state); + log('Set state $id $state'); + notifyListeners(); + } + + TestManager( + this.packCacheManager, + this._repository, + ); + + void clearStates() { + testHolder.clear(); + notifyListeners(); + } + + AbstractTestQuestion getTest(int index) => questions[index]; + + void nextTest() { + pageController.animateToPage( + pageController.page!.toInt() + 1, + duration: Duration(milliseconds: 300), + curve: Curves.ease, + ); + } + + void prevTest() { + pageController.animateToPage( + pageController.page!.toInt() - 1, + duration: Duration(milliseconds: 300), + curve: Curves.ease, + ); + } + + Future preloadImages(BuildContext context) async { + for (final question in questions) { + switch (question.questionType) { + case TestQuestionType.simple: + final images = [ + (question as SimpleTestQuestionBody).image, + ...question.buttons.map((e) => e.image) + ].whereNotNull(); + for (final image in images) { + try { + final testImage = TestImage.image(image); + await precacheImage( + (await testImage.loadImage())!, + context, + ); + } catch (e) { + rethrow; + } + } + case TestQuestionType.input_buttons: + final images = [ + (question as InputButtonsTestQuestionBody).image, + ].whereNotNull(); + for (final image in images) { + try { + final testImage = TestImage.image(image); + await precacheImage( + (await testImage.loadImage())!, + context, + ); + } catch (e) { + rethrow; + } + } + case TestQuestionType.matrix: + // TODO: Handle this case. + case TestQuestionType.match: + // TODO: Handle this case. + case TestQuestionType.undefined: + // TODO: Handle this case. + } + } + } + + Future loadImage(String packId, String path) async { + final bytes = await packCacheManager.loadPackFile(packId, path); + return MemoryImage(bytes!); + } +} diff --git a/lib/features/tests/test_page.dart b/lib/features/tests/test_page.dart new file mode 100644 index 0000000..b9b888d --- /dev/null +++ b/lib/features/tests/test_page.dart @@ -0,0 +1,204 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:pie_chart/pie_chart.dart'; + +import '../../di/locator.dart'; +import 'test_progress_widget.dart'; +import 'test_widgets/input_buttons_test.dart'; +import 'test_widgets/simple_test.dart'; + +@RoutePage() +class TestPage extends StatefulWidget { + final String testId; + + @override + State createState() => _TestPageState(); + + TestPage(this.testId); +} + +class _TestPageState extends State { + @override + void initState() { + super.initState(); + locator.testManager.setActiveTest(widget.testId); + locator.testManager.preloadImages(context); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: StreamBuilder( + stream: locator.testManager.testHolder.asStream, + builder: (context, s) { + if (!s.hasData || s.requireData!.questions.isEmpty) { + return Scaffold( + body: Center( + child: Text('Loading'), + ), + ); + } + + return Scaffold( + backgroundColor: locator.testManager.color.withOpacity(0.25), + body: SafeArea( + child: Builder(builder: (context) { + return Column( + children: [ + SizedBox( + height: + (64.h - 20.h - MediaQuery.of(context).padding.top) / + 2, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: const TestProgressWidget(), + ), + Expanded( + child: PageView.builder( + physics: NeverScrollableScrollPhysics(), //ClampingScrollPhysics(), + controller: locator.testManager.pageController, + itemBuilder: (context, index) { + if (index > 0) { + final state = locator.testManager.getState( + locator.testManager.questions[index - 1].id!, + ); + if (state?.isCorrect != true) { + return null; + } + } + if (index == locator.testManager.questions.length) { + return Column( + children: [ + Expanded( + child: PieChart( + dataMap: { + 'Правильно': locator.testManager.states + .where((e) => e.isCorrect) + .length + .toDouble(), + 'Ошибка': locator.testManager.states + .where((e) => + !e.isCorrect && e.isAnswered) + .length + .toDouble(), + 'Пропущено': locator.testManager.states + .where((e) => !e.isAnswered) + .length + .toDouble(), + }, + animationDuration: + Duration(milliseconds: 800), + chartLegendSpacing: 32, + chartRadius: + MediaQuery.of(context).size.width / + 3.2, + colorList: [ + Colors.lightGreenAccent[200]!, + Colors.redAccent[100]!, + Colors.white, + ], + initialAngleInDegree: 0, + chartType: ChartType.ring, + ringStrokeWidth: 32, + legendOptions: LegendOptions( + showLegendsInRow: false, + legendPosition: LegendPosition.right, + showLegends: true, + legendShape: BoxShape.circle, + legendTextStyle: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + chartValuesOptions: ChartValuesOptions( + showChartValueBackground: false, + showChartValues: true, + showChartValuesOutside: false, + decimalPlaces: 0, + chartValueStyle: Theme.of(context) + .textTheme + .titleMedium!, + ), + ), + ), + Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () { + AutoRouter.of(context).maybePop(); + }, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(8.0), + color: locator.testManager.color, + height: 60, + child: FittedBox( + child: Text( + 'В меню', + style: Theme.of(context) + .textTheme + .titleMedium, + )), + ), + ), + ), + ), + ], + ); + } + + return switch (locator.testManager + .getTest(index) + .questionType) { + TestQuestionType.simple => SimpleTestWidget( + locator.testManager.getTest(index) + as SimpleTestQuestionBody, + ), + TestQuestionType.input_buttons => + InputButtonsTestWidget( + locator.testManager.getTest(index) + as InputButtonsTestQuestionBody, + ), + TestQuestionType.matrix => Container(), + TestQuestionType.match => Container(), + // TODO: Handle this case. + TestQuestionType.undefined => + throw UnimplementedError(), + }; + }, + ), + ), + ], + ); + }), + ), + ); + }), + ); + } +} + +class _LifeWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + Icon(Icons.ac_unit_outlined), + SizedBox( + width: 2, + ), + Text( + '30', + style: TextStyle(fontSize: 22), + ) + ], + ); + } +} diff --git a/lib/features/tests/test_progress_widget.dart b/lib/features/tests/test_progress_widget.dart new file mode 100644 index 0000000..8b9640b --- /dev/null +++ b/lib/features/tests/test_progress_widget.dart @@ -0,0 +1,71 @@ +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/features/tests/progress_widget.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../../di/locator.dart'; +import '../../theme/themes.dart'; + +class TestProgressWidget extends StatefulWidget { + @override + State createState() => _TestProgressWidgetState(); + + const TestProgressWidget({super.key}); +} + +class _TestProgressWidgetState extends State { + List results = []; + int length = 0; + + Stream? timer; + + void _updateData() { + results = [ + Result.correct, + ]; + length = locator.testManager.questions.length + 1; + for (final state in locator.testManager.states) { + if (state.isAnswered) { + if (state.isCorrect) { + results.add(Result.correct); + } else { + // results.add(Result.wrong); + results.add(Result.skiped); + } + } else { + results.add(Result.skiped); + } + } + if (mounted) { + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + locator.testManager.removeListener(_updateData); + locator.testManager.addListener(_updateData); + timer = Stream.periodic( + Duration(seconds: 1), + (tick) => Duration(seconds: 300 - tick - 1), + ).startWith(Duration(seconds: 300)).take(300); + _updateData(); + } + + @override + Widget build(BuildContext context) { + return ProgressWidget( + results, + length, + locator.testManager.color, + timer: timer, + key: ValueKey('TEST PROGRESS'), + ); + } +} diff --git a/lib/features/tests/test_state_holder.dart b/lib/features/tests/test_state_holder.dart new file mode 100644 index 0000000..d575ee0 --- /dev/null +++ b/lib/features/tests/test_state_holder.dart @@ -0,0 +1,66 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'question_states/input_buttons_test_state.dart'; +import 'question_states/simple_test_state.dart'; +import 'question_states/test_question_state.dart'; + +class TestHolder { + final StreamController<_TestState?> _streamController = + StreamController.broadcast(); + _TestState __state; + + TestHolder() : __state = _TestState({}); + + void setQuestionState(int id, TestQuestionState state) => _state = _TestState( + Map.from(_state.questions..[id] = state), + ); + + void clear() { + _state = _TestState({}); + _streamController.add(_state); + } + + void setQuestions(List questions) { + final stateQuestions = {}; + final seed = Random().nextInt(999); + for (final question in questions) { + switch (question.questionType) { + case TestQuestionType.simple: + stateQuestions[question.id!] = SimpleTestState(seed: seed); + case TestQuestionType.input_buttons: + stateQuestions[question.id!] = InputButtonsTestState(seed: seed); + case TestQuestionType.matrix: + // TODO: Handle this case. + case TestQuestionType.match: + // TODO: Handle this case. + case TestQuestionType.undefined: + // TODO: Handle this case. + } + } + _state = _TestState(stateQuestions); + } + + TestQuestionState? testQuestionState(int id) => _state.questions[id]; + + _TestState get state => _state; + + _TestState get _state => __state; + + void set _state(_TestState state) { + __state = state; + _streamController.add(__state); + } + + Stream<_TestState?> get asStream => + _streamController.stream.startWith(_state); +} + +class _TestState { + final Map questions; + + _TestState(this.questions); +} diff --git a/lib/features/tests/test_widgets/input_buttons_test.dart b/lib/features/tests/test_widgets/input_buttons_test.dart new file mode 100644 index 0000000..d27f84a --- /dev/null +++ b/lib/features/tests/test_widgets/input_buttons_test.dart @@ -0,0 +1,214 @@ +import 'dart:math'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mnemo_cards/features/tests/test_manager.dart'; +import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../../../di/locator.dart'; +import '../../../widgets/game_card_widget.dart'; +import '../question_states/input_buttons_test_state.dart'; +import 'test_button.dart'; + +class InputButtonsTestWidget extends StatelessWidget { + final InputButtonsTestQuestionBody model; + + bool get isCorrect => state.answer.join() == model.answer; + + InputButtonsTestState get state => + manager.getState(model.id!) as InputButtonsTestState; + + TestManager get manager => locator.testManager; + + InputButtonsTestWidget(this.model, {super.key}); + + Widget _letter(BuildContext context, String text) => Padding( + padding: const EdgeInsets.all(4.0), + child: Material( + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4.0, + ), + ], + ), + child: Text( + text, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ); + + Widget _emptyCell(BuildContext context) => Padding( + padding: const EdgeInsets.all(4.0), + child: Material( + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4.0, + ), + ], + ), + child: Text( + ' ', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + final random = Random(state.seed); + final buttons = model.buttons..shuffle(random); + return LayoutBuilder(builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 2, + child: AspectRatio( + aspectRatio: 1, + child: Container( + margin: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(GameCardWidget.borderRadius), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (model.image != null) + TestImageWidget( + TestImage.image(model.image!), + ), + if (model.text != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Center( + child: Text( + model.text!, + style: theme.headlineLarge, + ), + ), + ), + ], + ), + ), + ), + ), + StatefulBuilder(builder: (context, setState) { + void buttonTapped((int, TestButtonDto) p) { + if (state.answer.fold(0, (p, n) => p + n.length) >= + model.answer.length) { + return; + } + setState(() { + manager.setState( + model.id!, + state.copyWith( + answer: [...state.answer, p.$2.text!], + ), + ); + manager.setState( + model.id!, + state.copyWith( + isAnswered: isCorrect, + isCorrect: isCorrect, + ), + ); + }); + print('${state.answer} ${model.answer} ${state.isCorrect}'); + if (isCorrect) { + Future.delayed( + Duration(milliseconds: 300), + manager.nextTest, + ); + } + } + + return Expanded( + child: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + manager.setState( + model.id!, + state.copyWith( + isAnswered: false, + isCorrect: false, + answer: [], + answerIndexes: [], + ), + ); + }); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ...state.answer.map((e) => _letter(context, e)), + ...model.answer + .substring( + state.answer.fold(0, (p, n) => p + n.length), + ) + .characters + .map((e) => _emptyCell(context)), + ], + ), + ), + ), + SizedBox( + height: 16.0, + ), + AbsorbPointer( + absorbing: state.isCorrect, + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...buttons.indexed.map( + (p) => Container( + width: p.$2.text!.length > 3 ? null : 80, + height: 60, + child: TestButton( + text: p.$2.text, + onTap: () => buttonTapped(p), + color: state.answer == p.$2 + ? isCorrect + ? Colors.green[200] + : Colors.red[200] + : Colors.white, + ), + ), + ), + ], + ), + ), + ], + ), + ); + }) + ], + ), + ); + }); + } +} diff --git a/lib/features/tests/test_widgets/simple_test.dart b/lib/features/tests/test_widgets/simple_test.dart new file mode 100644 index 0000000..dc5476c --- /dev/null +++ b/lib/features/tests/test_widgets/simple_test.dart @@ -0,0 +1,154 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/features/tests/test_manager.dart'; +import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../../../di/locator.dart'; +import '../../../widgets/game_card_widget.dart'; +import '../question_states/simple_test_state.dart'; +import 'test_button.dart'; + +class SimpleTestWidget extends StatelessWidget { + final SimpleTestQuestionBody _simpleTestModel; + + SimpleTestQuestionBody get model => _simpleTestModel; + + bool get isCorrect => state.answer.startsWith(model.answer); + + SimpleTestState get state => manager.getState(model.id!) as SimpleTestState; + + TestManager get manager => locator.testManager; + + Color get testColor => manager.color; + + SimpleTestWidget(this._simpleTestModel, {super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + final random = Random(state.seed); + final buttons = [...model.buttons]..shuffle(random); + return LayoutBuilder(builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (model.image != null) + Flexible( + flex: 4, + child: Container( + margin: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(GameCardWidget.borderRadius), + ), + child: TestImageWidget( + TestImage.image(model.image!), + ), + ), + ), + if (model.text != null) + Flexible( + flex: 1, + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Center( + child: Text( + model.text!, + style: theme.headlineLarge, + ), + ), + ), + ), + SizedBox( + height: 16.0, + ), + StatefulBuilder(builder: (context, setState) { + void setAnswer(String answer) { + setState(() { + manager.setState( + model.id!, + state.copyWith( + answer: answer, + ), + ); + manager.setState( + model.id!, + state.copyWith( + isAnswered: true, + isCorrect: isCorrect, + ), + ); + }); + if (isCorrect) { + Future.delayed( + Duration(milliseconds: 300), + manager.nextTest, + ); + } + } + + bool buttonLine = (model.image != null && + buttons.where((element) => element.image != null).length != + 2); + final imageSide = buttonLine + ? constraints.maxWidth / 4 - 8 + : constraints.maxWidth / 2 - 16; + final spacing = buttonLine ? 4.0 : 8.0; + + return AbsorbPointer( + absorbing: state.isCorrect, + child: Wrap( + spacing: spacing, + runSpacing: 8.0, + children: buttons + .map( + (e) => e.isTextButton + ? Container( + width: constraints.maxWidth / 2 - 16, + height: 90.h, + child: TestButton( + text: e.text, + onTap: () { + setAnswer(e.id); + }, + // borderColor: testColor, + color: state.answer == e.id + ? isCorrect + ? Colors.green[200] + : Colors.red[200] + : Colors.white, + ), + ) + : SizedBox( + width: imageSide, + height: imageSide, + child: TestButton( + image: TestImage.image(e.image!), + onTap: () { + setAnswer(e.id); + }, + // borderColor: testColor, + color: e.id == state.answer + ? isCorrect + ? Colors.green[200] + : Colors.red[200] + : Colors.white, + ), + ), + ) + .toList(), + ), + ); + }) + ], + ), + ); + }); + } +} diff --git a/lib/features/tests/test_widgets/test_button.dart b/lib/features/tests/test_widgets/test_button.dart new file mode 100644 index 0000000..319685f --- /dev/null +++ b/lib/features/tests/test_widgets/test_button.dart @@ -0,0 +1,65 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mnemo_cards/theme/themes.dart'; + +import 'test_image.dart'; + +class TestButton extends StatelessWidget { + final String? text; + final TestImage? image; + final VoidCallback onTap; + final Color? color; + final Color? borderColor; + + TestButton({ + this.text, + this.image, + this.color, + this.borderColor, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + return Material( + borderRadius: BorderRadius.circular(12.0), + child: Container( + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: color, + // border: Border.all(color: borderColor ?? borderGray), + borderRadius: BorderRadius.circular(12.0), + boxShadow: const [ + // BoxShadow( + // color: Colors.black26, + // blurRadius: 4.0, + // ), + ], + ), + child: InkWell( + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (image != null) + TestImageWidget( + image!, + key: ValueKey(image), + ), + if (image != null && text != null) + SizedBox( + height: 4.0, + ), + if (text != null) + Text( + text!, + style: theme.titleLarge, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/tests/test_widgets/test_image.dart b/lib/features/tests/test_widgets/test_image.dart new file mode 100644 index 0000000..2efad3c --- /dev/null +++ b/lib/features/tests/test_widgets/test_image.dart @@ -0,0 +1,138 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:math' as m; +import 'dart:typed_data'; + +import 'package:flutter/cupertino.dart'; +import 'package:mnemo_cards/features/packs/images_holder.dart'; +import 'package:mnemo_cards/theme/themes.dart'; + +import '../../../di/locator.dart'; + +class TestImageWidget extends StatefulWidget { + final TestImage _testImage; + + const TestImageWidget(this._testImage, {super.key}); + + @override + State createState() => _TestImageWidgetState(); +} + +class _TestImageWidgetState extends State { + TestImage get _testImage => widget._testImage; + bool same = false; + Widget? cached; + + @override + void didUpdateWidget(TestImageWidget oldWidget) { + super.didUpdateWidget(oldWidget); + same = oldWidget._testImage == _testImage; + } + + Future _preloadImage() async { + try { + // final cache = _TestImageCache(); + // final cachedImage = cache.image(_testImage); + // if (cachedImage != null) { + // return cachedImage; + // } + final imageProvider = await _testImage.loadImage(); + if (imageProvider == null) { + return SizedBox.shrink(); + } + // cache.setImage(_testImage, imageProvider); + return Image(image: imageProvider); + } catch (e) { + log('Cant load image ${_testImage}packId: $e'); + } + return SizedBox(); + } + + @override + Widget build(BuildContext context) { + // final cache = _TestImageCache(); + if (same && cached != null) { + return cached!; + } + return FutureBuilder( + future: _preloadImage(), + // initialData: cache.image(_testImage), + builder: (_, s) { + if (s.data == null) { + return Container( + alignment: Alignment.center, + ); + } + cached = Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: white, + borderRadius: BorderRadius.circular(12), + ), + child: s.data, + ); + return cached!; + }, + ); + } +} + +class TestImage { + final String data; + final String key; + + TestImage({ + required this.data, + required this.key, + }); + + @override + bool operator ==(Object other) { + return other is TestImage && key == other.key; + } + + Future loadImage() async { + if (data.length > 50) { + Uint8List? bytes; + try { + bytes = base64Decode(data); + return MemoryImage(bytes); + } catch (e) { + log('Cant decode base64 test image'); + return null; + } + } else if (data.contains(':')) { + final packId = data.split(':').first; + final path = data.split(':').last; + return await locator.testManager.loadImage(packId, path); + } else { + return data.memoryImage; + } + } + + TestImage.image(String image) + : data = image, + key = image.substring(0, m.min(image.length, 50)); +} + +// class _TestImageCache { +// static final _instance = _TestImageCache._(); +// +// _TestImageCache get instance => _instance; +// +// factory _TestImageCache() => _instance; +// +// _TestImageCache._(); +// +// Map _images = {}; +// +// Image? image(TestImage key) { +// return _images[key]; +// } +// +// Image? setImage(TestImage key, ImageProvider provider) { +// return _images[key] = Image(image: provider); +// } +// +// void clear() => _images.clear(); +// } diff --git a/lib/features/yandex_ads/yandex_ads.dart b/lib/features/yandex_ads/yandex_ads.dart new file mode 100644 index 0000000..eddba16 --- /dev/null +++ b/lib/features/yandex_ads/yandex_ads.dart @@ -0,0 +1,42 @@ +import 'dart:developer'; + +import 'package:flutter/widgets.dart'; +import 'package:yandex_mobileads/mobile_ads.dart'; + +class YandexAds { + static BannerAd createBanner( + BoxConstraints constraints, { + String id = 'demo-banner-yandex', + String? key, + VoidCallback? onLoaded, + }) { + return BannerAd( + adUnitId: id, + // or 'demo-banner-yandex' + adSize: BannerAdSize.inline( + width: constraints.maxWidth.round(), + maxHeight: constraints.maxHeight.round(), + ), + adRequest: const AdRequest(), + onAdLoaded: () { + log('Loaded $key'); + onLoaded?.call(); + }, + onAdFailedToLoad: (error) { + // Ad failed to load with AdRequestError. + // Attempting to load a new ad from the onAdFailedToLoad() method is strongly discouraged. + }, + onAdClicked: () { + // Called when a click is recorded for an ad. + }, + onLeftApplication: () { + // Called when user is about to leave application (e.g., to go to the browser), as a result of clicking on the ad. + }, + onReturnedToApplication: () { + // Called when user returned to application after click. + }, + onImpression: (impressionData) { + // Called when an impression is recorded for an ad. + }); + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..f0ff4d2 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,86 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +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: + return macos; + 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: 'AIzaSyCq0V8JebjtqSgGSueouoLiprBROgPRBQw', + appId: '1:701767851968:web:714eb4fb5e3fd1ab2f7225', + messagingSenderId: '701767851968', + projectId: 'mnemo-cards', + authDomain: 'mnemo-cards.firebaseapp.com', + storageBucket: 'mnemo-cards.appspot.com', + measurementId: 'G-C37MMWS6WC', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyBGn7PVVDX-o7WipivtuBjdoH5nYEPsHms', + appId: '1:701767851968:android:6190df55346394732f7225', + messagingSenderId: '701767851968', + projectId: 'mnemo-cards', + storageBucket: 'mnemo-cards.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAodY8s0ntALNeeGiUiTx6eg4g2Ar6C1to', + appId: '1:701767851968:ios:5c9040634eac8c152f7225', + messagingSenderId: '701767851968', + projectId: 'mnemo-cards', + storageBucket: 'mnemo-cards.appspot.com', + androidClientId: '701767851968-3jgootslus3ie76t682j4v7glletloud.apps.googleusercontent.com', + iosClientId: '701767851968-8dqcmk706p08gujqbl2m9s4sq1aljibs.apps.googleusercontent.com', + iosBundleId: 'com.cinnabarflower.mnemoCards', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyAodY8s0ntALNeeGiUiTx6eg4g2Ar6C1to', + appId: '1:701767851968:ios:5c9040634eac8c152f7225', + messagingSenderId: '701767851968', + projectId: 'mnemo-cards', + storageBucket: 'mnemo-cards.appspot.com', + androidClientId: '701767851968-dvbvmte4m90dkqitp013s4nd5c0df1n8.apps.googleusercontent.com', + iosClientId: '701767851968-8dqcmk706p08gujqbl2m9s4sq1aljibs.apps.googleusercontent.com', + iosBundleId: 'com.cinnabarflower.mnemoCards', + ); + +} \ No newline at end of file diff --git a/lib/flags.dart b/lib/flags.dart new file mode 100644 index 0000000..45e5568 --- /dev/null +++ b/lib/flags.dart @@ -0,0 +1,3 @@ +import 'package:flutter/foundation.dart'; + +const ADMIN_BUILD = true; \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..33bb2c0 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,213 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:auto_route/annotations.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:jailbreak_root_detection/jailbreak_root_detection.dart'; +import 'package:mnemo_cards/di/locator.dart'; +import 'package:mnemo_cards/domain/router/app_router.dart'; +import 'package:mnemo_cards/managers/repository/firebase_config_repository.dart'; +import 'package:mnemo_cards/features/packs/pack_manager.dart'; +import 'package:mnemo_cards/theme/themes.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:no_screenshot/no_screenshot.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:yandex_mobileads/mobile_ads.dart'; + +import 'di/injector.dart'; +import 'domain/router/app_router.gr.dart'; +import 'firebase_options.dart'; +import 'flags.dart'; +import 'managers/repository/repository.dart'; + +final scaffoldKey = GlobalKey(); +final scaffoldMessengerKey = GlobalKey(); +SharedPreferences? globalSharedPreferences; + +final appRouter = AppRouter(ADMIN_BUILD); + +AndroidDeviceInfo? androidDeviceInfo; + +Future _screenOff() async => + ADMIN_BUILD || await NoScreenshot.instance.screenshotOff(); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + androidDeviceInfo = await DeviceInfoPlugin().androidInfo; + final isNotTrust = await JailbreakRootDetection.instance.isNotTrust; + if (isNotTrust && !ADMIN_BUILD) { + print('Not trusted'); + return; + } + final off = await _screenOff(); + if (!off) { + print('Cant disable screenhsot'); + return; + } + try { + globalSharedPreferences = await SharedPreferences.getInstance(); + await setInjections(); + await initFirebase(); + await locator.packCacheManager.init(); + await locator.packManager.init(); + await locator.userManager.init(); + await locator.favoriteCardsController.init(); + await locator.previewPackPoller.init(); + await locator.packUpdater.init(); + await MobileAds.initialize(); + runApp(const MyApp()); + } on Object catch (e, s) { + log(e.toString(), stackTrace: s); + exit(0); + } +} + +Future initFirebase() async { + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ).timeout(Duration(seconds: 2)); + await FirebaseRepository.update(); + } on Object catch (e, s) { + log('Firebase not inited', error: e, stackTrace: s); + } +} + +class MyApp extends StatelessWidget with WidgetsBindingObserver { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addObserver(this); + return ScreenUtilInit( + designSize: const Size(370, 800), + minTextAdapt: true, + splitScreenMode: true, + child: MaterialApp.router( + theme: lightTheme, + routerConfig: appRouter.config(), + scaffoldMessengerKey: scaffoldMessengerKey, + ), + ); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + _screenOff(); + break; + case AppLifecycleState.inactive: + _screenOff(); + break; + case AppLifecycleState.paused: + _screenOff(); + break; + case AppLifecycleState.detached: + break; + case AppLifecycleState.hidden: + break; + } + } +} + +@RoutePage() +class MainTabsPage extends StatefulWidget { + const MainTabsPage({super.key}); + + @override + State createState() => _MainTabsPageState(); +} + +class _MainTabsPageState extends State { + late List cardPacks; + late List cards; + CardPackDto? pack; + + late Repository repository; + late PackManager packManager; + + _Tabs _currentTab = _Tabs.home; + + @override + void initState() { + super.initState(); + repository = locator.repository; + packManager = locator.packManager; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + return AutoTabsRouter.tabBar( + key: scaffoldKey, + homeIndex: 0, + routes: [ + PageRouteInfo(HomePage.name), + // PageRouteInfo(ExplorePage.name), + // PageRouteInfo(ProfilePage.name), + ], + builder: (_, child, tabsController) => Scaffold( + body: child, + // bottomNavigationBar: Container( + // child: DotNavigationBar( + // currentIndex: tabsController.index, + // dotIndicatorColor: Colors.white, + // unselectedItemColor: Colors.grey[300], + // marginR: EdgeInsets.symmetric(horizontal: 32), + // itemPadding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), + // duration: Duration(milliseconds: 300), + // paddingR: EdgeInsets.zero, + // borderRadius: 32, + // enableFloatingNavBar: true, + // enablePaddingAnimation: false, + // splashBorderRadius: 32, + // boxShadow: [ + // BoxShadow( + // color: Colors.black26, + // blurRadius: 8.0, + // ) + // ], + // onTap: (i) { + // tabsController.index = i; + // }, + // items: [ + // /// Home + // DotNavigationBarItem( + // icon: Icon( + // Icons.home, + // size: 32, + // ), + // selectedColor: Colors.redAccent, + // ), + // + // /// Search + // DotNavigationBarItem( + // icon: Icon( + // Icons.search, + // size: 32, + // ), + // selectedColor: Colors.redAccent, + // ), + // + // /// Profile + // DotNavigationBarItem( + // icon: Icon( + // Icons.person, + // size: 32, + // ), + // selectedColor: Colors.redAccent, + // ), + // ], + // ), + // ), + ), + ); + } +} + +enum _Tabs { home, packs, profile } diff --git a/lib/managers/favorite_cards.dart b/lib/managers/favorite_cards.dart new file mode 100644 index 0000000..4145276 --- /dev/null +++ b/lib/managers/favorite_cards.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class FavoriteCardsController { + late final SharedPreferences _sharedPreferences; + final StreamController> controller = + StreamController.broadcast(); + List _state = []; + + FavoriteCardsController(); + + void set state(List cards) { + _state = cards; + _sharedPreferences.setStringList( + 'favorites', + cards.toList(), + ); + controller.add(_state); + } + + List get state { + state = (_sharedPreferences.getStringList('favorites') ?? []); + return _state; + } + + Future init() async { + _sharedPreferences = await SharedPreferences.getInstance(); + } + + Future switchFavoriteCard(GameCardDto cardDto) async { + final favorites = _sharedPreferences.getStringList('favorites') ?? []; + Set updatedFavorites = favorites.toSet(); + if (favorites.contains(cardDto.id.toString())) { + updatedFavorites.remove(cardDto.id.toString()); + } else { + updatedFavorites.add(cardDto.id.toString()); + } + state = updatedFavorites.toList(); + } + + bool isFavoriteCardString(String id) => state.contains(id); + + bool isFavoriteCard(int? id) => + id == null ? false : isFavoriteCardString(id.toString()); + + Stream> get asStream => controller.stream.startWith(_state); +} diff --git a/lib/managers/repository/api.dart b/lib/managers/repository/api.dart new file mode 100644 index 0000000..014b583 --- /dev/null +++ b/lib/managers/repository/api.dart @@ -0,0 +1,17 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +import '../../main.dart'; +import 'firebase_config_repository.dart'; + +mixin Api { + String get path => + kReleaseMode || (globalSharedPreferences?.getBool('env') ?? true) + //prod + ? 'https://89.19.210.178:8080' //FirebaseRepository.appConfig.path + : androidDeviceInfo!.isPhysicalDevice + //device to localhost + ? 'https://192.168.31.158:8080' + : //emulator to localhost + 'https://10.0.2.2:8080'; +} diff --git a/lib/managers/repository/dio_provider.dart b/lib/managers/repository/dio_provider.dart new file mode 100644 index 0000000..1dbb9b5 --- /dev/null +++ b/lib/managers/repository/dio_provider.dart @@ -0,0 +1,110 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mnemo_cards/domain/router/app_router.gr.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:http_certificate_pinning/http_certificate_pinning.dart'; +import 'package:flutter/foundation.dart'; +import 'package:collection/collection.dart'; + +import '../../di/locator.dart'; +import '../../main.dart'; + +class DioProvider { + late final Dio _dio; + + DioProvider(); + + Dio get dio => _dio; + + Future init() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String appVersion = packageInfo.version; + + _dio = Dio() + ..interceptors.addAll([ + InterceptorsWrapper(onResponse: (response, handler) { + // if (response.data is List) { + // handler.next(response..data = utf8.decode(response.data)); + // return; + // } + handler.next(response); + }), + // CertificatePinningInterceptor(allowedSHAFingerprints: [ + // '28:21:5C:FB:54:4F:4A:DC:F8:5E:4C:FE:44:C8:E0:7B:0B:09:9D:D0:A9:FA:60:1D:3B:03:FF:EC:25:1A:C5:CC', + // ]), + InterceptorsWrapper(onError: (exception, handler) { + if (kDebugMode) { + log(exception.requestOptions.path ?? ''); + log(exception.toString()); + final snackBar = SnackBar( + content: Text( + '${exception.requestOptions.path}\n${exception.requestOptions.data}\n${exception.toString()}'), + ); + ScaffoldMessenger.of(scaffoldKey.currentContext!) + .showSnackBar(snackBar); + } + // if (exception.response?.statusCode == 401 && + // !locator.userManager.hasUser) { + // try { + // if (!AutoRouter.of(scaffoldKey.currentContext!) + // .isRouteActive(ProfilePage.name)) { + // AutoRouter.of(scaffoldKey.currentContext!).push( + // PageRouteInfo(ProfilePage.name), + // ); + // } + // } catch (e) {} + // } + handler.next(exception); + }), + InterceptorsWrapper(onRequest: (request, handler) { + request.headers = {...request.headers, 'app_version': appVersion}; + handler.next(request); + }) + ]); + + // final sslCert = await rootBundle.load('assets/ca.crt'); + // final clientCert = await rootBundle.load('assets/client.crt'); + // final sslKey = await rootBundle.load('assets/client.key'); + SecurityContext securityContext = SecurityContext(withTrustedRoots: true); + // securityContext.setTrustedCertificatesBytes( + // sslCert.buffer.asInt8List(), + // ); + // securityContext.setTrustedCertificatesBytes( + // clientCert.buffer.asInt8List(), + // ); + // securityContext.usePrivateKeyBytes(sslKey.buffer.asInt8List()); + + (_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { + HttpClient httpClient = HttpClient(context: securityContext) + ..badCertificateCallback = + (X509Certificate cert, String host, int port) { + print(cert.issuer); + print('$host $port'); + final now = DateTime.now(); + final valid = + cert.startValidity.isBefore(now) && cert.endValidity.isAfter(now); + return valid && + const DeepCollectionEquality().equals( + cert.sha1, + _cert, + ); + return true; + }; + return httpClient; + }; + return this; + } + + static final _cert = + '0A:82:77:1E:97:98:05:76:FC:ED:76:89:87:12:E8:9E:92:14:94:D3' + .split(':') + .map((v) => int.parse(v, radix: 16)) + .toList(); +} diff --git a/lib/managers/repository/firebase_config_repository.dart b/lib/managers/repository/firebase_config_repository.dart new file mode 100644 index 0000000..c5b4769 --- /dev/null +++ b/lib/managers/repository/firebase_config_repository.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'package:cloud_firestore/cloud_firestore.dart'; + +class FirebaseRepository { + static FirebaseFirestore get _firestore => FirebaseFirestore.instance; + + static ConfigModel? _lastConfig; + + static ConfigModel get appConfig { + update(); + return _lastConfig!; + } + + static Future update() async { + _lastConfig = await _firestore + .collection('config') + .get() + .then((v) => v.docs.first) + .then((value) => value.data()) + .then((value) => ConfigModel(value['path'] as String)); + } +} + +class ConfigModel { + final String path; + + ConfigModel(this.path); +} diff --git a/lib/managers/repository/http_repository.dart b/lib/managers/repository/http_repository.dart new file mode 100644 index 0000000..43f2487 --- /dev/null +++ b/lib/managers/repository/http_repository.dart @@ -0,0 +1,151 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mnemo_cards/managers/repository/repository.dart'; +import 'package:mnemo_cards/managers/user_manager.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import '../../features/purchase/in_app_purchase.dart'; +import '../../main.dart'; +import 'api.dart'; +import 'firebase_config_repository.dart'; + +class HttpRepository extends Repository with Api { + final Dio _dio; + + HttpRepository(this._dio); + + @override + Future>> getPacks( + Map? packData) async { + final r = await _dio.get( + '$path/packs/', + queryParameters: packData, + ); + final ll = + (jsonDecode(r.data!) as List).cast>(); + return ll; + } + + @override + Future get user async => UserDto.empty; + + @override + Stream get userStream => + Stream.periodic(const Duration(seconds: 5)).map((event) => UserDto.empty); + + @override + Future updateUser(UserDto dto) async {} + + @override + Future<(UserDto, String)> createOrGetUser( + String externalId, + ExternalIdType idType, + String? name, + ) async { + final r = await _dio.post('$path/user/create', data: { + 'token': externalId, + 'tokenType': idType.name, + 'name': name, + }).catchError( + (error, stackTrace) => log('', error: error, stackTrace: stackTrace)); + //todo add client header encryption ?? + final authToken = r.headers[HttpHeaders.authorizationHeader]!.last; + return ( + UserDto.fromJson( + jsonDecode(r.data as String) as Map, + ), + authToken, + ); + } + + void setAuthToken(String? authToken) { + _dio.interceptors.add( + InterceptorsWrapper(onRequest: (options, handler) { + options.headers[HttpHeaders.authorizationHeader] = authToken; + handler.next(options); + }), + ); + } + + @override + Future getUser() async { + final r = await _dio.get('$path/user'); + if (r.statusCode == 200) { + return UserDto.fromJson( + jsonDecode(r.data as String) as Map, + ); + } + return null; + } + + @override + Future updateUserData(String id, Map data) async {} + + @override + Future>> getTests({String packId = ''}) async { + final r = await _dio.get( + '$path/tests/$packId', + ); + final ll = + (jsonDecode(r.data!) as List).cast>(); + return ll; + } + + @override + Future getTest(String id) async { + final r = await _dio.get( + '$path/test/$id', + ); + try { + return TestDto.fromJson( + jsonDecode(r.data as String) as Map, + ); + } catch (e, s) { + log(e.toString(), stackTrace: s); + return null; + } + } + + @override + Future checkPayment( + String itemId, + String key, + PaymentSystem paymentSystem, + ) async { + final r = await _dio.get( + '$path/check_payment/', + queryParameters: { + 'id': itemId, + 'token': key, + 'system': paymentSystem.name, + }, + ); + return jsonDecode(r.data ?? '{}')['result'] ?? false; + } + + @override + Future checkUserPayment() async { + final r = await _dio.get('$path/check_payments/'); + final result = jsonDecode(r.data ?? '{}')?['result'] ?? false; + return result; + } + + Future createPayment(String id, PaymentSystem paymentSystem) async { + final r = await _dio.get( + '$path/create_payment/', + queryParameters: { + 'id': id, + 'system': paymentSystem.name, + }, + ); + return jsonDecode(r.data)?['url'] as String?; + } +} diff --git a/lib/managers/repository/repository.dart b/lib/managers/repository/repository.dart new file mode 100644 index 0000000..451774d --- /dev/null +++ b/lib/managers/repository/repository.dart @@ -0,0 +1,36 @@ +import 'package:mnemo_cards/managers/user_manager.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../../features/purchase/in_app_purchase.dart'; + +abstract class Repository { + Future get user; + + Stream get userStream; + + Future updateUser(UserDto dto); + + Future<(UserDto, String)> createOrGetUser( + String externalId, + ExternalIdType idType, + String name, + ); + + Future getUser(); + + Future updateUserData(String id, Map data); + + Future>> getTests(); + + Future getTest(String id); + + Future checkPayment( + String itemId, + String key, + PaymentSystem PaymentSystem, + ); + + Future checkUserPayment(); + + Future createPayment(String id, PaymentSystem type); +} diff --git a/lib/managers/user_manager.dart b/lib/managers/user_manager.dart new file mode 100644 index 0000000..985a77f --- /dev/null +++ b/lib/managers/user_manager.dart @@ -0,0 +1,154 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:dio/dio.dart'; +import 'package:google_sign_in/google_sign_in.dart'; +import 'package:mnemo_cards/main.dart'; +import 'package:mnemo_cards/managers/repository/http_repository.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +import '../domain/router/app_router.dart'; +import '../domain/router/app_router.gr.dart'; +import 'user_state_holder.dart'; + +class UserManager { + CompositeSubscription? _subscription; + final HttpRepository _repository; + GoogleSignIn _googleSignIn = GoogleSignIn( + scopes: [ + 'email', + // 'https://www.googleapis.com/auth/contacts.readonly', + ], + // clientId: '01767851968-dvbvmte4m90dkqitp013s4nd5c0df1n8.apps.googleusercontent.com', + // "701767851968-bud97rud1d9qtqju96addn31nhm0oofu.apps.googleusercontent.com", + ); + + late final SharedPreferences? _sharedPreferences; + final UserStateHolder userStateHolder; + + UserManager(this._repository) : userStateHolder = UserStateHolder(); + + bool get hasUser => userStateHolder.user != null; + + Future login() async { + if (await _googleSignIn.isSignedIn()) { + await _googleSignIn.signOut(); + } + final account = await _googleSignIn.signIn(); + await Future.delayed(Duration(milliseconds: 300)); + } + + Future logout() async { + if (await _googleSignIn.isSignedIn()) { + await _googleSignIn.signOut(); + } + userStateHolder.clear(); + } + + Future _createUser( + String externalId, + ExternalIdType idType, + String? name, + ) async { + final p = await _repository.createOrGetUser(externalId, idType, name); + _setAuthToken(p.$2); + userStateHolder.setUser(p.$1); + } + + void _setAuthToken(String authToken) async { + _repository.setAuthToken(authToken); + await _sharedPreferences!.setString('authToken', authToken); + } + + void _clearAuthToken() async { + await _sharedPreferences!.remove('authToken'); + } + + Future _updateUser() async { + String? authToken = _sharedPreferences!.getString('authToken'); + if (authToken != null) { + _setAuthToken(authToken); + try { + final fetchedUser = await _repository.getUser(); + userStateHolder.setUser(fetchedUser!); + return; + } catch (e) { + _clearAuthToken(); + AppRouter.openAuthOrProfile(); + } + } + // not creating empty user + // if (Platform.isAndroid) { + // final deviceInfoPlugin = DeviceInfoPlugin(); + // final deviceInfo = await deviceInfoPlugin.androidInfo; + // final id = deviceInfo.id; + // await _createUser(id, ExternalIdType.device); + // } + } + + Future init() async { + _sharedPreferences = await SharedPreferences.getInstance(); + + //todo add ios + try { + await _updateUser(); + } on Object catch (e, s) { + log('User manager init error', error: e, stackTrace: s); + } + + _subscription = CompositeSubscription() + ..add(_googleSignIn.onCurrentUserChanged + .startWith(_googleSignIn.currentUser) + .distinct((p, n) => p?.id == n?.id) + .listen((account) async { + if (account != null) { + final name = account.displayName; + final token = + await account.authentication.then((value) => value.idToken); + await _createUser(token!, ExternalIdType.google, name); + } + })) + ..add(Stream.periodic(Duration(seconds: 10)).listen((_) async { + try { + if (userStateHolder.user == null) { + await _updateUser(); + } + } catch (e) { + log(e.toString()); + _clearAuthToken(); + if (e is DioException) { + if (e.response?.statusCode == 401) { + appRouter.push( + PageRouteInfo(ProfilePage.name), + ); + } + } + } + })) + ..add( + userStateHolder.asNullableStream + .map((user) => user != null) + .distinct() + .listen((hasUser) { + if (hasUser) { + appRouter.replace(PageRouteInfo(HomePage.name)); + } else { + AppRouter.openAuthOrProfile(); + } + }), + ); + } + + Future dispose() async { + await _subscription?.cancel(); + _subscription = null; + } +} + +enum ExternalIdType { google, device } diff --git a/lib/managers/user_state_holder.dart b/lib/managers/user_state_holder.dart new file mode 100644 index 0000000..3531795 --- /dev/null +++ b/lib/managers/user_state_holder.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; + + +class UserStateHolder extends ChangeNotifier { + UserDto? _dto; + final _streamController = StreamController.broadcast(); + + static final _instance = UserStateHolder._(); + + UserStateHolder._(); + + factory UserStateHolder() => _instance; + + void setUser(UserDto dto) { + _dto = dto.copyWith(); + notifyListeners(); + _streamController.add(_dto); + } + + void clear() { + _dto = null; + notifyListeners(); + _streamController.add(null); + } + + Stream get asStream => asNullableStream.whereNotNull(); + + Stream get asNullableStream => + _streamController.stream.startWith(_dto); + + UserDto? get user => _dto; +} diff --git a/lib/pages/auth_page.dart b/lib/pages/auth_page.dart new file mode 100644 index 0000000..5b34d5c --- /dev/null +++ b/lib/pages/auth_page.dart @@ -0,0 +1,111 @@ +import 'package:auto_route/annotations.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/di/locator.dart'; +import 'package:mnemo_cards/flags.dart'; +import 'package:mnemo_cards/widgets/header.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../main.dart'; +import '../theme/themes.dart'; +import '../widgets/shared_pref_button.dart'; + +@RoutePage() +class AuthPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + SizedBox( + height: 64.h - MediaQuery.of(context).padding.top, + ), + Text( + 'Mnemo cards', + style: TextStyle( + fontSize: 36.sp, + fontWeight: FontWeight.w500, + height: 0.85, + ), + textAlign: TextAlign.start, + ), + SizedBox( + height: 60.h, + ), + Expanded( + child: Image.asset( + 'images/cerdo.jpg', + fit: BoxFit.fitWidth, + ), + ), + if (ADMIN_BUILD) + SharedPrefButton( + enabledWidget: Text('PROD'), + disabledWidget: Text('TEST'), + spKey: 'env', + ), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 10.0.w, vertical: 5.0.h), + child: InkWell( + onTap: locator.userManager.login, + child: Container( + height: 90.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: borderGray), + borderRadius: BorderRadius.circular(16.0), + ), + padding: EdgeInsets.all(10.0.w), + child: Image.asset( + 'icons/google.png', + height: 88.h, + ), + ), + ), + ), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 10.0.w, vertical: 10.0.h), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: + 'Присоединяясь, я заявляю что прочитал и принимаю '), + // TextSpan( + // text: 'условия', + // recognizer: TapGestureRecognizer() + // ..onTap = + // () => launchUrl(Uri.parse('https://www.google.com')), + // style: TextStyle(decoration: TextDecoration.underline), + // ), + // TextSpan(text: ' и '), + TextSpan( + text: 'политику конфиденциальности', + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrl(Uri.parse( + 'https://www.freeprivacypolicy.com/live/1c2bce51-86c6-4a36-b226-b6c6f50ebf0f')), + style: + TextStyle(decoration: TextDecoration.underline)), + TextSpan(text: '.'), + ], + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w300, + height: 1.1, + color: borderGray, + ), + ), + textAlign: TextAlign.center), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart new file mode 100644 index 0000000..fbe5e86 --- /dev/null +++ b/lib/pages/explore_page.dart @@ -0,0 +1,48 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mnemo_cards/di/locator.dart'; + +import '../widgets/packs_menu_widget.dart'; + +@RoutePage() +class ExplorePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + return Column( + children: [ + Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(12.0), + child: Text( + 'Blah - blah', + style: theme.headlineSmall, + ), + ), + Expanded( + child: PacksMenuWidget( + locator.repository, + locator.packManager, + ), + ), + TextButton( + onPressed: () async { + await locator.packManager.clearCache(); + locator.testManager.clearStates(); + await locator.packUpdater.updateAvailablePacks(); + }, + child: Text('Clear cache'), + ), + TextButton( + onPressed: () async { + locator.userManager.login(); + }, + child: Text('Login'), + ), + ], + ); + } + + const ExplorePage(); +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart new file mode 100644 index 0000000..bbb6a62 --- /dev/null +++ b/lib/pages/home_page.dart @@ -0,0 +1,24 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:mnemo_cards/di/locator.dart'; + +import '../widgets/packs_menu_widget.dart'; + +@RoutePage() +class HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: PacksMenuWidget( + locator.repository, + locator.packManager, + ), + ), + ); + } + + const HomePage(); +} diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart new file mode 100644 index 0000000..4348ed1 --- /dev/null +++ b/lib/pages/profile_page.dart @@ -0,0 +1,207 @@ +import 'dart:io'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/domain/router/app_router.dart'; +import 'package:mnemo_cards/flags.dart'; +import 'package:mnemo_cards/widgets/header.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../admin/all_cards.dart'; +import '../di/locator.dart'; +import '../theme/themes.dart'; +import '../widgets/shared_pref_button.dart'; +import '../widgets/text_button.dart'; + +@RoutePage() +class ProfilePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + return Scaffold( + body: SafeArea( + child: Column( + children: [ + StreamBuilder( + stream: locator.userManager.userStateHolder.asStream, + builder: (context, snapshot) { + var title = snapshot.data?.name; + if (title == null || title.length > 10) { + title = 'Привет!'; + } + return Header( + title, + popText: 'назад', + hasPopButton: true, + trail: GestureDetector( + onTap: () async { + await locator.userManager.logout(); + final sp = await SharedPreferences.getInstance(); + sp.clear(); + AppRouter.openAuthOrProfile(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'icons/exit.png', + width: 25.w, + ), + ], + ), + ), + ); + }), + SizedBox( + height: 40.h, + ), + if (ADMIN_BUILD) + MaterialButton( + onPressed: () { + showDialog(context: context, builder: (c) => const AllCards()); + }, + child: Text('ALL CARDS'), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: Text( + 'Скоро тут будет прогресс, ачивки, настройки и другая важная информация', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: Text( + 'А сейчас есть переключатель звука:', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + ), + Expanded( + child: SharedPrefButton( + enabledWidget: Image.asset( + 'icons/sound_on.png', + width: 140.w, + height: 120.h, + ), + disabledWidget: Image.asset( + 'icons/sound_off.png', + width: 140.w, + height: 120.h, + ), + spKey: 'sound_on', + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: LinkButton( + 'Написать в тг', + () => launchUrl( + Uri.parse('https://t.me/mnemo_cards_bot'), + ), + ), + ), + SizedBox( + height: 30.0.h, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: LinkButton( + 'Очистить кэш', + () async { + await locator.packManager.clearCache(); + locator.testManager.clearStates(); + await locator.packUpdater.updateAvailablePacks(); + }, + ), + ), + Flexible( + child: LinkButton( + 'Страница в сторе', + () {}, + ), + ), + ], + ), + ), + SizedBox( + height: 30.0.h, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: LinkButton( + 'Политика конфиденциальности', + () { + launchUrl( + Uri.parse( + 'https://www.freeprivacypolicy.com/live/1c2bce51-86c6-4a36-b226-b6c6f50ebf0f'), + ); + }, + ), + ), + // Padding( + // padding: EdgeInsets.symmetric(horizontal: 10.0.w), + // child: LinkButton( + // 'Условия использования', + // () { + // + // }, + // ), + // ), + Padding( + padding: + EdgeInsets.symmetric(horizontal: 10.0.w, vertical: 10.0.h), + child: InkWell( + onTap: AutoRouter.of(context).maybePop, + child: Container( + height: 90.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: borderGray), + borderRadius: BorderRadius.circular(16.0), + ), + padding: EdgeInsets.all(10.0.w), + child: Text( + 'Назад', + style: TextStyle( + fontSize: 25.sp, + fontWeight: FontWeight.w500, + height: 0.85, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Future clearAndExit() async { + final sp = await SharedPreferences.getInstance(); + sp.clear(); + exit(0); + } + + const ProfilePage(); +} diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart new file mode 100644 index 0000000..7bd01d1 --- /dev/null +++ b/lib/theme/themes.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +const Color peach = Color(0xffffc994); +const Color green = Color(0xff3d5309); +const Color greenAccent = Color(0xff688d11); +const Color black = Color(0xff000000); +const Color white = Color(0xffffffff); +const Color yellow = Colors.yellowAccent; +const Color menuBlue = Color(0xff99C4E9); +const Color backgroundBlue = Color(0xFFf0f0f0); +const Color testBlue = Color(0xffD0EAFF); +const Color borderGray = Color(0xffABABAB); + +final lightTheme = ThemeData( + brightness: Brightness.light, + useMaterial3: true, + primaryColor: menuBlue, + scaffoldBackgroundColor: white, + appBarTheme: AppBarTheme( + backgroundColor: white, + ), + colorScheme: ColorScheme.fromSeed( + seedColor: menuBlue, + brightness: Brightness.light, + ), + fontFamily: GoogleFonts.inter().fontFamily, +); +final darkTheme = ThemeData( + brightness: Brightness.dark, + useMaterial3: true, + primaryColor: green, + scaffoldBackgroundColor: black, + colorScheme: ColorScheme.fromSeed( + seedColor: greenAccent, + brightness: Brightness.dark, + ), + fontFamily: GoogleFonts.istokWeb().fontFamily, +); diff --git a/lib/utils/color_helper.dart b/lib/utils/color_helper.dart new file mode 100644 index 0000000..443b728 --- /dev/null +++ b/lib/utils/color_helper.dart @@ -0,0 +1,22 @@ +import 'package:flutter/painting.dart'; + +extension ColorBrightness on Color { + Color darken([double amount = .1]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(this); + final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); + + return hslDark.toColor(); + } + + Color lighten([double amount = .1]) { + assert(amount >= 0 && amount <= 1); + + final hsl = HSLColor.fromColor(this); + final hslLight = + hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0)); + + return hslLight.toColor(); + } +} diff --git a/lib/utils/iterable_helper.dart b/lib/utils/iterable_helper.dart new file mode 100644 index 0000000..5c1f767 --- /dev/null +++ b/lib/utils/iterable_helper.dart @@ -0,0 +1,29 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +extension IterableHelper on Iterable { + T? firstWhereOrNull(bool Function(T) test) => where(test).firstOrNull; +} + +extension NullableIterableHelper on Iterable { + Iterable whereNotNull() => where((element) => element != null).cast(); +} + +extension ListHelper on List { + + // List shuffle([int? seed]) { + // Random r = Random(seed); + // var items = [...this]; + // for (var i = length - 1; i > 0; i--) { + // var n = r.nextInt(i + 1); + // + // var temp = items[i]; + // items[i] = items[n]; + // items[n] = temp; + // } + // + // return items; + // } + +} \ No newline at end of file diff --git a/lib/utils/string_helper.dart b/lib/utils/string_helper.dart new file mode 100644 index 0000000..178b5db --- /dev/null +++ b/lib/utils/string_helper.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +extension StringHelper on String { + String get capitalize => + "${this[0].toUpperCase()}${this.substring(1).toLowerCase()}"; + + Color? get asColor { + if (startsWith('#')) { + return replaceFirst('#', '0x').asColor; + } + if (startsWith('0x')) { + if (length > '0x001122'.length) { + return Color(int.parse(this)); + } else { + return Color(0xff000000 + int.parse(this)); + } + } else { + if (length == 'aabbcc'.length || length == 'ffaabbcc'.length) { + return '0x$this'.asColor; + } + } + return Colors.white; + } +} diff --git a/lib/widgets/card_game/card_game_header.dart b/lib/widgets/card_game/card_game_header.dart new file mode 100644 index 0000000..971c4fc --- /dev/null +++ b/lib/widgets/card_game/card_game_header.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +import '../../features/card_swiper/src/card_swiper_controller.dart'; +import '../header.dart'; + +class CardGameHeader extends StatelessWidget { + final CardSwiperController controller; + final CardPackDto cardPackDto; + + const CardGameHeader( + this.controller, + this.cardPackDto, + ); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + + return StreamBuilder( + stream: controller.asStream, + builder: (context, snapshot) => Header( + '${cardPackDto.title}', + // trailing: Text( + // '${snapshot.data}/${cardPackDto.size}', + // style: theme.headlineSmall, + // ), + ), + ); + } +} diff --git a/lib/widgets/card_game/card_game_main.dart b/lib/widgets/card_game/card_game_main.dart new file mode 100644 index 0000000..8e32187 --- /dev/null +++ b/lib/widgets/card_game/card_game_main.dart @@ -0,0 +1,201 @@ +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:mnemo_cards/features/packs/images_holder.dart'; +import 'package:mnemo_cards/flags.dart'; +import 'package:mnemo_cards/utils/color_helper.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; + +import '../../admin/add_card.dart'; +import '../../di/locator.dart'; +import '../../domain/router/app_router.dart'; +import '../../features/card_flipper/flip_card_controllers.dart'; +import '../../features/card_swiper/src/allowed_swipe_direction.dart'; +import '../../features/card_swiper/src/card_swiper.dart'; +import '../../features/card_swiper/src/card_swiper_controller.dart'; +import '../../main.dart'; +import '../game_card_widget.dart'; + +class CardGameMainWidget extends StatelessWidget { + final CardPackDto cardPack; + final int? startCardId; + final CardSwiperController cardSwiperController; + final FlipCardController flipCardController; + final int length; + + const CardGameMainWidget( + this.cardPack, + this.startCardId, + this.cardSwiperController, + this.flipCardController, + this.length, + ); + + @override + Widget build(BuildContext context) { + final numberOfCardsDisplayed = min(2, cardPack.cards.length); + final initialScale = 0.9; + int lastThreshold = 0; + List cards = cardPack.cards; + if (startCardId != null) { + final startCardIndex = cards.indexWhere( + (card) => card.id == startCardId, + ); + if (startCardIndex >= 0) { + cards = [ + ...cards.sublist(startCardIndex), + ...cards.sublist(0, startCardIndex), + ]; + } + } else { + cards = cards.toList()..shuffle(); + } + GameCardDto? activeCard() => cardSwiperController.currentIndex >= 0 + ? cards[cardSwiperController.currentIndex % cards.length] + : null; + + return PopScope( + canPop: false, + onPopInvoked: (v) { + if (!v) { + appRouter.popForced(activeCard()?.id); + } + }, + child: Material( + type: MaterialType.transparency, + child: LayoutBuilder(builder: (context, constraints) { + final cardWidth = constraints.maxWidth; + final cardHeight = cardWidth; + bool enabled = true; + return StatefulBuilder(builder: (context, setState) { + return Stack( + children: [ + _EndWidget(cardSwiperController, flipCardController, () { + setState(() { + enabled = false; + }); + AutoRouter.of(context).maybePop(); + }), + if (enabled) + Hero( + tag: activeCard()?.id ?? 'empty', + child: StreamBuilder( + stream: cardSwiperController.asStream + .map((index) => /*cards[index].isAd*/ false) + .pairwise() + .asyncMap((pair) async { + if (pair.first) { + await Future.delayed(Duration(seconds: 5)); + return false; + } + return pair.last; + }).timeout(Duration(seconds: 5), + onTimeout: (_) => false), + builder: (context, snapshot) { + return AbsorbPointer( + absorbing: snapshot.data ?? false, + child: CardSwiper( + controller: cardSwiperController, + padding: const EdgeInsets.symmetric( + vertical: 0.0, + horizontal: 0.0, + ), + cardsCount: length, + numberOfCardsDisplayed: numberOfCardsDisplayed, + allowedSwipeDirection: + AllowedSwipeDirection.symmetric( + horizontal: true, + ), + scale: initialScale, + backCardOffset: Offset(0, cardHeight * 0.1 / 2), + isLoop: false, + cardBuilder: ( + context, + cardIndex, + percentThresholdX, + percentThresholdY, + scale, + ) { + final index = cardIndex % cards.length; + final isFirstCard = + (cardSwiperController.currentIndex == + cardIndex); + if (cardSwiperController.currentIndex == + index) { + lastThreshold = percentThresholdX; + } + print('${cards[index].translation} $scale'); + final cardWidget = Center( + child: GameCardWidget( + cards[index], + cards[index].id.memoryImage, + flipCardController, + flipCardController.state + // || cards[index].isAd + , + isFirstCard, + isFirstCard + ? cardPack.color?.asColor + : cardPack.color?.asColor?.darken( + (scale - 1) / + (initialScale - 1) * + 0.075, + ), + ), + ); + return cardWidget; + }, + ), + ); + }), + ), + ], + ); + }); + }), + ), + ); + } +} + +class _EndWidget extends StatelessWidget { + final CardSwiperController cardSwiperController; + final FlipCardController flipCardController; + final VoidCallback callback; + + _EndWidget( + this.cardSwiperController, + this.flipCardController, + this.callback, + ); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Ok: ${cardSwiperController.swipedLeftIds.length}'), + Text('Repeat: ${cardSwiperController.swipedRightIds.length}'), + MaterialButton( + onPressed: () { + cardSwiperController.restart(); + }, + child: Text('restart'), + ), + MaterialButton( + onPressed: () { + callback(); + }, + child: Text('to pack'), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/card_game/card_game_page.dart b/lib/widgets/card_game/card_game_page.dart new file mode 100644 index 0000000..3861ae6 --- /dev/null +++ b/lib/widgets/card_game/card_game_page.dart @@ -0,0 +1,258 @@ +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/features/tests/progress_widget.dart'; +import 'package:mnemo_cards/main.dart'; +import 'package:mnemo_cards/theme/themes.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:rxdart/rxdart.dart'; +import '../../di/locator.dart'; +import '../../features/card_flipper/flip_card.dart'; +import '../../features/card_flipper/flip_card_controllers.dart'; +import '../../features/card_swiper/flutter_card_swiper.dart'; +import 'card_game_main.dart'; + +@RoutePage() +class CardGamePage extends StatefulWidget { + final int? startCardId; + final CardPackDto cardPack; + final bool infinite; + + const CardGamePage({ + required this.cardPack, + this.startCardId, + this.infinite = false, + super.key, + }); + + @override + State createState() => _CardGamePageState(); +} + +class _CardGamePageState extends State { + final CardSwiperController cardSwiperController = CardSwiperController(); + final FlipCardController flipCardController = FlipCardController(); + + GameCardDto? get activeCard { + final index = cardSwiperController.currentIndex; + final cards = widget.cardPack.cards; + if (index >= 0) { + return cards[index % cards.length]; + } + return null; + } + + int get gameLength => + widget.infinite ? 10000000 : widget.cardPack.cards.length; + + @override + Widget build(BuildContext context) { + // todo move to controller + final timer = Stream.periodic(Duration(seconds: 1), (tick) { + return Duration(seconds: tick + 1); + }).asBroadcastStream().take(3600).startWith(Duration.zero); + + return Scaffold( + body: PopScope( + child: SafeArea( + child: LayoutBuilder(builder: (context, constraints) { + return Column( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = + constraints.maxHeight / constraints.maxWidth; + return Column( + children: [ + if (aspectRatio > 14 / 9) + SizedBox( + height: (64.h - + 20.h - + MediaQuery.of(context).padding.top) / + 2, + ), + if (aspectRatio > 14 / 9) + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: StreamBuilder( + stream: cardSwiperController.asStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return SizedBox( + height: 30, + ); + } + return ProgressWidget( + widget.infinite + ? null + : List.filled( + cardSwiperController.progress, + Result.correct, + ), + widget.cardPack.cards.length, + widget.cardPack.color?.asColor ?? + borderGray, + timer: timer, + popText: widget.infinite ? 'к теме' : null, + key: ValueKey('CARDS PROGRESS'), + ); + }, + ), + ), + if (aspectRatio > 14 / 9) + SizedBox( + height: (64.h - + 20.h - + MediaQuery.of(context).padding.top) / + 2, + ), + Expanded( + child: Stack( + children: [ + CardGameMainWidget( + widget.cardPack, + widget.startCardId, + cardSwiperController, + flipCardController, + gameLength, + ), + if (aspectRatio <= 14 / 9) + Align( + alignment: Alignment.topLeft, + child: IconButton( + onPressed: + AutoRouter.of(context).maybePop, + icon: Icon( + Icons.arrow_back_ios, + color: Colors.black26, + ), + ), + ) + ], + ), + ), + ], + ); + }, + ), + ), + if (constraints.maxHeight / constraints.maxWidth > 17 / 9) + Hero( + tag: 'bottom', + createRectTween: (a, b) => RectTween(begin: a, end: b), + child: Material( + color: Colors.transparent, + child: StreamBuilder( + stream: cardSwiperController.asStream, + builder: (context, snapshot) { + return AnimatedContainer( + duration: Duration(milliseconds: 200), + height: activeCard == null ? 0 : 90.h, + padding: EdgeInsets.only( + left: 10.0.w, + right: 10.0.w, + bottom: 10.0.h, + ), + child: Row( + children: [ + Expanded( + flex: 1, + child: GestureDetector( + onTap: () async { + if (activeCard != null) + await locator.favoriteCardsController + .switchFavoriteCard(activeCard!); + }, + child: StreamBuilder( + stream: locator + .favoriteCardsController.asStream, + initialData: locator + .favoriteCardsController.state, + builder: (context, snapshot) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: borderGray), + borderRadius: + BorderRadius.circular(16.0), + ), + padding: EdgeInsets.all(10.0.w), + child: Image.asset( + "icons/heart.png", + width: 29.w, + height: 29.h, + color: locator + .favoriteCardsController + .isFavoriteCard( + activeCard?.id, + ) + ? Colors.red + : Colors.grey, + ), + ); + }), + ), + ), + SizedBox( + width: 10.0.w, + ), + Expanded( + flex: 3, + child: InkWell( + onTap: () async { + final direction = Random().nextBool() + ? CardSwiperState.swipeLeft + : CardSwiperState.swipeRight; + cardSwiperController.swipe( + direction: direction, + ); + }, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: borderGray), + borderRadius: + BorderRadius.circular(16.0), + ), + padding: EdgeInsets.all(10.0.w), + child: Text( + 'Дальше', + style: TextStyle( + fontSize: 25.sp, + fontWeight: FontWeight.w500, + height: 0.85, + ), + ), + ), + ), + ), + ], + ), + ); + }), + ), + ) + ], + ); + }), + ), + ), + ); + } + + @override + void dispose() { + // cardSwiperController.removeListener(cardSwiperListener); + cardSwiperController.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/card_game/card_game_swipe_helper.dart b/lib/widgets/card_game/card_game_swipe_helper.dart new file mode 100644 index 0000000..46a11b7 --- /dev/null +++ b/lib/widgets/card_game/card_game_swipe_helper.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import '../../features/card_swiper/src/card_swiper_controller.dart'; + +class CardGameSwipeHelper extends StatelessWidget { + final CardSwiperController cardSwiperController; + + const CardGameSwipeHelper(this.cardSwiperController); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + + return Container( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 0.0, + ), + child: StreamBuilder( + stream: cardSwiperController.asStream, + builder: (context, snapshot) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.circle, + color: Colors.green, + ), + SizedBox( + width: 4.0, + ), + Text( + 'Помню ${cardSwiperController.swipedLeftIds.isEmpty ? '' : cardSwiperController.swipedLeftIds.length}', + style: theme.labelMedium?.copyWith( + color: Colors.green, + fontWeight: FontWeight.w600, + ), + ) + ], + ), + Row( + children: [ + Text( + 'Повторить ${cardSwiperController.swipedRightIds.isEmpty ? '' : cardSwiperController.swipedRightIds.length}', + style: theme.labelMedium?.copyWith( + color: Colors.yellow[600], + fontWeight: FontWeight.w600, + ), + ), + SizedBox( + width: 4.0, + ), + Icon( + Icons.circle, + color: Colors.yellow[600], + ), + ], + ), + ], + ); + } + ), + ); + } +} diff --git a/lib/widgets/card_pack/available_pack.dart b/lib/widgets/card_pack/available_pack.dart new file mode 100644 index 0000000..2e3a95e --- /dev/null +++ b/lib/widgets/card_pack/available_pack.dart @@ -0,0 +1,510 @@ +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/animation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/features/packs/images_holder.dart'; +import 'package:mnemo_cards/main.dart'; +import 'package:mnemo_cards/utils/iterable_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:yandex_mobileads/mobile_ads.dart'; +import '../../domain/router/app_router.gr.dart'; +import '../../features/yandex_ads/yandex_ads.dart'; +import '../game_card_widget.dart'; + +import '../../di/locator.dart'; +import '../../theme/themes.dart'; +import 'card_pack_header.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; + +class AvailablePack extends StatelessWidget { + final CardPackDto cardPack; + final _CardsViewController _cardsViewController = _CardsViewController(); + + AvailablePack(this.cardPack); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + CardPackHeader.fromDto(cardPack), + Expanded( + child: ListView( + children: [ + StatefulBuilder(builder: (context, setState) { + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 0.h), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: cardPack.color?.asColor ?? + Colors.grey.withOpacity(0.5)), + ), + ), + clipBehavior: Clip.antiAlias, + child: _CardsView(_cardsViewController, cardPack), + ), + SizedBox( + height: 10.h, + ), + Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0.h, + horizontal: 10.w, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _Button('view.png', () async { + _cardsViewController.toggleExpanded(); + }), + _Button('shuffle.png', () async { + final index = + Random().nextInt(cardPack.cards.length); + await _cardsViewController.animateTo(index); + AutoRouter.of(context).push( + PageRouteInfo( + CardGamePage.name, + args: CardGamePageArgs( + cardPack: cardPack, + startCardId: cardPack.cards[index].id, + infinite: true, + // id: cardPack.dto.id.toString(), + ), + ), + ); + }), + // _Button('chat.png', () {}), + _Button('heart.png', () {}), + ], + ), + ), + ], + ); + }), + _AdTile(), + _TestsSection(cardPack.id.toString()), + // _TestButton( + // title: 'Быстрый тест', + // subtitle: 'Лучший результат: 99 / 100', + // time: '3', + // timeSubtitle: 'мин', + // onTap: () {}, + // ), + // _TestButton( + // title: 'Полный тест', + // subtitle: 'Лучший результат: 99 / 100', + // time: '10', + // timeSubtitle: 'мин', + // onTap: () {}, + // ), + ], + ), + ), + Hero( + tag: 'bottom', + child: SizedBox( + height: 0, + width: 300.w, + ), + ), + ], + ); + } +} + +class _TestsSection extends StatelessWidget { + final String packId; + + const _TestsSection(this.packId); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: locator.testManager.loadTests(packId: packId), + builder: (context, snapshot) { + return Column( + children: [ + if (snapshot.hasData) + ...snapshot.data!.map( + (dto) => _TestButton( + title: dto.name, + subtitle: null, + time: dto.time, + timeSubtitle: dto.timeSubtitle, + onTap: () { + AutoRouter.of(context).push( + PageRouteInfo(TestPage.name, + args: TestPageArgs(testId: dto.id!)), + ); + }, + ), + ), + ], + ); + }, + ); + } +} + +class _AdTile extends StatelessWidget { + const _AdTile(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + return Container( + height: 80, + margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.white, + width: 4.0, + ), + borderRadius: BorderRadius.circular(12.0), + ), + child: Center( + child: LayoutBuilder(builder: (context, constraints) { + return AdWidget( + bannerAd: YandexAds.createBanner(constraints)..loadAd()); + }), + ), + ); + } +} + +class _Button extends StatelessWidget { + final String asset; + final VoidCallback onTap; + + _Button( + this.asset, + this.onTap, + ); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + width: 110.w, + height: 60.h, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: borderGray), + borderRadius: BorderRadius.circular(16.0), + ), + alignment: Alignment.center, + child: Image.asset( + 'icons/$asset', + width: 29.w, + // height: 29.h, + fit: BoxFit.scaleDown, + ), + ), + ); + } +} + +class _TestButton extends StatelessWidget { + final VoidCallback onTap; + final String title; + final String? subtitle; + final String? time; + final String? timeSubtitle; + + _TestButton({ + required this.title, + required this.onTap, + this.subtitle, + this.time, + this.timeSubtitle, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: 5.0.h, + horizontal: 10.w, + ), + child: InkWell( + onTap: onTap, + child: Container( + height: 120.h, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: borderGray), + borderRadius: BorderRadius.circular(16.0), + ), + padding: EdgeInsets.only( + top: 12.0.h, + left: 10.0.w, + right: 10.0.w, + bottom: 20.h, + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: TextStyle( + fontSize: 25.sp, + fontWeight: FontWeight.w500, + height: 0.85, + ), + ), + if (subtitle != null) + Text( + subtitle!, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w300, + height: 0.85, + ), + ), + ], + ), + ), + Container( + width: 75.w, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (time != null) + Text( + time!, + style: TextStyle( + fontSize: 40.sp, + fontWeight: FontWeight.w400, + height: 0.85), + ), + if (timeSubtitle != null) + Text( + timeSubtitle!, + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w400, + height: 0.85), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _CardBuilder extends StatelessWidget { + final GameCardDto card; + final CardPackDto cardPack; + final MemoryImage image; + final Function(GameCardDto) onTap; + + _CardBuilder( + this.cardPack, + this.card, + this.image, + this.onTap, + ); + + @override + Widget build(BuildContext context) { + return GestureDetector( + key: ObjectKey(card), + onTap: () => onTap(card), + child: Hero( + tag: card.id, + child: GameCardDecoration( + alpha: 0.0, + child: FrontCard( + card, + image, + alpha: 0, + ), + ), + createRectTween: (a, b) => RectTween(begin: a, end: b), + flightShuttleBuilder: (flightC, a, dir, fromC, toC) => AnimatedBuilder( + animation: a, + builder: (context, child) => GameCardDecoration( + alpha: a.value, + child: FrontCard( + card, + card.memoryImage!, + alpha: a.value, + ), + ), + ), + ), + ); + } +} + +class _CardsView extends StatefulWidget { + final CardPackDto cardPack; + final _CardsViewController controller; + + _CardsView(this.controller, this.cardPack); + + @override + State createState() => _CardsViewState(); +} + +class _CardsViewState extends State<_CardsView> + with SingleTickerProviderStateMixin { + CardPackDto get cardPack => widget.cardPack; + + _CardsViewController get controller => widget.controller; + + late AnimationController _animationController; + + bool get expanded => controller._expanded; + + void _onStateChanged() { + if (expanded) { + _animationController.animateTo(1.0); + } else { + _animationController.animateBack(0.0); + } + // setState(() {}); + } + + @override + void didUpdateWidget(_CardsView oldWidget) { + super.didUpdateWidget(oldWidget); + controller.addListener(_onStateChanged); + } + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 500), + reverseDuration: Duration(milliseconds: 500)); + controller.addListener(_onStateChanged); + } + + @override + void dispose() { + controller.removeListener(_onStateChanged); + _animationController.dispose(); + super.dispose(); + } + + void onCardTap(GameCardDto card) async { + final playing = appRouter.push( + PageRouteInfo( + CardGamePage.name, + args: CardGamePageArgs( + cardPack: cardPack, + startCardId: card.id, + infinite: true, + // id: cardPack.dto.id.toString(), + ), + ), + ); + final id = await playing; + if (id is int) { + final index = cardPack.cards.indexWhere((c) => c.id == id); + if (index >= 0) { + print('jump to $index'); + controller.jumpTo(index); + } + } + } + + @override + Widget build(BuildContext context) { + final cardWidth = MediaQuery.of(context).size.width / 3; + final imageWidth = cardWidth * 0.93; + final packCards = cardPack.cards; + controller.imageWidth = imageWidth; + final animation = _animationController.view; + return AnimatedBuilder( + animation: animation, + builder: (context, child) { + return SingleChildScrollView( + controller: controller._cardsScrollController, + scrollDirection: animation.value > 0 ? Axis.vertical : Axis.horizontal, + physics: animation.value > 0 ? NeverScrollableScrollPhysics() : null, + child: Container( + alignment: Alignment.topCenter, + height: cardWidth * 4 / 3 + + (packCards.length * 4 / 9 * cardWidth - cardWidth * 4 / 3) * + animation.value, + child: Wrap( + children: List.generate(packCards.length, (index) { + final card = packCards[index]; + final memoryImage = card.id.memoryImage; + if (memoryImage == null) return SizedBox(); + return Container( + height: cardWidth * 4 / 3, + width: cardWidth, + padding: EdgeInsets.symmetric( + horizontal: cardWidth - imageWidth - 1, + vertical: cardWidth - imageWidth - 1, + ), + alignment: Alignment.topCenter, + child: Container( + height: imageWidth * 4 / 3, + child: _CardBuilder( + cardPack, card, memoryImage, onCardTap)), + ); + }), + ), + ), + ); + }); + } +} + +class _CardsViewController extends ChangeNotifier { + bool _expanded = false; + final ScrollController _cardsScrollController = + ScrollController(keepScrollOffset: true); + + double imageWidth = 0.0; + + void setExpanded(bool expanded) { + if (_expanded != expanded) { + _expanded = expanded; + notifyListeners(); + } + } + + void toggleExpanded() => setExpanded(!_expanded); + + _CardsViewController(); + + Future animateTo(int index) { + final millis = + ((_cardsScrollController.offset / imageWidth - index).abs() * 30.0) + .ceil(); + return _cardsScrollController.animateTo( + imageWidth * index, + duration: Duration(milliseconds: millis), + curve: Curves.easeInOut, + ); + } + + void jumpTo(int index) { + final millis = + ((_cardsScrollController.offset / imageWidth - index).abs() * 30.0) + .ceil(); + return _cardsScrollController.jumpTo( + imageWidth * index, + ); + } +} diff --git a/lib/widgets/card_pack/buy_pack_page.dart b/lib/widgets/card_pack/buy_pack_page.dart new file mode 100644 index 0000000..b2d0664 --- /dev/null +++ b/lib/widgets/card_pack/buy_pack_page.dart @@ -0,0 +1,171 @@ +import 'dart:convert'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import '../items/builder.dart'; + +import '../../di/locator.dart'; +import '../../domain/router/app_router.dart'; +import '../../theme/themes.dart'; +import 'card_pack_header.dart'; + +class BuyPack extends StatelessWidget { + final CardPackBuyDto dto; + + const BuyPack(this.dto); + + @override + Widget build(BuildContext context) { + return Container( + // color: dto.color?.asColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CardPackHeader(dto.title, dto.subtitle, dto.color?.asColor), + Expanded( + child: Column( + children: [ + Padding( + padding: + EdgeInsets.symmetric(horizontal: 10.0.w, vertical: 18.h), + child: LayoutBuilder(builder: (context, constraints) { + final cardWidth = constraints.maxWidth / 3; + final imageWidth = cardWidth * 0.9; + return SizedBox( + height: 150.h, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: dto.cards.length, + itemExtent: cardWidth, + itemBuilder: (BuildContext context, int index) { + final card = dto.cards[index]; + if (card.image != null) { + return Container( + alignment: Alignment.center, + width: imageWidth, + margin: EdgeInsets.symmetric( + horizontal: cardWidth - imageWidth - 1, + vertical: 1, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: borderGray, + ), + ), + child: Image.memory(base64Decode(card.image!)), + ); + } + return SizedBox.shrink(); + }, + ), + ); + }), + ), + SizedBox( + height: 32.h, + ), + ...?dto.items?.build(), + ], + ), + ), + _BuyButton(dto), + SizedBox( + height: 16.h, + ), + ], + ), + ); + } +} + +class _BuyButton extends StatelessWidget { + final CardPackBuyDto dto; + + _BuyButton(this.dto); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: 5.0.h, + horizontal: 10.w, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + InkWell( + onTap: () async { + if (locator.userManager.hasUser) { + final result = locator.purchaseService.buyPack(dto); + } else { + AppRouter.openAuth(); + } + }, + child: Container( + height: 120.h, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: borderGray), + borderRadius: BorderRadius.circular(16.0), + ), + padding: EdgeInsets.all(10.0.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Купить', + style: TextStyle( + fontSize: 25.sp, + fontWeight: FontWeight.w500, + height: 0.85, + ), + ), + SizedBox( + height: 8.0.h, + ), + if (dto.price != null) + Text( + dto.price!, + style: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w400, + height: 1.0, + ), + ), + ], + ), + ), + ), + TextButton( + style: ButtonStyle( + overlayColor: + MaterialStateColor.resolveWith((states) => Colors.white), + ), + onPressed: () async { + if (locator.userManager.hasUser) { + await locator.purchaseService.buyPackWithYooMoney(dto, context); + } else { + AppRouter.openAuth(); + } + }, + child: Text( + 'Оплатить через YooMoney', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + decoration: TextDecoration.underline, + color: borderGray, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/card_pack/card_pack_header.dart b/lib/widgets/card_pack/card_pack_header.dart new file mode 100644 index 0000000..a6cd2e4 --- /dev/null +++ b/lib/widgets/card_pack/card_pack_header.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import '../header.dart'; + +class CardPackHeader extends StatelessWidget { + final String? title; + final String? subtitle; + final Color? color; + + CardPackHeader(this.title, this.subtitle, this.color); + + CardPackHeader.fromDto(CardPackDto? dto) + : title = dto?.title, + subtitle = dto?.subtitle, + color = dto?.color?.asColor; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Header( + title, + subtitle: subtitle, + popText: 'к темам', + hasPopButton: true, + ), + SizedBox( + height: 40.h, + ), + Container( + color: color ?? Colors.grey.withOpacity(0.5), + height: 1, + ) + ], + ); + } +} diff --git a/lib/widgets/card_pack/card_pack_page.dart b/lib/widgets/card_pack/card_pack_page.dart new file mode 100644 index 0000000..d4429f6 --- /dev/null +++ b/lib/widgets/card_pack/card_pack_page.dart @@ -0,0 +1,89 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:auto_route/annotations.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/domain/router/app_router.dart'; +import 'package:mnemo_cards/domain/router/app_router.gr.dart'; +import 'package:mnemo_cards/features/packs/images_holder.dart'; +import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart'; +import 'package:mnemo_cards/utils/iterable_helper.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards/widgets/game_card_widget.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:yandex_mobileads/mobile_ads.dart'; +import '../../main.dart'; +import '../items/builder.dart'; + +import '../../di/locator.dart'; +import '../../theme/themes.dart'; +import '../header.dart'; +import '../mnemo_text.dart'; +import 'available_pack.dart'; +import 'buy_pack_page.dart'; +import 'card_pack_header.dart'; + +@RoutePage() +class CardPackPage extends StatefulWidget { + final String cardPackId; + + const CardPackPage(this.cardPackId, {super.key}); + + @override + State createState() => _CardPackPage(); +} + +class _CardPackPage extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: StreamBuilder( + stream: locator.packManager.getCardPackStream(widget.cardPackId), + initialData: null, + builder: (context, snapshot) { + if (!snapshot.hasData) return _Loading(); + final response = snapshot.requireData; + if (response is CardPackBuyDto) + return BuyPack(response); + else if (response is CardPackDto) return AvailablePack(response); + return Column( + children: [ + CardPackHeader.fromDto(null), + Expanded( + child: Center( + child: Text('Error'), + ), + ), + ], + ); + }), + ), + ); + } + + @override + void initState() { + super.initState(); + } +} + +class _Loading extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + CardPackHeader.fromDto(null), + Expanded( + child: Center( + child: Text('Loading...'), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/game_card_widget.dart b/lib/widgets/game_card_widget.dart new file mode 100644 index 0000000..9dc91d1 --- /dev/null +++ b/lib/widgets/game_card_widget.dart @@ -0,0 +1,423 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_tts/flutter_tts.dart'; +import 'package:mnemo_cards/di/locator.dart'; +import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart'; +import 'package:flutter/painting.dart'; +import 'package:mnemo_cards/utils/color_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:yandex_mobileads/mobile_ads.dart'; + +import '../features/card_flipper/flip_card.dart'; +import '../features/card_flipper/flip_card_controllers.dart'; +import '../features/card_flipper/flip_side.dart'; +import '../theme/themes.dart'; +import 'image_fade.dart'; +import 'mnemo_text.dart'; + +class GameCardWidget extends StatefulWidget { + static const borderRadius = 32.0; + + final GameCardDto card; + final MemoryImage? image; + final bool showFront; + final bool isFirstCard; + final FlipCardController? flipCardController; + final Color? backColor; + + const GameCardWidget( + this.card, + this.image, + this.flipCardController, + this.showFront, + this.isFirstCard, + this.backColor, { + super.key, + }); + + @override + State createState() => _GameCardWidgetState(); +} + +class GameCardDecoration extends StatelessWidget { + final Widget child; + final double alpha; + + GameCardDecoration({required this.child, this.alpha = 1.0}); + + @override + Widget build(BuildContext context) => Container( + padding: EdgeInsets.all(8.0 * alpha), + margin: EdgeInsets.all(4.0 * alpha), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + if (alpha > 0) + BoxShadow( + color: Colors.black.withOpacity(0.25 * alpha), + blurRadius: 4.0 * alpha, + ), + ], + borderRadius: BorderRadius.circular( + 8 + (GameCardWidget.borderRadius - 8) * alpha), + border: Border.all( + color: borderGray.withOpacity(1.0 - alpha * alpha), + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + 8 + (GameCardWidget.borderRadius - 8) * alpha), + child: child, + ), + ); +} + +class _GameCardWidgetState extends State { + FlipCardController? get _flipCardController => widget.flipCardController; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + print('${widget.card.original}'); + + final frontWidget = FrontCard(widget.card, widget.image); + final backWidget = _BackWidget( + widget.card, + widget.backColor, + ); + Duration? dragStart; + Duration? dragUpdate; + + return GameCardDecoration( + child: Material( + color: Colors.transparent, + child: GestureDetector( + onVerticalDragStart: (start) { + dragStart = start.sourceTimeStamp; + }, + onVerticalDragUpdate: (update) { + dragUpdate = update.sourceTimeStamp; + }, + onVerticalDragEnd: (update) { + final pps = update.velocity.pixelsPerSecond; + if (widget.isFirstCard && + pps.dy.abs() > 2 * pps.dx.abs() && + pps.dy.abs() > 70 && + (dragStart != null && + dragUpdate != null && + (dragUpdate!.inMilliseconds - dragStart!.inMilliseconds) > + 50)) { + _flipCardController?.flipcard(); + } + }, + behavior: HitTestBehavior.deferToChild, + onDoubleTap: () async { + await locator.favoriteCardsController + .switchFavoriteCard(widget.card); + }, + child: (widget.isFirstCard) + ? FlipCard( + rotateSide: RotateSide.left, + onTapFlipping: false, + axis: FlipAxis.horizontal, + controller: _flipCardController!, + frontWidget: frontWidget, + backWidget: backWidget, + ) + : widget.showFront + ? frontWidget + : backWidget, + ), + ), + ); + } +} + +class _BackWidget extends StatelessWidget { + final GameCardDto card; + final Color? _backColor; + + const _BackWidget( + this.card, + this._backColor, + ); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + final backColor = _backColor ?? Colors.lightBlueAccent[200]; + return Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: backColor, + boxShadow: const [BoxShadow()], + borderRadius: BorderRadius.circular(32.0), + ), + child: Center( + child: MnemoText( + card.back ?? card.translation ?? '', + textStyle: theme.displayMedium!.copyWith(color: Colors.white), + maxLines: 2, + softWrap: true, + ), + ), + ); + } +} + +class FrontCard extends StatelessWidget { + final GameCardDto card; + final MemoryImage? image; + final double alpha; + + FrontCard(this.card, this.image, {super.key, this.alpha = 1.0}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme; + return StatefulBuilder(builder: (context, setState) { + return AbsorbPointer( + absorbing: alpha != 1, + child: LayoutBuilder(builder: (context, constrains) { + const targetTextAr = 10 / 9; + const targetImageAr = 16 / 9; + double ar = constrains.maxHeight / constrains.maxWidth; + if (constrains.maxHeight.isInfinite) { + ar = 16 / 9; + } + final textScale = min(1.0, ar / targetTextAr) * alpha; + final imageScale = min(1.0, pow(ar / targetImageAr, 1.5 * alpha)); + return Material( + borderOnForeground: false, + type: MaterialType.transparency, + child: Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: + EdgeInsets.all(8.0 * imageScale * imageScale * alpha), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: ar > 1 + ? MainAxisAlignment.spaceEvenly + : MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + GestureDetector( + child: MnemoText( + card.original ?? '', + textStyle: TextStyle( + fontSize: 27 * textScale, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + ), + onTap: () async { + final sp = await SharedPreferences.getInstance(); + if (sp.getBool('sound_on') != false && + card.original != null) { + FlutterTts flutterTts = FlutterTts(); + await flutterTts.setLanguage('es-ES'); + await flutterTts.setSpeechRate(0.4); + flutterTts.speak(card.original!); + } + }, + ), + if (alpha > 0) + GestureDetector( + onTap: () async { + final sp = + await SharedPreferences.getInstance(); + if (sp.getBool('sound_on') != false && + card.translation != null) { + FlutterTts flutterTts = FlutterTts(); + await flutterTts.setLanguage('ru'); + await flutterTts.setSpeechRate(0.5); + flutterTts.speak( + card.translation!, + ); + } + }, + child: MnemoText( + card.translation?.toLowerCase(), + textStyle: TextStyle( + fontSize: 20 * textScale * alpha, + fontWeight: FontWeight.w400, + color: theme.headlineSmall!.color! + .withOpacity(0.5 * alpha), + ), + maxLines: 1, + // group: smallSize, + ), + ), + if (alpha > 0 && + ar > 1 && + card.transcription != null && + card.transcriptionMnemo != null) + Padding( + padding: EdgeInsets.only(top: 4.0.h * alpha), + child: GestureDetector( + onTap: () async { + final sp = + await SharedPreferences.getInstance(); + if (sp.getBool('sound_on') != false) { + FlutterTts flutterTts = FlutterTts(); + await flutterTts.setLanguage('ru'); + await flutterTts.setSpeechRate(0.5); + flutterTts.speak( + "${card.transcription} - ${card.transcriptionMnemo}", + ); + } + }, + child: MnemoText( + "[${card.transcription}] - ${card.transcriptionMnemo}", + textStyle: TextStyle( + fontSize: 20 * textScale * alpha, + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.center, + maxLines: 2, + // group: smallSize, + ), + ), + ), + ], + ), + SizedBox( + width: constrains.maxHeight / ar * imageScale, + height: constrains.maxHeight / ar * imageScale, + child: ImageFade( + key: ValueKey(card.id), + image: image, + syncDuration: Duration.zero, + ), + ), + GestureDetector( + onTap: () async { + final sp = await SharedPreferences.getInstance(); + if (sp.getBool('sound_on') != false) { + FlutterTts flutterTts = FlutterTts(); + await flutterTts.setLanguage('ru'); + await flutterTts.setSpeechRate(0.5); + flutterTts.speak( + card.mnemo!.replaceAll(RegExp(r'[\]\[]'), '')); + } + }, + child: MnemoText( + card.mnemo, + textStyle: TextStyle( + fontSize: 20 * textScale, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + if (alpha > 0) + Align( + alignment: ar > 4 / 3 + ? Alignment.bottomCenter + : Alignment.topCenter, + child: Text( + "mnemo cards", + style: TextStyle( + fontSize: 10 * textScale, color: Colors.black26), + maxLines: 1, + // group: smallSize, + ), + ), + if (false && + alpha > 0 && + locator.favoriteCardsController.isFavoriteCard(card.id)) + Align( + alignment: + ar > 4 / 3 ? Alignment.bottomLeft : Alignment.topLeft, + child: Padding( + padding: EdgeInsets.all(8.0.w), + child: Image.asset( + "icons/heart.png", + width: 8.w, + height: 8.h, + ), + ), + ), + if (alpha > 0) + Opacity( + opacity: alpha, + child: Container( + padding: EdgeInsets.all(4.0.h * alpha), + alignment: Alignment.topRight, + child: IconButton( + icon: Icon( + Icons.volume_up, + size: (26 * textScale).toDouble(), + ), + onPressed: () async { + final sp = await SharedPreferences.getInstance(); + if (sp.getBool('sound_on') != false) { + FlutterTts flutterTts = FlutterTts(); + await flutterTts.setLanguage('es-ES'); + await flutterTts.setSpeechRate(0.5); + flutterTts.speak(card.original!); + } + }, + ), + ), + ), + ], + ), + ); + }), + ); + }); + } +} + +class _BannerWidget extends StatelessWidget { + final BoxConstraints constraints; + final String adId; + final String adKey; + late final BannerAd bannerAd; + + bool bannerLoaded = false; + + _BannerWidget(this.constraints, this.adId, this.adKey) + : super(key: ValueKey(adKey)); + + @override + Widget build(BuildContext context) { + BannerAd? bannerAd; + bool bannerLoaded = false; + return StatefulBuilder(builder: (context, setState) { + bannerAd ??= YandexAds.createBanner( + BoxConstraints( + maxHeight: constraints.maxHeight, + maxWidth: constraints.maxWidth, + ), + id: adId, + key: adKey, + onLoaded: () { + bannerLoaded = true; + setState(() {}); + }, + ); + return (bannerLoaded || true) + ? Container( + alignment: Alignment.center, + child: AdWidget( + bannerAd: bannerAd!, + ), + ) + : Center( + child: CircularProgressIndicator(), + ); + }); + } +} diff --git a/lib/widgets/header.dart b/lib/widgets/header.dart new file mode 100644 index 0000000..ac135f0 --- /dev/null +++ b/lib/widgets/header.dart @@ -0,0 +1,115 @@ +import 'dart:math'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class Header extends StatelessWidget { + final String? title; + final String? subtitle; + + final bool hasPopButton; + final String? popText; + final Widget? trail; + + Header( + this.title, { + this.subtitle, + this.hasPopButton = false, + this.popText, + this.trail, + }); + + @override + Widget build(BuildContext context) { + final topPadding = 64.h - + MediaQuery.of(context).padding.top - + (hasPopButton ? 20.0.h : 0.0); + return Container( + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: max( + topPadding / 2, + 0, + ), + ), + if (hasPopButton) + Padding( + padding: EdgeInsets.only(left: 10.0.w), + child: TapRegion( + onTapInside: AutoRouter.of(context).maybePop, + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: 30.h, + child: Row( + children: [ + Image.asset( + 'icons/back.png', + color: Colors.black, + width: 17, + height: 23.h, + ), + if (popText != null) + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + popText!, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + ), + ), + ), + ], + ), + ), + ), + ), + SizedBox( + height: max( + topPadding / 2, + 0, + ), + ), + Container( + padding: EdgeInsets.only(left: 20.0.w, right: 24.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (title != null) + Text( + title!, + style: TextStyle( + fontSize: 36.sp, + fontWeight: FontWeight.w500, + height: 0.85, + ), + textAlign: TextAlign.start, + ), + if (trail != null) trail!, + ], + ), + ), + if (subtitle != null) + Container( + padding: EdgeInsets.only(top: 4.0.h, left: 22.0.w), + alignment: Alignment.centerLeft, + child: Text( + subtitle!, + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.w300, + height: 0.85, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/image_fade.dart b/lib/widgets/image_fade.dart new file mode 100644 index 0000000..255670e --- /dev/null +++ b/lib/widgets/image_fade.dart @@ -0,0 +1,355 @@ +import 'package:flutter/widgets.dart'; +import 'dart:ui' as ui; + +/// Signature used by [ImageFade.errorBuilder] to build the widget that will be displayed +/// if an error occurs while loading an image. +typedef ImageFadeErrorBuilder = Widget Function( + BuildContext context, + Object exception, +); + +/// Signature used by [ImageFade.loadingBuilder] to build the widget that will be displayed +/// while an image is loading. `progress` returns a value between 0 and 1 indicating load progress. +typedef ImageFadeLoadingBuilder = Widget Function( + BuildContext context, + double progress, + ImageChunkEvent? chunkEvent, +); + +/// A widget that displays a [placeholder] widget while a specified [image] loads, +/// then cross-fades to the loaded image. Can optionally display loading progress +/// and errors. +/// +/// If [image] is subsequently changed, it will cross-fade to the new image once it +/// finishes loading. +/// +/// Setting [image] to null will cross-fade back to the [placeholder]. +/// +/// ```dart +/// ImageFade( +/// placeholder: Image.asset('assets/myPlaceholder.png'), +/// image: NetworkImage('https://backend.example.com/image.png'), +/// ) +/// ``` +class ImageFade extends StatefulWidget { + /// Creates a widget that displays a [placeholder] widget while a specified [image] loads, + /// then cross-fades to the loaded image. + const ImageFade({ + Key? key, + this.placeholder, + this.image, + this.curve = Curves.linear, + this.duration = const Duration(milliseconds: 300), + this.syncDuration, + this.width, + this.height, + this.scale = 1, + this.fit = BoxFit.scaleDown, + this.alignment = Alignment.center, + this.repeat = ImageRepeat.noRepeat, + this.matchTextDirection = false, + this.excludeFromSemantics = false, + this.semanticLabel, + this.loadingBuilder, + this.errorBuilder, + }) : super(key: key); + + /// Widget layered behind the loaded images. Displayed when [image] is null or is loading initially. + final Widget? placeholder; + + /// The image to display. Subsequently changing the image will fade the new image over the previous one. + final ImageProvider? image; + + /// The curve of the fade-in animation. + final Curve curve; + + /// The duration of the fade-in animation. + final Duration duration; + + /// An optional duration for fading in a synchronously loaded image (ex. from memory), error, or placeholder. + /// For example, you could set this to `Duration.zero` to immediately display images that are already loaded. + /// If omitted, [duration] will be used. + final Duration? syncDuration; + + /// The width to display at. See [Image.width] for more information. + final double? width; + + /// The height to display at. See [Image.height] for more information. + final double? height; + + /// The scale factor for drawing this image at its intended size. See [RawImage.scale] for more information. + final double scale; + + /// How to draw the image within its bounds. Defaults to [BoxFit.scaleDown]. See [Image.fit] for more information. + final BoxFit fit; + + /// How to align the image within its bounds. See [Image.alignment] for more information. + final Alignment alignment; + + /// How to paint any portions of the layout bounds not covered by the image. See [Image.repeat] for more information. + final ImageRepeat repeat; + + /// Whether to paint the image in the direction of the [TextDirection]. See [Image.matchTextDirection] for more information. + final bool matchTextDirection; + + /// Whether to exclude this image from semantics. See [Image.excludeFromSemantics] for more information. + final bool excludeFromSemantics; + + /// A Semantic description of the image. See [Image.semanticLabel] for more information. + final String? semanticLabel; + + /// A builder that specifies the widget to display while an image is loading. + /// See [ImageFadeLoadingBuilder] for more information. + final ImageFadeLoadingBuilder? loadingBuilder; + + /// A builder that specifies the widget to display if an error occurs while an image is loading. + /// This will be faded in over previous content, so you may want to set an opaque background on it. + final ImageFadeErrorBuilder? errorBuilder; + + @override + State createState() => _ImageFadeState(); +} + +class _ImageFadeState extends State with TickerProviderStateMixin { + _ImageResolver? _resolver; + Widget? _front; + Widget? _back; + + late final AnimationController _controller; + Widget? _fadeFront; + Widget? _fadeBack; + + bool? _sync; // could use onImage synchronousCall, but this is more forgiving + bool _shouldBuildFront = false; + + @override + void initState() { + _controller = AnimationController(vsync: this); + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Can't call this in initState because createLocalImageConfiguration throws errors: + _update(context); + } + + @override + void didUpdateWidget(ImageFade old) { + // not called on init + super.didUpdateWidget(old); + _update(context, old); + } + + void _update(BuildContext context, [ImageFade? old]) { + final ImageProvider? image = widget.image; + final ImageProvider? oldImage = old?.image; + if (image == oldImage) return; + + _back = null; + _shouldBuildFront = false; + + if (_resolver != null) { + // move previous loaded image to back & cancel any active loads. + if (_resolver!.complete) _back = _fadeBack = _front; + _resolver!.dispose(); + } + + // load the new image: + _front = _sync = null; + _resolver = image == null + ? null + : _ImageResolver( + image, + context, + onError: _handleComplete, + onComplete: _handleComplete, + width: widget.width, + height: widget.height, + ); + + // start transition to placeholder if there's no new image: + if (_back != null && _resolver == null) _buildTransition(); + } + + void _handleComplete(_ImageResolver resolver) { + if (_sync == null) _sync = true; + // defer building the front content until build so we have an active context. + setState(() => _shouldBuildFront = true); + } + + void _buildFront(BuildContext context) { + _shouldBuildFront = false; + _ImageResolver resolver = _resolver!; + _front = resolver.error + ? widget.errorBuilder?.call(context, resolver.exception!) + : _getImage(resolver.image); + _buildTransition(); + } + + void _buildTransition() { + final bool out = _front == null; // no new image + + // use the "fast" duration if sync load, error, or placeholder: + bool fast = (_sync != false || _resolver?.error == true || out); + Duration duration = (fast ? widget.syncDuration : null) ?? widget.duration; + + // Fade in for duration, out for 1/2 as long: + _controller.duration = duration * (out ? 1 : 3 / 2); + + _fadeFront = _buildFade( + child: _front, + opacity: CurvedAnimation( + parent: _controller, + curve: Interval(0.0, 2 / 3, curve: widget.curve), + ), + ); + + _fadeBack = _buildFade( + child: _back, + opacity: Tween(begin: 1.0, end: 0).animate( + CurvedAnimation( + parent: _controller, + curve: Interval(out ? 0.0 : 2 / 3, 1.0), + ), + ), + ); + + if (_front != null || _back != null) _controller.forward(from: 0); + } + + Widget? _buildFade({Widget? child, required Animation opacity}) { + if (child == null) return null; + // if the child is a loaded image, we can fade its opacity directly for better performance: + return (child is RawImage) + ? _getImage(child.image, opacity: opacity) + : FadeTransition(child: child, opacity: opacity); + } + + RawImage _getImage(ui.Image? image, {Animation? opacity}) { + return RawImage( + image: image, + width: widget.width, + height: widget.height, + scale: widget.scale, + fit: widget.fit, + alignment: widget.alignment, + repeat: widget.repeat, + matchTextDirection: widget.matchTextDirection, + opacity: opacity, + ); + } + + @override + Widget build(BuildContext context) { + if (_sync == null) _sync = false; + if (_shouldBuildFront) _buildFront(context); + Widget? front = _fadeFront, back = _fadeBack; + + bool inLoad = _resolver != null && !_resolver!.complete; + if (inLoad && widget.loadingBuilder != null) { + _ImageResolver resolver = _resolver!; + front = AnimatedBuilder( + animation: resolver.notifier, + builder: (_, __) => widget.loadingBuilder!( + context, + resolver.notifier.value, + resolver.chunkEvent, + ), + ); + } + + List kids = []; + if (widget.placeholder != null) kids.add(widget.placeholder!); + if (back != null) kids.add(back); + if (front != null) kids.add(front); + + Widget content = Container( + width: widget.width, + height: widget.height, + child: kids.isEmpty + ? null + : Stack(fit: StackFit.passthrough, children: kids), + ); + + if (widget.excludeFromSemantics) return content; + + String? label = widget.semanticLabel; + return Semantics( + container: label != null, + image: true, + label: label ?? "", + child: content, + ); + } + + @override + void dispose() { + _resolver?.dispose(); + _controller.dispose(); + super.dispose(); + } +} + +// Simplifies working with image loading events and states. +class _ImageResolver { + _ImageResolver( + ImageProvider provider, + BuildContext context, { + required this.onComplete, + required this.onError, + double? width, + double? height, + }) { + Size? size = width != null && height != null ? Size(width, height) : null; + ImageConfiguration config = + createLocalImageConfiguration(context, size: size); + _listener = ImageStreamListener(_handleComplete, + onChunk: _handleProgress, onError: _handleError); + _stream = provider.resolve(config); + _stream.addListener(_listener); // Called sync if already completed. + notifier = ValueNotifier(0); + } + + Object? exception; + ImageChunkEvent? chunkEvent; + late final ValueNotifier notifier; + + final Function(_ImageResolver resolver) onComplete; + final Function(_ImageResolver resolver) onError; + + late final ImageStream _stream; + late final ImageStreamListener _listener; + ImageInfo? _imageInfo; + bool _complete = false; + + ui.Image? get image => _imageInfo?.image; + + bool get complete => _complete; + + bool get error => exception != null; + + void _handleComplete(ImageInfo imageInfo, bool sync) { + _imageInfo = imageInfo; + _complete = true; + onComplete(this); + } + + void _handleProgress(ImageChunkEvent event) { + chunkEvent = event; + notifier.value = event.expectedTotalBytes != null + ? event.cumulativeBytesLoaded / event.expectedTotalBytes! + : 0.0; + } + + void _handleError(Object exc, StackTrace? _) { + exception = exc; + _complete = true; + onError(this); + } + + void dispose() { + _stream.removeListener(_listener); + } +} diff --git a/lib/widgets/items/builder.dart b/lib/widgets/items/builder.dart new file mode 100644 index 0000000..8c5d161 --- /dev/null +++ b/lib/widgets/items/builder.dart @@ -0,0 +1,67 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:mnemo_cards/theme/themes.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +class ItemBuilder { + static Widget build(Item item) { + switch (item.type) { + case ItemType.text: + return TextItemBuilder(item as TextItem); + default: + log('Undefined item type ${item.type} ${item.id}'); + return SizedBox.shrink(); + } + } +} + +extension ItemExt on Item { + Widget build() => ItemBuilder.build(this); +} + +extension ItemIterableExt on Iterable { + Iterable build() => map((e) => e.build()); +} + +class TextItemBuilder extends StatelessWidget { + final TextItem item; + + TextItemBuilder(this.item); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: item.hasBorder + ? Border.all( + color: item.color?.asColor ?? borderGray, + ) + : null, + ), + child: Column( + children: [ + if (item.title != null) + Text( + item.title!, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 24, + color: Colors.black, + ), + ), + if (item.subtitle != null) + Text( + item.subtitle!, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black, + ), + ) + ], + ), + ); + } +} diff --git a/lib/widgets/mnemo_text.dart b/lib/widgets/mnemo_text.dart new file mode 100644 index 0000000..48d089f --- /dev/null +++ b/lib/widgets/mnemo_text.dart @@ -0,0 +1,84 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; + +class MnemoText extends StatelessWidget { + final String? mnemo; + final TextStyle? textStyle; + final TextAlign? textAlign; + final Color? color; + final AutoSizeGroup? group; + final int? maxLines; + final bool? softWrap; + + const MnemoText( + this.mnemo, { + this.textStyle, + this.textAlign, + this.color, + this.group, + this.maxLines, + this.softWrap, + super.key, + }); + + @override + Widget build(BuildContext context) { + if (mnemo == null) return SizedBox.shrink(); + final matches = RegExp(r'\{(.*?)\}').allMatches(mnemo!); + Color? color = this.color; + color ??= Colors.red; + + List spans = []; + + RegExpMatch? lastMatch; + + for (final match in matches) { + var mnemoText = matches.isEmpty + ? '' + : mnemo! + .substring(match.start, match.end) + .replaceAll(RegExp(r'[\}\{]'), ''); + if (mnemoText.startsWith('#')) { + // #123456mnemo + color = mnemoText.substring(0, 7).asColor; + mnemoText = mnemoText.substring(7); + } + spans.addAll([ + if (lastMatch == null) + TextSpan(text: mnemo!.substring(0, match.start), style: textStyle) + else + TextSpan( + text: mnemo!.substring(lastMatch.end, match.start), + style: textStyle), + TextSpan( + text: mnemoText, + style: textStyle?.copyWith(color: color) ?? TextStyle(color: color), + ), + ]); + lastMatch = match; + } + + if (lastMatch == null) { + spans.add( + TextSpan(text: mnemo, style: textStyle), + ); + } else { + spans.add(TextSpan( + text: mnemo!.substring(lastMatch.end), + style: textStyle, + )); + } + + return AutoSizeText.rich( + maxLines: maxLines, + softWrap: softWrap, + TextSpan( + children: spans, + ), + textAlign: textAlign ?? TextAlign.center, + group: group, + ); + } +} diff --git a/lib/widgets/packs_menu_widget.dart b/lib/widgets/packs_menu_widget.dart new file mode 100644 index 0000000..c431034 --- /dev/null +++ b/lib/widgets/packs_menu_widget.dart @@ -0,0 +1,386 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/features/packs/pack_manager.dart'; +import 'package:mnemo_cards/managers/user_state_holder.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards/widgets/header.dart'; +import 'package:mnemo_cards/widgets/shared_pref_button.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../admin/add_package.dart'; +import '../di/locator.dart'; +import '../domain/router/app_router.dart'; +import '../domain/router/app_router.gr.dart'; +import '../flags.dart'; +import '../managers/repository/repository.dart'; +import '../theme/themes.dart'; +import 'package:collection/collection.dart'; + +class PacksMenuWidget extends StatefulWidget { + final Repository repository; + final PackManager packManager; + final ScrollController? scrollController; + + const PacksMenuWidget( + this.repository, + this.packManager, { + this.scrollController, + super.key, + }); + + @override + State createState() => _PacksMenuWidgetState(); +} + +class _PacksMenuWidgetState extends State { + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final cardHeight = 130.h; + final theme = Theme.of(context); + + return StreamBuilder( + stream: widget.packManager.packsPreviewStream.asyncMap((packs) async { + if (packs != null) { + await _precache(packs); + } + return packs; + }), + builder: (context, snapshot) { + return Column( + children: [ + Header( + 'Темы', + trail: GestureDetector( + onTap: () { + AppRouter.openAuthOrProfile(); + }, + child: Image.asset( + 'icons/profile.png', + width: 27.w, + height: 27.h, + ), + ), + ), + if (ADMIN_BUILD) + Column( + children: [ + GestureDetector( + onTap: () { + locator.packManager.clearCache(); + }, + child: Container( + alignment: Alignment.center, + color: Colors.white, + margin: EdgeInsets.all(8.0), + child: Text('Clear cache'), + height: 40.h, + ), + ), + SharedPrefButton( + enabledWidget: Text('PROD'), + disabledWidget: Text('TEST'), + spKey: 'env', + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: RefreshIndicator( + onRefresh: locator.previewPackPoller.poll, + child: ListView( + scrollDirection: Axis.vertical, + children: [ + if (snapshot.data == null) + ...[1, 2, 3, 4].map( + (e) => _PackButton4Shimmer( + constraints.maxWidth, + cardHeight, + ), + ), + if (snapshot.hasData) + ...snapshot.data! + .map((pack) => _PackButton4(pack, cardHeight)), + ], + ), + ), + ), + ), + ], + ); + }, + ); + }); + } + + Future _precache(List packs) async { + for (final pack in packs) { + if (pack.imageBase64 != null) { + try { + final bytes = base64Decode(pack.imageBase64!); + await precacheImage(MemoryImage(bytes), context); + } catch (e) { + log('${pack.id} cover not cached'); + } + } + } + } +} + +class _PackButton4 extends StatelessWidget { + final CardPackPreviewDto pack; + final double cardHeight; + + const _PackButton4( + this.pack, + this.cardHeight, + ); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return InkWell( + onTap: () { + AutoRouter.of(context).push( + PageRouteInfo( + CardPackPage.name, + args: CardPackPageArgs(cardPackId: pack.id), + ), + ); + }, + child: Container( + margin: EdgeInsets.all(4.0.h), + alignment: Alignment.center, + height: cardHeight, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: pack.color?.asColor?.withOpacity(0.9) ?? + Colors.grey.withOpacity(0.5), + ), + borderRadius: BorderRadius.circular(12.0), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + decoration: BoxDecoration( + // border: Border.all( + // color: pack.dto.color?.asColor?.withOpacity(0.9) ?? + // Colors.grey.withOpacity(0.5), + // ), + // borderRadius: BorderRadius.circular(12.0), + image: pack.imageBase64 == null + ? null + : DecorationImage( + image: MemoryImage( + base64Decode(pack.imageBase64!), + ), + ), + ), + margin: EdgeInsets.all(2.0.w), + width: cardHeight, + height: cardHeight, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + pack.title, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 24.sp, + height: 0.9, + ), + ), + ), + if (pack.subtitle != null && pack.subtitle!.isNotEmpty) + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + pack.subtitle!, + style: TextStyle( + fontWeight: FontWeight.w300, + fontSize: 16.sp, + ), + ), + ), + Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Text( + pack.cards.toString(), + style: TextStyle( + fontWeight: FontWeight.w200, + fontSize: 15.sp, + ), + ), + Image.asset( + 'icons/cards.png', + width: 18.w, + height: 16.h, + ) + ], + ), + ), + if (pack.price != null && !pack.isAvailable) + Text( + pack.price!, + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 20.sp, + ), + ), + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _PackButton4Shimmer extends StatelessWidget { + final double cardHeight; + final double cardWidth; + + const _PackButton4Shimmer( + this.cardWidth, + this.cardHeight, + ); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.white, + highlightColor: Colors.white, + child: Container( + alignment: Alignment.center, + height: cardHeight, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: Colors.grey.withOpacity(0.5), + ), + borderRadius: BorderRadius.circular(12.0), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + color: Colors.white, + width: cardHeight, + height: cardHeight, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 30, + width: (cardWidth - cardHeight) * 0.7, + color: Colors.white, + ), + Container( + height: 20, + width: (cardWidth - cardHeight) * 0.5, + color: Colors.white, + ), + Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 20, + width: (cardWidth - cardHeight) * 0.1, + color: Colors.white, + ), + Container( + height: 25, + width: (cardWidth - cardHeight) * 0.1, + color: Colors.white, + ), + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _PackButtonShimmer extends StatelessWidget { + final double cardWidth; + final double cardHeight; + + const _PackButtonShimmer( + this.cardWidth, + this.cardHeight, + ); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.white, + highlightColor: Colors.grey[200]!, + child: Container( + width: cardWidth, + height: cardHeight, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + alignment: Alignment.center, + width: cardWidth, + height: cardWidth, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.0), + )), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + color: Colors.white, + ), + width: cardWidth * 0.7, + height: 30, + alignment: Alignment.center, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/shared_pref_button.dart b/lib/widgets/shared_pref_button.dart new file mode 100644 index 0000000..188203e --- /dev/null +++ b/lib/widgets/shared_pref_button.dart @@ -0,0 +1,70 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPrefButton extends StatefulWidget { + final Function(bool)? onClick; + final String spKey; + final Widget enabledWidget; + final Widget disabledWidget; + final bool toggleOnTap; + + SharedPrefButton({ + required this.enabledWidget, + required this.disabledWidget, + required this.spKey, + this.toggleOnTap = true, + this.onClick, + }); + + @override + State createState() => _SharedPrefButtonState(); +} + +class _SharedPrefButtonState extends State { + SharedPreferences? _sp; + bool value = false; + + StreamSubscription? _streamSubscription; + + @override + void dispose() { + _streamSubscription?.cancel(); + _streamSubscription = null; + super.dispose(); + } + + @override + void initState() { + super.initState(); + SharedPreferences.getInstance().then((v) { + _sp = v; + value = _sp!.getBool(widget.spKey) ?? false; + if (mounted) { + setState(() {}); + } + }); + _streamSubscription = Stream.periodic(const Duration(milliseconds: 500), + (_) => _sp?.getBool(widget.spKey) ?? false).distinct().listen((v) { + value = v; + if (mounted) { + setState(() {}); + } + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.toggleOnTap + ? () { + value = !value; + _sp?.setBool(widget.spKey, value); + setState(() {}); + } + : null, + child: value ? widget.enabledWidget : widget.disabledWidget, + ); + } +} diff --git a/lib/widgets/tests_menu_widget.dart b/lib/widgets/tests_menu_widget.dart new file mode 100644 index 0000000..08c2b0a --- /dev/null +++ b/lib/widgets/tests_menu_widget.dart @@ -0,0 +1,269 @@ +import 'dart:developer'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/features/tests/test_manager.dart'; +import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart'; +import 'package:mnemo_cards/utils/string_helper.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../domain/router/app_router.gr.dart'; +import '../managers/repository/repository.dart'; +import '../theme/themes.dart'; +import 'header.dart'; + +class TestsMenuWidget extends StatefulWidget { + final Repository repository; + final TestManager testManager; + final ScrollController? scrollController; + + const TestsMenuWidget( + this.repository, + this.testManager, { + this.scrollController, + super.key, + }); + + @override + State createState() => _TestsMenuWidgetState(); +} + +class _TestsMenuWidgetState extends State { + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final cardWidth = 125.w; + final cardHeight = cardWidth; + final theme = Theme.of(context); + + return FutureBuilder( + future: widget.testManager.loadTests(), + builder: (context, snapshot) { + return Container( + alignment: Alignment.topCenter, + child: SingleChildScrollView( + controller: widget.scrollController, + physics: AlwaysScrollableScrollPhysics(), + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + children: [ + Header('Test'), + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + height: cardHeight + 8, + child: ListView( + padding: EdgeInsets.all(4.0), + scrollDirection: Axis.horizontal, + children: [ + if (snapshot.data == null) + ...[1, 2, 3, 4].map( + (e) => _PackButtonShimmer( + cardWidth, + cardHeight, + ), + ), + if (snapshot.hasData) + ...snapshot.data!.map((dto) => + _TestButton(dto, cardWidth, cardHeight)), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }); + } + + Future _precache(List packs) async { + for (final pack in packs) { + // if (pack.dto != null) { + // try { + // await precacheImage(MemoryImage(pack.cover!), context); + // } catch (e) { + // log('${pack.dto.id} cover not cached'); + // } + // } + // for (final card in pack.cards.take(3)) { + // try { + // await precacheImage(card.image, context); + // } catch (_) { + // log('${pack.dto.id} - ${card.dto.id} image not cached'); + // } + // } + } + } +} + +class _CategoryTitle extends StatelessWidget { + final String text; + + _CategoryTitle(this.text); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).textTheme.titleLarge?.copyWith( + fontSize: 28.sp, + fontWeight: FontWeight.w800, + color: menuBlue, + ); + return Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.only(left: 16.0), + child: Text( + text, + style: theme, + textAlign: TextAlign.start, + ), + ); + } +} + +class _TestButton extends StatelessWidget { + final TestDto dto; + final double cardWidth; + final double cardHeight; + + const _TestButton( + this.dto, + this.cardWidth, + this.cardHeight, + ); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 5.0), + child: InkWell( + onTap: () { + AutoRouter.of(context).push( + PageRouteInfo(TestPage.name, args: TestPageArgs(testId: dto.id!)), + ); + }, + child: Container( + width: cardWidth, + height: cardHeight, + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: dto.color?.asColor, + borderRadius: BorderRadius.circular(12.0), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4.0, + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 7, + child: (dto.cover != null) + ? TestImageWidget(TestImage.image(dto.cover!)) + : SizedBox.shrink(), + ), + Expanded( + flex: 3, + child: Column( + children: [ + Text( + '18', + style: textTheme.headlineSmall + ?.copyWith(fontWeight: FontWeight.w800), + ), + Text('из', + style: textTheme.labelLarge + ?.copyWith(fontWeight: FontWeight.w800)), + Text( + '32', + style: textTheme.headlineSmall + ?.copyWith(fontWeight: FontWeight.w800), + ), + ], + ), + ) + ], + ), + FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + dto.name, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + fontSize: 16.sp, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _PackButtonShimmer extends StatelessWidget { + final double cardWidth; + final double cardHeight; + + const _PackButtonShimmer( + this.cardWidth, + this.cardHeight, + ); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.white, + highlightColor: Colors.grey[200]!, + child: Container( + width: cardWidth, + height: cardHeight, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + alignment: Alignment.center, + width: cardWidth, + height: cardWidth, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.0), + )), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + color: Colors.white, + ), + width: cardWidth * 0.7, + height: 30, + alignment: Alignment.center, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/text_button.dart b/lib/widgets/text_button.dart new file mode 100644 index 0000000..fcb3a9a --- /dev/null +++ b/lib/widgets/text_button.dart @@ -0,0 +1,29 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class LinkButton extends StatelessWidget { + final VoidCallback? onPressed; + final String text; + + LinkButton(this.text, this.onPressed); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: Padding( + padding: EdgeInsets.only(top: 2.0.h, bottom: 2.0.h), + child: Text( + text, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + color: Colors.black, + decoration: TextDecoration.underline, + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..cb94631 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1353 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "2350805d7afefb0efe7acd325cb19d3ae8ba4039b906eade3807ffb69938a01f" + url: "https://pub.dev" + source: hosted + version: "1.3.33" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + archive: + dependency: "direct main" + description: + name: archive + sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + url: "https://pub.dev" + source: hosted + version: "3.5.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + auto_route: + dependency: "direct main" + description: + name: auto_route + sha256: "6cad3f408863ffff2b5757967c802b18415dac4acb1b40c5cdd45d0a26e5080f" + url: "https://pub.dev" + source: hosted + version: "8.1.3" + auto_route_generator: + dependency: "direct dev" + description: + name: auto_route_generator + sha256: ba28133d3a3bf0a66772bcc98dade5843753cd9f1a8fb4802b842895515b67d3 + url: "https://pub.dev" + source: hosted + version: "8.0.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.dev" + source: hosted + version: "2.4.9" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: e461ea9ab23959102a780efcbccfe33c2ac46269928bc57093bbc0b526afc801 + url: "https://pub.dev" + source: hosted + version: "4.17.3" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "2e0b8db9a759ffc71086019f1bd27237e5e888ab1e99c507067ff8616acdfa24" + url: "https://pub.dev" + source: hosted + version: "6.2.3" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "37b6974bef5b0a7ecd31037ffb7d7bfe6bb9d2ac6c064fbea395411ef0a64d55" + url: "https://pub.dev" + source: hosted + version: "3.12.3" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + copy_with_extension: + dependency: transitive + description: + name: copy_with_extension + sha256: fbcf890b0c34aedf0894f91a11a579994b61b4e04080204656b582708b5b1125 + url: "https://pub.dev" + source: hosted + version: "5.0.4" + copy_with_extension_gen: + dependency: "direct main" + description: + name: copy_with_extension_gen + sha256: "51cd11094096d40824c8da629ca7f16f3b7cea5fc44132b679617483d43346b0" + url: "https://pub.dev" + source: hosted + version: "5.0.4" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + dio: + dependency: "direct main" + description: + name: dio + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + url: "https://pub.dev" + source: hosted + version: "5.4.3+1" + dot_navigation_bar: + dependency: "direct main" + description: + name: dot_navigation_bar + sha256: "753e1d91644e39beddd0a4ed7e366f37a95e38cafb601c3b7496120ae0532f63" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + url: "https://pub.dev" + source: hosted + version: "8.0.3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "372d94ced114b9c40cb85e18c50ac94a7e998c8eec630c50d7aec047847d27bf" + url: "https://pub.dev" + source: hosted + version: "2.31.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + url: "https://pub.dev" + source: hosted + version: "5.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9" + url: "https://pub.dev" + source: hosted + version: "2.17.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_colorpicker: + dependency: "direct main" + description: + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + url: "https://pub.dev" + source: hosted + version: "2.0.19" + flutter_screenutil: + dependency: "direct main" + description: + name: flutter_screenutil + sha256: "8cf100b8e4973dc570b6415a2090b0bfaa8756ad333db46939efc3e774ee100d" + url: "https://pub.dev" + source: hosted + version: "5.9.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_tts: + dependency: "direct main" + description: + name: flutter_tts + sha256: aed2a00c48c43af043ed81145fd8503ddd793dafa7088ab137dbef81a703e53d + url: "https://pub.dev" + source: hosted + version: "4.0.2" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct main" + description: + name: freezed + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 + url: "https://pub.dev" + source: hosted + version: "2.5.2" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "9482364c9f8b7bd36902572ebc3a7c2b5c8ee57a9c93e6eb5099c1a9ec5265d8" + url: "https://pub.dev" + source: hosted + version: "0.3.1+1" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: "0b8787cb9c1a68ad398e8010e8c8766bfa33556d2ab97c439fb4137756d7308f" + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: "7647893c65e6720973f0e579051c8f84b877b486614d9f70a404259c41a4632e" + url: "https://pub.dev" + source: hosted + version: "6.1.23" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: a058c9880be456f21e2e8571c1126eaacd570bdc5b6c6d9d15aea4bdf22ca9fe + url: "https://pub.dev" + source: hosted + version: "5.7.6" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: fc0f14ed45ea616a6cfb4d1c7534c2221b7092cc4f29a709f0c3053cc3e821bd + url: "https://pub.dev" + source: hosted + version: "0.12.4" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_certificate_pinning: + dependency: "direct main" + description: + name: http_certificate_pinning + sha256: "12b4848113d50c570af93e94bed64004caba14dde87719310d1e4556686c0afa" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + url: "https://pub.dev" + source: hosted + version: "4.1.7" + in_app_purchase: + dependency: "direct main" + description: + name: in_app_purchase + sha256: "960f26a08d9351fb8f89f08901f8a829d41b04d45a694b8f776121d9e41dcad6" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + in_app_purchase_android: + dependency: transitive + description: + name: in_app_purchase_android + sha256: "25b4bc74d4d990c18a889ea9486cb9029285955f38ca81ff335c8936edf9e66d" + url: "https://pub.dev" + source: hosted + version: "0.3.5" + in_app_purchase_platform_interface: + dependency: transitive + description: + name: in_app_purchase_platform_interface + sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + in_app_purchase_storekit: + dependency: transitive + description: + name: in_app_purchase_storekit + sha256: c13e4fee493dff3e956ebd24f80656f5ddbf876a8d12817d6fbe52d90e7c2068 + url: "https://pub.dev" + source: hosted + version: "0.3.15" + injectable: + dependency: transitive + description: + name: injectable + sha256: fb722c86cf8233008e4db41c696a6145721f45dc8aeba91103e3128c3d63c9c6 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + injectable_generator: + dependency: "direct dev" + description: + name: injectable_generator + sha256: "2ca3ada337eac0ef6b82f8049c970ddb63947738fdf32ac6cbef8d1567d7ba05" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + jailbreak_root_detection: + dependency: "direct main" + description: + name: jailbreak_root_detection + sha256: "03c7bb8ba1c12ea2b75400b152e89ae8cf50abf0c88777a5e564af4a04aeebf2" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct main" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + mnemo_cards_common: + dependency: "direct main" + description: + path: "../mnemo_cards_common" + relative: true + source: path + version: "0.0.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + no_screenshot: + dependency: "direct main" + description: + name: no_screenshot + sha256: c8621208e3e01e5b9c6d8f4611241465436618ffd3da8340bd533f8241a33fc5 + url: "https://pub.dev" + source: hosted + version: "0.0.1+6" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + url: "https://pub.dev" + source: hosted + version: "8.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.dev" + source: hosted + version: "3.0.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pie_chart: + dependency: "direct main" + description: + name: pie_chart + sha256: "58e6a46999ac938bfa1c3e5be414d6e149f037647197dca03ba3614324c12c82" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + retrofit: + dependency: transitive + description: + name: retrofit + sha256: "13a2865c0d97da580ea4e3c64d412d81f365fd5b26be2a18fca9582e021da37a" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + story: + dependency: "direct main" + description: + name: story + sha256: "0cff3c02d5ad1d9c1cf79481b8fe4a4f2f859e56b351644e96b8b209e6a110a5" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + url: "https://pub.dev" + source: hosted + version: "6.2.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: dad3313c9ead95517bb1cae5e1c9d20ba83729d5a59e5e83c0a2d66203f27f91 + url: "https://pub.dev" + source: hosted + version: "3.16.1" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 + url: "https://pub.dev" + source: hosted + version: "3.13.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + url: "https://pub.dev" + source: hosted + version: "5.5.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + yandex_mobileads: + dependency: "direct main" + description: + name: yandex_mobileads + sha256: "3c98919a96edb1c30f6407df1f599cf6ca24eb8e45e88477117d71ff3ee73147" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + yookassa_client: + dependency: "direct main" + description: + name: yookassa_client + sha256: "667d04d0e2d8c7e5180d26a3966587cc99d38fb887d318e36562a202d1adb6e2" + url: "https://pub.dev" + source: hosted + version: "1.0.2" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.2" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..40aaf1d --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,170 @@ +name: mnemo_cards +description: Mnemo +# 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+4 + +environment: + sdk: '>=3.1.3 <4.0.0' + +# 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 + + mnemo_cards_common: + path: ../mnemo_cards_common + + get_it: + shared_preferences: + rxdart: + firebase_core: ^2.30.1 +# firebase_crashlytics: +# firebase_storage: + cloud_firestore: ^4.17.2 + freezed: + json_serializable: + json_annotation: ^4.7.0 + auto_size_text: ^3.0.0 + path_provider: + google_fonts: + auto_route: + flutter_tts: ^4.0.2 + dio: ^5.3.3 + copy_with_extension_gen: ^5.0.4 + archive: ^3.4.6 + shimmer: ^3.0.0 + google_sign_in: + device_info_plus: ^9.1.2 + package_info_plus: ^8.0.0 + yandex_mobileads: ^6.1.0 + in_app_purchase: ^3.2.0 + dot_navigation_bar: ^1.0.2 + story: ^1.1.0 + flutter_screenutil: ^5.9.0 + pie_chart: ^5.4.0 + yookassa_client: ^1.0.2 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + webview_flutter: ^4.7.0 + flutter_colorpicker: ^1.1.0 + jailbreak_root_detection: ^1.1.1 + no_screenshot: ^0.0.1+6 + + file_picker: + http_certificate_pinning: ^2.1.3 + url_launcher: ^6.2.6 + + +dev_dependencies: + flutter_test: + sdk: flutter + auto_route_generator: + build_runner: + injectable_generator: + flutter_lints: ^2.0.0 + + flutter_launcher_icons: ^0.13.1 + flutter_native_splash: ^2.4.0 + +# The following section is specific to Flutter packages. +flutter: + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - images/cerdo.jpg + - assets/ + - icons/ + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "images/cerdo.jpg" + min_sdk_android: 21 # android min sdk min:16, default 21 + +flutter_native_splash: +# Only one parameter can be used, color and background_image cannot both be set. + color: "#ffffff" + #background_image: "assets/background.png" + + # Optional parameters are listed below. To enable a parameter, uncomment the line by removing + # the leading # character. + + # The image parameter allows you to specify an image used in the splash screen. It must be a + # png file and should be sized for 4x pixel density. + image: images/cerdo_big.jpg + + # The branding property allows you to specify an image used as branding in the splash screen. + # It must be a png file. It is supported for Android, iOS and the Web. For Android 12, + # see the Android 12 section below. + #branding: assets/dart.png + + # To position the branding image at the bottom of the screen you can use bottom, bottomRight, + # and bottomLeft. The default values is bottom if not specified or specified something else. + #branding_mode: bottom + + # The color_dark, background_image_dark, image_dark, branding_dark are parameters that set the background + # and image when the device is in dark mode. If they are not specified, the app will use the + # parameters from above. If the image_dark parameter is specified, color_dark or + # background_image_dark must be specified. color_dark and background_image_dark cannot both be + # set. + #color_dark: "#042a49" + #background_image_dark: "assets/dark-background.png" + #image_dark: assets/splash-invert.png + #branding_dark: assets/dart_dark.png + + # From Android 12 onwards, the splash screen is handled differently than in previous versions. + # Please visit https://developer.android.com/guide/topics/ui/splash-screen + # Following are specific parameters for Android 12+. + android_12: + # The image parameter sets the splash screen icon image. If this parameter is not specified, + # the app's launcher icon will be used instead. + # Please note that the splash screen will be clipped to a circle on the center of the screen. + # App icon with an icon background: This should be 960×960 pixels, and fit within a circle + # 640 pixels in diameter. + # App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle + # 768 pixels in diameter. + image: images/cerdo_big.jpg + + # Splash screen background color. + color: "#ffffff" + + # App icon background color. + #icon_background_color: "#111111" + + # The branding property allows you to specify an image used as branding in the splash screen. + #branding: assets/dart.png + + # The image_dark, color_dark, icon_background_color_dark, and branding_dark set values that + # apply when the device is in dark mode. If they are not specified, the app will use the + # parameters from above. + #image_dark: assets/android12splash-invert.png + #color_dark: "#042a49" + #icon_background_color_dark: "#eeeeee" + + # The android, ios and web parameters can be used to disable generating a splash screen on a given + # platform. + #android: false + #ios: false + #web: false diff --git a/some.json b/some.json new file mode 100644 index 0000000..a2adec2 --- /dev/null +++ b/some.json @@ -0,0 +1,10 @@ +{ + "admin": true, + "id": 3, + "name": null, + "purchases": [ + "27", + "28" + ], + "subscription": false +} \ No newline at end of file diff --git a/test.json b/test.json new file mode 100644 index 0000000..54fbffa --- /dev/null +++ b/test.json @@ -0,0 +1,17 @@ +{ + "cards": [ + 1, + 2, + -1, + 3, + 4, + 5, + 6 + ], + "color": "0xff8CCBFF", + "cover": "cards/pata.jpg", + "id": 91906190, + "name": "Original", + "size": 7, + "version": "10" +} \ No newline at end of file