commit ff0a034d6caf9eb616c20bda5c5634435719a10a Author: ZuoZuo <68836346+Mrz-sakura@users.noreply.github.com> Date: Thu Apr 9 21:32:23 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3dafe1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..f70d82d --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: android + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: ios + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + + # 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..4435dd3 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Aslan + +Aslan + +## 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..d4e0f0c --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..c908258 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/aliyun-repos.init.gradle.kts b/android/aliyun-repos.init.gradle.kts new file mode 100644 index 0000000..92763c7 --- /dev/null +++ b/android/aliyun-repos.init.gradle.kts @@ -0,0 +1,28 @@ +val mirrorRepos = listOf( + "https://maven.aliyun.com/repository/google", + "https://maven.aliyun.com/repository/public", + "https://maven.aliyun.com/repository/gradle-plugin", +) + +fun org.gradle.api.artifacts.dsl.RepositoryHandler.addMirrorRepos() { + mirrorRepos.forEach { url -> + maven(url) + } +} + +settingsEvaluated { + pluginManagement.repositories.apply { + addMirrorRepos() + mavenCentral() + gradlePluginPortal() + google() + } +} + +gradle.beforeProject { + buildscript.repositories.apply { + addMirrorRepos() + mavenCentral() + google() + } +} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..27d3e42 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,90 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") + id("com.google.gms.google-services") +} + +android { + namespace = "com.org.yumi" + compileSdk = 36 + ndkVersion = "28.2.13676358" + + signingConfigs { + create("release") { + storeFile = file("./yumi.jks") + storePassword = "2025abc" + keyAlias = "yumi" + keyPassword = "2025abc" + } + + getByName("debug") { + storeFile = file("./yumi_debug.jks") + storePassword = "2025abc" + keyAlias = "yumi" + keyPassword = "2025abc" + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + + defaultConfig { + multiDexEnabled = true + manifestPlaceholders["appName"] = "Yumi" + manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher" + manifestPlaceholders["appIconRound"] = "@mipmap/icon_logo_round" + applicationId = "com.org.yumi" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + + ndk { + // 指定支持的 ABI 架构 + abiFilters.add("arm64-v8a") + abiFilters.add("armeabi-v7a") + abiFilters.add("x86_64") + // 如果需要支持 x86 模拟器,可以添加 'x86', 'x86_64' + } + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + isShrinkResources = false + isZipAlignEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs["release"] + } + getByName("debug") { + isMinifyEnabled = false + signingConfig = signingConfigs["release"] + } + } +} +dependencies { + implementation("androidx.multidex:multidex:2.0.1") +// implementation(platform("com.google.firebase:firebase-bom:34.0.0")) +// implementation("com.google.firebase:firebase-auth") +// implementation("com.google.firebase:firebase-core:16.0.8") +// implementation("com.google.android.gms:play-services-auth:21.1.1") +} + + +flutter { + source = "../.." +} diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..bc69a4e --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,47 @@ +{ + "project_info": { + "project_number": "980005024266", + "project_id": "yumi-c3b30", + "storage_bucket": "yumi-c3b30.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:980005024266:android:581aa38059aa318d9c65f3", + "android_client_info": { + "package_name": "com.org.yumi" + } + }, + "oauth_client": [ + { + "client_id": "980005024266-sl5h466pe90jsjmoi4jcd7bqhmckieec.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.org.yumi", + "certificate_hash": "9ab21924bfbe2c56555860ebabf23c4099fd7412" + } + }, + { + "client_id": "980005024266-dgtthe3q98k8tk873rfdrsnu5ot61p09.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAbxU9QpbnC5PrrhUjDSK1camiotDlF3qE" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "980005024266-dgtthe3q98k8tk873rfdrsnu5ot61p09.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} diff --git a/android/app/info.txt b/android/app/info.txt new file mode 100644 index 0000000..9a39b54 --- /dev/null +++ b/android/app/info.txt @@ -0,0 +1,3 @@ +yumi.jks签名信息: +pwd:2025abc +Alias:yumi \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..fbecac5 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,39 @@ +# Flutter 基础 +-keep class io.flutter.** { *; } +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } + +# Play Core 保留 +-keep class com.google.android.play.core.** { *; } +-keep interface com.google.android.play.core.** { *; } + +# 根据 missing_rules.txt 添加的规则 +-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication +-dontwarn com.google.android.play.core.splitinstall.SplitInstallException +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest +-dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState +-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener +-dontwarn com.google.android.play.core.tasks.OnFailureListener +-dontwarn com.google.android.play.core.tasks.OnSuccessListener +-dontwarn com.google.android.play.core.tasks.Task + +# 保留这些类以确保功能正常 +-keep class com.google.android.play.core.splitcompat.** { *; } +-keep class com.google.android.play.core.splitinstall.** { *; } +-keep class com.google.android.play.core.tasks.** { *; } +-keep class io.flutter.embedding.engine.deferredcomponents.** { *; } + +# 应用特定 +-keep class com.org.yumi.** { *; } + +# Provider +-keep class * extends ChangeNotifier { *; } +-keep class * implements Listenable { *; } + +# 其他保留 +-keepattributes *Annotation* +-keepattributes Signature \ 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..8ffe024 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..09a35e3 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/tkm/likei/variant1/MainActivity.kt b/android/app/src/main/kotlin/com/tkm/likei/variant1/MainActivity.kt new file mode 100644 index 0000000..e3fe890 --- /dev/null +++ b/android/app/src/main/kotlin/com/tkm/likei/variant1/MainActivity.kt @@ -0,0 +1,13 @@ +package com.org.yumi + +import android.content.Intent +import android.net.Uri +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) // 这行必须存在! + } +} 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..1cb7aa2 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + 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..8403758 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + 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..bab4549 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-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..6f8ffa1 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-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..97304cd 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-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..1a5b3c6 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-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..181965c 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/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..360a160 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..954cc8c --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,21 @@ + + + + #FF5722 + #D84315 + #FF9800 + + + #FFFFFF + #000000 + #00000000 + + + #F5F5F5 + #FFFFFF + + + #212121 + #757575 + #BDBDBD + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..a1746ad --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Yumi + \ No newline at end of file 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..308ea4f --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..68ea404 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,18 @@ + + + + + + + + \ 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..8ffe024 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..aef8ba5 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("com.google.gms.google-services") version "4.4.3" apply false +} +buildscript { + repositories { + google() + mavenCentral() + // 使用阿里云镜像加速 + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/public") } + maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + } +} + + +allprojects { + repositories { + // 使用阿里云镜像加速 + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/public") } + maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + // 备用官方仓库 + google() + mavenCentral() + } +} + +// 以下是你的现有配置 +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} + +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..b7cda7b --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6e36e2c --- /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-8.11.1-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..04bc745 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/public") } + maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json new file mode 100644 index 0000000..b279e2f --- /dev/null +++ b/assets/l10n/intl_ar.json @@ -0,0 +1,640 @@ +{ + "signInWithGoogle": "سيج إنويسجوغل", + "or": "اور", + "signInWithYourAccount": "سيج ، إن ، فايس ، ويوا كونتي", + "signInWithApple": "سيج إنويس أبل", + "loginRepresentsAgreementTo": "تسجيل الدخول يمثل الموافقة على", + "termsofService": "تيمز بنسيفيتش", + "privaceyPolicy": "بريفاسيبوليس", + "tips": "تيبس", + "mine": "خاصتي", + "searchNoDataTips": "أدخل معرف الغرفة أو المستخدم الذي تريد البحث عنه.", + "party": "حفلة", + "event": "حدث", + "yes": "نعم", + "sound2": "صوت", + "games": "ألعاب", + "myRoom": "غرفتي", + "other": "آخر", + "startYourBrandNewJourney": "ابدأ رحلتك الجديدة تمامًا", + "maliciousHarassment": "التحرش الخبيث", + "aboutMe": "عنّي", + "bag": "حقيبة", + "roomEdit": "تحرير الغرفة", + "cancelRoomPassword": "هل أنت متأكد أنك تريد حذف كلمة مرور الغرفة؟", + "roomMemberFee": "رسوم عضو الغرفة", + "blockedList2": "قائمة المحظورين", + "roomTheme2": "موضوع الغرفة", + "roomPassword": "كلمة مرور الغرفة", + "numberOfMic": "عدد الميكروفونات", + "pleaseEnterContent": "يرجى إدخال المحتوى", + "profilePhoto": "صورة الملف الشخصي", + "noHistoricalRecordsAvailable": "لا توجد سجلات تاريخية متاحة.", + "inviteNewUsersToEarnCoins": "ادعُ مستخدمين جدد لكسب العملات", + "crateMyRoom": "أنشئ غرفتك الخاصة.", + "casualInteraction": "التفاعل العفوي", + "haveGamePlayingTips": "لديك لعبة جارية، يرجى الخروج من اللعبة الحالية أولاً، هل أنت متأكد من الخروج؟", + "historicalTour": "جولة تاريخية", + "gameCenter": "مركز الألعاب", + "returnToVoiceChat": "العودة إلى الدردشة الصوتية؟", + "game": "لعبة", + "invite": "يدعو", + "claim": "مطالبة", + "recent": "حديث", + "popularEvents": "الأحداث الشهيرة", + "exitGameMode": "الخروج من وضع اللعبة", + "enterTheRoom": "ادخل الغرفة", + "inviteGoRoomTips": "دائمًا هنا من أجلك، في المطر أو الشمس. تعال وقل مرحبًا!", + "confirmInviteThisUserToTheRoom": "هل تؤكد دعوة هذا المستخدم (المعرف: {1}) إلى الغرفة؟", + "complete": "مكتمل", + "copyLink": "نسخ الرابط", + "shareTo": "مشاركة إلى", + "faceBook": "فيسبوك", + "whatsApp": "واتساب", + "snapChat": "سناب شات", + "taskNameRoomOwnerSendRedPacket": "مالك الغرفة يرسل مظروفًا أحمر", + "taskNameRoomNewMember": "أعضاء جدد في الغرفة", + "taskNameRoomOwnerSendGiftUser": "مالك الغرفة يرسل هدية", + "taskNameRoomMicUser120Min": "الأعضاء بقضاء 120+ دقيقة على الميكروفون", + "taskNameRoomMicUser60Min": "الأعضاء بقضاء 60+ دقيقة على الميكروفون", + "taskNameRoomMicUser30Min": "الأعضاء بقضاء 30+ دقيقة على الميكروفون", + "taskNamePersonalSendGift": "أرسل هدية لمستخدم", + "taskNameRoomOnlineUserCount": "الأعضاء المتصلون في الغرفة", + "taskNameRoomOwnerInviteMic": "دعوة عضو إلى الميكروفون", + "taskNameRoomUserSendGiftGold": "العملات المهداة من الأعضاء", + "taskNameRoomOwnerSendGiftGold": "العملات المهداة من مالك الغرفة", + "taskNamePersonalMagicGiftGold": "العملات المكتسبة من إرسال هدايا سحرية", + "taskNamePersonalLuckyGiftGold": "العملات المكتسبة من إرسال هدايا الحظ", + "taskNameRoomOwnerMicTime": "مالك الغرفة يتحدث عبر الميكروفون في الغرفة", + "taskNamePersonalActiveInRoom": "كن نشطًا في غرف الآخرين", + "taskNamePersonalGameConsume": "إنفاق في اللعبة", + "taskNamePersonalMicInRoom": "اذهب إلى المايك", + "dailyCoinBonanzaRules": "قواعد مهرجان العملات اليومي", + "magic": "سحر", + "dailyCoinBonanzaRulesDetail": "1. يمكن إكمال المهام الشخصية اليومية ومهام صاحب الغرفة اليومية مرة واحدة لكل مستخدم يوميًا. يتم إعادة تعيين المهام عند الساعة 00:00:00 بتوقيت السعودية.\n2. يمكن إكمال المهام الشخصية اليومية فقط في غرف الآخرين؛ ويمكن إكمال مهام صاحب الغرفة فقط في غرفتك الخاصة.\n3. يتم احتساب مهام تقديم الهدايا فقط على الهدايا المرسلة في الغرف، وليس الهدايا المرسلة في الخلاصات.\n4. إذا تم إنشاء عدة حسابات باستخدام نفس الجهاز أو نفس بطاقة SIM بأي شكل من الأشكال، يمكن المطالبة بالمكافآت لجميع المهام مرة واحدة فقط.", + "roomOwnerTasks": "مهام صاحب الغرفة", + "personalTasks": "المهام الشخصية", + "noPromptsToday": "لا توجد مطالبات اليوم.", + "coins4": "عملات", + "getPaidToRefer": "احصل على أجر عن الإحالة", + "forMoreRewardsPleaseCheckTheTaskCenter": "لمزيد من المكافآت، يرجى التحقق من مركز المهام", + "weekStart": "بداية الأسبوع", + "kingQuuen": "ملك-ملكة", + "ramadan": "رمضان", + "like": "مثل", + "updateNow": "تحديث الآن", + "skip2": "تخطي", + "importantReminder": "تذكير مهم", + "discard": "تخلص", + "deleteCommentTips": "هل أنت متأكد أنك تريد حذف هذا التعليق؟", + "deleteSuccessful": "تم الحذف بنجاح!", + "itemsLeft": "الأصناف المتبقية", + "replySucc": "تم الرد بنجاح", + "showLess": "عرض أقل", + "showMore": "عرض المزيد", + "comment": "تعليق", + "more": "أكثر", + "swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt": "*نصيحة: اسحب إلى اليسار على منطقة الشاشة العائمة لإغلاقها بسرعة.", + "enterThisVoiceChatRoom": "هل تريد دخول هذه الغرفة الصوتية؟", + "roomAnnouncement": "إعلان الغرفة", + "saySomething": "قل شيئاً...", + "catchFirstComment": "التقط التعليق الأول", + "posting": "نشر", + "trend": "اتجاه", + "reply": "إجابة", + "customizedGiftRules": "قواعد الهدايا المخصصة", + "rulesUpload": "القواعد والرفع", + "customized": "مخصص", + "coins3": "عملات", + "customizedGiftRulesContent": "كيفية الحصول على هديتي المُخصصة:\n\n1. يتم تحديد أهلية المستخدم للحصول على هدية مُخصصة بناءً على مستوى ثروته الحالي.\n\n(1) إذا كان مستوى ثروة المستخدم ≥ 35: يمكن للمستخدم تحميل فيديو واحد فقط للهدية المُخصصة. سنستخدم صورة ملفه الشخصي الحالية كصورة للهدية، والفيديو المُقدم كتأثير على الهدية في قسم \"التخصيص\". سيستغرق الإنتاج بعض الوقت، وسنُعلمك فور توفر الهدية في المتجر.\n\n(2) إذا كان مستوى ثروة المستخدم ≥ 45: يمكن للمستخدم تحميل فيديو واحد فقط للهدية المُخصصة. سنستخدم صورة ملفه الشخصي الحالية كصورة للهدية، والفيديو المُقدم كتأثير على الهدية في قسم \"التخصيص\". سيستغرق الإنتاج بعض الوقت، وسنُعلمك فور توفر الهدية في المتجر.\n\n2. يمكنك المشاركة في أنشطة مُحددة على التطبيق، وبعد استيفاء الشروط، تواصل معنا عبر قسم \"الرسائل\" ← \"اتصل بنا\". يرجى تزويدنا بلقطات شاشة للنشاط والفيديو الذي ترغب باستخدامه في الهدية المُخصصة. سنستخدم صورة ملفك الشخصي الحالية كصورة للهدية، والفيديو المُقدم كتأثير لها، ثم نُدرجها ضمن قسم \"مُخصصة\". سيستغرق الإنتاج بعض الوقت، وسنُعلمك فور توفرها في المتجر.\n\nمدة صلاحية الهدايا المُخصصة وكيفية تمديدها:\nستكون الهدايا المُخصصة الحصرية متاحة لمدة 30 يومًا. ولتمكين المستخدمين المؤثرين والمُلهمين من الاستمرار في الاستمتاع بهذه التجربة الجديدة وعرض أسلوبهم الشخصي، يُمكن لمستخدمي الهدايا المُخصصة تمديد مدة صلاحية جميع هداياهم المُخصصة لمدة 30 يومًا إضافية عن طريق شحن رصيدهم بمبلغ 500 دولار أمريكي شهريًا.", + "clearCache": "مسح الكاش", + "multiple": "متعدد", + "areYouSureYouWantToClearLocalCache": "هل أنت متأكد أنك تريد مسح الذاكرة المؤقتة المحلية؟", + "luckGiftSpecialEffects": "تأثيرات الرسوم المتحركة للهدية السعيدة", + "receivedFromALuckyGift": "تم الاستلام من هدية محظوظة/سحرية", + "successfullyRemovedFromTheBlacklist": "تمت الإزالة من القائمة السوداء بنجاح!", + "successfullyAddedToTheBlacklist": "تمت الإضافة إلى القائمة السوداء بنجاح!", + "youAreCurrentlyCPRelationshipPleaseDissolve": "أنت حاليا في علاقة CP. الرجاء حلها أولا.", + "areYouSureToCancelBlacklist": "هل أنت متأكد من إلغاء القائمة السوداء؟", + "areYouSureYouWantToBlockThisUser": "هل أنت متأكد أنك تريد حظر هذا المستخدم؟", + "removeFromBlacklist": "إزالة من القائمة السوداء", + "moveToBlacklist": "نقل إلى القائمة السوداء", + "userBlacklist": "قائمة الحظر للمستخدمين", + "redEnvelopeNotYetClaimed": "المظروف الأحمر لم يُستلم بعد", + "redEnvelopeAmount": "مبلغ الظرف الأحمر: {1} عملات", + "followed": "تبع", + "newMessage": "رسالة جديدة", + "keep": "يحفظ", + "open": "فتح", + "clearCacheSuccessfully": "تم مسح ذاكرة التخزين المؤقت بنجاح", + "thisUserHasBeenBlacklisted": "تم إدراج هذا المستخدم في القائمة السوداء.", + "thisFeatureIsCurrentlyUnavailable": "هذه الخاصية غير متوفرة حاليا.", + "pleaseSelectTheRecipient": "المرجو اختيار المستلم", + "searchMemberIdHint": "يرجى إدخال رقم هوية العضو", + "unclaimedRedEnvelopes": "الأظرفة الحمراء غير المطالب بها يتم استردادها خلال 24 ساعة.", + "sentARedEnvelope": "أرسل ظرف أحمر.", + "theRedEnvelopeHasExpired": "الظرف الأحمر منتهي الصلاحية", + "acceptedYour": "{1} قبل طلبك", + "youAccepted": "لقد قبلت {1} ", + "openRedPackDialogTip": "الظرف الأحمر", + "wishingYouHappinessEveryDay": "أتمنى لك السعادة كل يوم.", + "createRoomSuccsess": "تم إنشاء الغرفة بنجاح!", + "contactUs": "اتصل بنا", + "specialEffectsManagement": "إدارة المؤثرات الخاصة", + "canSendMsgTips": "كلا الطرفين يجب أن يتابع بعضهما البعض قبل أن يتمكنا من إرسال رسائل خاصة.", + "systemAnnouncementTips1": "احذر الاحتيال:", + "systemAnnouncementTips": "تحقق من المعلومات عن طريق القنوات الرسمية فقط. لا تقم بتنزيل برامج من طرف ثالث، ولا تشارك البيانات الشخصية، ولا تحول المال بناءً على طلبات خارجية. بطاقات هوية الموظفين الرسمية هي فقط 10000 و10003 و10086. في حالة أي شك، توقف وقدم التقرير عبر", + "systemAnnouncement": "إعلان النظام", + "doNotClickUnfamiliarTips": "ماتضغطش على الروابط الغير معروفة، حيث قد تكشف معلوماتك الشخصية. متشاركش أبدا بطاقة الهوية أو معلومات البطاقة البنكية ديالك مع أي واحد.", + "atTag": "@تاغ", + "sayHi2": "قل مرحبا", + "msgSendRedEnvelopeTips": "سيتم فرض رسوم خدمة بنسبة 10% على المظاريف الحمراء، ولن يحصل المستلمون إلا على 90% من قيمة المظروف. يجب أن يكون مستوى ثروة المرسل أعلى من المستوى 10.", + "reapply": "أعد التقديم", + "cancelRequest": "إلغاء الطلب", + "supporter": "مشجع", + "numberOfSign": "عدد العلامة: {1}", + "hostWeeklyRank": "تصنيف المضيف الأسبوعي", + "supporterWeeklyRank": "ترتيب الأسبوعي للمشجعين", + "memberList": "قائمة الأعضاء", + "treasureChest": "صندوق الكنز", + "applicationRecord": "سجل الطلبات", + "pending": "قيد الانتظار", + "appUpdateTip": "التطبيق لديه نسخة جديدة ({1})، هل تود الذهاب وتحميلها؟", + "reconcile": "يصالح", + "separated": "منفصل", + "areYouSureYouWantToSpend5": "{1} اعترف بالمشاعر ليك؛ إذا قبلتي، غادي توليو ثنائي.", + "areYouSureYouWantToSpend6": "{1} يريد العودة للتقارب معك. إذا قررت المصالحة، سيتم استعادة جميع بياناتك السابقة.", + "reconcileInvitationTips": "إذا الطرف الآخر رفض دعوة CP، غتترجع العملات ديالك للمحفظة ديالك.", + "reconcileInvitation": "دعوة للمصالحة", + "areYouSureYouWantToSpend4": "إرسال دعوة للمصالحة لهدا المستخدم؟", + "partWaysTips": "إذا قرر أحد الشريكين في العلاقة الانفصال، سيكون هناك فترة تهدئة لمدة 7 أيام. خلال هذه الفترة، يمكن للطرفين اختيار المصالحة، وسيتم استعادة كل البيانات عند المصالحة. إذا لم يتم اختيار المصالحة بنهاية فترة السبعة أيام، سيتم مسح بيانات الزوجين.", + "areYouSureYouWantToPartWaysWithYourCP": "هل أنت متأكد أنك تريد الانفصال عن شريكك؟", + "partWays": "افترقوا", + "firstDay": "اليوم الأول:{1}", + "timeSpentTogether": "الوقت المشترك: {1} أيام", + "areYouSureYouWantToSpend3": "إذا الطرف الآخر رفض دعوة CP، غتترجع العملات ديالك للمحفظة ديالك.", + "areYouSureYouWantToSpend2": "هل تريد إرسال دعوة CP لهذا المستخدم؟", + "areYouSureYouWantToSpend": "هل أنت متأكد أنك تريد أن تنفق", + "underReview": "قيد المراجعة", + "doYouWantToDeleteIt": "هل تريد حذفه؟", + "myPhoto": "صورتي", + "balanceNotEnough": "رصيد العملات الذهبية غير كافي. واش بغيت تزود الرصيد؟", + "skip": "تخطي {1}", + "chooseFromAblum": "اختر من الألبوم", + "information": "معلومات", + "spaceBackground": "خلفية الفضاء", + "editProfile": "تعديل الملف الشخصي", + "sendTheCpRequest": "أرسل طلب CP", + "addCp": "أضف CP", + "numberOfMyCPs": "عدد نقاط السيطرة ديالي:({1}/{2})", + "relationShip": "علاقة", + "props": "الدعائم", + "couple2": "زوج", + "dice": "نرد", + "operationsAreTooFrequent": "العمليات متكررة بشكل مفرط", + "luckNumber": "رقم الحظ", + "rps": "حجر ورقة مقص", + "couple": "زوج {1}:", + "noMatchedCP": "لا يوجد CP مطابق", + "adminInviteRechargeAgent": "ندعوك لتصبح وكيلا لإعادة الشحن.", + "ownerIncomeCoins": "دخل المالك: {1} عملة", + "allGames": "كل الألعاب", + "fishClass": "فئة الأسماك", + "raceSeries": "سلسلة السباق", + "others": "آخرون", + "slotsClass": "فئة السلوتس", + "greedyClass": "الفئة الجشعة", + "hotGames": "الألعاب الرائجة", + "sent": "أُرسل", + "termsOfServicePrivacyPolicyTips": "بمتابعتك، أنت توافق على شروط الخدمة و سياسة الخصوصية", + "and": " اندر ", + "chatBox": "صندوق الشات", + "confirm": "تأكيد", + "cancel": "إلغاء", + "join": "انضم", + "vistors": "زوار", + "fans": "المعجبون", + "items": "عناصر", + "letGoToWatch": "هيا نذهب للمشاهدة!", + "launchedARocket": "أطلق صاروخ", + "sendUserId": "أرسل معرف المستخدم:{1}", + "giveUpIdentity": "تخلى عن الهوية", + "leaveRoomIdentityTips": "هل أنت متأكد من أنك تريد التخلي عن هوية الغرفة؟", + "joinMemberTips2": "هل تريد تأكيد انضمامك إلى الغرفة كعضو؟", + "sureUnfollowThisRoom": "متأكد بغيتي تحيد المتابعة لهاذ الغرفة؟", + "settings": "الإعدادات", + "alreadyAnTourist": "أنت بالفعل سائح", + "deleteConversationTips": "هل أنت متأكد أنك تريد حذف سجل المحادثة مع هذا المستخدم؟", + "account": "أكونتي", + "youHaventFollowed": "أنت لم تتابع أي غرفة", + "messageHasBeenRecalled": "تم استرجاع هذه الرسالة", + "propMessagePrompt": "موجه رسالة دعائية", + "deleteFromMyDevice": "احذف من جهازي", + "deleteOnAllDevices": "حذف على جميع الأجهزة", + "inputUserId": "أدخل معرف المستخدم", + "common": "شائع", + "useCoupontips": "هل أنت متأكد أنك تريد استخدام القسيمة؟", + "searchUserId": "ابحث عن معرف المستخدم", + "receiveSucc": "تم المطالبة بنجاح", + "reject": "رفض", + "sendUser": "إرسال المستخدم", + "bio": "معلومات شخصية", + "delete": "حذف", + "sendCoupontips": "هل أنت متأكد أنك تريد إرسال هذه القسيمة لهذا المستخدم؟", + "recallThisMessage": "تذكر هذه الرسالة؟", + "copy": "نسخ", + "youDontHaveAnyCouponsYet": "ليس لديك أي كوبونات بعد", + "hobby": "هواية", + "recall": "استدعاء", + "accept": "اقبل", + "signedin": "تم التوقيع", + "following": "التالي", + "inviteYouToBecomeBD": "ندعوك لتصبح BD", + "confirmAcceptTheInvitation": "هل تريد تأكيد قبول الدعوة؟", + "confirmDeclineTheInvitation": "تأكيد رفض الدعوة؟", + "friends": "أصدقاء", + "language": "اللغة", + "feedback": "تعليق", + "coupon": "قسيمة", + "get": "احصل", + "couponRecord": "سجل استخدام القسيمة", + "inRoom": "في الغرفة", + "inRocket": "في الصاروخ", + "roomRocketHelpTips": "1. إرسال الهدايا في الغرفة يزيد من طاقة الصاروخ. *هدية واحدة من العملات الذهبية = نقطة طاقة صاروخية واحدة؛ الهدايا المحظوظة تزيد من طاقة الصاروخ بنسبة 4% من قيمة العملة الذهبية للهدية.\n2. بعد اكتمال شحن الطاقة، يمكنك إطلاق الصاروخ. سيتم توزيع المكافآت تلقائيًا بعد إطلاق الصاروخ.\n3. تقدم فئات الصواريخ المختلفة مكافآت مختلفة.\n4. عند إطلاق الصاروخ، يمكن لجميع المستخدمين في الغرفة المطالبة بمكافأة الصاروخ.\n5. سيتم تحديث طاقة الصاروخ في الساعة 0:00 كل يوم.", + "searchCouponHint": "ابحث عن كوبون", + "search": "بحث", + "checkInSuccessful": "تسجيل الوصول ناجح", + "about": "حوالي", + "inviteYouToBecomeHost": "ندعوك لتصبح مضيفًا", + "receive": "استقبل", + "sginTips": "ستحصل على المكافأة في أول مرة تسجل دخولك فيها كل يوم. إذا قمت بقطع تسجيل دخولك، ستُحسب المكافأة من أول يوم تسجل فيه دخولك مرة أخرى.", + "aboutUs": "حولنا", + "theme": "موضوع", + "home": "المنزل", + "luck": "حظ", + "bDLeaderInviteYouToBecomeBDLeader": "ندعوك لتصبح قائد تطوير الأعمال", + "win2": "فوز {1}", + "level": "مستوى", + "wealthLevel": "مستوى الثروة", + "userLevel": "مستوى المستخدم", + "themeGoToUploadTips": "1.سيتم مراجعة التحميل خلال 24 ساعة بعد نجاحه. 2.\n سيتم إرجاع جميع العملات إذا فشلت المراجعة.", + "goToUpload": "اذهب لتحميل", + "rechargeAgency": "وكالة الشحن", + "logout": "تسجيل الخروج", + "adminCenter": "مركز الإدارة", + "explore": "استكشاف", + "micManagement": "إدارة الميكروفون", + "followList": "قائمة المتابعة", + "fansList": "قائمة المعجبين", + "pleaseSelectTheTypeContent": "يرجى اختيار نوع المحتوى المخالف", + "me": "أنا", + "socialPrivilege": "امتياز اجتماعي", + "spam": "بريد مزعج", + "inappropriateContent": "محتوى غير لائق", + "illegalInformation": "معلومات غير قانونية", + "personalAttack": "هجوم شخصي", + "identity": "الهوية", + "adjust": "تعديل", + "roomReward": "مكافأة الغرفة", + "insufhcientGoldsGoToRecharge": "الذهب غير كافي، عاود الشحن دابا!", + "received": "تم الاستلام", + "goToRecharge": "اذهب لإعادة الشحن", + "warning": "تحذير", + "ownerSendTheRedEnvelope": "المالك أرسل عملات المكافأة", + "rewardCoins": "عملات المكافأة:{1} عملة", + "lastWeekProgress": "تقدم الأسبوع الماضي", + "currentProgress": "التقدم الحالي", + "coins2": "{1} عملات", + "roomReward2": "مكافأة الغرفة:{1}", + "currentStage": "المرحلة الحالية:{1}", + "fraud": "احتيال", + "redEnvelopeTips2": "إذا لم يتم المطالبة بالمظروف الأحمر ضمن الفترة المحددة، ستُعاد العملات المتبقية إلى المستخدم الذي أرسل المظروف الأحمر.", + "remainingNumberTips": "العدد المتبقي المتاح: ({1}/{2})", + "collectionTimeTips": "وقت الجمع:{1}({2}/{3})", + "sendARedEnvelope": "أرسل ظرفاً أحمر", + "sendRedPackConfirmTips": "هل أنت متأكد أنك تريد إرسال الحزمة الحمراء؟", + "redEnvelopeSendingRecords": "سجلات إرسال الظرف الأحمر:", + "countdownMinutes": "دقائق العد التنازلي:", + "number2": "رقم:", + "redEnvelopeRecTips2": "تم المطالبة بجميع المظاريف الحمراء.", + "redEnvelopeRecTips3": "انتهى وقت جمع الظرف الأحمر!", + "openTheTreasureChest": "افتح صندوق الكنز", + "redEnvelopeRecTips1": "تم إيداع العملات المكتسبة في محفظتك.", + "redEnvelope": "ظرف أحمر", + "redEnvelopeTips1": "عملات:", + "roomTools": "أدوات الغرفة:", + "entertainment": "الترفيه:", + "reportSucc": "تم الإبلاغ بنجاح", + "reportInputTips": "من فضلك وصف المشكل بأكبر قدر ممكن من التفاصيل باش نقدر نفهموه ونحلوه.", + "pornography": "الاباحية", + "screenshotTips": "لقطة شاشة (حتى 3)", + "roomEditing": "تحرير الغرفة", + "picture": "صورة", + "inputDesHint": "من فضلك وصف المشكلة بأكبر قدر ممكن من التفاصيل باش نقدر نفهموها ونحلها.", + "description": "الوصف:", + "roomNotice": "إشعار الغرفة", + "roomTheme": "موضوع الغرفة", + "roomProfilePicture": "صورة الملف الشخصي للغرفة", + "userProfilePicture": "صورة ملف المستخدم", + "userName": "اسم المستخدم", + "pleaseSelectTheTypeToProcess": "يرجى تحديد النوع لمعالجته:", + "userEditing": "تعديل المستخدم", + "enterTheUserId": "أدخل معرف المستخدم", + "enterTheRoomId": "أدخل رقم الغرفة", + "theImageSizeCannotExceed": "حجم الصورة لا يمكن أن يتجاوز 4 ميغابايت", + "bdLeader": "زعيم بنك التنمية", + "kickedOutOfRoom": "تم طرده من الغرفة", + "lockTheMic": "اقفل الميكروفون", + "openTheMic": "افتح الميكروفون", + "finish": "أنهى", + "removeTheMic": "أزل الميكروفون", + "leavelTheMic": "اترك الميكروفون", + "unlockTheMic": "افتح الميكروفون", + "muteTheMic": "كتم الميكروفون", + "inviteToTheMicrophone": "دعوة للتحدث في الميكروفون", + "takeTheMic": "خذ الميكروفون", + "openUserProfleCard": "افتح بطاقة ملف المستخدم", + "crop": "محصول", + "host": "مضيف", + "agent": "وكالة", + "ra": "را", + "bd": "بي دي", + "unread": "غير مقروء", + "read": "اقرأ", + "image": "[صورة]", + "video": "[فيديو]", + "sound": "[صوت]", + "gift2": "[هدية]", + "clickHereToStartChatting": "قل شيئًا.....", + "receivedAMessage": "[تلقى رسالة]", + "giftCounter": "عداد الهدايا", + "wins": "يفوز", + "deleteAccount": "حذف الحساب", + "becomeAgent": "أصبح وكيلا", + "male": "ذكر", + "setAccount": "تعيين الحساب", + "female": "أنثى", + "album": "ألبوم", + "loadingFailedClickToRetry": "فشل التحميل، انقر لإعادة المحاولة", + "releaseToLoadMore": "حرر للتحميل المزيد", + "haveMyLimits": "---لدي حدودي.---", + "pullToLoadMore": "اسحب للتحميل المزيد", + "enterNickname": "أدخل الاسم المستعار", + "camera": "كاميرا", + "system": "نظام", + "selectYourCountry": "اختر دولتك", + "inviteCode": "رمز الدعوة", + "yesterday": "أمس {1}", + "monday": "الاثنين {1}", + "tuesday": "الثلاثاء {1}", + "wednesday": "الأربعاء {1}", + "thursday": "الخميس {1}", + "friday": "الجمعة {1}", + "saturday": "السبت {1}", + "sunday": "الأحد {1}", + "notifcation": "إشعار", + "inviteYouToBecomeAgent": "ندعوك لتصبح وكالة", + "theVideoSizeCannotExceed": "لا يمكن أن يتجاوز حجم الفيديو 50 ميغابايت", + "fromLuckyGifts": "من الهدايا المحظوظة", + "confirmSwitchMicModelTips": "هل تؤكد تغيير وضعية الجلوس؟", + "classicMic": "ميكروفون كلاسيكي {1}", + "number": "رقم", + "micTheme": "ثيم المايك", + "chat": "دردشة", + "rejected": "مرفوض", + "refuse": "رفض", + "boxContributeTips": "لقد تم الاستثمار بالفعل اليوم، رجاءً لا تستثمر مرة أخرى", + "help": "مساعدة", + "approved": "معتمد", + "onlineUsers": "المستخدمون عبر الإنترنت ({1}/{2}):", + "applyToJoin": "قدّم للانضمام", + "hostList": "قائمة المضيف", + "supporterList": "قائمة الداعمين", + "upToMembers": "حتى {1} أعضاء", + "upToAdmins": "حتى {1} مسؤولين", + "levelPrivileges": "امتيازات المستوى", + "agree": "موافق", + "joinRequest": "طلب الانضمام", + "gameRules": "قوانين اللعبة:", + "gift": "هدية", + "charmGameRulesTips": "قم بتشغيل لوحة التحكم لعرض العملات الهدايا المستلمة لجميع المستخدمين على الميكروفون، 1 عملة = 1 نقطة (هدية محظوظة 1 عملة = 0.04 نقطة).", + "charm": "تعويذة هدية", + "chats": "الدردشات", + "myMusic": "موسيقاي", + "membershipFeeTips1": "من فضلك حدد رسوم الانضمام لغرفتك. يمكن للمستخدمين الانضمام إلى غرفتك من خلال دفع الرسوم.", + "membershipFeeTips2": "الذهب المطلوب للعضو في الغرفة. صاحب الغرفة سيحصل على 50٪ من الذهب.", + "membershipFee": "رسوم العضوية", + "touristsSendText": "السائح يرسل نص", + "freePrice": "الرسوم: 0-10000", + "pleaseChatFfriendly": "رجاءً تحدث بطريقة ودية", + "kickPrevention": "الوقاية من الركلات", + "daily": "يوميًا", + "start": "ابدأ", + "win": "فوز", + "stop": "توقف", + "add": "أضف", + "scrollToTheBottom": "قم بالتمرير إلى الأسفل", + "apple": "تفاحة", + "music": "موسيقى", + "free": "مجاناً", + "goToUpgrade": "اذهب إلى الترقية", + "google": "جوجل", + "exclusiveEmojiWillBeReleasedAfterBecoming": "سيتم إصدار الرموز التعبيرية الحصرية بعد أن تصبح", + "preventBeingBlocked": "منع الحظر", + "enableRankIncognitoMode": "تمكين وضع الترتيب الخفي", + "avoidBeingKicked": "تجنب الركل", + "privileges": "{1} الامتيازات", + "andAboveUsers": "{1} والمستخدمون أعلاه", + "everyone": "الجميع", + "basicPermissions": "الأذونات الأساسية", + "mysteriousInvisibility": "الاختفاء الغامض", + "antiBlock": "مضاد للانسداد", + "privateChat": "دردشة خاصة", + "storeDiscount": "خصم المتجر {1}", + "membershipFreeChatSpeak": "دردشة وتحدث بدون عضوية", + "priorityRoomSorting": "فرز الغرف حسب الأولوية", + "userColoredID": "معرّف المستخدم الملون", + "setLoginPassword": "حدد كلمة مرور تسجيل الدخول", + "theTwoPasswordsDoNotMatch": "كلمتا المرور لا تتطابقتان.", + "resetLoginPasswordtTips2": "كلمة المرور يجب أن تكون بطول 8-16 حرف ويجب أن تكون مزيج من الأحرف الإنجليزية الكبيرة والصغيرة والأرقام (ليس فقط أرقام)", + "confirmYourPassword": "أكد كلمة المرور الخاصة بك", + "enterYourNewPassword": "أدخل كلمة مرورك الجديدة", + "setYourPassword": "عيّن كلمة المرور ديالك", + "enterYourOldPassword": "أدخل كلمة السر القديمة الخاصة بك", + "inputYourOldPassword": "أدخل كلمة المرور القديمة الخاصة بك", + "resetLoginPasswordtTips1": "سجل الدخول باستخدام معرف المستخدم أو معرف النمط. من الآمن أكثر تسجيل الدخول باستخدام حساب لايكي.", + "resetLoginPassword": "إعادة تعيين كلمة مرور تسجيل الدخول", + "localMusic": "الموسيقى المحلية", + "confirmSwitchMicThemeTips": "هل تؤكد تغيير نمط المقعد؟", + "follow2": "اتبع:{1}", + "fans2": "المعجبون:{1}", + "vistors2": "الزوار:{1}", + "personal2": "شخصي:", + "dailyTaskRewardBonus": "مكافأة مهمة يومية ({1} نقطة خبرة)", + "userLevelXPBoost": "زيادة خبرة مستوى المستخدم ({1} XP)", + "enterRoomName": "أدخل اسم الغرفة", + "theMembershipFee": "رسوم العضوية", + "theModificationsMade": "التعديلات التي تمت هذه المرة لن يتم حفظها بعد الخروج.", + "touristsTakeToTheMic": "السياح ياخذو الميكروفون.", + "weekly": "أسبوعي", + "conntinue": "استمر", + "logIn": "تسجيل الدخول", + "sayHi": "قُل مرحبا..", + "password": "كلمة السر", + "enterAccount": "دخول الحساب", + "enterPassword": "أدخل كلمة المرور", + "roomName": "اسم الغرفة", + "enterRoomTips": "{1} ادخل الغرفة", + "startVoiceParty": "ابدأ حفلة صوتية!", + "freeChatSpeak": "دردش بحرية وتحدث", + "roomMember": "عضو الغرفة", + "popular": "مشهور", + "coinsReceived": "العملات المستلمة", + "hotRooms": "غرف حارة", + "viewMore": "عرض المزيد", + "noData": "لا توجد بيانات", + "recommend": "يوصي", + "follow": "تابع", + "monthly": "شهريًا", + "message": "رسالة", + "bdCenter": "مركز BD", + "history": "تاريخ", + "users": "المستخدمون", + "rooms": "غرف", + "days": "أيام", + "permanent": "دائم", + "unFollow": "إلغاء المتابعة", + "searchInputHint": "أدخل رقم الحساب / الغرفة", + "kickRoomTips": "لقد تم طردك من الغرفة", + "joinRoomTips": "غرفة متضامة!", + "roomSetting": "تجهيز الغرفة", + "roomDetails": "تفاصيل الغرفة", + "systemRoomTips": "كن مهذبا ومحترما. يُمنع منعاً باتاً أي محتوى إباحي أو مبتذل في yumi. بمجرد اكتشافه، سيتم حظر الحساب بشكل دائم. يرجى الالتزام بوعي بلوائح منصة yumi.", + "copiedToClipboard": "تم النسخ إلى الحافظة", + "recharge": "إعادة الشحن", + "agentCenter": "مركز الوكالة", + "report": "تقرير", + "done": "تمّ", + "improvementTasks": "مهام التحسين", + "followedYou": "تابعتك", + "followSucc": "تمت المتابعة بنجاح", + "edit": "تحرير", + "task": "مهمة", + "save": "احفظ", + "go": "اذهب", + "deleteAccount2": "حذف الحساب({1}s)", + "areYouSureYouWantToDeleteYourAccount": "هل أنت متأكد أنك تريد حذف حسابك؟", + "enterRoomConfirmTips": "هل أنت متأكد أنك تريد دخول الغرفة؟", + "dailyTasks": "المهام اليومية", + "nickName": "لقب", + "giftSpecialEffects": "هدية المؤثرات الخاصة", + "country": "دولة", + "basicFeatures": "الميزات الأساسية", + "floatingAnimationInGlobal": "الرسوم المتحركة العائمة في العالم", + "joinMemberTips": "إذا كنت زائرًا في الغرفة، لا يمكنك أخذ الميكروفون.", + "countryRegion": "البلد والمنطقة", + "gender": "جنس", + "likedYourComment": "أعجبتني طاقتك.", + "deleteAccountTips2": "*إذا غيرت رأيك، يمكنك تسجيل الدخول مرة أخرى إلى حسابك الحالي خلال سبعة أيام، وسنقوم تلقائيًا باستعادة حسابك. إذا لم تتم عملية الاستعادة خلال سبعة أيام، فسيتم حذف الحساب نهائيًا", + "deleteAccountTips": "لديك صلاحيات إدارية كاملة على هذا الحساب. إذا كنت تنوي حذف الحساب، يُرجى الانتباه إلى المخاطر التالية المرتبطة بهذه العملية:\n\n1. بمجرد حذف الحساب بنجاح، لن تتمكن من تسجيل الدخول إليه. حذف الحساب إجراء نهائي.\n\n2. بعد حذف الحساب بنجاح، لن تتمكن من استعادة أي بيانات. سيتم حذف جميع المعلومات (بما في ذلك الغرف، والأصدقاء)، والعملة الافتراضية، والهدايا، والعناصر الافتراضية نهائيًا ولن يكون بالإمكان استعادتها.\n\n3. فترة السماح: إذا لم تقم باستعادة الحساب، فلن تتمكن من الوصول إلى صفحة الشراء، أو صفحة السحب، أو أي صفحات أخرى في التطبيق.\n\n4. خلال فترة السماح أو بعد حذف الحساب، ستشير صفحة الملف الشخصي إلى أنه قد تم حذفه. لحماية حسابك من البحث أو الوصول إليه من قِبل الآخرين، سيتم حذف معلوماتك الشخصية من الأنظمة المتعلقة بالوظائف اليومية. عندما يتعلق حذف الحساب بالأمن القومي، أو الإجراءات المدنية أو الجنائية، أو حماية الحقوق والمصالح المشروعة لأطراف ثالثة، يحتفظ المسؤول بحقه في رفض طلب حذف حساب المستخدم.\n\nإذا كنت متأكدًا من رغبتك في حذف جميع بياناتك الشخصية من حسابك الحالي، يُرجى النقر على \"حذف الحساب\".", + "accountDeletionNotice": "إشعار حذف الحساب:", + "entryVehicleAnimation2": "يمكن للمستخدمين الحاصلين على صلاحيات VlP4 أو أعلى استخدام هذه الوظيفة لتعطيل رسوميات المركبات المتحركة.", + "entryVehicleAnimation": "الرسوم المتحركة لدخول السيارة", + "man": "رجل", + "woman": "امرأة", + "all": "الكل", + "activity": "نشاط", + "knapsack": "حقيبه", + "areYouRureRoRecharge": "هل أنت متأكد من شحن ؟", + "clearMessage": "مسح رسائل الشاشة", + "pleaseSelectaItem": "يرجى اختيار البند", + "birthday": "عيد ميلاد", + "pleaseEnterNickname": "يرجى إدخال لقب.", + "pleaseSelectYourCountry": "يرجى تحديد بلدك.", + "pleaseSelectYourGender": "يرجى تحديد جنسك.", + "dateOfBirth": "تاريخ الميلاد", + "mute": "صامت", + "exit": "خروج", + "send": "أرسل", + "goldList": "القائمة الذهبية", + "coins": "النقود المعدنية", + "lockTheRoom": "أغلق الغرفة", + "giftGivingSuccessful": "إن تقديم الهدايا كان ناجحًا.", + "hostCenter": "مركز الاستضافة", + "allOnMicrophone": "الجميع على الميكروفون", + "usersOnMicrophone": "المستخدمون على الميكروفون", + "mInimize": "احتفظ", + "shop": "متجر", + "expirationTime": "وقت الانتهاء", + "roomOwner": "مالك الغرفة", + "owner": "مالك", + "admin": "المشرف", + "member": "عضو", + "guest": "ضيف", + "submit": "تقديم", + "enter": "دخل", + "unLockTheRoom": "افتح الغرفة", + "setRoomPassword": "تعيين كلمة مرور الغرفة", + "inputRoomPassword": "إدخال كلمة مرور الغرفة", + "operationSuccessful": "كانت العملية ناجحة.", + "adminByHomeowner": "يتم تعيين كمسؤول من قبل مالك المنزل.", + "memberByHomeowner": "يتم تعيين كأعضاء من قبل صاحب المنزل.", + "touristByHomeowner": "تم تعيين كسائح من قبل صاحب المنزل.", + "setUpAnIdentity": "أعد إعداد هوية.", + "allInTheRoom": "كل شيء في الغرفة.", + "theAccountPasswordCannotBeEmpty": "لا يمكن أن يكون الحساب أو كلمة المرور خالية.", + "goldListort": "القائمة الذهبية", + "rechargeList": "قائمة إعادة الشحن", + "becomeHost": "قدّم لتصبح مضيفًاً", + "alreadyAnAdministrator": "مسؤول بالفعل.ً", + "alreadyAnMember": "أنت بالفعل عضو.ً", + "touristsCannotSendMessages": "لا يستطيع السائحون إرسال الرسائل", + "touristsAreNotAllowedToGoOnTheMic": "لا يسمح للسياح بالدخول إلى الميكروفون", + "superFans": "المشجعين الخارقين:ً", + "special": "خاص", + "custom": "مخصص", + "store": "دكان", + "viewFrame": "عرض الإطار", + "headdress": "إطارات", + "mountains": "مركبات", + "buy": "ابتاع", + "visitorList": "قائمة الزوار", + "spendCoinsToGainExperiencePoints": "اصرف العملات لكسب نقاط الخبرة", + "howToUpgrade": "كيف يمكن الترقية؟", + "higherLevelFancierAvatarFrame": "مستوى أعلى، شارات/إطار صور أفخم.", + "use": "استخدم", + "expired": "منتهي", + "day": "يوم", + "obtain": "الحصول على", + "backTheRoom": "الغرفة الخلفية", + "toConsume": "الاستهلاك", + "profile": "بروفايل", + "wallet": "محفظة", + "giftwall": "جيفت وول", + "announcement": "إعلان", + "blockedList": "قائمة محظورة", + "renewal": "تجديد", + "country2": "بلد:", + "sendTo": "أرسل إلى", + "credits": "الاعتمادات: {1}", + "successfullyUnloaded": "تم تفريغها بنجاح", + "unUse": "استخدام واحد", + "successfulWear": "ملابس ناجحة", + "confirmUnUseTips": "هل تؤكد على إزالته؟", + "inUse": "قيد الاستخدام", + "confirmUseTips": "هل تريد التأكيد على استخدامه؟", + "pleaseUploadUserAvatar": "يرجى رفع صورة شخصية.", + "myItems": "أشيائي", + "confirmBuyTips": "هل أنت متأكد من أنك تريد الشراء؟", + "purchaseIsSuccessful": "الشراء ناجح", + "purchase": "ابتاع", + "invitesYouToTheMicrophone": "{1} يدعوك إلى الميكروفون", + "english": "الإنجليزية", + "chinese": "الصينية", + "arabic": "العربية", + "darkMode": "الوضع الداكن", + "lightMode": "الوضع الفاتح", + "systemDefault": "النظام الافتراضي", + "pleaseGetOnTheMicFirst": "من فضلك استخدم الميكروفون أولاً.", + "duration2": "المدة:{1}" +} diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json new file mode 100644 index 0000000..e2790f3 --- /dev/null +++ b/assets/l10n/intl_bn.json @@ -0,0 +1,643 @@ +{ + "signInWithGoogle": "Google দিয়ে লগইন করুন", + "or": "অথবা", + "signInWithYourAccount": "আপনার অ্যাকাউন্ট দিয়ে লগইন করুন", + "signInWithApple": "Apple দিয়ে লগইন করুন", + "loginRepresentsAgreementTo": "লগইন করলে আপনি নিম্নলিখিতগুলি স্বীকার করছেন:", + "termsofService": "সেবা শর্তাবলী", + "privaceyPolicy": "গোপনীয়তা নীতি", + "tips": "টিপস", + "mine": "আমার", + "games": "খেলাধুলা", + "party": "পার্টি", + "yes": "হ্যাঁ", + "bag": "ব্যাগ", + "sound2": "শব্দ", + "startYourBrandNewJourney": "আপনার সম্পূর্ণ নতুন যাত্রা শুরু করুন", + "searchNoDataTips": "আপনি যে রুম বা ব্যবহারকারীর আইডি অনুসন্ধান করতে চান তা লিখুন।", + "event": "ইভেন্ট", + "other": "অন্য", + "maliciousHarassment": "দূরাচার হয়রানি", + "roomEdit": "রুম এডিট", + "cancelRoomPassword": "আপনি কি নিশ্চিত যে আপনি রুমের পাসওয়ার্ডটি মুছে ফেলতে চান?", + "roomMemberFee": "রুম সদস্য ফি", + "blockedList2": "ব্লক করা তালিকা", + "roomTheme2": "রুম থিম", + "roomPassword": "রুম পাসওয়ার্ড", + "numberOfMic": "মাইকের সংখ্যা", + "pleaseEnterContent": "অনুগ্রহ করে বিষয়বস্তু লিখুন", + "profilePhoto": "প্রোফাইল ছবি", + "aboutMe": "আমার সম্পর্কে", + "noHistoricalRecordsAvailable": "কোনও ঐতিহাসিক রেকর্ড পাওয়া যায়নি।", + "inviteNewUsersToEarnCoins": "নতুন ব্যবহারকারীকে আমন্ত্রণ করুন কয়েন উপার্জনের জন্য", + "crateMyRoom": "নিজের রুম তৈরি করুন।", + "myRoom": "আমার ঘর", + "recent": "সাম্প্রতিক", + "popularEvents": "জনপ্রিয় ইভেন্ট", + "casualInteraction": "সাধারণ আলোচনাচিত্র", + "haveGamePlayingTips": "আপনার গেমটি চলমান আছে, দয়া করে প্রথমে বর্তমান গেমটি ছাড়ুন, আপনি কি নিশ্চিতভাবে বের হতে চান?", + "historicalTour": "ঐতিহাসিক ভ্রমণ", + "gameCenter": "গেম সেন্টার", + "returnToVoiceChat": "ভয়েস চ্যাটে ফিরে যেতে চাই?", + "exitGameMode": "গেম মোড থেকে বের হন", + "enterTheRoom": "ঘরে প্রবেশ করো", + "inviteGoRoomTips": "সব সময় তোমার জন্য এখানে আছি, বৃষ্টি হোক বা রোদ। এসে সালাম জানাও!", + "invite": "আমন্ত্রণ করা", + "confirmInviteThisUserToTheRoom": "এই ব্যবহারকারী (ID:{1}) কে রুমে নিমন্ত্রণ নিশ্চিত করবেন?", + "clearCacheSuccessfully": "ক্যাশ সফলভাবে মুছে ফেলা হয়েছে", + "sent": "পাঠানো হয়েছে", + "keep": "রাখুন", + "open": "খুলুন", + "deleteAccountTips2": "*আপনি যদি মন পরিবর্তন করেন, তবে সাত দিনের মধ্যে আপনার বর্তমান অ্যাকাউন্টে আবার লগইন করতে পারবেন এবং আপনার অ্যাকাউন্ট স্বয়ংক্রিয়ভাবে পুনরুদ্ধার করা হবে। সাত দিনের মধ্যে পুনরুদ্ধার না করলে, অ্যাকাউন্ট স্থায়ীভাবে মুছে ফেলা হবে", + "deleteAccountTips": "আপনার এই অ্যাকাউন্টে সম্পূর্ণ প্রশাসনিক অধিকার রয়েছে। আপনি যদি অ্যাকাউন্ট মুছে ফেলতে চান, তবে এই ক্রিয়াটির সাথে সম্পর্কিত নিম্নলিখিত ঝুঁকিগুলি কृপয়া ভুলবেন না:\n1.অ্যাকাউন্ট সফলভাবে মুছে ফেলা হলে, আপনি আর আপনার বর্তমান অ্যাকাউন্টে লগইন করতে পারবেন না। অ্যাকাউন্ট মুছে ফেলা একটি স্থায়ী ক্রিয়া।\n2. অ্যাকাউন্ট সফলভাবে মুছে ফেলা হলে, আপনি অ্যাকাউন্টের কোনো ডেটা পুনরুদ্ধার করতে পারবেন না। সমস্ত তথ্য (রুম, বন্ধু সহ), ভার্চুয়াল মুদ্রা, উপহার এবং ভার্চুয়াল আইটেম স্থায়ীভাবে মুছে ফেলা হবে এবং পুনরুদ্ধার করা যাবে না।\n3. কুলডাউন সময়: আপনি যদি অ্যাকাউন্ট পুনরুদ্ধার না করেন, তবে আপনি অ্যাপের ক্রয় পৃষ্ঠা, পেমেন্ট উত্তোলন পৃষ্ঠা বা অন্য কোনো পৃষ্ঠায় অ্যাক্সেস করতে পারবেন না।\n4.কুলডাউন সময়কালীন বা অ্যাকাউন্ট মুছে ফেলার পর, প্রোফাইল পৃষ্ঠা এটি মুছে ফেলা হয়েছে বলে দেখাবে। আপনার অ্যাকাউন্ট অন্যদের দ্বারা সনাক্ত বা অ্যাক্সেস করা থেকে বাধা দেওয়ার জন্য, আপনার ব্যক্তিগত তথ্য দৈনন্দিন ফাংশন সম্পর্কিত সিস্টেম থেকে মুছে ফেলা হবে। অ্যাকাউন্ট মুছে ফেলা জাতীয় নিরাপত্তা, নাগরিক বা দণ্ডী মামলা বা তৃতীয় পক্ষের বৈধ অধিকার ও সুবিধার রক্ষণসংস্কারকে অন্তর্ভুক্ত করলে, সরকারি প্রশাসন ব্যবহারকারীর অ্যাকাউন্ট মুছে ফেলার অনুরোধ প্রত্যাখ্যান করার অধিকার সংরক্ষণ করে।\nআপনি যদি আপনার বর্তমান অ্যাকাউন্টের সমস্ত ব্যক্তিগত ডেটা মুছে ফেলতে নিশ্চিত হন, তবে কृপয়া \"অ্যাকাউন্ট মুছে ফেলুন\" বোতামে ক্লিক করুন।", + "accountDeletionNotice": "অ্যাকাউন্ট মুছে ফেলার নোটিশ:", + "thisUserHasBeenBlacklisted": "এই ব্যবহারকারী ব্ল্যাকলিস্টে যুক্ত করা হয়েছে।", + "trend": "ট্রেন্ড", + "like": "পছন্দ করুন", + "more": "আরও", + "discard": "বাতিল করুন", + "catchFirstComment": "প্রথম মন্তব্য ক্যাচ করুন", + "reply": "উত্তর দিন", + "posting": "পোস্ট করা হচ্ছে", + "multiple": "একাধিক", + "successfullyRemovedFromTheBlacklist": "ব্ল্যাকলিস্ট থেকে সফলভাবে সরানো হয়েছে!", + "successfullyAddedToTheBlacklist": "ব্ল্যাকলিস্টে সফলভাবে যুক্ত করা হয়েছে!", + "youAreCurrentlyCPRelationshipPleaseDissolve": "আপনি বর্তমানে একটি CP সম্পর্কে আছেন।\nকृপয়া প্রথমে সম্পর্কটি সমাধান করুন।", + "areYouSureToCancelBlacklist": "আপনি কি ব্ল্যাকলিস্ট বাতিল করতে চান?", + "areYouSureYouWantToBlockThisUser": "আপনি কি এই ব্যবহারকারীকে ব্লক করতে চান?", + "removeFromBlacklist": "ব্ল্যাকলিস্ট থেকে সরান", + "moveToBlacklist": "ব্ল্যাকলিস্টে যুক্ত করুন", + "userBlacklist": "ব্যবহারকারী ব্ল্যাকলিস্ট", + "specialEffectsManagement": "বিশেষ প্রভাব ব্যবস্থাপনা", + "wishingYouHappinessEveryDay": "আপনাকে প্রতিদিন সুখের কামনা করছি।", + "newMessage": "নতুন বার্তা", + "createRoomSuccsess": "রুম সফলভাবে তৈরি হয়েছে!", + "contactUs": "আমাদের সাথে যোগাযোগ করুন", + "systemAnnouncementTips1": "ধোঁকাধন্ডা থেকে সতর্ক:", + "systemAnnouncementTips": "শুধুমাত্র অফিসিয়াল চ্যানেলের মাধ্যমে তথ্য যাচাই করুন। কখনওই তৃতীয় পক্ষের সফটওয়্যার ডাউনলোড করবেন না, ব্যক্তিগত তথ্য শেয়ার করবেন না বা বাহ্যিক অনুরোধের উপর অর্থ ট্রান্সফার করবেন না। অফিসিয়াল কর্মী আইডি শুধুমাত্র 10000, 10003 এবং 10086। আপনার কোনো সন্দেহ থাকলে, ক্রিয়াটি বন্ধ করুন এবং এর মাধ্যমে রিপোর্ট করুন", + "systemAnnouncement": "সিস্টেম ঘোষণা", + "doNotClickUnfamiliarTips": "অপরিচিত লিঙ্কে ক্লিক করবেন না, কারণ এগুলি আপনার ব্যক্তিগত তথ্য প্রকাশ করতে পারে। আপনার আইডি নম্বর বা ব্যাংক কার্ডের বিবরণ কখনওই কারো সাথে শেয়ার করবেন না।", + "atTag": "@ট্যাগ", + "sayHi2": "হাই", + "canSendMsgTips": "ব্যক্তিগত বার্তা পাঠানোর জন্য উভয় পক্ষকে একে অপরকে ফলো করতে হবে।", + "msgSendRedEnvelopeTips": "*লাল খাম之上 10% সেবা ফি কেটে নেওয়া হবে এবং প্রাপক শুধুমাত্র লাল খামের মানের 90% পাবেন। প্রেরকের সম্পদ স্তর 10 তম স্তরের থেকে বেশি হতে হবে।", + "reapply": "আবার আবেদন করুন", + "cancelRequest": "অনুরোধ বাতিল করুন", + "pending": "পেন্ডিং", + "supporter": "সমর্থক", + "coinsReceived": "কয়েন প্রাপ্ত হয়েছে", + "numberOfSign": "সাইন ইন সংখ্যা: {1}", + "hostWeeklyRank": "হোস্ট সাপ্তাহিক র‌্যাঙ্ক", + "supporterWeeklyRank": "সমর্থক সাপ্তাহিক র‌্যাঙ্ক", + "memberList": "সদস্য তালিকা", + "treasureChest": "সম্পদ বাক্স", + "applicationRecord": "আবেদন রেকর্ড", + "appUpdateTip": "অ্যাপের একটি নতুন সংস্করণ আছে ({1}), কি ডাউনলোড করবেন?", + "ownerIncomeCoins": "মালিকের আয়:{1} কয়েন", + "game": "গেম", + "skip2": "স্কিপ করুন", + "coins4": "কয়েন", + "claim": "দাবি", + "complete": "সম্পূর্ণ", + "shareTo": "শেয়ার করুন", + "copyLink": "লিঙ্ক কপি করুন", + "weekStart": "সপ্তাহের শুরু", + "faceBook": "ফেসবুক", + "whatsApp": "হোয়াটসঅ্যাপ", + "snapChat": "স্ন্যাপচ্যাট", + "taskNameRoomNewMember": "রুমে নতুন সদস্যের সংখ্যা", + "taskNameRoomOwnerSendRedPacket": "রুম মালিক একটি লাল লিফলেট পাঠান", + "taskNameRoomOwnerSendGiftUser": "রুম মালিক উপহার পাঠান", + "taskNameRoomMicUser120Min": "১২০+ মিনিট মাইকে কাটানো সদস্য", + "taskNameRoomMicUser60Min": "৬০+ মিনিট মাইকে কাটানো সদস্য", + "taskNameRoomMicUser30Min": "৩০+ মিনিট মাইকে কাটানো সদস্য", + "taskNamePersonalSendGift": "অন্যের রুমে মাইক্রোফোনে আসুন", + "taskNameRoomOnlineUserCount": "রুমের অনলাইন সদস্যরা", + "taskNameRoomOwnerInviteMic": "সদস্যকে মাইকে আমন্ত্রণ করুন", + "taskNameRoomUserSendGiftGold": "সদস্যদের দেওয়া উপহার কয়েন", + "taskNameRoomOwnerSendGiftGold": "রুম মালিকের দেওয়া উপহার কয়েন", + "taskNamePersonalMagicGiftGold": "যাদুকরী উপহার পাঠিয়ে জয় করা কয়েন", + "taskNamePersonalLuckyGiftGold": "ভাগ্যবান উপহার পাঠিয়ে জয় করা কয়েন", + "taskNameRoomOwnerMicTime": "রুমের মালিক রুমে মাইকে কথা বলেন", + "taskNamePersonalActiveInRoom": "অন্যের রুমে সক্রিয় হোন", + "taskNamePersonalGameConsume": "গেম ব্যয়", + "taskNamePersonalMicInRoom": "মাইকে যান", + "forMoreRewardsPleaseCheckTheTaskCenter": "আরও পুরস্কারের জন্য কृপয়া টাস্ক সেন্টার চেক করুন", + "kingQuuen": "রাজা-রানী", + "dailyCoinBonanzaRulesDetail": "১. দৈনিক ব্যক্তিগত কাজ এবং দৈনিক রুম মালিকের কাজ প্রতিটি ব্যবহারকারীর জন্য দিনে একবার সম্পন্ন করা যেতে পারে। কাজগুলি সৌদি সময় ০০:০০:০০ এ রিসেট হয়।\n২. দৈনিক ব্যক্তিগত কাজ কেবল অন্যের রুমে সম্পন্ন করা যায়; রুম মালিকের কাজ কেবল আপনার নিজের রুমে সম্পন্ন করা যায়।\n৩. উপহার দেওয়ার কাজগুলিতে কেবল রুমে পাঠানো উপহারগুলি গণ্য হবে, ফিডে পাঠানো উপহার নয়।\n৪. যদি একই ডিভাইস বা একই সিম কার্ড ব্যবহার করে একাধিক অ্যাকাউন্ট তৈরি করা হয়, সমস্ত কাজের পুরস্কার কেবল একবারই দাবি করা যেতে পারে।", + "dailyCoinBonanzaRules": "দৈনিক কয়েন বোনাঞ্জার নিয়ম", + "roomOwnerTasks": "রুম মালিকের কাজ", + "personalTasks": "ব্যক্তিগত কাজ", + "getPaidToRefer": "প্রস্তাব দেওয়ার জন্য অর্থ উপার্জন করুন", + "ramadan": "রমজান", + "noPromptsToday": "আজ কোনো প্রম্পট নেই।", + "updateNow": "এখন update করুন", + "allGames": "সমস্ত গেম", + "fishClass": "মাছ শ্রেণী", + "greedyClass": "লোভী শ্রেণী", + "raceSeries": "রেস সিরিজ", + "slotsClass": "স্লট শ্রেণী", + "others": "অন্যান্য", + "hotGames": "হট গেমস", + "chatBox": "চ্যাট বক্স", + "termsOfServicePrivacyPolicyTips": "এগিয়ে যাওয়ার মাধ্যমে আপনি সেবা শর্তাবলী এবং গোপনীয়তা নীতি স্বীকার করছেন", + "and": " এবং ", + "pleaseSelectTheTypeContent": "কृপয়া লঙ্ঘনকারী কন্টেন্টের টাইপ নির্বাচন করুন।", + "illegalInformation": "বেআইনি তথ্য", + "inappropriateContent": "অনুপযুক্ত কন্টেন্ট", + "personalAttack": "ব্যক্তিগত আক্রমণ", + "confirm": "নিশ্চিত করুন", + "spam": "স্প্যাম", + "countdownMinutes": "কাউন্টডাউন মিনিট :", + "number2": "নম্বর:", + "fraud": "ধোঁকাধন্ডা", + "received": "প্রাপ্ত হয়েছে", + "currentProgress": "বর্তমান অগ্রগতি", + "currentStage": "বর্তমান স্টেজ:{1}", + "roomReward2": "রুম পুরস্কার:{1}", + "roomReward": "রুম পুরস্কার", + "ownerSendTheRedEnvelope": "মালিক পুরস্কার কয়েন পাঠিয়েছেন।", + "rewardCoins": "পুরস্কার কয়েন:{1} কয়েন", + "lastWeekProgress": "গত সপ্তাহের অগ্রগতি", + "redEnvelopeTips2": "*লাল খাম সময়সীমার মধ্যে দাবি না করলে, বাকি কয়েন প্রেরক ব্যবহারকারীকে ফেরত দেওয়া হবে।", + "goToRecharge": "রিচার্জ করতে যান", + "deleteAccount2": " অ্যাকাউন্ট মুছে ফেলুন({1}সেকেন্ড)", + "areYouSureYouWantToDeleteYourAccount": " আপনি কি আপনার অ্যাকাউন্ট মুছে ফেলতে চান?", + "insufhcientGoldsGoToRecharge": "সোনা অপর্যাপ্ত, দ্রুত রিচার্জ করুন!", + "coins2": "{1}কয়েন", + "remainingNumberTips": "বাকি ব্যবহারযোগ্য সংখ্যা:({1}/{2})", + "collectionTimeTips": "সংগ্রহের সময়:{1}({2}/{3})", + "sendARedEnvelope": "লাল খাম পাঠান", + "sendRedPackConfirmTips": "আপনি কি লাল প্যাক পাঠাতে চান?", + "redEnvelopeSendingRecords": "লাল খাম প্রেরণ রেকর্ড:", + "redEnvelope": "লাল খাম", + "redEnvelopeRecTips2": "সব লাল খাম দাবি করা হয়েছে।", + "redEnvelopeRecTips3": "লাল খাম সংগ্রহের সময় শেষ হয়ে গেছে!", + "openTheTreasureChest": "সম্পদ বাক্স খুলুন", + "redEnvelopeRecTips1": "বিজয়ী কয়েন আপনার ওয়ালেটে জমা হয়েছে।", + "redEnvelopeTips1": "কয়েন:", + "roomTools": "রুম টুলস:", + "entertainment": "বিনোদন:", + "reportSucc": "রিপোর্ট সফল", + "pornography": "পর্নোগ্রাফি", + "reportInputTips": "সমস্যাটি বুঝতে এবং সমাধান করতে আমাদের সাহায্য করার জন্য কृপয়া সমস্যাটি যতটা সম্ভব বিস্তারিতভাবে বর্ণনা করুন।", + "cancel": "বাতিল করুন", + "join": "যোগদান করুন", + "items": "আইটেম", + "vistors": "ভিজিটর", + "fans": "ফ্যান", + "balanceNotEnough": "সোনা কয়েন ব্যালেন্স অপর্যাপ্ত। আপনি কি রিচার্জ করতে চান?", + "skip": "{1} স্কিপ করুন", + "letGoToWatch": "চলুন দেখতে যাই!", + "launchedARocket": "রকেট চালু করেছে", + "sendUserId": "ব্যবহারকারী ID পাঠান:{1}", + "giveUpIdentity": "আইডেন্টিটি ছেড়ে দিন", + "leaveRoomIdentityTips": "আপনি কি রুম আইডেন্টিটি ছেড়ে দিতে চান?", + "joinMemberTips2": "আপনি কি সদস্য হিসেবে রুমে যোগদান করতে নিশ্চিত করছেন?", + "sureUnfollowThisRoom": "আপনি কি এই রুমকে আনফলো করতে চান?", + "welcomeMessage": "আমাদের অ্যাপে স্বাগতম, {name}!", + "settings": "সেটিংস", + "account": "অ্যাকাউন্ট", + "common": "সাধারণ", + "delete": "মুছে ফেলুন", + "copy": "কপি করুন", + "bio": "জীবনী", + "useCoupontips": "আপনি কি কুপন ব্যবহার করতে চান?", + "searchUserId": "ব্যবহারকারী ID সার্চ করুন", + "sendUser": "ব্যবহারকারীকে পাঠান", + "hobby": "শখ", + "sendCoupontips": "আপনি কি এই কুপনটি এই ব্যবহারকারীকে পাঠাতে চান?", + "youDontHaveAnyCouponsYet": "আপনার এখনও কোনো কুপন নেই।", + "recall": "ফিরে ডাকুন", + "youHaventFollowed": "আপনি কোনো রুম ফলো করেন না", + "deleteFromMyDevice": "আমার ডিভাইস থেকে মুছে ফেলুন", + "deleteOnAllDevices": "সমস্ত ডিভাইসে মুছে ফেলুন", + "messageHasBeenRecalled": "এই বার্তা ফিরে ডাকা হয়েছে", + "recallThisMessage": "আপনি কি এই বার্তা ফিরে ডাকতে চান?", + "language": "ভাষা", + "feedback": "ফিডব্যাক", + "signedin": "সাইন ইন করা হয়েছে", + "receiveSucc": "সফলভাবে দাবি করা হয়েছে", + "about": "সম্পর্কে", + "aboutUs": "আমাদের সম্পর্কে", + "theme": "থিম", + "wealthLevel": "সম্পদ স্তর", + "userLevel": "ব্যবহারকারী স্তর", + "goToUpload": "আপলোড করতে যান", + "logout": "লগআউট", + "luck": "ভাগ্য", + "level": "স্তর", + "themeGoToUploadTips": "1.আপলোড সফল হলে 24 ঘন্টার মধ্যে রিভিউ করা হবে।\n2.রিভিউ ব্যর্থ হলে সমস্ত কয়েন ফেরত দেওয়া হবে।", + "home": "হোম", + "explore": "অন্বেষণ", + "me": "আমি", + "socialPrivilege": "সামাজিক সুবিধা", + "information": "তথ্য", + "myPhoto": "আমার ফটো", + "areYouSureYouWantToSpend3": "*অন্য পক্ষ CP আমন্ত্রণ প্রত্যাখ্যান করলে, আপনার কয়েন আপনার ওয়ালেটে ফেরত দেওয়া হবে।", + "areYouSureYouWantToSpend": "আপনি কি খরচ করতে চান", + "areYouSureYouWantToSpend2": "এই ব্যবহারকারীকে CP আমন্ত্রণ পাঠাতে?", + "underReview": "রিভিউ চলছে", + "doYouWantToDeleteIt": "আপনি কি এটি মুছে ফেলতে চান?", + "chooseFromAblum": "অ্যালবাম থেকে নির্বাচন করুন", + "spaceBackground": "স্পেস ব্যাকগ্রাউন্ড", + "editProfile": "প্রোফাইল সম্পাদনা করুন", + "sendTheCpRequest": "CP অনুরোধ পাঠান", + "addCp": "CP যোগ করুন", + "partWays": "পথ ভেদ করুন", + "reconcile": "মিলন করুন", + "separated": "বিচ্ছিন্ন", + "areYouSureYouWantToSpend5": "{1} আপনাকে তার অনুভূতি প্রকাশ করেছে; আপনি যদি গ্রহণ করেন তবে আপনি জোড়া হবেন।", + "areYouSureYouWantToSpend6": "{1} আপনার সাথে আবার মিলিত হতে চান। আপনি যদি মিলনের সিদ্ধান্ত নেন, তবে আপনার সমস্ত পূর্বের ডেটা পুনরুদ্ধার করা হবে।", + "reconcileInvitationTips": "*অন্য পক্ষ CP আমন্ত্রণ প্রত্যাখ্যান করলে, আপনার কয়েন আপনার ওয়ালেটে ফেরত দেওয়া হবে।", + "reconcileInvitation": "মিলন আমন্ত্রণ", + "areYouSureYouWantToSpend4": "এই ব্যবহারকারীকে মিলন আমন্ত্রণ পাঠাতে?", + "partWaysTips": "*জোড়ার একজন পার্টনার পথ ভেদ করার সিদ্ধান্ত নিলে, 7 দিনের কুলডাউন সময় থাকবে। এই সময়কালীন উভয় পক্ষই মিলনের সিদ্ধান্ত নিতে পারে এবং মিলনের ক্ষেত্রে সমস্ত ডেটা পুনরুদ্ধার করা হবে। 7 দিনের সময় শেষ হলে মিলন না করলে, জোড়া ডেটা মুছে ফেলা হবে।", + "areYouSureYouWantToPartWaysWithYourCP": "আপনি কি আপনার CP와 পথ ভেদ করতে চান?", + "timeSpentTogether": "একসাথে কাটানো সময়: {1} দিন", + "firstDay": "প্রথম দিন:{1}", + "numberOfMyCPs": "আমার CP সংখ্যা:({1}/{2})", + "props": "প্রপস", + "win": "বিজয়ী", + "dice": "ডাইস", + "rps": "কাগজ-কাঁচা-কামড়া", + "operationFail": "অপারেশন ব্যর্থ হয়েছে।", + "likedYourComment": "আপনার মন্তব্য পছন্দ করেছেন।", + "doYouWantToKeepTheDraft": "আপনি কি ড্রাফট রাখতে চান?", + "operationsAreTooFrequent": "অপারেশন খুব ঘন ঘন", + "luckNumber": "ভাগ্য নম্বর", + "relationShip": "সম্পর্ক", + "couple": "জোড়া {1}:", + "couple2": "জোড়া", + "reject": "প্রত্যাখ্যান করুন", + "accept": "গ্রহণ করুন", + "noMatchedCP": "মিলে যাওয়া CP নেই", + "inviteYouToBecomeBD": "আপনাকে BD बनতে আমন্ত্রণ জানাচ্ছি।", + "adminInviteRechargeAgent": "আপনাকে রিচার্জ এজেন্ট बनতে আমন্ত্রণ জানাচ্ছি।", + "confirmAcceptTheInvitation": "আপনি কি আমন্ত্রণ গ্রহণ করতে নিশ্চিত করছেন?", + "confirmDeclineTheInvitation": "আপনি কি আমন্ত্রণ প্রত্যাখ্যান করতে নিশ্চিত করছেন?", + "host": "হোস্ট", + "following": "ফলো করা হচ্ছে", + "agent": "এজেন্ট", + "approved": "অনুমোদিত", + "onlineUsers": "অনলাইন ব্যবহারকারী({1}/{2}):", + "applyToJoin": "যোগদানের জন্য আবেদন করুন", + "supporterList": "সমর্থক তালিকা", + "hostList": "হোস্ট তালিকা", + "upToAdmins": "সর্বাধিক {1} অ্যাডমিন", + "upToMembers": "সর্বাধিক {1} সদস্য", + "levelPrivileges": "স্তরের অধিকার", + "ra": "RA", + "roomAnnouncement": "রুম ঘোষণা", + "help": "সহায়তা", + "rejected": "প্রত্যাখ্যান করা হয়েছে", + "boxContributeTips": "আজ ইতিমধ্যে অবদান রাখা হয়েছে, কृপয়া আবার অবদান না রাখুন", + "bd": "BD", + "coupon": "কুপন", + "search": "সার্চ", + "get": "পান করুন", + "inRocket": "রকেটে", + "roomRocketHelpTips": "1. রুমে উপহার পাঠান রকেট শক্তি বাড়ায়। *1 সোনা কয়েন উপহার = 1 রকেট শক্তি পয়েন্ট; ভাগ্যশালী উপহার রকেট শক্তি উপহারের সোনা কয়েন মানের 4% পরিমাণে বাড়ায়।\n2. রকেট শক্তি সম্পূর্ণভাবে পূর্ণ হলে, রুম রকেট চালু করতে পারে। চালু করার পর পুরস্কার স্বয়ংক্রিয়ভাবে বিতরণ করা হবে।\n3. বিভিন্ন রকেট স্তর বিভিন্ন পুরস্কার প্রদান করে।\n4. রকেট চালু হলে, রুমের সমস্ত ব্যবহারকারী রকেট পুরস্কার দাবি করতে পারে।5. রকেট শক্তি প্রতিদিন 00:00 এ শূন্য হয়ে যায়।", + "couponRecord": "কুপন ব্যবহার রেকর্ড", + "inRoom": "রুমে", + "searchCouponHint": "কুপন সার্চ করুন", + "giftCounter": "উপহার কাউন্টার", + "bDLeaderInviteYouToBecomeBDLeader": "আপনাকে BD লিডার बनতে আমন্ত্রণ জানাচ্ছি", + "wins": "বিজয়", + "inviteYouToBecomeHost": "আপনাকে হোস্ট बनতে আমন্ত্রণ জানাচ্ছি।", + "friends": "বন্ধু", + "deleteConversationTips": "আপনি কি এই ব্যবহারকারীর সাথে চ্যাট ইতিহাস মুছে ফেলতে চান?", + "propMessagePrompt": "প্রপ মেসেজ টিপস", + "inputUserId": "ব্যবহারকারী ID লিখুন", + "fromLuckyGifts": "ভাগ্যশালী/জাদुई উপহার থেকে", + "receive": "প্রাপ্ত করুন", + "checkInSuccessful": "সাইন ইন সফল", + "sginTips": "আপনি প্রতিদিন প্রথমবার সাইন ইন করলে পুরস্কার পাবেন। আপনি যদি সাইন ইন বন্ধ করেন, তবে আবার সাইন ইন করলে পুরস্কার প্রথম দিন থেকে গণনা করা হবে।", + "popular": "জনপ্রিয়", + "recommend": "সুপারিশ", + "follow": "ফলো করুন", + "history": "ইতিহাস", + "hotRooms": "হট রুম", + "viewMore": "আরও দেখুন", + "noData": "ডেটা নেই", + "users": "ব্যবহারকারী", + "rooms": "রুম", + "coins": "কয়েন", + "unread": "অপঠিত", + "read": "পঠিত", + "image": "[ছবি]", + "video": "[ভিডিও]", + "sound": "[সাউন্ড]", + "gift2": "[উপহার]", + "clickHereToStartChatting": "কিছু বলুন.....", + "receivedAMessage": "[মেসেজ প্রাপ্ত]", + "confirmSwitchMicModelTips": "আপনি কি চেয়ার মোড পরিবর্তন করতে নিশ্চিত করছেন?", + "number": "নম্বর", + "album": "অ্যালবাম", + "camera": "ক্যামেরা", + "system": "সিস্টেম", + "notifcation": "নোটিফিকেশন", + "inviteYouToBecomeAgent": "আপনাকে এজেন্ট बनতে আমন্ত্রণ জানাচ্ছি।", + "myMusic": "আমার মিউজিক", + "add": "যোগ করুন", + "pullToLoadMore": "লোড করার জন্য টানুন", + "loadingFailedClickToRetry": "লোড ব্যর্থ, পুনরায় চেষ্টা করার জন্য ক্লিক করুন", + "releaseToLoadMore": "লোড করার জন্য ছেড়ে দিন", + "haveMyLimits": "---আমার নিজস্ব সীমা আছে।---", + "music": "মিউজিক", + "free": "বিনামূল্যে", + "charm": "আকর্ষণ", + "start": "শুরু করুন", + "stop": "বন্ধ করুন", + "chats": "চ্যাট", + "refuse": "প্রত্যাখ্যান করুন", + "agree": "সম্মত হন", + "thisFeatureIsCurrentlyUnavailable": "এই ফিচারটি বর্তমানে অনুপলব্ধ।", + "pleaseSelectTheRecipient": "কृপয়া প্রাপক নির্বাচন করুন।", + "searchMemberIdHint": "কৃপয়া সদস্যের আইডি নম্বর লিখুন", + "unclaimedRedEnvelopes": "দাবি না করা লাল খাম 24 ঘন্টার মধ্যে ফেরত দেওয়া হবে।", + "redEnvelopeNotYetClaimed": "লাল খাম এখনও দাবি করা হয়নি।", + "redEnvelopeAmount": "লাল খামের পরিমাণ: {1} কয়েন", + "sentARedEnvelope": "লাল খাম পাঠিয়েছে।", + "theRedEnvelopeHasExpired": "লাল খামের মেয়াদ শেষ হয়ে গেছে।", + "joinRequest": "যোগদানের অনুরোধ", + "scrollToTheBottom": "নিচে স্ক্রোল করুন", + "gameRules": "গেম নিয়ম:", + "charmGameRulesTips": "মিটার প্যানেল খুলুন, মাইক্রোফোনে সমস্ত ব্যবহারকারী প্রাপ্ত উপহার কয়েন দেখান,1 কয়েন উপহার = 1 পয়েন্ট (ভাগ্যশালী উপহার 0.04 পয়েন্ট)।", + "inputYourOldPassword": "আপনার পুরানো পাসওয়ার্ড লিখুন", + "enterYourOldPassword": "আপনার পুরানো পাসওয়ার্ড লিখুন", + "setYourPassword": "আপনার পাসওয়ার্ড সেট করুন", + "enterYourNewPassword": "আপনার নতুন পাসওয়ার্ড লিখুন", + "confirmYourPassword": "আপনার পাসওয়ার্ড নিশ্চিত করুন", + "theTwoPasswordsDoNotMatch": "দুটি পাসওয়ার্ড মিলছে না।", + "resetLoginPasswordtTips2": "পাসওয়ার্ড 8-16 অক্ষরের দৈর্ঘ্যের হতে হবে এবং বড়/ছোট ইংরেজি অক্ষর এবং সংখ্যার সমন্বয়ে গঠিত হতে হবে (শুধুমাত্র সংখ্যা নয়)", + "resetLoginPassword": "লগইন পাসওয়ার্ড রিসেট করুন", + "resetLoginPasswordtTips1": "আপনার ব্যবহারকারী ID বা কাস্টম ID দিয়ে লগইন করুন। আজী অ্যাকাউন্ট দিয়ে লগইন করা আরও নিরাপদ।", + "localMusic": "লোকাল মিউজিক", + "setLoginPassword": "লগইন পাসওয়ার্ড সেট করুন", + "confirmSwitchMicThemeTips": "আপনি কি চেয়ার স্টাইল পরিবর্তন করতে নিশ্চিত করছেন?", + "micTheme": "মাইক্রো থিম", + "classicMic": "ক্লাসিক {1} মাইক্রো", + "yesterday": "গতকাল {1}", + "monday": "সোমবার {1}", + "tuesday": "মঙ্গলবার {1}", + "wednesday": "বুধবার {1}", + "thursday": "বৃহস্পতিবার {1}", + "friday": "শুক্রবার {1}", + "saturday": "শনিবার {1}", + "sunday": "রবিবার {1}", + "acceptedYour": "{1} আপনার আমন্ত্রণ গ্রহণ করেছে", + "youAccepted": "আপনি {1} এর আমন্ত্রণ গ্রহণ করেছেন", + "openRedPackDialogTip": "লাল খাম", + "micManagement": "মাইক্রো ম্যানেজমেন্ট", + "bdCenter": "BD সেন্টার", + "rechargeAgency": "রিচার্জ এজেন্সি", + "adminCenter": "অ্যাডমিন সেন্টার", + "goldList": "সোনা তালিকা", + "followList": "ফলো তালিকা", + "fansList": "ফ্যান তালিকা", + "daily": "দৈনন্দিন", + "female": "মহিলা", + "male": "পুরুষ", + "identity": "আইডেন্টিটি", + "adjust": "সামঞ্জস্য করুন", + "warning": "সতর্কতা", + "screenshotTips": "স্ক্রিনশট (সর্বাধিক 3)", + "roomNotice": "রুম নোটিশ", + "roomTheme": "রুম থিম", + "description": "বর্ণনা:", + "inputDesHint": "সমস্যাটি বুঝতে এবং সমাধান করতে আমাদের সাহায্য করার জন্য কृপয়া সমস্যাটি যতটা সম্ভব বিস্তারিতভাবে বর্ণনা করুন।", + "roomProfilePicture": "রুম প্রোফাইল ছবি", + "userProfilePicture": "ব্যবহারকারী প্রোফাইল ছবি", + "userName": "ব্যবহারকারী নাম", + "pleaseSelectTheTypeToProcess": "কৃপয়া প্রক্রিয়াকরণের টাইপ নির্বাচন করুন:", + "roomEditing": "রুম সম্পাদনা", + "setAccount": "অ্যাকাউন্ট সেট করুন", + "userEditing": "ব্যবহারকারী সম্পাদনা", + "enterTheUserId": "ব্যবহারকারী ID লিখুন", + "enterTheRoomId": "রুম ID লিখুন", + "deleteAccount": "অ্যাকাউন্ট মুছে ফেলুন", + "becomeAgent": "এজেন্ট बनুন", + "enterNickname": "উপনাম লিখুন", + "selectYourCountry": "আপনার দেশ নির্বাচন করুন", + "inviteCode": "আমন্ত্রণ কোড", + "magic": "জাদু", + "luckGiftSpecialEffects": "ভাগ্যশালী উপহার অ্যানিমেশন প্রভাব", + "theVideoSizeCannotExceed": "ভিডিও আকার 50M অতিক্রম করতে পারবেন না", + "weekly": "সাপ্তাহিক", + "customizedGiftRulesContent": "আমি কীভাবে কাস্টমাইজড গিফট পেতে পারি:\n1.বর্তমান সম্পদ স্তর অনুসারে ব্যবহারকারীর কাস্টমাইজড গিফট পাওয়ার যোগ্যতা নির্ধারণ করুন.\n(1) ব্যবহারকারীর সম্পদ স্তর ≥35 হলে: \nব্যবহারকারী কাস্টমাইজড গিফটের জন্য একবার ভিডিও আপলোড করতে পারেন। আমরা ব্যবহারকারীর বর্তমান প্রোফাইল ছবিকে গিফট ইমেজ হিসেবে এবং প্রদত্ত ভিডিওকে \"কাস্টম\" এর উপর গিফট প্রভাব হিসেবে ব্যবহার করব। প্রDUCTION কিছুটা সময় নেবে এবং স্টোরে উপলব্ধ হওয়ার সাথে সাথে আপনাকে অবহিত করব।\n(2) ব্যবহারকারীর সম্পদ স্তর ≥45 হলে: \nব্যবহারকারী কাস্টমাইজড গিফটের জন্য একবার ভিডিও আপলোড করতে পারেন। আমরা ব্যবহারকারীর বর্তমান প্রোফাইল ছবিকে গিফট ইমেজ হিসেবে এবং প্রদত্ত ভিডিওকে \"কাস্টম\" এর উপর গিফট প্রভাব হিসেবে ব্যবহার করব। প্রDUCTION কিছুটা সময় নেবে এবং স্টোরে উপলব্ধ হওয়ার সাথে সাথে আপনাকে অবহিত করব।\n2.অ্যাপের নির্দিষ্ট কার্যকলাপে অংশ নিতে পারেন এবং মানদণ্ড পূরণ করার পর \"মেসেজ\" → \"আমাদের সাথে যোগাযোগ করুন\" বিভাগ থেকে আমাদের সাথে যোগাযোগ করতে পারেন। কার্যকলাপের স্ক্রিনশট এবং কাস্টমাইজড গিফটের জন্য ব্যবহার করতে চান এমন ভিডিও প্রদান করুন। আমরা আপনার বর্তমান প্রোফাইল ছবিকে গিফট ইমেজ হিসেবে এবং প্রদত্ত ভিডিওকে গিফট প্রভাব হিসেবে ব্যবহার করব, তারপর \"কাস্টম\" এর অধীনে তালিকাভুক্ত করা হবে। প্রDUCTION কিছুটা সময় নেবে এবং স্টোরে উপলব্ধ হওয়ার সাথে সাথে আপনাকে অবহিত করব।\nকাস্টমাইজড গিফটের মেয়াদী সময় & কীভাবে সম্প্রসারিত করব:\nকাস্টমাইজড কাস্টম গিফটগুলি র্যাকে 30 দিনের মেয়াদী সময়সহ থাকবে। কার্যকর এবং অনুপ্রেরণামূলক ব্যবহারকারীদের এই নতুন অভিজ্ঞতা চালিয়ে যেতে এবং তাদের ব্যক্তিগত স্টাইল প্রদর্শন করতে সাহায্য করার জন্য, কাস্টমাইজড গিফট ধারণকারী ব্যবহারকারীরা যেকোনো মাসে 500 মার্কিন ডলার রিচার্জ করে তাদের সমস্ত কাস্টমাইজড গিফটের র্যাক সময় 30 দিন বাড়াতে পারেন।", + "customizedGiftRules": "কাস্টমাইজড গিফট নিয়ম", + "rulesUpload": "নিয়ম&আপলোড", + "monthly": "মাসিক", + "message": "মেসেজ", + "clearCache": "ক্যাশ মুছে ফেলুন", + "customized": "কাস্টমাইজড", + "searchInputHint": "অ্যাকাউন্ট/রুম নম্বর লিখুন", + "kickRoomTips": "আপনাকে রুম থেকে বের করা হয়েছে।", + "joinRoomTips": "রুমে যোগদান করেছেন !", + "roomSetting": "রুম সেটিংস", + "roomDetails": "রুম বিবরণ", + "systemRoomTips": "শিষ্টাচার এবং সম্মানজনক হোন। আজীতে কোনো পর্নোগ্রাফিক বা অনুপযুক্ত কন্টেন্ট একেবারে নিষিদ্ধ। সনাক্ত হলে, অ্যাকাউন্ট স্থায়ীভাবে ব্লক করা হবে। কृপয়া আজী প্ল্যাটফর্মের নিয়মাবলী সচেতনভাবে অনুসরণ করুন।", + "copiedToClipboard": "ক্লিপবোর্ডে কপি করা হয়েছে", + "recharge": "রিচার্জ করুন", + "receivedFromALuckyGift": "ভাগ্যশালী/জাদुई গিফট থেকে প্রাপ্ত হয়েছে।", + "followedYou": "আপনাকে ফলো করেছেন", + "agentCenter": "এজেন্ট সেন্টার", + "areYouSureYouWantToClearLocalCache": "আপনি কি লোকাল ক্যাশ মুছে ফেলতে চান?", + "clearMessage": "স্ক্রিন মেসেজ মুছে ফেলুন", + "report": "রিপোর্ট করুন", + "coins3": "কয়েন", + "giftSpecialEffects": "গিফট বিশেষ প্রভাব", + "basicFeatures": "মৌলিক বৈশিষ্ট্য", + "task": "টাস্ক", + "importantReminder": "গুরুত্বপূর্ণ স্মারক", + "entryVehicleAnimation": "প্রবেশ যান অ্যানিমেশন", + "floatingAnimationInGlobal": "গ্লোবালে ভাসমান অ্যানিমেশন", + "entryVehicleAnimation2": "VIP4 বা তার বেশি অধিকার ધারণকারী ব্যবহারকারীরা ফাংশন ব্যবহার করে গাড়ির অ্যানিমেশন বন্ধ করতে পারেন।", + "dailyTasks": "দৈনন্দিন টাস্ক", + "enterRoomConfirmTips": "আপনি কি রুমে যেতে চান?", + "followSucc": "সফলভাবে ফলো করা হয়েছে", + "goldListort": "সোনা তালিকা", + "rechargeList": "রিচার্জ তালিকা", + "edit": "সম্পাদনা করুন", + "swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt": "*টিপস: ভাসমান স্ক্রিন এলাকায় বামে সвайপ করে দ্রুত বন্ধ করুন।", + "enterThisVoiceChatRoom": "আপনি কি এই ভয়েস চ্যাট রুমে যেতে চান?", + "go": "যান", + "done": "সম্পন্ন", + "improvementTasks": "সুশোধন টাস্ক", + "save": "সংরক্ষণ করুন", + "kickPrevention": "কিক প্রতিরোধ", + "freeChatSpeak": "বিনামূল্যে চ্যাট & কথা বলুন", + "nickName": "উপনাম", + "gender": "লিঙ্গ", + "unFollow": "আনফলো করুন", + "days": "দিন", + "permanent": "স্থায়ী", + "country": "দেশ", + "birthday": "জন্মদিন", + "man": "পুরুষ", + "woman": "মহিলা", + "apple": "Apple", + "google": "Google", + "everyone": "সবার", + "dailyTaskRewardBonus": "দৈনন্দিন কাজের পুরস্কার বোনাস({1} XP)", + "userLevelXPBoost": "ব্যবহারকারীর স্তরের এক্সপি বৃদ্ধি({1} এক্সপি)", + "goToUpgrade": "আপগ্রেড করতে যান", + "exclusiveEmojiWillBeReleasedAfterBecoming": "এক্সক্লুসিভ ইমোজি হওয়ার পরে মুক্তি পাবে", + "preventBeingBlocked": "ব্লক হওয়া প্রতিরোধ করুন", + "enableRankIncognitoMode": "র‌্যাঙ্ক ইনকগনিটো মোড সক্ষম করুন", + "avoidBeingKicked": "পেটানো থেকে বিরত থাকুন", + "privileges": "{1} সুবিধাসমূহ", + "andAboveUsers": "{1} এবং উপরের ব্যবহারকারীরা", + "basicPermissions": "মূল অনুমতিসমূহ", + "mysteriousInvisibility": "রহস্যময় অদৃশ্যতা", + "antiBlock": "ব্লক-বিরোধী", + "shop": "দোকান", + "expirationTime": "মেয়াদ শেষ হওয়ার সময়", + "privateChat": "ব্যক্তিগত চ্যাট", + "storeDiscount": "স্টোর ডিসকাউন্ট {1} ছাড়", + "membershipFreeChatSpeak": "সদস্যতা ছাড়াই চ্যাট ও কথা বলা", + "startVoiceParty": "ভয়েস পার্টি শুরু করুন!", + "enterRoomTips": "{1} রুমে প্রবেশ করেছেন", + "roomName": "রুম নাম", + "roomMember": "রুম সদস্য", + "priorityRoomSorting": "প্রাথমিক কক্ষ শ্রেণীবিন্যাস", + "userColoredID": "ব্যবহারকারী রঙিন পরিচয়পত্র", + "pleaseSelectYourCountry": "কৃপয়া আপনার দেশ নির্বাচন করুন।", + "pleaseSelectYourGender": "কৃপয়া আপনার লিঙ্গ নির্বাচন করুন।", + "pleaseEnterNickname": "কৃপয়া একটি উপনাম লিখুন।", + "countryRegion": "দেশ&অঞ্চল", + "dateOfBirth": "জন্ম তারিখ", + "mute": "মিউট করুন", + "exit": "প্রস্থান", + "pleaseSelectaItem": "কৃপয়া একটি আইটেম নির্বাচন করুন", + "areYouRureRoRecharge": "আপনি কি রিচার্জ করতে চান?", + "mInimize": "রাখো", + "roomOwner": "রুমের মালিক", + "hostCenter": "হোস্ট সেন্টার", + "allOnMicrophone": "সবাই মাইক্রোফোনে", + "usersOnMicrophone": "মাইক্রোফোনে ব্যবহারকারী", + "allInTheRoom": "সবাই রুমে", + "send": "পাঠান", + "crop": "ক্রপ করুন", + "finish": "শেষ করুন", + "takeTheMic": "মাইক্রো নিন", + "openTheMic": "মাইক্রো খুলুন", + "muteTheMic": "মাইক্রো মিউট করুন", + "unlockTheMic": "মাইক্রো আনলক করুন", + "leavelTheMic": "মাইক্রো ছেড়ে দিন", + "lockTheMic": "মাইক্রো লক করুন", + "removeTheMic": "মাইক্রো সরান", + "inviteToTheMicrophone": "মাইক্রোফোনে আমন্ত্রণ দিন", + "openUserProfleCard": "ব্যবহারকারী প্রোফাইল কার্ড খুলুন", + "obtain": "প্রাপ্ত করুন", + "win2": "{1} বিজয়", + "backTheRoom": "রুমে ফিরে যান", + "toConsume": "খরচ করার জন্য", + "howToUpgrade": "কীভাবে উন্নত করব?", + "spendCoinsToGainExperiencePoints": "অভিজ্ঞতা পয়েন্ট পাওয়ার জন্য কয়েন খরচ করুন", + "higherLevelFancierAvatarFrame": "উচ্চতর স্তর, আরও সুন্দর ব্যাজ/প্রোফাইল ফ্রেম", + "all": "সব", + "gift": "গিফট", + "chat": "চ্যাট", + "owner": "মালিক", + "store": "স্টোর", + "admin": "অ্যাডমিন", + "member": "সদস্য", + "guest": "অতিথি", + "submit": "জমা দিন", + "membershipFee": "সদস্যপদ ফি", + "membershipFeeTips1": "কৃপয়া আপনার রুমের জন্য সদস্যপদ ফি সেট করুন। ব্যবহারকারীরা ফি পরিশোধ করে আপনার রুমে যোগদান করতে পারেন।", + "membershipFeeTips2": "রুম সদস্য হওয়ার জন্য ব্যবহারকারীর প্রয়োজনীয় সোনা।রুম মালিক সোনার 50% পাবেন।", + "freePrice": "ফি:0-10000", + "touristsSendText": "পর্যটক টেক্সট পাঠায়", + "touristsTakeToTheMic": "পর্যটক মাইক্রো নেয়", + "theMembershipFee": "সদস্যপদ ফি", + "theModificationsMade": "এবার করা পরিবর্তনগুলি প্রস্থানের পর সংরক্ষণ করা হবে না", + "viewFrame": "ফ্রেম দেখুন", + "enterRoomName": "রুম নাম লিখুন", + "headdress": "হেডড্রেস", + "mountains": "গাড়ি", + "purchaseIsSuccessful": "ক্রয় সফল", + "buy": "কিনুন", + "followed": "ফলো করা হয়েছে", + "follow2": "ফলো:{1}", + "fans2": "ফ্যান:{1}", + "vistors2": "ভিজিটর:{1}", + "personal2": "ব্যক্তিগত:", + "conntinue": "চালিয়ে যান", + "confirmBuyTips": "আপনি কি কিনতে চান?", + "purchase": "ক্রয়", + "setRoomPassword": "রুম পাসওয়ার্ড সেট করুন", + "inputRoomPassword": "রুম পাসওয়ার্ড লিখুন", + "enter": "প্রবেশ করুন", + "deleteCommentTips": "আপনি কি এই মন্তব্য মুছে ফেলতে চান?", + "deleteSuccessful": "মুছে ফেলা সফল!", + "itemsLeft": "বাকি আইটেম", + "password": "পাসওয়ার্ড", + "replySucc": "উত্তর সফল", + "comment": "মন্তব্য", + "showMore": "আরও দেখুন", + "showLess": "কম দেখুন", + "enterPassword": "পাসওয়ার্ড লিখুন", + "enterAccount": "অ্যাকাউন্ট লিখুন", + "logIn": "লগইন করুন", + "saySomething": "কিছু বলুন...", + "sayHi": "হাই..", + "pleaseChatFfriendly": "কৃপয়া বন্ধুত্বপূর্ণভাবে চ্যাট করুন", + "unLockTheRoom": "রুম আনলক করুন", + "operationSuccessful": "অপারেশন সফল হয়েছে।", + "adminByHomeowner": "হোম ওনার দ্বারা অ্যাডমিন হিসেবে নিযুক্ত করা হয়েছে।", + "memberByHomeowner": "হোম ওনার দ্বারা সদস্য হিসেবে নিযুক্ত করা হয়েছে।", + "touristByHomeowner": "হোম ওনার দ্বারা পর্যটক হিসেবে নিযুক্ত করা হয়েছে।", + "becomeHost": "হোস্ট হতে আবেদন করুন", + "superFans": " সুপার ফ্যান:", + "setUpAnIdentity": "আইডেন্টিটি সেট আপ করুন", + "kickedOutOfRoom": "রুম থেকে বের করা হয়েছে", + "knapsack": "ব্যাকপ্যাক", + "bdLeader": "BD লিডার", + "picture": "ছবি", + "theImageSizeCannotExceed": "ছবির আকার 4M অতিক্রম করতে পারবেন না", + "activity": "কার্যকলাপ", + "alreadyAnAdministrator": "ইতিমধ্যে অ্যাডমিন", + "alreadyAnMember": "ইতিমধ্যে সদস্য", + "alreadyAnTourist": "ইতিমধ্যে পর্যটক", + "touristsCannotSendMessages": "পর্যটক মেসেজ পাঠাতে পারেন না", + "touristsAreNotAllowedToGoOnTheMic": "পর্যটকদের মাইক্রোফোনে যাওয়ার অনুমতি নেই", + "lockTheRoom": "রুম লক করুন", + "special": "বিশেষ", + "visitorList": "ভিজিটর তালিকা", + "successfulWear": "সফলভাবে পরা হয়েছে", + "confirmUnUseTips": "আপনি কি অপসারণ করতে নিশ্চিত করছেন?", + "custom": "কাস্টম", + "myItems": "আমার আইটেম", + "use": "ব্যবহার করুন", + "unUse": "ব্যবহার না করুন", + "renewal": "পুনর্নবীকরণ", + "wallet": "ওয়ালেট", + "profile": "প্রোফাইল", + "giftwall": "গিফট ওয়াল", + "announcement": "ঘোষণা", + "blockedList": "ব্লক করা তালিকা", + "country2": "দেশ:", + "sendTo": "পাঠানো হবে", + "credits": "ক্রেডিট: {1}", + "successfullyUnloaded": "সফলভাবে আনলোড করা হয়েছে", + "expired": "মেয়াদ শেষ", + "day": "দিন", + "inUse": "ব্যবহারে", + "confirmUseTips": "আপনি কি ব্যবহার করতে নিশ্চিত করছেন?", + "pleaseUploadUserAvatar": "কৃপয়া একটি প্রোফাইল ছবি আপলোড করুন।", + "joinMemberTips": "আপনি যদি রুমে পর্যটক হন, তবে আপনি মাইক্রোফোন নিতে পারবেন না।", + "giftGivingSuccessful": "গিফট দেওয়া সফল।", + "theAccountPasswordCannotBeEmpty": "অ্যাকাউন্ট বা পাসওয়ার্ড খালি হতে পারে না।", + "invitesYouToTheMicrophone": "{1} আপনাকে মাইক্রোফোনে আমন্ত্রণ জানাচ্ছে", + "english": "ইংরেজি", + "chinese": "চীনা", + "arabic": "আরবি", + "darkMode": "ডার্ক মোড", + "lightMode": "লাইট মোড", + "systemDefault": "সিস্টেম ডিফল্ট", + "pleaseGetOnTheMicFirst": "কৃপয়া প্রথমে মাইক্রোফোনে যান।", + "duration2": "সময়:{1}" +} diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json new file mode 100644 index 0000000..c624327 --- /dev/null +++ b/assets/l10n/intl_en.json @@ -0,0 +1,643 @@ +{ + "signInWithGoogle": "Sign in with Google", + "or": "Or", + "signInWithYourAccount": "Sign in with your account", + "signInWithApple": "Sign in with Apple", + "loginRepresentsAgreementTo": "Login represents agreement to", + "termsofService": "Terms of service", + "privaceyPolicy": "Privacey Policy", + "tips": "Tips", + "searchNoDataTips": "Enter the room or user lD you want to search.", + "games": "Games", + "mine": "Mine", + "party": "Party", + "other": "Other", + "yes": "Yes", + "bag": "Bag", + "sound2": "Sound", + "startYourBrandNewJourney": "Start Your Brand New Journey", + "maliciousHarassment": "Malicious harassment", + "event": "Event", + "roomEdit": "Room Edit", + "cancelRoomPassword": "Are you sure you want to delete the room password?", + "roomMemberFee": "Room Member Fee", + "roomTheme2": "Room Theme", + "blockedList2": "Blocked List", + "roomPassword": "Room Password", + "numberOfMic": "Number of Mic", + "pleaseEnterContent": "Please enter content", + "profilePhoto": "Profile Photo", + "aboutMe": "About me", + "myRoom": "My room", + "noHistoricalRecordsAvailable": "No historical records available.", + "inviteNewUsersToEarnCoins": "Invite new users to earn coins", + "crateMyRoom": "CREATE YOUR OWN ROOM.", + "casualInteraction": "Casual Interaction", + "haveGamePlayingTips": "You have a game in progress. Please exit the current game. Are you sure you want to exit?", + "historicalTour": "Historical Tour", + "gameCenter": "Game Center", + "returnToVoiceChat": "Return to voice chat?", + "exitGameMode": "Exit Game Mode", + "enterTheRoom": "Enter the room", + "invite": "Invite", + "recent": "Recent", + "popularEvents": "Popular Events", + "inviteGoRoomTips": "Always here for you, rain or shine. Drop in and say hi!", + "confirmInviteThisUserToTheRoom": "Confirm invite this user(ID:{1}) to the room?", + "clearCacheSuccessfully": "Clear cache successfully", + "sent": "Sent", + "keep": "Keep", + "open": "Open", + "deleteAccountTips2": "*lf you change your mind, you can log back into your current account with in seven days, and we will automatically restore your account. lf no restoration occurs within seven days, the account will be permanently deleted", + "deleteAccountTips": "You have full administrative rights to this account. lf you intend to delete the account, please be aware of the following risks associated with this operation:\n1.Once the account is successfully deleted, you will no longer be able to login to your current account. Account deletion is a permanent action.\n2. After the account is successfully deleted, you will not be able to recover any account data. All information (including rooms, friends), virtual currency, gifts, and virtual items will be permanently deleted and cannot be restored.\n3. Cooling-off period: lf you do not restore the account, you will be unable to access the purchase page, withdrawal page, or any other pages on the app.\n4.During the cooling-off period or after the account has been deleted, the profile page will indicate that it has been deleted. To protect your account from being searched or accessed by others, your personal information will be removed from systems related to daily functions. When account deletion involves national security, civil or criminal proceedings, or the protection of the legitimate rights and interests of third parties, the official reserves the right to reject the user's account deletion request.\nIf you are certain that you want to delete all personal data from your current account, please click \"Delete Account.\"", + "accountDeletionNotice": "Account Deletion Notice:", + "thisUserHasBeenBlacklisted": "This user has been blacklisted.", + "trend": "Trend", + "like": "Like", + "more": "More", + "discard": "Discard", + "catchFirstComment": "Catch first comment", + "reply": "Reply", + "posting": "Posting", + "multiple": "Multiple", + "successfullyRemovedFromTheBlacklist": "Successfully removed from the blacklist!", + "successfullyAddedToTheBlacklist": "Successfully added to the blacklist!", + "youAreCurrentlyCPRelationshipPleaseDissolve": "You are currently in a CP relationship.\nPlease dissolve it first.", + "areYouSureToCancelBlacklist": "Are you sure to cancel blacklist?", + "areYouSureYouWantToBlockThisUser": "Are you sure you want to block this user?", + "removeFromBlacklist": "Remove from blacklist", + "moveToBlacklist": "Move to blacklist", + "userBlacklist": "User blacklist", + "specialEffectsManagement": "Special effects management", + "wishingYouHappinessEveryDay": "Wishing you happiness every day.", + "newMessage": "New message", + "createRoomSuccsess": "Create room succsess!", + "contactUs": "Contact Us", + "systemAnnouncementTips1": "Guard against fraud:", + "systemAnnouncementTips": "Verify information solely through official channels. Never download third-party software, share personal data, or transfer money based on external requests. Official staff IDs are only 10000, 10003, and 10086. For any suspicion, stop and report via", + "systemAnnouncement": "System Announcement", + "doNotClickUnfamiliarTips": "Do not click unfamiliar links, as they can expose your personal information. Never share your ID or bank card details with anyone.", + "atTag": "@Tag", + "sayHi2": "Say Hi", + "canSendMsgTips": "Both parties need to follow each other before they can send private messages.", + "msgSendRedEnvelopeTips": "*A 10% service fee will be charged on red envelopes, and recipients will only receive 90% of the red envelope's value. The sender's wealth level must be higher than Level 10.", + "reapply": "Reapply", + "cancelRequest": "Cancel Request", + "pending": "Pending", + "supporter": "Supporter", + "coinsReceived": "Coins Received", + "numberOfSign": "Number of sign: {1}", + "hostWeeklyRank": "Host Weekly Rank", + "supporterWeeklyRank": "Supporter Weekly Rank", + "memberList": "Member List", + "treasureChest": "Treasure Chest", + "applicationRecord": "Application Record", + "appUpdateTip": "The app has a new version ({1}), please go and download it?", + "ownerIncomeCoins": "Owner's income:{1} coins", + "game": "Game", + "skip2": "Skip", + "coins4": "Coins", + "weekStart": "Week-Start", + "forMoreRewardsPleaseCheckTheTaskCenter": "For more rewards,please check the task center", + "kingQuuen": "King-Quuen", + "ramadan": "Ramadan", + "updateNow": "Update Now", + "allGames": "All Games", + "fishClass": "Fish Class", + "greedyClass": "Greedy Class", + "raceSeries": "Race Series", + "slotsClass": "Slots Class", + "others": "Others", + "hotGames": "Hot Games", + "chatBox": "Chat box", + "termsOfServicePrivacyPolicyTips": "By continuing you agree to the Terms of Service& Privacy Policy", + "and": " and ", + "pleaseSelectTheTypeContent": "Please select the type of offending content.", + "illegalInformation": "Illegal information", + "inappropriateContent": "Inappropriate content", + "personalAttack": "Personal attack", + "confirm": "Confirm", + "spam": "Spam", + "countdownMinutes": "Countdown Minutes :", + "number2": "Number:", + "fraud": "Fraud", + "received": "Received", + "currentProgress": "Current progress", + "currentStage": "Current stage:{1}", + "roomReward2": "Room Reward:{1}", + "roomReward": "Room Reward", + "expirationTime": "Expiration time", + "ownerSendTheRedEnvelope": "Owner send the Reward coins.", + "rewardCoins": "Reward coins:{1} coins", + "lastWeekProgress": "Last week's progress", + "redEnvelopeTips2": "*If the red envelope is not claimed within the time limit, the remaining coins will be returned to the user who sent the red envelope.", + "goToRecharge": "Go to recharge", + "deleteAccount2": " Delete Account({1}s)", + "areYouSureYouWantToDeleteYourAccount": " Are you sure you want to delete your account?", + "insufhcientGoldsGoToRecharge": "Insufhcient golds,recharge now!", + "coins2": "{1}Coins", + "remainingNumberTips": "Remaining number of available:({1}/{2})", + "collectionTimeTips": "Collection time:{1}({2}/{3})", + "sendARedEnvelope": "Send a red envelope", + "sendRedPackConfirmTips": "Are you sure you want to send the red packet?", + "redEnvelopeSendingRecords": "Red envelope sending records:", + "redEnvelope": "Red Envelope", + "redEnvelopeRecTips2": "The red envelopes have all been claimed.", + "redEnvelopeRecTips3": "The red envelope collection time has expired!", + "openTheTreasureChest": "Open the treasure chest", + "redEnvelopeRecTips1": "The earned coins have been deposited into your wallet.", + "redEnvelopeTips1": "Coins:", + "roomTools": "Room Tools:", + "entertainment": "Entertainment:", + "reportSucc": "Report successful", + "pornography": "Pornography", + "reportInputTips": "Please describe the problem in as much detail as possible sothat we can understand and solve it.", + "cancel": "Cancel", + "join": "Join", + "items": "Items", + "vistors": "Vistors", + "fans": "Fans", + "balanceNotEnough": "Insufficient gold coin balance. Do you want to go to top up?", + "skip": "Skip {1}", + "letGoToWatch": "Let's go to watch!", + "launchedARocket": "launched a rocket", + "sendUserId": "Send UserID:{1}", + "giveUpIdentity": "Give up identity", + "leaveRoomIdentityTips": "Are you sure you want to give up the room identity?", + "joinMemberTips2": "Do you want to confirm joining the room as a member?", + "sureUnfollowThisRoom": "Sure to unfollow this room?", + "welcomeMessage": "Welcome to our application, {name}!", + "settings": "Settings", + "account": "Account", + "common": "Common", + "delete": "Delete", + "copy": "Copy", + "bio": "Bio", + "useCoupontips": "Are you sure you want to use the coupon?", + "searchUserId": "Search user's ID", + "sendUser": "Send User", + "hobby": "Hobby", + "sendCoupontips": "Are you sure you want to send this coupon to this user?", + "youDontHaveAnyCouponsYet": "You don't have any coupons yet.", + "recall": "Recall", + "youHaventFollowed": "You haven't followed any room", + "deleteFromMyDevice": "Delete from my device", + "deleteOnAllDevices": "Delete on all devices", + "messageHasBeenRecalled": "This message has been recalled", + "recallThisMessage": "Recall this message?", + "language": "Language", + "feedback": "Feedback", + "signedin": "Signed in", + "receiveSucc": "Successfully claimed", + "about": "About", + "aboutUs": "About Us", + "theme": "Theme", + "wealthLevel": "Wealth Level", + "userLevel": "User Level", + "goToUpload": "Go to upload", + "logout": "Logout", + "luck": "Luck", + "level": "Level", + "themeGoToUploadTips": "1.Review within24 hours after the upload is successful.\n2.All coins will be returned if the review fails.", + "home": "Home", + "explore": "Explore", + "me": "Me", + "socialPrivilege": "Social privilege", + "information": "Information", + "myPhoto": "My Photo", + "areYouSureYouWantToSpend3": "*lf the other party declines the CP invitation, your coins willbe returned to your wallet.", + "areYouSureYouWantToSpend": "Are you sure you want to spend", + "areYouSureYouWantToSpend2": "to send a CP invitation to this user?", + "underReview": "Under review", + "doYouWantToDeleteIt": "Do you want to delete it?", + "chooseFromAblum": "Choose from Ablum", + "spaceBackground": "Space Background", + "editProfile": "Edit Profile", + "sendTheCpRequest": "Send the CP request", + "addCp": "Add CP", + "partWays": "Part Ways", + "reconcile": "Reconcile", + "separated": "Separated", + "areYouSureYouWantToSpend5": "{1} confessed the feeling to you; if you accept, you will become a couple.", + "areYouSureYouWantToSpend6": "{1} wants to get back together with you. lf you decide to reconcile, all of your previous data will be restored.", + "reconcileInvitationTips": "*lf the other party declines the CP invitation, your coins willbe returned to your wallet.", + "reconcileInvitation": "Reconcile Invitation", + "areYouSureYouWantToSpend4": "to send a reconciliation invitation to this user?", + "partWaysTips": "*lf one partner in a couple chooses to part ways, there willbe a 7-day cooling-off period. During this time, both partiescan choose to reconcile, and all data will be restored uponreconciliation. lf no reconciliation is chosen by the end ofthe 7-day period, the couple's data will be cleared.", + "areYouSureYouWantToPartWaysWithYourCP": "Are you sure you want to part ways with your CP?", + "timeSpentTogether": "Time spent together: {1} days", + "firstDay": "First day:{1}", + "numberOfMyCPs": "Number of my CPs:({1}/{2})", + "props": "Props", + "win": "Win", + "dice": "Dice", + "rps": "RPS", + "operationFail": "The operation was fail.", + "likedYourComment": "Liked your Comment.", + "doYouWantToKeepTheDraft": "Do you want to keep the draft?", + "operationsAreTooFrequent": "Operations are too frequent", + "luckNumber": "Luck Number", + "relationShip": "Relationship", + "couple": "Couple {1}:", + "couple2": "Couple", + "reject": "Reject", + "accept": "Accept", + "noMatchedCP": "No matched CP", + "inviteYouToBecomeBD": "Invite you to become a BD.", + "adminInviteRechargeAgent": "Invite you to become a Recharge agent.", + "confirmAcceptTheInvitation": "Confirm to accept the invitation?", + "confirmDeclineTheInvitation": "Confirm decline of the invitation?", + "host": "Host", + "following": "Following", + "agent": "Agency", + "approved": "Approved", + "onlineUsers": "Online Users({1}/{2}):", + "applyToJoin": "Apply to join", + "supporterList": "Supporter List", + "hostList": "Host List", + "upToAdmins": "Up to {1} Admins", + "upToMembers": "Up to {1} Members", + "levelPrivileges": "Level privileges", + "ra": "RA", + "roomAnnouncement": "Room Announcement", + "help": "Help", + "rejected": "Rejected", + "boxContributeTips": "Investment has already been made today, please do not invest again", + "bd": "BD", + "coupon": "Coupon", + "search": "Search", + "get": "Get", + "inRocket": "In the rocket", + "roomRocketHelpTips": "1. Sending gifts in the room increases rocket energy. *1 gold coin gift = 1 rocket energy point; lucky gifts increase rocket energy by 4% of the gift's gold coin value.\n2. Once the rocket energy is fully charged, the room can launch the rocket. Rewards will be automatically distributed after launch.\n3. Different rocket levels offer different rewards.\n4. When the rocket launches, all users in the room can claim the rocket reward.5. Rocket energy is reset at 00:00 every day.", + "couponRecord": "Coupon usage record", + "inRoom": "In Room", + "searchCouponHint": "Search Coupon", + "giftCounter": "Gift counter", + "bDLeaderInviteYouToBecomeBDLeader": "Invite you to become a BDLeader", + "wins": "wins", + "inviteYouToBecomeHost": "Invite you to become a host.", + "friends": "Friends", + "deleteConversationTips": "Are you sure you want to delete the chat history with this user?", + "propMessagePrompt": "Prop message prompt", + "inputUserId": "Enter User ID", + "fromLuckyGifts": "from lucky/magic gifts", + "receive": "Receive", + "checkInSuccessful": "Check-in successful", + "sginTips": "You will get reward at the frst time you log in eachday. lf you interrupt your login, the reward will becalculated from the frst day when you log in again.", + "popular": "Popular", + "recommend": "Recommend", + "follow": "Follow", + "history": "History", + "hotRooms": "Hot Rooms", + "viewMore": "View more", + "noData": "No data", + "users": "Users", + "rooms": "Rooms", + "coins": "coins", + "unread": "Unread", + "read": "Read", + "image": "[Image]", + "video": "[Video]", + "sound": "[Sound]", + "gift2": "[Gift]", + "clickHereToStartChatting": "Say something.....", + "receivedAMessage": "[Received a message]", + "confirmSwitchMicModelTips": "Do you confirm the switch in seating mode?", + "number": "Number", + "album": "Album", + "camera": "Camera", + "system": "System", + "notifcation": "Notice", + "inviteYouToBecomeAgent": "Invite you to become a agency.", + "myMusic": "My Music", + "add": "Add", + "pullToLoadMore": "Pull to load more", + "loadingFailedClickToRetry": "Loading failed, click to retry", + "releaseToLoadMore": "Release to load more", + "haveMyLimits": "---I have my limits.---", + "music": "Music", + "free": "Free", + "charm": "Gift charm", + "start": "Start", + "stop": "Stop", + "chats": "Chats", + "refuse": "Refuse", + "agree": "Agree", + "thisFeatureIsCurrentlyUnavailable": "This feature is currently unavailable.", + "pleaseSelectTheRecipient": "Please select the recipient.", + "searchMemberIdHint": "Please enter the member's ID", + "unclaimedRedEnvelopes": "Unclaimed red envelopes are refunded in 24 hours.", + "redEnvelopeNotYetClaimed": "Red envelope not yet claimed.", + "redEnvelopeAmount": "Red envelope amount: {1} coins", + "sentARedEnvelope": "sent a red envelope.", + "theRedEnvelopeHasExpired": "The red envelope has expired.", + "joinRequest": "Join Request", + "scrollToTheBottom": "Scroll to the bottom", + "gameRules": "Game Rules:", + "charmGameRulesTips": "Turn on the dashboard to show received gift coin of all users on mic,1 coin = 1 score (Lucky gift 1 coin = 0.04 score).", + "inputYourOldPassword": "Input your old password", + "enterYourOldPassword": "Enter your old password", + "setYourPassword": "Set your password", + "enterYourNewPassword": "Enter your new password", + "confirmYourPassword": "Confirm your password", + "theTwoPasswordsDoNotMatch": "The two passwords do not match.", + "resetLoginPasswordtTips2": "The password must be 8-16 characters long and must be a combination ofuppercase and lowercase English letters and numbers (not just numbers)", + "resetLoginPassword": "Reset Login Password", + "resetLoginPasswordtTips1": "Log in with your user ID or vanity ID. lt is safer to log in with a yumi account.", + "localMusic": "Local Music", + "setLoginPassword": "Set Login Password", + "confirmSwitchMicThemeTips": "Do you confirm the switch of seat style?", + "micTheme": "Mic Theme", + "classicMic": "Classic {1} Mic", + "yesterday": "Yesterday {1}", + "monday": "Monday {1}", + "tuesday": "Tuesday {1}", + "wednesday": "Wednesday {1}", + "thursday": "Thursday {1}", + "friday": "Friday {1}", + "saturday": "Saturday {1}", + "sunday": "Sunday {1}", + "acceptedYour": "{1} accepted your ", + "youAccepted": "You accepted {1} ", + "openRedPackDialogTip": "The red envelope", + "micManagement": "Mic Management", + "bdCenter": "BD Center", + "rechargeAgency": "Recharge Agency", + "adminCenter": "Admin Center", + "goldList": "Gold List", + "followList": "Follow List", + "fansList": "Fans List", + "daily": "Daily", + "female": "Female", + "male": "Male", + "identity": "Identity", + "adjust": "Adjust", + "warning": "Warning", + "screenshotTips": "Screenshot (Up to 3)", + "roomNotice": "Room notice", + "roomTheme": "Room theme", + "description": "Description:", + "inputDesHint": "Please describe the problem in as much detail as possible sothat we can understand and solve it.", + "roomProfilePicture": "Room profile picture", + "userProfilePicture": "User profile picture", + "userName": "User name", + "pleaseSelectTheTypeToProcess": "Please select the type to process:", + "roomEditing": "Room Editing", + "setAccount": "Set Account", + "userEditing": "User Editing", + "enterTheUserId": "Enter the userId", + "enterTheRoomId": "Enter the roomId", + "deleteAccount": "Delete Account", + "becomeAgent": "Become a agent", + "enterNickname": "Enter Nickname", + "selectYourCountry": "Select your country", + "inviteCode": "Invite Code", + "magic": "Magic", + "luckGiftSpecialEffects": "Lucky gift animation effets", + "theVideoSizeCannotExceed": "The video size cannot exceed 50M", + "weekly": "Weekly", + "customizedGiftRulesContent": "How to get my customized gift:\n1.Determine whether the user is eligible to receive a customized gift based on their current wealth level.\n(1) When the user's wealth level is ≥35: 
The user can upload a video for the customized gift once. We will use the user's current profile picture as the gift image and the provided video as the gift effect on \"Customized.\" Production will take some time, and we will notify you as soon as it is available in the store.\n(2) When the user's wealth level is ≥45: 
The user can upload a video for the customized gift once. We will use the user's current profile picture as the gift image and the provided video as the gift effect on \"Customized.\" Production will take some time, and we will notify you as soon as it is available in the store.\n2.You can participate in specific activities on app and, after meeting the criteria, contact us through the \"Message\" → \"Contact us\" section. Provide screenshots of the activity and the video you wish to use for the customized gift. We will use your current profile picture as the gift image and the provided video as the gift effect, then list it under \"Customized.\" Production will take some time, and we will notify you as soon as it is available in the store.\nCustomized Gift Validity Period & How to Extend It:\nThe exclusive customized gifts will be available on the shelf for a validity period of 30 days. To enable influential and inspiring users to continue enjoying this new experience and showcase their personal style, users who own customized gifts can extend the shelf time of all their customized gifts by 30 days by recharging $500 in any given month.", + "customizedGiftRules": "Customized Gift Rules", + "rulesUpload": "Rules&Upload", + "monthly": "Monthly", + "message": "Message", + "clearCache": "Clear Cache", + "customized": "Customized", + "searchInputHint": "Enter account/room number", + "kickRoomTips": "You have been kicked out of the room.", + "joinRoomTips": "joined room !", + "roomSetting": "Room Setting", + "roomDetails": "Room Details", + "systemRoomTips": "Be polite and respectful. Any pornographic or vulgar content is strictly prohibited in yumi. Once discovered, the account will be banned permanently. Please consciously abide by the regulations of the yumi platform.", + "copiedToClipboard": "Copied to clipboard", + "recharge": "Recharge", + "receivedFromALuckyGift": "Received from a lucky/magic gift.", + "followedYou": "Followed you", + "agentCenter": "Agency Center", + "areYouSureYouWantToClearLocalCache": "Are you sure you want to clear local cache?", + "clearMessage": "Clear screen messages", + "report": "Report", + "coins3": "Coins", + "giftSpecialEffects": "Gift special effects", + "basicFeatures": "Basic Features", + "task": "Task", + "importantReminder": "Important Reminder", + "entryVehicleAnimation": "Entry vehicle animation", + "floatingAnimationInGlobal": "Floating animation in global", + "entryVehicleAnimation2": "Users with VlP4 or higher privileges can use the functionto disable vehicle animations.", + "dailyTasks": "Daily Tasks", + "enterRoomConfirmTips": "Are you sure you want to enter the room?", + "followSucc": "Followed successfully", + "goldListort": "Gold List", + "rechargeList": "Recharge List", + "edit": "Edit", + "swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt": "*Tip: Swipe left on the floating screen area to quickly close it.", + "enterThisVoiceChatRoom": "Enter this voice chat room?", + "go": "Go", + "done": "Done", + "improvementTasks": "Improvement Tasks", + "save": "Save", + "kickPrevention": "Kick prevention", + "freeChatSpeak": "Free Chat & Speak", + "nickName": "NickName", + "gender": "Gender", + "unFollow": "Unfollow", + "days": "Days", + "permanent": "Permanent", + "country": "Country", + "birthday": "Birthday", + "man": "Man", + "woman": "Woman", + "apple": "Apple", + "google": "Google", + "startVoiceParty": "Start a voice party!", + "enterRoomTips": "{1} Enter the room", + "roomName": "Room Name", + "roomMember": "Room Member", + "pleaseSelectYourCountry": "Please select your country.", + "pleaseSelectYourGender": "Please select your gender.", + "pleaseEnterNickname": "Please enter a nickname.", + "countryRegion": "Country&Region", + "dateOfBirth": "Date of birth", + "mute": "Mute", + "exit": "Exit", + "mysteriousInvisibility": "Mysterious invisibility", + "antiBlock": "Anti-Block", + "privateChat": "Private Chat", + "storeDiscount": "Store Discount {1} Off", + "membershipFreeChatSpeak": "Membership-free Chat & Speak", + "priorityRoomSorting": "Priority Room Sorting", + "userColoredID": "User Colored ID", + "pleaseSelectaItem": "Please select a item", + "areYouRureRoRecharge": "Are you sure to recharge?", + "mInimize": "Keep", + "everyone": "Everyone", + "shop": "Shop", + "roomOwner": "Room Owner", + "dailyTaskRewardBonus": "Daily Task Reward Bonus({1} XP)", + "userLevelXPBoost": "User Level XP Boost({1} XP)", + "andAboveUsers": "{1} And Above Users", + "privileges": "{1} Privileges", + "basicPermissions": "Basic permissions", + "hostCenter": "Host Center", + "allOnMicrophone": "All on mic", + "usersOnMicrophone": "Users on mic", + "allInTheRoom": "All in room", + "send": "Send", + "crop": "Crop", + "goToUpgrade": "Go to upgrade", + "exclusiveEmojiWillBeReleasedAfterBecoming": "Exclusive emoji will be released after becoming", + "preventBeingBlocked": "Prevent Being Blocked", + "enableRankIncognitoMode": "Enable Rank Incognito Mode", + "avoidBeingKicked": "Avoid Being Kicked", + "finish": "Finish", + "takeTheMic": "Take the mic", + "openTheMic": "Open the mic", + "muteTheMic": "Mute the mic", + "unlockTheMic": "Unlock the mic", + "leavelTheMic": "Leavel the mic", + "lockTheMic": "Lock the mic", + "removeTheMic": "Remove the mic", + "inviteToTheMicrophone": "Invite to the microphone", + "openUserProfleCard": "Open user profle card", + "obtain": "obtain", + "win2": "Win {1}", + "backTheRoom": "Back room", + "toConsume": "To consume", + "howToUpgrade": "How to upgrade?", + "spendCoinsToGainExperiencePoints": "Spend Coins to gain experience points", + "higherLevelFancierAvatarFrame": "Higher level, fancier badges/avatar frame", + "all": "All", + "gift": "Gift", + "chat": "Chat", + "owner": "Owner", + "store": "Store", + "admin": "Admin", + "member": "Member", + "guest": "Guest", + "submit": "Submit", + "claim": "Claim", + "complete": "Complete", + "shareTo": "Share to", + "copyLink": "Copy Link", + "faceBook": "Facebook", + "whatsApp": "Whatsapp", + "snapChat": "Snapchat", + "taskNamePersonalGameConsume": "Game Spending", + "taskNameRoomNewMember": "New Room Members", + "taskNameRoomOwnerSendRedPacket": "Room Owner sends one red packet", + "taskNameRoomOwnerSendGiftUser": "Room Owner Sends Gift", + "taskNameRoomMicUser120Min": "Members with 120+ minutes on mic", + "taskNameRoomMicUser60Min": "Members with 60+ minutes on mic", + "taskNameRoomMicUser30Min": "Members with 30+ minutes on mic", + "taskNamePersonalSendGift": "Send Gift to User", + "taskNameRoomOnlineUserCount": "Room online members", + "taskNameRoomOwnerInviteMic": "Invite member to Mic", + "taskNameRoomUserSendGiftGold": "Coins gifted by members", + "taskNameRoomOwnerSendGiftGold": "Coins gifted by room owner", + "taskNamePersonalMagicGiftGold": "Coins Won by Sending Magic Gifts", + "taskNamePersonalLuckyGiftGold": "Coins Won by Sending Lucky Gifts", + "taskNameRoomOwnerMicTime": "Room Owner goes on mic in the room", + "taskNamePersonalActiveInRoom": "Be Active in Others' Rooms", + "taskNamePersonalMicInRoom": "Go on Mic", + "dailyCoinBonanzaRulesDetail": "1. Daily Personal Tasks and Daily Room Owner Tasks can be completed once per user per day. Tasks reset at 00:00:00 Saudi time.\n2.Daily personal tasks can only be completed in others' rooms; room owner tasks can only be completed in your own room.\n3. Gift-giving tasks only count gifts sent in rooms, not gifts sent on feeds.\n4. If multiple accounts are created using the same device or the same SIM card in any way, rewards for all tasks can only be claimed once.", + "dailyCoinBonanzaRules": "Daily Coin Bonanza Rules", + "roomOwnerTasks": "Room Owner Tasks", + "personalTasks": "Personal Tasks", + "noPromptsToday": "No prompts today.", + "getPaidToRefer": "Get Paid to Refer", + "membershipFee": "Membership Fee", + "membershipFeeTips1": "Please set the membership fee for your room. Users can join your room by paying the fee.", + "membershipFeeTips2": "The golds required for the user to become the room member.The room owner will get 50% of the golds.", + "freePrice": "Fee:0-10000", + "touristsSendText": "Tourist send text", + "touristsTakeToTheMic": "Tourists take to the mic", + "theMembershipFee": "The membership fee", + "theModificationsMade": "The modifications made this time will not be saved after exiting", + "viewFrame": "View Frame", + "enterRoomName": "Enter room name", + "headdress": "Frames", + "mountains": "Vehicles", + "purchaseIsSuccessful": "Purchase is successful", + "buy": "Buy", + "followed": "Followed", + "follow2": "Follow:{1}", + "fans2": "Fans:{1}", + "vistors2": "Vistors:{1}", + "personal2": "Personal:", + "conntinue": "Conntinue", + "confirmBuyTips": "Are you sure you want to buy?", + "purchase": "Purchase", + "setRoomPassword": "Set room password", + "inputRoomPassword": "Input room password", + "enter": "Enter", + "deleteCommentTips": "Are you sure you want to delete this comment?", + "deleteSuccessful": "Deletion successful!", + "itemsLeft": "Items left", + "password": "Password", + "replySucc": "Reply successful", + "comment": "Comment", + "showMore": "Show more", + "showLess": "Show less", + "enterPassword": "Enter Password", + "enterAccount": "Enter Account", + "logIn": "Log In", + "saySomething": "Say something...", + "sayHi": "Say Hi..", + "pleaseChatFfriendly": "Please chat friendly", + "unLockTheRoom": "Unlock The Room", + "operationSuccessful": "The operation was successful.", + "adminByHomeowner": "is set as an administrator by the homeowner.", + "memberByHomeowner": "is set as members by the homeowner.", + "touristByHomeowner": "is set as a tourist by the homeowner.", + "becomeHost": "Apply To Become A Host", + "superFans": "Super fans:", + "setUpAnIdentity": "Set up an identity", + "kickedOutOfRoom": "Kicked out of room", + "knapsack": "Knapsack", + "bdLeader": "BD Leader", + "picture": "Picture", + "theImageSizeCannotExceed": "The image size cannot exceed 4M", + "activity": "Activity", + "alreadyAnAdministrator": "Already an administrator", + "alreadyAnMember": "Already an member", + "alreadyAnTourist": "Already an tourist", + "touristsCannotSendMessages": "Tourists cannot send messages", + "touristsAreNotAllowedToGoOnTheMic": "Tourists are not allowed to go on the mic", + "lockTheRoom": "Lock The Room", + "special": "Special", + "visitorList": "Visitor List", + "successfulWear": "Successful wear", + "confirmUnUseTips": "Do you confirm to remove it?", + "custom": "Custom", + "myItems": "My items", + "use": "Use", + "unUse": "Un use", + "renewal": "Renewal", + "wallet": "Wallet", + "profile": "Profile", + "giftwall": "Giftwall", + "announcement": "Announcement", + "blockedList": "Blocked list", + "country2": "Country:", + "sendTo": "Send to", + "credits": "Credits: {1}", + "successfullyUnloaded": "Successfully unloaded", + "expired": "Expired", + "day": "Day", + "inUse": "In use", + "confirmUseTips": "Do you want to confirm using it?", + "pleaseUploadUserAvatar": "Please upload an avatar.", + "joinMemberTips": "If you are a visitor in the room, you cannot take the microphone.", + "giftGivingSuccessful": "Gift giving successful.", + "theAccountPasswordCannotBeEmpty": "The account or password cannot be empty.", + "invitesYouToTheMicrophone": "{1} invites you to the microphone", + "english": "English", + "chinese": "Chinese", + "arabic": "Arabic", + "darkMode": "Dark Mode", + "lightMode": "Light Mode", + "systemDefault": "System Default", + "pleaseGetOnTheMicFirst": "Please get on the mic first.", + "duration2": "Duration:{1}" +} diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json new file mode 100644 index 0000000..db3e44e --- /dev/null +++ b/assets/l10n/intl_tr.json @@ -0,0 +1,643 @@ +{ + "signInWithGoogle": "Google ile Giriş Yap", + "or": "Veya", + "signInWithYourAccount": "Hesabınız ile Giriş Yap", + "signInWithApple": "Apple ile Giriş Yap", + "loginRepresentsAgreementTo": "Giriş yapmak, aşağıdakilere kabul ettiğiniz anlamına gelir", + "termsofService": "Hizmet Şartları", + "privaceyPolicy": "Gizlilik Politikası", + "tips": "İpuçları", + "searchNoDataTips": "Aramak istediğiniz oda veya kullanıcı kimliğini girin.", + "mine": "Benimki", + "party": "Parti", + "myRoom": "Odam", + "other": "Diğer", + "yes": "Evet", + "bag": "Çanta", + "sound2": "Ses", + "startYourBrandNewJourney": "Yepyeni Yolculuğunuza Başlayın", + "maliciousHarassment": "Kötü niyetli taciz", + "numberOfMic": "Mikrofon Sayısı", + "pleaseEnterContent": "Lütfen içerik girin", + "profilePhoto": "Profil Fotoğrafı", + "aboutMe": "Hakkımda", + "roomEdit": "Oda Düzenle", + "cancelRoomPassword": "Oda şifresini silmek istediğinizden emin misiniz?", + "roomMemberFee": "Oda Üyelik Ücreti", + "blockedList2": "Engellenen Liste", + "roomTheme2": "Oda Teması", + "roomPassword": "Oda Şifresi", + "noHistoricalRecordsAvailable": "Mevcut tarihi kayıt yok.", + "inviteNewUsersToEarnCoins": "Yeni kullanıcıları davet ederek coin kazanın", + "crateMyRoom": "KENDİ ODANIZI OLUŞTURUN.", + "event": "Etkinlik", + "clearCacheSuccessfully": "Önbellek başarıyla temizlendi", + "sent": "Gönderildi", + "keep": "Sakla", + "open": "Aç", + "recent": "Son", + "popularEvents": "Popüler Etkinlikler", + "confirmInviteThisUserToTheRoom": "Bu kullanıcıyı (ID:{1}) odaya davet etmeyi onaylıyor musunuz?", + "deleteAccountTips2": "*Fikrinizi değiştirirseniz, yedi gün içinde mevcut hesabınıza tekrar giriş yapabilir ve hesapınız otomatik olarak geri yüklenecektir. Yedi gün içinde geri yüklenmezse, hesap kalıcı olarak silinir", + "deleteAccountTips": "Bu hesap üzerinde tam yönetim haklarınız bulunur. Hesabı silmek istiyorsanız, bu işlemin ilişkili olduğu aşağıdaki riskleri lütfen unutmayın:\n1.Hesap başarıyla silindikten sonra, mevcut hesabınıza artık giriş yapamazsınız. Hesap silme kalıcı bir eylemdir.\n2. Hesap başarıyla silindikten sonra, hesap verilerinin hiçbirini kurtaramazsınız. Tüm bilgiler (odalar, arkadaşlar dahil), sanal para, hediyeler ve sanal eşyalar kalıcı olarak silinir ve geri yüklenemez.\n3. Soğuma süresi: Hesabı geri yüklemezseniz, uygulamadaki satın alma sayfasına, para çekme sayfasına veya diğer herhangi bir sayfaya erişemezsiniz.\n4.Soğuma süresi boyunca veya hesap silindikten sonra, profil sayfası silindiğini gösterecektir. Hesabınızın başkaları tarafından aranmasını veya erişilmesini engellemek için, kişisel bilgilerinizin günlük işlevlerle ilgili sistemlerden kaldırılacaktır. Hesap silme ulusal güvenliği, siviller veya cezai davaları veya üçüncü tarafların geçerli hak ve menfaatlerinin korunmasını içerdiğinde, resmi yönetim kullanıcının hesap silme isteğini reddetme hakkını saklı tutar.\nEğer mevcut hesabınızdaki tüm kişisel verileri silmekten eminseniz, lütfen \"Hesabı Sil\" butonuna tıklayın.", + "accountDeletionNotice": "Hesap Silme Bildirimi:", + "thisUserHasBeenBlacklisted": "Bu kullanıcı kara listeye alındı.", + "trend": "Trend", + "like": "Beğen", + "more": "Daha Fazla", + "discard": "Vur", + "catchFirstComment": "İlk Yorumu Yakala", + "reply": "Cevapla", + "posting": "Gönderiliyor", + "multiple": "Çoklu", + "successfullyRemovedFromTheBlacklist": "Kara listeden başarıyla kaldırıldı!", + "successfullyAddedToTheBlacklist": "Kara listeye başarıyla eklendi!", + "youAreCurrentlyCPRelationshipPleaseDissolve": "Şu anda bir CP ilişkisindesiniz.\nLütfen önce ilişkiyi sonlandırın.", + "areYouSureToCancelBlacklist": "Kara listeyi iptal etmek istediğinizden emin misiniz?", + "areYouSureYouWantToBlockThisUser": "Bu kullanıcıyı engellemek istediğinizden emin misiniz?", + "removeFromBlacklist": "Kara listeden kaldır", + "moveToBlacklist": "Kara listeye ekle", + "userBlacklist": "Kullanıcı kara listesi", + "specialEffectsManagement": "Özel Efekt Yönetimi", + "wishingYouHappinessEveryDay": "Her gün mutlu olmanızı dileyiyoruz.", + "newMessage": "Yeni mesaj", + "createRoomSuccsess": "Oda başarıyla oluşturuldu!", + "contactUs": "Bize Ulaşın", + "systemAnnouncementTips1": "Sahtekarlığa karşı dikkat:", + "systemAnnouncementTips": "Bilgileri yalnızca resmi kanallar aracılığıyla doğrulayın. Hiçbir zaman üçüncü taraf yazılımı indirme, kişisel verileri paylaşma veya dış istekler üzerine para transferi yapmayın. Resmi personel kimlik numaraları yalnızca 10000, 10003 ve 10086'dır. Herhangi bir şüpheniz olursa, işlemi durdurun ve üzerinden bildirin", + "systemAnnouncement": "Sistem Duyurusu", + "doNotClickUnfamiliarTips": "Tanımadığınız bağlantılara tıklayın, çünkü bunlar kişisel bilgilerinizi ifşa edebilir. Kimlik numaranızı veya banka kartı detaylarınızı asla kimseyle paylaşmayın.", + "atTag": "@Etiket", + "sayHi2": "Merhaba De", + "canSendMsgTips": "Özel mesaj göndermek için her iki tarafın da birbirini takip etmesi gerekir.", + "msgSendRedEnvelopeTips": "*Kırmızı zarflar üzerinde %10 hizmet ücreti kesilecektir ve alıcılar yalnızca kırmızı zarfın değerinin %90'ını alacaktır. Göndericinin servet seviyesi 10. Seviyeden yüksek olmalıdır.", + "reapply": "Tekrar Başvur", + "cancelRequest": "İsteği İptal Et", + "pending": "Beklemede", + "supporter": "Destekçi", + "coinsReceived": "Jetton Alındı", + "numberOfSign": "Giriş Sayısı: {1}", + "hostWeeklyRank": "Sunucu Haftalık Sıralaması", + "supporterWeeklyRank": "Destekçi Haftalık Sıralaması", + "memberList": "Üye Listesi", + "treasureChest": "Hazine Sandığı", + "applicationRecord": "Başvuru Kaydı", + "appUpdateTip": "Uygulamanın yeni bir sürümü var ({1}), lütfen indirin mi?", + "ownerIncomeCoins": "Sahibin Geliri:{1} jetton", + "game": "Oyun", + "skip2": "Atla", + "coins4": "Jetton", + "weekStart": "Hafta Başlangıcı", + "forMoreRewardsPleaseCheckTheTaskCenter": "Daha fazla ödül için lütfen görev merkezini kontrol edin", + "kingQuuen": "Kral-Kraliçe", + "ramadan": "Ramazan", + "updateNow": "Şimdi Güncelle", + "allGames": "Tüm Oyunlar", + "fishClass": "Balık Sınıfı", + "greedyClass": "Aşırı İstekli Sınıfı", + "raceSeries": "Yarış Serisi", + "slotsClass": "Slot Sınıfı", + "others": "Diğerleri", + "hotGames": "Popüler Oyunlar", + "chatBox": "Sohbet Kutusu", + "termsOfServicePrivacyPolicyTips": "Devam ederek Hizmet Şartları'nı ve Gizlilik Politikası'nı kabul ediyorsunuz", + "and": " ve ", + "pleaseSelectTheTypeContent": "Lütfen ihlalci içeriğin türünü seçin.", + "illegalInformation": "Yasaksız Bilgi", + "inappropriateContent": "Uygunsuz İçerik", + "personalAttack": "Kişisel Saldırı", + "confirm": "Onayla", + "spam": "İstenmeyen Mesaj", + "countdownMinutes": "Geri Sayım Dakikaları :", + "number2": "Numara:", + "fraud": "Sahtekarlık", + "received": "Alındı", + "currentProgress": "Mevcut İlerleme", + "currentStage": "Mevcut Aşama:{1}", + "roomReward2": "Oda Ödülü:{1}", + "roomReward": "Oda Ödülü", + "ownerSendTheRedEnvelope": "Sahip ödül jettonlarını gönderdi.", + "rewardCoins": "Ödül jettonları:{1} jetton", + "lastWeekProgress": "Geçen Haftanın İlerlemesi", + "redEnvelopeTips2": "*Kırmızı zarf zaman sınırı içinde talep edilmezse, kalan jettonlar gönderen kullanıcıya iade edilecektir.", + "goToRecharge": "Yüklemeye Git", + "deleteAccount2": " Hesabı Sil({1}s)", + "areYouSureYouWantToDeleteYourAccount": " Hesabınızı silmek istediğinizden emin misiniz?", + "insufhcientGoldsGoToRecharge": "Altın yetersiz, hemen yükle!", + "coins2": "{1}Jetton", + "remainingNumberTips": "Kalan Kullanılabilir Sayı:({1}/{2})", + "collectionTimeTips": "Toplama Zamanı:{1}({2}/{3})", + "sendARedEnvelope": "Kırmızı Zarf Gönder", + "sendRedPackConfirmTips": "Kırmızı paket göndermek istediğinizden emin misiniz?", + "redEnvelopeSendingRecords": "Kırmızı zarf gönderim kayıtları:", + "redEnvelope": "Kırmızı Zarf", + "redEnvelopeRecTips2": "Kırmızı zarfların hepsi talep edildi.", + "redEnvelopeRecTips3": "Kırmızı zarf toplama zamanı doldu!", + "openTheTreasureChest": "Hazine Sandığını Aç", + "redEnvelopeRecTips1": "Kazanan jettonlar cüzdanınıza yatırıldı.", + "redEnvelopeTips1": "Jettonlar:", + "roomTools": "Oda Araçları:", + "entertainment": "Eğlence:", + "reportSucc": "Bildirim başarılı", + "pornography": "Pornografi", + "reportInputTips": "Sorunu anlayabilmemiz ve çözebilmemiz için lütfen sorunu mümkün olduğunca detaylı anlatın.", + "cancel": "İptal", + "join": "Katıl", + "items": "Eşyalar", + "vistors": "Ziyaretçiler", + "fans": "Hayranlar", + "balanceNotEnough": "Altın jetton bakiyesi yetersiz. Yüklemek ister misiniz?", + "skip": "{1} Atla", + "letGoToWatch": "Haydi izlemeye gidelim!", + "launchedARocket": "roket fırlattı", + "sendUserId": "Kullanıcı ID'si Gönder:{1}", + "giveUpIdentity": "Kimliği Bırak", + "leaveRoomIdentityTips": "Oda kimliğini bırakmak istediğinizden emin misiniz?", + "joinMemberTips2": "Üye olarak odaya katılmayı onaylamak ister misiniz?", + "sureUnfollowThisRoom": "Bu odayı takipten çıkarmak istediğinizden emin misiniz?", + "welcomeMessage": "Uygulamamıza hoş geldiniz, {name}!", + "settings": "Ayarlar", + "account": "Hesap", + "common": "Genel", + "delete": "Sil", + "copy": "Kopyala", + "bio": "Biyografi", + "useCoupontips": "Kuponu kullanmak istediğinizden emin misiniz?", + "searchUserId": "Kullanıcı ID'sini Ara", + "sendUser": "Kullanıcıya Gönder", + "hobby": "Hobi", + "sendCoupontips": "Bu kuponu bu kullanıcıya göndermek istediğinizden emin misiniz?", + "youDontHaveAnyCouponsYet": "Henüz hiç kuponunuz yok.", + "recall": "Geri Çağır", + "youHaventFollowed": "Hiç oda takip etmediniz", + "deleteFromMyDevice": "Cihazımdan Sil", + "deleteOnAllDevices": "Tüm Cihazlarda Sil", + "messageHasBeenRecalled": "Bu mesaj geri çağırıldı", + "recallThisMessage": "Bu mesajı geri çağırmak ister misiniz?", + "language": "Dil", + "feedback": "Geri Bildirim", + "signedin": "Giriş Yapıldı", + "receiveSucc": "Başarıyla talep edildi", + "about": "Hakkında", + "aboutUs": "Hakkımızda", + "theme": "Tema", + "wealthLevel": "Servet Seviyesi", + "userLevel": "Kullanıcı Seviyesi", + "goToUpload": "Yüklemeye Git", + "logout": "Çıkış Yap", + "luck": "Şans", + "level": "Seviye", + "themeGoToUploadTips": "1.Yükleme başarılı olduktan sonra 24 saat içinde inceleme yapılacaktır.\n2.Inceleme başarısız olursa tüm jettonlar iade edilecektir.", + "home": "Anasayfa", + "explore": "Keşfet", + "me": "Ben", + "socialPrivilege": "Sosyal ayrıcalık", + "games": "Oyunlar", + "casualInteraction": "Günlük Etkileşim", + "haveGamePlayingTips": "Oynadığınız bir oyun devam ediyor, lütfen önce mevcut oyundan çıkın, çıkmak istediğinizden emin misiniz?", + "historicalTour": "Tarihi Tur", + "gameCenter": "Oyun Merkezi", + "returnToVoiceChat": "Sesli sohbete geri dön?", + "exitGameMode": "Oyun Modundan Çık", + "enterTheRoom": "Odaya gir", + "inviteGoRoomTips": "Her zaman senin için buradayım, yağmurda ya da güneşte. Gel ve merhaba de!", + "invite": "Davet et", + "information": "Bilgi", + "myPhoto": "Fotoğraflarım", + "areYouSureYouWantToSpend3": "*Diğer taraf CP davetini reddederse, jettonlarınız cüzdanınıza iade edilecektir.", + "areYouSureYouWantToSpend": "Harcamak istediğinizden emin misiniz", + "areYouSureYouWantToSpend2": "bu kullanıcıya CP daveti göndermek için?", + "underReview": "İnceleniyor", + "doYouWantToDeleteIt": "Silmek ister misiniz?", + "chooseFromAblum": "Albümdan Seç", + "spaceBackground": "Mekan Arka Planı", + "editProfile": "Profili Düzenle", + "sendTheCpRequest": "CP İsteğini Gönder", + "addCp": "CP Ekle", + "partWays": "Yollarını Ayırmak", + "reconcile": "Uzlaşmak", + "separated": "Ayırılmış", + "areYouSureYouWantToSpend5": "{1} size hislerini ifade etti; kabul ederseniz çift olacaksınız.", + "areYouSureYouWantToSpend6": "{1} sizinle tekrar bir araya gelmek istiyor. Uzlaşmaya karar verirseniz, tüm önceki verileriniz geri yüklenecektir.", + "reconcileInvitationTips": "*Diğer taraf CP davetini reddederse, jettonlarınız cüzdanınıza iade edilecektir.", + "reconcileInvitation": "Uzlaşma Daveti", + "areYouSureYouWantToSpend4": "bu kullanıcıya uzlaşma daveti göndermek için?", + "partWaysTips": "*Çiftin bir partneri yollarını ayırmayı seçerse, 7 günlük bir soğuma süresi olacaktır. Bu süre boyunca her iki taraf da uzlaşmayı seçebilir ve uzlaşma durumunda tüm veriler geri yüklenecektir. 7 günlük sürenin sonunda uzlaşma seçilmezse, çift verileri temizlenecektir.", + "areYouSureYouWantToPartWaysWithYourCP": "CP'nizle yollarınızı ayırmak istediğinizden emin misiniz?", + "timeSpentTogether": "Birlikte Geçirilen Zaman: {1} gün", + "firstDay": "İlk Gün:{1}", + "numberOfMyCPs": "CP'lerim Sayısı:({1}/{2})", + "props": "Özellikler", + "win": "Kazanan", + "dice": "Zar", + "rps": "Kağıt-Kaçak-Makas", + "operationFail": "İşlem başarısız oldu.", + "likedYourComment": "Yorumunuzu beğendi.", + "doYouWantToKeepTheDraft": "Taslağı saklamak ister misiniz?", + "operationsAreTooFrequent": "İşlemler çok sık", + "luckNumber": "Şans Numarası", + "relationShip": "İlişki", + "couple": "Çift {1}:", + "couple2": "Çift", + "reject": "Reddet", + "accept": "Kabul Et", + "noMatchedCP": "Eşleşen CP yok", + "inviteYouToBecomeBD": "Sizi BD yapmaya davet ediyoruz.", + "adminInviteRechargeAgent": "Sizi Yükleme temsilcisi yapmaya davet ediyoruz.", + "confirmAcceptTheInvitation": "Daveti kabul etmek için onaylıyor musunuz?", + "confirmDeclineTheInvitation": "Daveti reddetmek için onaylıyor musunuz?", + "host": "Sunucu", + "following": "Takip Edilenler", + "agent": "Temsilcilik", + "approved": "Onaylandı", + "onlineUsers": "Çevrimiçi Kullanıcılar({1}/{2}):", + "applyToJoin": "Katılmak İçin Başvur", + "supporterList": "Destekçi Listesi", + "hostList": "Sunucu Listesi", + "upToAdmins": "En Fazla {1} Yönetici", + "upToMembers": "En Fazla {1} Üye", + "levelPrivileges": "Seviye Hakları", + "ra": "RA", + "roomAnnouncement": "Oda Duyurusu", + "help": "Yardım", + "rejected": "Reddedildi", + "boxContributeTips": "Bugün zaten yatırım yapıldı, lütfen tekrar yatırım yapmayın", + "bd": "BD", + "coupon": "Kupon", + "search": "Ara", + "get": "Al", + "inRocket": "Rokette", + "roomRocketHelpTips": "1. Odada hediye göndermek roket enerjisini artırır. *1 altın jetton hediyesi = 1 roket enerji puanı; şanslı hediyeler roket enerjisini hediyenin altın jetton değerinin %4'ü kadar artırır.\n2. Roket enerjisi tamamen dolduğunda, oda roketi fırlatabilir. Fırlatmadan sonra ödüller otomatik olarak dağıtılacaktır.\n3. Farklı roket seviyeleri farklı ödüller sunar.\n4. Roket fırlatıldığında, odadaki tüm kullanıcılar roket ödülünü talep edebilir.5. Roket enerjisi her gün 00:00'da sıfırlanır.", + "couponRecord": "Kupon Kullanım Kaydı", + "inRoom": "Odada", + "searchCouponHint": "Kupon Ara", + "giftCounter": "Hediye Sayacı", + "bDLeaderInviteYouToBecomeBDLeader": "Sizi BD Lideri yapmaya davet ediyoruz", + "wins": "kazanmalar", + "inviteYouToBecomeHost": "Sizi sunucu yapmaya davet ediyoruz.", + "friends": "Arkadaşlar", + "deleteConversationTips": "Bu kullanıcıyla olan sohbet geçmişini silmek istediğinizden emin misiniz?", + "propMessagePrompt": "Özellik Mesaj İpucu", + "inputUserId": "Kullanıcı ID'sini Girin", + "fromLuckyGifts": "şanslı/sihirli hediyelerden", + "receive": "Al", + "checkInSuccessful": "Giriş başarılı", + "sginTips": "Her gün ilk defa giriş yaptığınızda ödül alacaksınız. Girişi keserseniz, tekrar giriş yaptığınızda ödül ilk günden itibaren hesaplanacaktır.", + "popular": "Popüler", + "recommend": "Öner", + "follow": "Takip Et", + "history": "Tarihçe", + "hotRooms": "Popüler Odalar", + "viewMore": "Daha Fazlasını Gör", + "noData": "Veri yok", + "users": "Kullanıcılar", + "rooms": "Odalar", + "coins": "jetton", + "unread": "Okunmadı", + "read": "Okundu", + "image": "[Görsel]", + "video": "[Video]", + "sound": "[Ses]", + "gift2": "[Hediye]", + "clickHereToStartChatting": "Bir şey söyle.....", + "receivedAMessage": "[Mesaj alındı]", + "confirmSwitchMicModelTips": "Koltuk modunu değiştirmek için onaylıyor musunuz?", + "number": "Numara", + "album": "Albüm", + "camera": "Kamera", + "system": "Sistem", + "notifcation": "Bildirim", + "inviteYouToBecomeAgent": "Sizi temsilcilik yapmaya davet ediyoruz.", + "myMusic": "Müziklerim", + "add": "Ekle", + "pullToLoadMore": "Yüklemek için çekin", + "loadingFailedClickToRetry": "Yükleme başarısız, tekrar denemek için tıklayın", + "releaseToLoadMore": "Yüklemek için bırakın", + "haveMyLimits": "---Kendi sınırlarım var.---", + "music": "Müzik", + "free": "Ücretsiz", + "charm": "Hediye Çekiciliği", + "start": "Başlat", + "stop": "Durdur", + "chats": "Sohbetler", + "refuse": "Reddet", + "agree": "Kabul Et", + "thisFeatureIsCurrentlyUnavailable": "Bu özellik şu anda kullanılamıyor.", + "pleaseSelectTheRecipient": "Lütfen alıcıyı seçin.", + "searchMemberIdHint": "Lütfen üyenin kimlik numarasını girin", + "unclaimedRedEnvelopes": "Talep edilmemiş kırmızı zarflar 24 saat içinde iade edilir.", + "redEnvelopeNotYetClaimed": "Kırmızı zarf henüz talep edilmedi.", + "redEnvelopeAmount": "Kırmızı zarf miktarı: {1} jetton", + "sentARedEnvelope": "kırmızı zarf gönderdi.", + "theRedEnvelopeHasExpired": "Kırmızı zarfın süresi doldu.", + "joinRequest": "Katılma İsteği", + "scrollToTheBottom": "En alta kaydır", + "gameRules": "Oyun Kuralları:", + "charmGameRulesTips": "Gösterge panelini açın, mikrodaki tüm kullanıcıların aldığı hediye jettonunu gösterin,1 jetton = 1 puan (Şanslı hediye 1 jetton = 0.04 puan).", + "inputYourOldPassword": "Eski şifrenizi girin", + "enterYourOldPassword": "Eski şifrenizi girin", + "setYourPassword": "Şifrenizi ayarlayın", + "enterYourNewPassword": "Yeni şifrenizi girin", + "confirmYourPassword": "Şifrenizi onaylayın", + "theTwoPasswordsDoNotMatch": "İki şifre eşleşmiyor.", + "resetLoginPasswordtTips2": "Şifre 8-16 karakter uzunluğunda olmalı ve büyük/küçük harf İngilizce harfleri ile sayılardan oluşan bir kombinasyon olmalıdır (yalnızca sayılar olmamalı)", + "resetLoginPassword": "Giriş Şifresini Sıfırla", + "resetLoginPasswordtTips1": "Kullanıcı ID'niz veya özel ID'niz ile giriş yapın. yumi hesabı ile giriş yapmak daha güvenlidir.", + "localMusic": "Yerel Müzik", + "setLoginPassword": "Giriş Şifresi Ayarlayın", + "confirmSwitchMicThemeTips": "Koltuk stili değiştirmeyi onaylıyor musunuz?", + "micTheme": "Mikro Tema", + "classicMic": "Klasik {1} Mikro", + "yesterday": "Dün {1}", + "monday": "Pazartesi {1}", + "tuesday": "Salı {1}", + "wednesday": "Çarşamba {1}", + "thursday": "Perşembe {1}", + "friday": "Cuma {1}", + "saturday": "Cumartesi {1}", + "sunday": "Pazar {1}", + "acceptedYour": "{1} sizin davetinizi kabul etti", + "youAccepted": "Sen {1} davetini kabul ettin", + "openRedPackDialogTip": "Kırmızı zarf", + "micManagement": "Mikro Yönetimi", + "bdCenter": "BD Merkezi", + "rechargeAgency": "Yükleme Temsilciliği", + "adminCenter": "Yönetici Merkezi", + "goldList": "Altın Listesi", + "followList": "Takip Listesi", + "fansList": "Hayran Listesi", + "daily": "Günlük", + "female": "Kadın", + "male": "Erkek", + "identity": "Kimlik", + "adjust": "Ayarlama", + "warning": "Uyarı", + "screenshotTips": "Ekran Görüntüsü (En Fazla 3)", + "roomNotice": "Oda Bildirimi", + "roomTheme": "Oda Tema", + "description": "Açıklama:", + "inputDesHint": "Sorunu anlayabilmemiz ve çözebilmemiz için lütfen sorunu mümkün olduğunca detaylı anlatın.", + "roomProfilePicture": "Oda Profil Fotoğrafı", + "userProfilePicture": "Kullanıcı Profil Fotoğrafı", + "userName": "Kullanıcı Adı", + "pleaseSelectTheTypeToProcess": "Lütfen işlem türünü seçin:", + "roomEditing": "Oda Düzenleme", + "setAccount": "Hesap Ayarla", + "userEditing": "Kullanıcı Düzenleme", + "enterTheUserId": "Kullanıcı ID'sini Girin", + "enterTheRoomId": "Oda ID'sini Girin", + "deleteAccount": "Hesabı Sil", + "becomeAgent": "Temsilci Ol", + "enterNickname": "Takma Ad Girin", + "selectYourCountry": "Ülkenizi Seçin", + "inviteCode": "Davet Kodu", + "magic": "Sihir", + "luckGiftSpecialEffects": "Şanslı hediye animasyon efektleri", + "theVideoSizeCannotExceed": "Video boyutu 50M'yi geçemez", + "weekly": "Haftalık", + "customizedGiftRulesContent": "Özel hediyemi nasıl alabilirim:\n1.Kullanıcının özel hediye almaya haklı olup olmadığını mevcut servet seviyesine göre belirleyin.\n(1) Kullanıcının servet seviyesi ≥35 olduğunda: \nKullanıcı özel hediye için bir kez video yükleyebilir. Kullanıcının mevcut profil fotoğrafını hediye görüntüsü olarak ve sağlanan videoyu \"Özel\" üzerindeki hediye efekti olarak kullanacağız. Üretim biraz zaman alacak ve mağazada kullanılabilir hale gelir gelmez sizi bilgilendireceğiz.\n(2) Kullanıcının servet seviyesi ≥45 olduğunda: \nKullanıcı özel hediye için bir kez video yükleyebilir. Kullanıcının mevcut profil fotoğrafını hediye görüntüsü olarak ve sağlanan videoyu \"Özel\" üzerindeki hediye efekti olarak kullanacağız. Üretim biraz zaman alacak ve mağazada kullanılabilir hale gelir gelmez sizi bilgilendireceğiz.\n2.Uygulamadaki belirli etkinliklere katılabilir ve kriterleri karşıladıktan sonra \"Mesaj\" → \"Bize Ulaşın\" bölümünden bizimle iletişime geçebilirsiniz. Etkinliğin ekran görüntülerini ve özel hediye için kullanmak istediğiniz videoyu sağlayın. Mevcut profil fotoğrafınızı hediye görüntüsü olarak ve sağlanan videoyu hediye efekti olarak kullanacağız, ardından \"Özel\" altında listelenecektir. Üretim biraz zaman alacak ve mağazada kullanılabilir hale gelir gelmez sizi bilgilendireceğiz.\nÖzel Hediye Geçerlilik Süresi & Nasıl Uzatılır:\nÖzel özel hediyeler 30 günlük bir geçerlilik süresiyle raflarda bulunacaktır. Etkili ve ilham verici kullanıcıların bu yeni deneyimi devam ettirmesini ve kişisel tarzlarını sergilemesini sağlamak için, özel hediyelere sahip kullanıcılar herhangi bir ayda 500 ABD doları yükleyerek tüm özel hediyelerinin raf zamanını 30 gün uzatabilir.", + "customizedGiftRules": "Özel Hediye Kuralları", + "rulesUpload": "Kurallar&Yükleme", + "monthly": "Aylık", + "message": "Mesaj", + "clearCache": "Önbelleği Temizle", + "customized": "Özel", + "searchInputHint": "Hesap/oda numarasını girin", + "kickRoomTips": "Odadan atıldınız.", + "joinRoomTips": "odaya katıldı !", + "roomSetting": "Oda Ayarları", + "roomDetails": "Oda Detayları", + "systemRoomTips": "Neysevi ve saygılı olun. yumi'de herhangi bir pornografik veya uygunsuz içerik kesinlikle yasaktır. Keşfedilirse, hesap kalıcı olarak engellenecektir. Lütfen yumi platformunun düzenlemelerini bilinçli olarak takip edin.", + "copiedToClipboard": "Panoya kopyalandı", + "recharge": "Yükle", + "receivedFromALuckyGift": "Şanslı/sihirli bir hediyeden alındı.", + "followedYou": "Seni takip etti", + "agentCenter": "Temsilci Merkezi", + "areYouSureYouWantToClearLocalCache": "Yerel önbelleği temizlemek istediğinizden emin misiniz?", + "clearMessage": "Ekran mesajlarını temizle", + "report": "Bildir", + "coins3": "Jetton", + "giftSpecialEffects": "Hediye Özel Efektleri", + "basicFeatures": "Temel Özellikler", + "task": "Görev", + "importantReminder": "Önemli Hatırlatma", + "entryVehicleAnimation": "Giriş Aracı Animasyonu", + "floatingAnimationInGlobal": "Küreselde Süzme Animasyon", + "entryVehicleAnimation2": "VIP4 veya daha yüksek haklara sahip kullanıcılar araç animasyonlarını devre dışı bırakmak için fonksiyonu kullanabilir.", + "dailyTasks": "Günlük Görevler", + "enterRoomConfirmTips": "Odaya girmek istediğinizden emin misiniz?", + "followSucc": "Başarıyla takip edildi", + "goldListort": "Altın Listesi", + "rechargeList": "Yükleme Listesi", + "edit": "Düzenle", + "swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt": "*İpucu: Süzme ekran alanına sola kaydırarak hızlıca kapatın.", + "enterThisVoiceChatRoom": "Bu sesli sohbet odasına girmek ister misiniz?", + "go": "Git", + "done": "Bitti", + "improvementTasks": "Geliştirme Görevleri", + "save": "Kaydet", + "kickPrevention": "Atma Koruması", + "freeChatSpeak": "Ücretsiz Sohbet & Konuşma", + "nickName": "Takma Ad", + "gender": "Cinsiyet", + "unFollow": "Takipten Çık", + "days": "Günler", + "permanent": "Kalıcı", + "country": "Ülke", + "birthday": "Doğum Günü", + "man": "Erkek", + "woman": "Kadın", + "apple": "Apple", + "dailyTaskRewardBonus": "Günlük Görev Ödül Bonusu ({1} XP)", + "userLevelXPBoost": "Kullanıcı Seviyesi XP Artışı({1} XP)", + "google": "Google", + "mysteriousInvisibility": "Gizemli görünmezlik", + "antiBlock": "Tıkanmayı Önleyici", + "privateChat": "Özel Sohbet", + "everyone": "Herkes", + "goToUpgrade": "Yükseltmeye git", + "exclusiveEmojiWillBeReleasedAfterBecoming": "Özel emoji olunduktan sonra yayınlanacak", + "preventBeingBlocked": "Engellenmekten Kaçının", + "enableRankIncognitoMode": "Rütbe Gizli Modunu Etkinleştir", + "avoidBeingKicked": "Tekme Yemekten Kaçının", + "privileges": "{1} Ayrıcalıklar", + "andAboveUsers": "{1} ve üzeri kullanıcılar", + "basicPermissions": "Temel izinler", + "storeDiscount": "Mağaza İndirimi {1} Tutarında", + "membershipFreeChatSpeak": "Üyeliksiz Sohbet ve Konuşma", + "priorityRoomSorting": "Öncelikli Oda Sıralaması", + "userColoredID": "Kullanıcı Renkli Kimliği", + "startVoiceParty": "Sesli parti başlat!", + "enterRoomTips": "{1} odaya girdi", + "roomName": "Oda Adı", + "roomMember": "Oda Üyesi", + "pleaseSelectYourCountry": "Lütfen ülkenizi seçin.", + "pleaseSelectYourGender": "Lütfen cinsiyetinizi seçin.", + "pleaseEnterNickname": "Lütfen bir takma ad girin.", + "countryRegion": "Ülke&Bölge", + "dateOfBirth": "Doğum Tarihi", + "mute": "Sesi Kapat", + "exit": "Çıkış", + "pleaseSelectaItem": "Lütfen bir öğe seçin", + "areYouRureRoRecharge": "Yüklemek istediğinizden emin misiniz?", + "mInimize": "Tutmak", + "shop": "Mağaza", + "expirationTime": "Son kullanma süresi", + "roomOwner": "Oda Sahibi", + "hostCenter": "Sunucu Merkezi", + "allOnMicrophone": "Hepsi Mikrodadır", + "usersOnMicrophone": "Mikrodaki Kullanıcılar", + "allInTheRoom": "Hepsi Odadadır", + "send": "Gönder", + "crop": "Kırp", + "finish": "Bitir", + "takeTheMic": "Mikroyu Al", + "openTheMic": "Mikroyu Aç", + "muteTheMic": "Mikronun Sesini Kapat", + "unlockTheMic": "Mikroyu Kilidini Aç", + "leavelTheMic": "Mikroyu Bırak", + "lockTheMic": "Mikroyu Kilitle", + "removeTheMic": "Mikroyu Kaldır", + "inviteToTheMicrophone": "Mikroya Davet Et", + "openUserProfleCard": "Kullanıcı Profili Kartını Aç", + "obtain": "elde etmek", + "win2": "{1} Kazan", + "backTheRoom": "Odaya Dön", + "toConsume": "Tüketmek İçin", + "howToUpgrade": "Nasıl yükseltirim?", + "spendCoinsToGainExperiencePoints": "Deneyim puanı kazanmak için jetton harcayın", + "higherLevelFancierAvatarFrame": "Daha yüksek seviye, daha şık rozetler/profil çerçevesi", + "all": "Tümü", + "gift": "Hediye", + "chat": "Sohbet", + "owner": "Sahip", + "store": "Mağaza", + "admin": "Yönetici", + "member": "Üye", + "guest": "Misafir", + "submit": "Gönder", + "membershipFee": "Üyelik Ücreti", + "membershipFeeTips1": "Lütfen odanız için üyelik ücretini ayarlayın. Kullanıcılar ücreti ödeyerek odanıza katılabilir.", + "membershipFeeTips2": "Kullanıcı'nın oda üyesi olması için gerekli altınlar.Oda sahibi altınların %50'sini alacaktır.", + "freePrice": "Ücret:0-10000", + "touristsSendText": "Turist metin gönderir", + "touristsTakeToTheMic": "Turist mikro alır", + "theMembershipFee": "Üyelik Ücreti", + "theModificationsMade": "Bu sefer yapılan değişiklikler çıkıştan sonra kaydedilmeyecektir", + "viewFrame": "Çerçeveyi Gör", + "enterRoomName": "Oda adını girin", + "headdress": "Çerçeveler", + "mountains": "Araçlar", + "purchaseIsSuccessful": "Satın alma başarılı", + "buy": "Satın Al", + "followed": "Takip Edildi", + "follow2": "Takip:{1}", + "fans2": "Hayranlar:{1}", + "vistors2": "Ziyaretçiler:{1}", + "personal2": "Kişisel:", + "conntinue": "Devam Et", + "confirmBuyTips": "Satın almak istediğinizden emin misiniz?", + "purchase": "Satın Alma", + "setRoomPassword": "Oda Şifresi Ayarlayın", + "inputRoomPassword": "Oda şifresini girin", + "enter": "Gir", + "deleteCommentTips": "Bu yorumu silmek istediğinizden emin misiniz?", + "deleteSuccessful": "Silme başarılı!", + "itemsLeft": "Kalan Eşyalar", + "password": "Şifre", + "replySucc": "Cevap başarılı", + "comment": "Yorum", + "showMore": "Daha Fazlasını Göster", + "showLess": "Daha Azını Göster", + "enterPassword": "Şifre Girin", + "enterAccount": "Hesap Girin", + "logIn": "Giriş Yap", + "saySomething": "Bir şey söyle...", + "sayHi": "Merhaba..", + "pleaseChatFfriendly": "Lütfen arkadaşça sohbet edin", + "unLockTheRoom": "Odayı Kilidini Aç", + "operationSuccessful": "İşlem başarılı oldu.", + "adminByHomeowner": "ev sahibi tarafından yönetici olarak atandı.", + "memberByHomeowner": "ev sahibi tarafından üye olarak atandı.", + "touristByHomeowner": "ev sahibi tarafından turist olarak atandı.", + "becomeHost": "Ev Sahibi Olmak İçin Başvur", + "superFans": " Süper hayranlar:", + "setUpAnIdentity": "Kimlik Kur", + "kickedOutOfRoom": "Odadan Atıldı", + "knapsack": "Sırt Çantası", + "bdLeader": "BD Lideri", + "picture": "Resim", + "claim": "İddia", + "taskNameRoomNewMember": "Yeni Oda Üyeleri", + "taskNameRoomOwnerSendRedPacket": "Oda Sahibi bir kırmızı paket gönderiyor", + "taskNameRoomOwnerSendGiftUser": "Oda Sahibi Hediye Gönderir", + "taskNameRoomMicUser120Min": "Mikrofon süresi 120+ dakika olan üyeler", + "taskNameRoomMicUser60Min": "Mikrofon süresi 60+ dakika olan üyeler", + "taskNameRoomMicUser30Min": "Mikrofon süresi 30+ dakika olan üyeler", + "taskNamePersonalSendGift": "Kullanıcıya Hediye Gönder", + "taskNameRoomOnlineUserCount": "Odadaki Çevrimiçi Üyeler", + "taskNameRoomOwnerInviteMic": "Üyeyi Mikrofona Davet Et", + "taskNameRoomUserSendGiftGold": "Üyelerin hediye ettiği jetonlar", + "taskNameRoomOwnerSendGiftGold": "Oda sahibinin hediye ettiği jetonlar", + "taskNamePersonalMagicGiftGold": "Büyülü Hediyeler Göndererek Kazanılan Jetonlar", + "taskNamePersonalLuckyGiftGold": "Şanslı Hediyeler Göndererek Kazanılan Jetonlar", + "taskNameRoomOwnerMicTime": "Oda Sahibi odada mikrofona geçer", + "taskNamePersonalActiveInRoom": "Başkalarının Odalarında Aktif Ol", + "taskNamePersonalGameConsume": "Oyun Harcaması", + "taskNamePersonalMicInRoom": "Mikrofonu Aç", + "complete": "Tamamla", + "shareTo": "Paylaş", + "faceBook": "Facebook", + "whatsApp": "Whatsapp", + "snapChat": "Snapchat", + "dailyCoinBonanzaRulesDetail": "1. Günlük Kişisel Görevler ve Günlük Oda Sahibi Görevleri, kullanıcı başına günde bir kez tamamlanabilir. Görevler, Suudi zamanı ile 00:00:00'da sıfırlanır.\n2. Günlük kişisel görevler yalnızca başkalarının odalarında tamamlanabilir; oda sahibi görevleri yalnızca kendi odanızda tamamlanabilir.\n3. Hediye verme görevleri yalnızca odalarda gönderilen hediyeleri sayar, akışlarda gönderilen hediyeleri saymaz.\n4. Aynı cihaz veya aynı SIM kart kullanılarak birden fazla hesap oluşturulursa, tüm görevlerin ödülleri yalnızca bir kez alınabilir.", + "copyLink": "Bağlantıyı Kopyala", + "dailyCoinBonanzaRules": "Günlük Jeton Çılgınlığı Kuralları", + "roomOwnerTasks": "Oda Sahibi Görevleri", + "personalTasks": "Kişisel Görevler", + "noPromptsToday": "Bugün hiçbir istem yok.", + "getPaidToRefer": "Tavsiye Ederek Para Kazanın", + "theImageSizeCannotExceed": "Görsel boyutu 4M'yi geçemez", + "activity": "Etkinlik", + "alreadyAnAdministrator": "Zaten yönetici", + "alreadyAnMember": "Zaten üye", + "alreadyAnTourist": "Zaten turist", + "touristsCannotSendMessages": "Turistler mesaj gönderemez", + "touristsAreNotAllowedToGoOnTheMic": "Turistlerin mikroya çıkmasına izin verilmiyor", + "lockTheRoom": "Odayı Kilitle", + "special": "Özel", + "visitorList": "Ziyaretçi Listesi", + "successfulWear": "Başarıyla takıldı", + "confirmUnUseTips": "Kaldırmak için onaylıyor musunuz?", + "custom": "Özel", + "myItems": "Eşyalarım", + "use": "Kullan", + "unUse": "Kullanmama", + "renewal": "Yenileme", + "wallet": "Cüzdan", + "profile": "Profil", + "giftwall": "Hediye Duvarı", + "announcement": "Duyuru", + "blockedList": "Engellenen Liste", + "country2": "Ülke:", + "sendTo": "Gönderilecek Kişi", + "credits": "Krediler: {1}", + "successfullyUnloaded": "Başarıyla çıkarıldı", + "expired": "Süresi Doldu", + "day": "Gün", + "inUse": "Kullanımda", + "confirmUseTips": "Kullanmak için onaylıyor musunuz?", + "pleaseUploadUserAvatar": "Lütfen bir profil fotoğrafı yükleyin.", + "joinMemberTips": "Eğer odada turist iseniz, mikroyu alamazsınız.", + "giftGivingSuccessful": "Hediye verme başarılı.", + "theAccountPasswordCannotBeEmpty": "Hesap veya şifre boş olamaz.", + "invitesYouToTheMicrophone": "{1} seni mikroya davet ediyor", + "english": "İngilizce", + "chinese": "Çince", + "arabic": "Arapça", + "darkMode": "Karanlık Mod", + "lightMode": "Açık Mod", + "systemDefault": "Sistem Varsayılanı", + "pleaseGetOnTheMicFirst": "Lütfen önce mikroya çıkın.", + "duration2": "Süre:{1}" +} diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..e8f3dda --- /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/fonts/.keep b/fonts/.keep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fonts/.keep @@ -0,0 +1 @@ + diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..ad322bc --- /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..391a902 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..6ed344c --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,5 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" + +APP_DISPLAY_NAME=yumi +PRODUCT_BUNDLE_IDENTIFIER=com.org.yumi diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..e5efcef --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,5 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" + +APP_DISPLAY_NAME=yumi +PRODUCT_BUNDLE_IDENTIFIER=com.org.yumi diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..a09ea13 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,84 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + + target.build_configurations.each do |config| + # 这里的配置会告诉编译器在编译时剔除未使用的权限代码 + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## 1. 禁用 日历 (NSCalendarsUsageDescription) + 'PERMISSION_EVENTS=0', + 'PERMISSION_EVENTS_FULL_ACCESS=0', + + ## 2. 禁用 提醒事项 + 'PERMISSION_REMINDERS=0', + + ## 3. 禁用 通讯录 (NSContactsUsageDescription) + 'PERMISSION_CONTACTS=0', + + ## 4. 禁用 语音识别 (NSSpeechRecognitionUsageDescription) + 'PERMISSION_SPEECH_RECOGNIZER=0', + + ## 5. 禁用 运动与健身 (NSMotionUsageDescription) + 'PERMISSION_SENSORS=0', + + ## 6. 禁用 Siri/助手 (NSSiriUsageDescription) + 'PERMISSION_ASSISTANT=0', + + ## 7. 禁用 媒体库 (NSAppleMusicUsageDescription) + 'PERMISSION_MEDIA_LIBRARY=1', + + ## 8. 禁用 蓝牙 (如果你没用到蓝牙,建议也关掉) + 'PERMISSION_BLUETOOTH=0', + + ## --- 以下是建议保留的常见权限 (设为 1),如果你的 App 也没用到,也可以改为 0 --- + 'PERMISSION_CAMERA=1', + 'PERMISSION_MICROPHONE=1', + 'PERMISSION_PHOTOS=1', + 'PERMISSION_LOCATION=0', + 'PERMISSION_NOTIFICATIONS=1', + 'PERMISSION_APP_TRACKING_TRANSPARENCY=0', + 'PERMISSION_CRITICAL_ALERTS=0', + ] + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..eae30da --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,498 @@ +PODS: + - agora_rtc_engine (6.5.3): + - AgoraIrisRTC_iOS (= 4.5.2-build.1) + - AgoraRtcEngine_iOS (= 4.5.2) + - Flutter + - AgoraInfra_iOS (1.2.13.1) + - AgoraIrisRTC_iOS (4.5.2-build.1) + - AgoraRtcEngine_iOS (4.5.2): + - AgoraRtcEngine_iOS/AIAEC (= 4.5.2) + - AgoraRtcEngine_iOS/AIAECLL (= 4.5.2) + - AgoraRtcEngine_iOS/AINS (= 4.5.2) + - AgoraRtcEngine_iOS/AINSLL (= 4.5.2) + - AgoraRtcEngine_iOS/AudioBeauty (= 4.5.2) + - AgoraRtcEngine_iOS/ClearVision (= 4.5.2) + - AgoraRtcEngine_iOS/ContentInspect (= 4.5.2) + - AgoraRtcEngine_iOS/FaceCapture (= 4.5.2) + - AgoraRtcEngine_iOS/FaceDetection (= 4.5.2) + - AgoraRtcEngine_iOS/LipSync (= 4.5.2) + - AgoraRtcEngine_iOS/ReplayKit (= 4.5.2) + - AgoraRtcEngine_iOS/RtcBasic (= 4.5.2) + - AgoraRtcEngine_iOS/SpatialAudio (= 4.5.2) + - AgoraRtcEngine_iOS/VideoAv1CodecDec (= 4.5.2) + - AgoraRtcEngine_iOS/VideoAv1CodecEnc (= 4.5.2) + - AgoraRtcEngine_iOS/VideoCodecDec (= 4.5.2) + - AgoraRtcEngine_iOS/VideoCodecEnc (= 4.5.2) + - AgoraRtcEngine_iOS/VirtualBackground (= 4.5.2) + - AgoraRtcEngine_iOS/VQA (= 4.5.2) + - AgoraRtcEngine_iOS/AIAEC (4.5.2) + - AgoraRtcEngine_iOS/AIAECLL (4.5.2) + - AgoraRtcEngine_iOS/AINS (4.5.2) + - AgoraRtcEngine_iOS/AINSLL (4.5.2) + - AgoraRtcEngine_iOS/AudioBeauty (4.5.2) + - AgoraRtcEngine_iOS/ClearVision (4.5.2) + - AgoraRtcEngine_iOS/ContentInspect (4.5.2) + - AgoraRtcEngine_iOS/FaceCapture (4.5.2) + - AgoraRtcEngine_iOS/FaceDetection (4.5.2) + - AgoraRtcEngine_iOS/LipSync (4.5.2) + - AgoraRtcEngine_iOS/ReplayKit (4.5.2) + - AgoraRtcEngine_iOS/RtcBasic (4.5.2): + - AgoraInfra_iOS (= 1.2.13.1) + - AgoraRtcEngine_iOS/SpatialAudio (4.5.2) + - AgoraRtcEngine_iOS/VideoAv1CodecDec (4.5.2) + - AgoraRtcEngine_iOS/VideoAv1CodecEnc (4.5.2) + - AgoraRtcEngine_iOS/VideoCodecDec (4.5.2) + - AgoraRtcEngine_iOS/VideoCodecEnc (4.5.2) + - AgoraRtcEngine_iOS/VirtualBackground (4.5.2) + - AgoraRtcEngine_iOS/VQA (4.5.2) + - app_links (6.4.1): + - Flutter + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): + - AppAuth/Core + - AppCheckCore (11.2.0): + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - audioplayers_darwin (0.0.1): + - Flutter + - FlutterMacOS + - device_info_plus (0.0.1): + - Flutter + - Firebase/Auth (11.15.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 11.15.0) + - Firebase/CoreOnly (11.15.0): + - FirebaseCore (~> 11.15.0) + - Firebase/Crashlytics (11.15.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 11.15.0) + - firebase_auth (5.7.0): + - Firebase/Auth (= 11.15.0) + - firebase_core + - Flutter + - firebase_core (3.15.2): + - Firebase/CoreOnly (= 11.15.0) + - Flutter + - firebase_crashlytics (4.3.10): + - Firebase/Crashlytics (= 11.15.0) + - firebase_core + - Flutter + - FirebaseAppCheckInterop (11.15.0) + - FirebaseAuth (11.15.0): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.15.0) + - FirebaseCoreExtension (~> 11.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GTMSessionFetcher/Core (< 5.0, >= 3.4) + - RecaptchaInterop (~> 101.0) + - FirebaseAuthInterop (11.15.0) + - FirebaseCore (11.15.0): + - FirebaseCoreInternal (~> 11.15.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseCoreInternal (11.15.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseCrashlytics (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - FirebaseRemoteConfigInterop (~> 11.0) + - FirebaseSessions (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - FirebaseInstallations (11.15.0): + - FirebaseCore (~> 11.15.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseRemoteConfigInterop (11.15.0) + - FirebaseSessions (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseCoreExtension (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesSwift (~> 2.1) + - Flutter (1.0.0) + - flutter_foreground_task (0.0.1): + - Flutter + - flutter_image_compress_common (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder + - flutter_inappwebview_ios (0.0.1): + - Flutter + - flutter_inappwebview_ios/Core (= 0.0.1) + - OrderedSet (~> 6.0.3) + - flutter_inappwebview_ios/Core (0.0.1): + - Flutter + - OrderedSet (~> 6.0.3) + - fluttertoast (0.0.2): + - Flutter + - google_sign_in_ios (0.0.1): + - AppAuth (>= 1.7.4) + - Flutter + - FlutterMacOS + - GoogleSignIn (~> 8.0) + - GTMSessionFetcher (>= 3.4.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleSignIn (8.0.0): + - AppAuth (< 2.0, >= 1.7.3) + - AppCheckCore (~> 11.0) + - GTMAppAuth (< 5.0, >= 4.1.1) + - GTMSessionFetcher/Core (~> 3.3) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMAppAuth (4.1.1): + - AppAuth/Core (~> 1.7) + - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.5.0): + - GTMSessionFetcher/Full (= 3.5.0) + - GTMSessionFetcher/Core (3.5.0) + - GTMSessionFetcher/Full (3.5.0): + - GTMSessionFetcher/Core + - HydraAsync (2.0.6) + - image_cropper (0.0.4): + - Flutter + - TOCropViewController (~> 2.6.1) + - image_picker_ios (0.0.1): + - Flutter + - in_app_purchase_storekit (0.0.1): + - Flutter + - FlutterMacOS + - iris_method_channel (0.0.1): + - Flutter + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): + - libwebp/webp + - libwebp/mux (1.5.0): + - libwebp/demux + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): + - libwebp/sharpyuv + - loading_indicator_view_plus (0.0.1): + - Flutter + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - on_audio_query_ios (0.0.1): + - Flutter + - SwiftyBeaver + - OrderedSet (6.0.3) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter + - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) + - QGVAPlayer (1.0.19) + - RecaptchaInterop (101.0.0) + - SDWebImage (5.21.7): + - SDWebImage/Core (= 5.21.7) + - SDWebImage/Core (5.21.7) + - SDWebImageWebPCoder (0.15.0): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sign_in_with_apple (0.0.1): + - Flutter + - social_sharing_plus (0.0.1): + - Flutter + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - store_redirect (0.0.1): + - Flutter + - SwiftyBeaver (1.9.5) + - tancent_vap (0.0.1): + - Flutter + - QGVAPlayer (= 1.0.19) + - tencent_cloud_chat_sdk (8.0.0): + - Flutter + - HydraAsync + - TXIMSDK_Plus_iOS_XCFramework (= 8.3.6498) + - TOCropViewController (2.6.1) + - TXIMSDK_Plus_iOS_XCFramework (8.3.6498) + - url_launcher_ios (0.0.1): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - video_thumbnail (0.0.1): + - Flutter + - libwebp + - wakelock_plus (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - agora_rtc_engine (from `.symlinks/plugins/agora_rtc_engine/ios`) + - app_links (from `.symlinks/plugins/app_links/ios`) + - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) + - Flutter (from `Flutter`) + - flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) + - image_cropper (from `.symlinks/plugins/image_cropper/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) + - iris_method_channel (from `.symlinks/plugins/iris_method_channel/ios`) + - loading_indicator_view_plus (from `.symlinks/plugins/loading_indicator_view_plus/ios`) + - on_audio_query_ios (from `.symlinks/plugins/on_audio_query_ios/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) + - social_sharing_plus (from `.symlinks/plugins/social_sharing_plus/ios`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - store_redirect (from `.symlinks/plugins/store_redirect/ios`) + - tancent_vap (from `.symlinks/plugins/tancent_vap/ios`) + - tencent_cloud_chat_sdk (from `.symlinks/plugins/tencent_cloud_chat_sdk/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) + +SPEC REPOS: + trunk: + - AgoraInfra_iOS + - AgoraIrisRTC_iOS + - AgoraRtcEngine_iOS + - AppAuth + - AppCheckCore + - Firebase + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseAuthInterop + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseCrashlytics + - FirebaseInstallations + - FirebaseRemoteConfigInterop + - FirebaseSessions + - GoogleDataTransport + - GoogleSignIn + - GoogleUtilities + - GTMAppAuth + - GTMSessionFetcher + - HydraAsync + - libwebp + - Mantle + - nanopb + - OrderedSet + - PromisesObjC + - PromisesSwift + - QGVAPlayer + - RecaptchaInterop + - SDWebImage + - SDWebImageWebPCoder + - SwiftyBeaver + - TOCropViewController + - TXIMSDK_Plus_iOS_XCFramework + +EXTERNAL SOURCES: + agora_rtc_engine: + :path: ".symlinks/plugins/agora_rtc_engine/ios" + app_links: + :path: ".symlinks/plugins/app_links/ios" + audioplayers_darwin: + :path: ".symlinks/plugins/audioplayers_darwin/darwin" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_crashlytics: + :path: ".symlinks/plugins/firebase_crashlytics/ios" + Flutter: + :path: Flutter + flutter_foreground_task: + :path: ".symlinks/plugins/flutter_foreground_task/ios" + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + google_sign_in_ios: + :path: ".symlinks/plugins/google_sign_in_ios/darwin" + image_cropper: + :path: ".symlinks/plugins/image_cropper/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + in_app_purchase_storekit: + :path: ".symlinks/plugins/in_app_purchase_storekit/darwin" + iris_method_channel: + :path: ".symlinks/plugins/iris_method_channel/ios" + loading_indicator_view_plus: + :path: ".symlinks/plugins/loading_indicator_view_plus/ios" + on_audio_query_ios: + :path: ".symlinks/plugins/on_audio_query_ios/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sign_in_with_apple: + :path: ".symlinks/plugins/sign_in_with_apple/ios" + social_sharing_plus: + :path: ".symlinks/plugins/social_sharing_plus/ios" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + store_redirect: + :path: ".symlinks/plugins/store_redirect/ios" + tancent_vap: + :path: ".symlinks/plugins/tancent_vap/ios" + tencent_cloud_chat_sdk: + :path: ".symlinks/plugins/tencent_cloud_chat_sdk/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + video_thumbnail: + :path: ".symlinks/plugins/video_thumbnail/ios" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" + +SPEC CHECKSUMS: + agora_rtc_engine: 0c7d50312967c4dc31c3c45e50589ce48f57e08a + AgoraInfra_iOS: 3691b2b277a1712a35ae96de25af319de0d73d08 + AgoraIrisRTC_iOS: eab58c126439adf5ec99632828a558ea216860da + AgoraRtcEngine_iOS: 97e2398a2addda9057815a2a583a658e36796ff6 + app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 + device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e + firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b + firebase_core: 995454a784ff288be5689b796deb9e9fa3601818 + firebase_crashlytics: 30dcd6dfd2fe895c0848af46722a4227346c19aa + FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df + FirebaseAuth: a6575e5fbf46b046c58dc211a28a5fbdd8d4c83b + FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd + FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e + FirebaseCoreExtension: edbd30474b5ccf04e5f001470bdf6ea616af2435 + FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 + FirebaseCrashlytics: e09d0bc19aa54a51e45b8039c836ef73f32c039a + FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 + FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa + FirebaseSessions: b9a92c1c51bbb81e78fc3142cda6d925d700f8e7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + HydraAsync: 8d589bd725b0224f899afafc9a396327405f8063 + image_cropper: 655b3ba703c9e15e3111e79151624d6154288774 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6 + iris_method_channel: 7d661cf3259b3009ae423508470dbeb9374446ee + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + loading_indicator_view_plus: 33a5d3527e9c6a7b470c79878613d01c4fa1fb65 + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 + QGVAPlayer: a0bca68c9bd6f1c8de5ac2d10ddf98be6038cce9 + RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf + SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 + social_sharing_plus: e6024862e5a4be59ef8c97a93558cba1043628bf + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + store_redirect: 55fd455802ceab09803b2df6e68f19a58815266a + SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82 + tancent_vap: 4917210cc7e916023fd112655170d8e3d074f482 + tencent_cloud_chat_sdk: 55e5fffe20f6b7937a26a674ccccb639563a9790 + TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 + TXIMSDK_Plus_iOS_XCFramework: 5d1933192fb3b7ef2fe933f1623de4a0486a7fe2 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 + +PODFILE CHECKSUM: a6f49a93e5f85201a2efdadcd7cf184b0e310894 + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..76f33f6 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,780 @@ +// !$*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 */; }; + 3AA5D06ED1878042C8461CFA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5787BE1D8BF9BB6960F7B587 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 46D37906416913B201350BFF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8CD6B2D6477A38E56A72C61 /* Pods_RunnerTests.framework */; }; + 525B8C722ED013520007B725 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 525B8C712ED013520007B725 /* GoogleService-Info.plist */; }; + 52DF38A12F110B0B0086FBE4 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52DF38A02F110B0B0086FBE4 /* StoreKit.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2DBA1462EA0F13259D54391C /* 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 = ""; }; + 3144A376A151695EA2D1A4FE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3E47BBEC2076B0DEF3312C28 /* 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 = ""; }; + 525B8C712ED013520007B725 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 52DF389D2F0E79020086FBE4 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; + 52DF38A02F110B0B0086FBE4 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + 52DF38A22F110EC10086FBE4 /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = ""; }; + 5787BE1D8BF9BB6960F7B587 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 628A4C79D49FE8929E845244 /* 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 = ""; }; + 6FE5DAD7939E244362D1F7DA /* 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 = ""; }; + 7147A3F76DA0C2CFC820ED1C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 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 = ""; }; + D8CD6B2D6477A38E56A72C61 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 52DF38A12F110B0B0086FBE4 /* StoreKit.framework in Frameworks */, + 3AA5D06ED1878042C8461CFA /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A0E84D3ADAE5F6DDD480F35D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 46D37906416913B201350BFF /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03AFDC00C5136405689DA84C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 52DF38A02F110B0B0086FBE4 /* StoreKit.framework */, + 5787BE1D8BF9BB6960F7B587 /* Pods_Runner.framework */, + D8CD6B2D6477A38E56A72C61 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 3B8051ED51CBDB3CF1DF0F5B /* Pods */ = { + isa = PBXGroup; + children = ( + 7147A3F76DA0C2CFC820ED1C /* Pods-Runner.debug.xcconfig */, + 6FE5DAD7939E244362D1F7DA /* Pods-Runner.release.xcconfig */, + 3E47BBEC2076B0DEF3312C28 /* Pods-Runner.profile.xcconfig */, + 3144A376A151695EA2D1A4FE /* Pods-RunnerTests.debug.xcconfig */, + 628A4C79D49FE8929E845244 /* Pods-RunnerTests.release.xcconfig */, + 2DBA1462EA0F13259D54391C /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 3B8051ED51CBDB3CF1DF0F5B /* Pods */, + 03AFDC00C5136405689DA84C /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 52DF38A22F110EC10086FBE4 /* RunnerRelease.entitlements */, + 52DF389D2F0E79020086FBE4 /* RunnerDebug.entitlements */, + 525B8C712ED013520007B725 /* GoogleService-Info.plist */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + CFD1FCCAE92AFBF15E8B607E /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + A0E84D3ADAE5F6DDD480F35D /* 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 = ( + 9AAFF935C9514CBA898ADFC8 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + F1AFD9E9E8DBA4F1609CE22F /* [CP] Embed Pods Frameworks */, + 5DF67264ADC5268AC3021CED /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 525B8C722ED013520007B725 /* GoogleService-Info.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 5DF67264ADC5268AC3021CED /* [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; + }; + 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"; + }; + 9AAFF935C9514CBA898ADFC8 /* [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; + }; + CFD1FCCAE92AFBF15E8B607E /* [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; + }; + F1AFD9E9E8DBA4F1609CE22F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 35; + DEVELOPMENT_TEAM = S9X2AJ2US9; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2.0; + PRODUCT_BUNDLE_IDENTIFIER = com.org.yumi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3144A376A151695EA2D1A4FE /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.yumi.yumi.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 = 628A4C79D49FE8929E845244 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.yumi.yumi.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 = 2DBA1462EA0F13259D54391C /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.yumi.yumi.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 35; + DEVELOPMENT_TEAM = F33K8VUZ62; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2.0; + PRODUCT_BUNDLE_IDENTIFIER = com.org.yumi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 35; + DEVELOPMENT_TEAM = F33K8VUZ62; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.2.0; + PRODUCT_BUNDLE_IDENTIFIER = com.org.yumi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + 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..c4b79bd --- /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..fc6bf80 --- /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..af0309c --- /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..bbabc4e --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..17ccc03 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..c2cba09 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..8be1cec --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"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":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"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":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@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"}} \ No newline at end of file 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..879503c 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..e03f697 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..a4a7cbc 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..c26b0c5 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..5d3adbe 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..3d50c36 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..8e5585d 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..a4a7cbc 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..690398a 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..40a9548 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..7cff2c2 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..58af325 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..e8b01af 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..2f36d02 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..40a9548 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..33e7037 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..bab4549 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..1a5b3c6 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..b386803 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..efbff97 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..32f26cb 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/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..d08a4de --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea 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..9da19ea 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..9da19ea 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..65a94b5 --- /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..497371e --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..1d74234 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..aa092d5 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 52796773508-dk441c9e7tdmqv47mbdt2vpbn2clrii8.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.52796773508-dk441c9e7tdmqv47mbdt2vpbn2clrii8 + ANDROID_CLIENT_ID + 52796773508-2ruai8tqhp0knuofo02ob1cmheu76u2d.apps.googleusercontent.com + API_KEY + AIzaSyAeeeuw_zKUQmdUUiOXD4u_BvQqaV3w9Gk + GCM_SENDER_ID + 52796773508 + PLIST_VERSION + 1 + BUNDLE_ID + com.org.yumi + PROJECT_ID + yumi-7b503 + STORAGE_BUCKET + yumi-7b503.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:52796773508:ios:7dfa56d19e1711ab65ded1 + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..f9b7b0c --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,91 @@ + + + + +LSApplicationQueriesSchemes + + fb + fbapi + fb-messenger-api + whatsapp + snapchat + + CADisableMinimumFrameDurationOnPhone + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + yumi + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + yumi + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSAppleMusicUsageDescription + We need access to the media library so that you can play your favorite local music files in the voice background. + NSCameraUsageDescription + We need access to your camera so you can take a photo and make it a profile picture, or take and share a picture in real time in a voice chat room. + NSMicrophoneUsageDescription + We need access to your microphone so that you can make live calls, record, and send voice messages with other users in the voice dating room. + NSPhotoLibraryAddUsageDescription + We need your permission to save beautiful images you take or edit in the app to your phone album. + NSPhotoLibraryUsageDescription + We need access to your photo album so you can choose photos from it to update your profile picture or share moments from your life in chat. + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UIViewControllerBasedStatusBarAppearance + + BGTaskSchedulerPermittedIdentifiers + + com.pravera.flutter_foreground_task.refresh + + UIBackgroundModes + + audio + + com.apple.developer.applesignin + + Default + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..fae207f --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements new file mode 100644 index 0000000..82e827f --- /dev/null +++ b/ios/Runner/RunnerDebug.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/ios/Runner/RunnerRelease.entitlements b/ios/Runner/RunnerRelease.entitlements new file mode 100644 index 0000000..82e827f --- /dev/null +++ b/ios/Runner/RunnerRelease.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.applesignin + + Default + + + diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..4d206de --- /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/lib/app/config/app_config.dart b/lib/app/config/app_config.dart new file mode 100644 index 0000000..76fb85c --- /dev/null +++ b/lib/app/config/app_config.dart @@ -0,0 +1,161 @@ +import 'package:flutter/foundation.dart'; +import 'package:yumi/app/config/configs/sc_variant1_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; + +/// 应用配置基类 +/// 定义单一应用配置接口 +abstract class AppConfig { + /// 应用显示名称 + String get appName; + + /// 包名/应用ID + String get packageName; + + /// API服务器地址 + String get apiHost; + + /// 图片服务器地址 + String get imgHost; + + /// 隐私协议URL + String get privacyAgreementUrl; + + /// 用户协议URL + String get userAgreementUrl; + + /// 应用下载链接(Google Play) + String get appDownloadUrlGoogle; + + /// 应用下载链接(App Store) + String get appDownloadUrlApple; + + /// 主播代理URL + String get anchorAgentUrl; + + /// 主持人中心URL + String get hostCenterUrl; + + /// BD中心URL + String get bdCenterUrl; + + /// BD领导中心URL + String get bdLeaderUrl; + + /// 代币销售URL + String get coinSellerUrl; + + /// 管理员URL + String get adminUrl; + + /// 代理中心URL + String get agencyCenterUrl; + + /// 游戏国王URL + String get gamesKingUrl; + + /// 财富榜URL + String get wealthRankUrl; + + /// 魅力榜URL + String get charmRankUrl; + + /// 房间榜URL + String get roomRankUrl; + + /// 邀请新用户URL + String get inviteNewUserUrl; + + /// 主题主颜色 + int get primaryColor; + + /// 腾讯IM App ID + String get tencentImAppid; + + /// Agora RTC App ID + String get agoraRtcAppid; + + /// 游戏App ID + num get gameAppid; + + /// 游戏渠道 + String get gameAppChannel; + + /// 全服广播大群ID + String get bigBroadcastGroup; + + /// 管理账号 + String get imAdmin; + + /// 是否审核模式 + bool get isReview; + + /// 礼物特效开关 + bool get isGiftSpecialEffects; + + /// 入场秀开关 + bool get isEntryVehicleAnimation; + + /// 全局飘屏开关 + bool get isFloatingAnimationInGlobal; + + /// 幸运礼物特效开关 + bool get isLuckGiftSpecialEffects; + + /// 获取功能开关 + Map get featureFlags; + + /// 获取业务逻辑策略 + BusinessLogicStrategy get businessLogicStrategy; + + /// 获取所有配置的Map(用于调试) + Map toMap(); + + /// 验证配置是否有效 + /// 检查必要的配置项是否已正确设置 + /// 如果配置无效,抛出 [StateError] + void validate(); + + /// 当前应用的配置实例 + static AppConfig? _current; + + /// 从环境变量获取variant配置 + /// 支持通过--dart-define=VARIANT=variant2传递参数 + /// 默认使用variant1 + static String _getVariant() { + const variant = String.fromEnvironment('VARIANT', defaultValue: 'variant1'); + return variant; + } + + /// 获取当前配置实例 + static AppConfig get current { + if (_current == null) { + throw StateError( + 'AppConfig not initialized. Call AppConfig.initialize() first.', + ); + } + return _current!; + } + + /// 初始化应用配置 + /// 支持通过环境变量选择variant(variant1或variant2) + /// 使用--dart-define=VARIANT=variant2传递参数 + static void initialize() { + final variant = _getVariant(); + switch (variant) { + case 'variant1': + default: + _current = SCVariant1Config(); + debugPrint('AppConfig initialized for variant1 '); + } + + // 验证配置是否有效 + try { + _current!.validate(); + } catch (e) { + // validate方法在调试模式下不会抛出异常 + // 只有在发布模式下验证失败才会抛出异常 + debugPrint('应用配置验证失败: $e'); + rethrow; + } + } +} diff --git a/lib/app/config/asset_loader.dart b/lib/app/config/asset_loader.dart new file mode 100644 index 0000000..fc68e5c --- /dev/null +++ b/lib/app/config/asset_loader.dart @@ -0,0 +1,28 @@ +/// 资源加载器 +class AssetLoader { + /// 获取资源路径 + /// [path] 相对路径,如 'sc_images/icon.png' + static String getAssetPath(String path) { + return 'assets/$path'; + } + + /// 获取图片资源路径 + static String image(String path) { + return getAssetPath('sc_images/$path'); + } + + /// 获取本地化文件路径 + static String localization(String path) { + return getAssetPath('l10n/$path'); + } + + /// 获取配置文件路径 + static String config(String path) { + return getAssetPath('config/$path'); + } + + /// 获取共享资源路径 + static String shared(String path) { + return 'assets/shared/$path'; + } +} \ No newline at end of file diff --git a/lib/app/config/business_logic_strategy.dart b/lib/app/config/business_logic_strategy.dart new file mode 100644 index 0000000..e7a4c04 --- /dev/null +++ b/lib/app/config/business_logic_strategy.dart @@ -0,0 +1,1612 @@ +import 'package:flutter/material.dart'; + +/// 业务逻辑策略接口 +/// 定义页面类可用的差异化业务逻辑方法 +abstract class BusinessLogicStrategy { + /// 获取首页Tab页配置 + /// 返回: List<Widget> - 首页的页面组件列表 + List getHomeTabPages(BuildContext context); + + /// 获取首页Tab标签配置 + /// 返回: List<Widget> - 首页的Tab标签列表 + List getHomeTabLabels(BuildContext context); + + + /// 获取首页初始Tab索引 + /// 返回: int - 初始Tab索引 + int getHomeInitialTabIndex(); + + /// 处理头像点击事件 + /// 参数: context - BuildContext + void onAvatarTap(BuildContext context); + + /// 判断是否显示首充提示 + /// 返回: bool - 是否显示首充提示 + bool shouldShowFirstRechargePrompt(); + + /// 获取首充提示位置配置 + /// 返回: Map<String, double?> - 包含top/bottom/start/end位置,未设置的键返回null + Map getFirstRechargePosition(); + + /// 处理首充提示点击事件 + /// 参数: context - BuildContext + void onFirstRechargeTap(BuildContext context); + + /// === 登录页面差异化方法 === + + /// 获取登录页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getLoginBackgroundImage(); + + /// 获取登录页面应用图标路径 + /// 返回: String - 应用图标资源路径 + String getLoginAppIcon(); + + /// 获取登录按钮背景颜色 + /// 返回: Color - 按钮背景颜色 + Color getLoginButtonColor(); + + /// 获取登录按钮渐变颜色(可选) + /// 返回: List<Color>? - 渐变颜色列表,null表示使用纯色 + List? getLoginButtonGradient(); + + /// 获取登录按钮文本颜色 + /// 返回: Color - 按钮文本颜色 + Color getLoginButtonTextColor(); + + /// 获取登录标签文本颜色 + /// 返回: Color - 标签文本颜色 + Color getLoginLabelTextColor(); + + /// 获取登录输入框装饰 + /// 返回: BoxDecoration - 输入框装饰样式 + BoxDecoration getLoginInputDecoration(); + + /// 获取登录页面主要内边距 + /// 返回: EdgeInsets - 页面内边距 + EdgeInsets getLoginPagePadding(); + + /// 是否显示登录页面状态栏 + /// 返回: bool - 是否显示状态栏 + bool shouldShowLoginStatusBar(); + + /// 获取登录输入框提示文本颜色 + /// 返回: Color - 提示文本颜色 + Color getLoginHintTextColor(); + + /// 获取登录输入框文本颜色 + /// 返回: Color - 输入框文本颜色 + Color getLoginInputTextColor(); + + + /// 是否显示密码房间图标 + /// 返回: bool - 是否显示密码图标 + bool shouldShowPasswordRoomIcon(); + + /// === 探索页面差异化方法 === + + /// 获取探索页面房间显示阈值 + /// 返回: int - 显示在抽屉上方的房间数量阈值 + int getExploreRoomDisplayThreshold(); + + /// 获取探索页面网格视图图标路径 + /// 返回: String - 网格视图图标资源路径 + String getExploreGridIcon(); + + /// 获取探索页面列表视图图标路径 + /// 返回: String - 列表视图图标资源路径 + String getExploreListIcon(); + + /// 获取探索页面排名图标路径模式 + /// 参数: isGrid - 是否为网格视图 + /// 参数: rank - 排名(1-based) + /// 返回: String - 排名图标资源路径 + String getExploreRankIconPattern(bool isGrid, int rank); + + /// 获取探索页面房间边框颜色 + /// 返回: Color - 房间边框颜色 + Color getExploreRoomBorderColor(); + + /// 获取探索页面房间边框宽度 + /// 返回: double - 房间边框宽度 + double getExploreRoomBorderWidth(); + + /// 获取探索页面抽屉内容网格列数 + /// 返回: int - 网格列数 + int getExploreDrawerGridCrossAxisCount(); + + /// 获取探索页面房间名称滚动阈值 + /// 返回: int - 房间名称长度超过此值时显示滚动效果 + int getExploreRoomNameMarqueeThreshold(); + + /// 获取探索页面房间描述滚动阈值 + /// 返回: int - 房间描述长度超过此值时显示滚动效果 + int getExploreRoomDescMarqueeThreshold(); + + /// === 个人主页差异化方法 === + + /// 获取个人主页默认背景图像路径 + /// 返回: String - 默认背景图像资源路径 + String getMePageDefaultBackgroundImage(); + + /// 获取个人主页性别背景图像路径 + /// 参数: isFemale - 是否为女性用户 + /// 返回: String - 性别背景图像资源路径 + String getMePageGenderBackgroundImage(bool isFemale); + + /// 获取个人主页CP对话框头像图像路径 + /// 返回: String - CP对话框头像图像资源路径 + String getMePageCpDialogHeadImage(); + + /// 获取个人主页默认头像图像路径 + /// 返回: String - 默认头像图像资源路径 + String getMePageDefaultAvatarImage(); + + /// 获取个人主页ID背景图像路径 + /// 参数: hasSpecialId - 是否有特殊ID + /// 返回: String - ID背景图像资源路径 + String getMePageIdBackgroundImage(bool hasSpecialId); + + /// 获取个人主页复制ID图标路径 + /// 返回: String - 复制ID图标资源路径 + String getMePageCopyIdIcon(); + + /// 获取个人主页性别年龄背景图像路径 + /// 参数: isFemale - 是否为女性用户 + /// 返回: String - 性别年龄背景图像资源路径 + String getMePageGenderAgeBackgroundImage(bool isFemale); + + /// 获取个人主页访客关注粉丝背景图像路径 + /// 参数: isFemale - 是否为女性用户 + /// 返回: String - 访客关注粉丝背景图像资源路径 + String getMePageVisitorsFollowFansBackgroundImage(bool isFemale); + + /// 获取个人主页编辑用户信息图标路径 + /// 返回: String - 编辑用户信息图标资源路径 + String getMePageEditUserInfoIcon(); + + /// 获取个人主页闪耀文本颜色 + /// 返回: Color - 闪耀文本颜色 + Color getMePageShineColor(); + + /// 获取个人主页渐变颜色 + /// 参数: isFemale - 是否为女性用户 + /// 返回: List<Color> - 渐变颜色列表 + List getMePageGradientColors(bool isFemale); + + /// 获取个人主页Tab指示器渐变颜色 + /// 参数: isFemale - 是否为女性用户 + /// 返回: List<Color> - Tab指示器渐变颜色列表 + List getMePageTabIndicatorGradient(bool isFemale); + + /// 获取个人主页访客关注粉丝文本颜色 + /// 返回: Color - 访客关注粉丝文本颜色 + Color getMePageVisitorsFollowFansTextColor(); + + /// 获取个人主页初始Tab索引 + /// 返回: int - 初始Tab索引 + int getMePageInitialTabIndex(); + + /// 获取个人主页滚动透明度计算阈值 + /// 返回: double - 滚动透明度计算阈值 + double getMePageScrollOpacityThreshold(); + + /// 获取个人主页昵称滚动显示阈值 + /// 返回: int - 昵称长度超过此值时显示滚动效果 + int getMePageNicknameScrollThreshold(); + + /// 获取个人主页年龄显示宽度阈值 + /// 返回: int - 年龄超过此值时调整显示宽度 + int getMePageAgeDisplayWidthThreshold(); + + /// 获取个人主页头像显示透明度阈值 + /// 返回: double - 透明度超过此值时显示头像 + double getMePageOpacityThresholdForAvatarDisplay(); + + /// === 热门页面差异化方法 === + + /// 获取热门页面排行榜背景图像路径 + /// 参数: leaderboardType - 排行榜类型 ('room', 'wealth', 'charm') + /// 返回: String - 背景图像资源路径 + String getPopularLeaderboardBackgroundImage(String leaderboardType); + + /// === 游戏页面差异化方法 === + + /// 获取游戏页面排名背景图像路径 + /// 参数: rank - 排名 (1, 2, 3, 4) + /// 返回: String - 排名背景图像资源路径 + String getGameRankBackgroundImage(int rank); + + /// 获取游戏页面排名图标路径 + /// 参数: rank - 排名 (1, 2, 3, 4) + /// 返回: String - 排名图标资源路径 + String getGameRankIcon(int rank); + + /// 获取游戏页面主题颜色 + /// 返回: Color - 游戏页面主题颜色 + Color getGamePageThemeColor(); + + /// 获取游戏页面背景颜色 + /// 返回: Color - 游戏页面背景颜色 + Color getGamePageBackgroundColor(); + + /// 获取游戏页面列数配置 + /// 返回: int - 网格列数 + int getGamePageGridCrossAxisCount(); + + /// 获取游戏页面网格高度配置 + /// 返回: double - 网格高度比例 + double getGamePageGridChildAspectRatio(); + + /// 获取游戏页面排名文本颜色 + /// 参数: rank - 排名 (1, 2, 3, 4) + /// 返回: Color - 排名文本颜色 + Color getGameRankTextColor(int rank); + + /// 获取游戏页面渐变第二颜色 + /// 返回: Color - 渐变第二颜色 + Color getGamePageGradientSecondColor(); + + /// 获取游戏页面热门标签背景图像路径 + /// 返回: String - 热门标签背景图像资源路径 + String getGameHotTagBackgroundImage(); + + /// 获取游戏页面新游戏标签图像路径 + /// 返回: String - 新游戏标签图像资源路径 + String getGameNewTagImage(); + + /// 获取游戏页面遮罩图像路径 + /// 返回: String - 游戏页面遮罩图像资源路径 + String getGamePageMaskImage(); + + /// === VIP页面差异化方法 === + + /// 获取VIP页面背景图像路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - VIP背景图像资源路径 + String getVipPageBackgroundImage(int vipLevel); + + /// 获取VIP页面图标路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - VIP图标资源路径 + String getVipPageIcon(int vipLevel); + + /// 获取VIP页面按钮渐变颜色 + /// 返回: List<Color> - 按钮渐变颜色列表 + List getVipPageButtonGradient(); + + /// 获取VIP页面网格列数配置 + /// 返回: int - 网格列数 + int getVipPageGridCrossAxisCount(); + + /// 获取VIP页面网格高度配置 + /// 返回: double - 网格高度比例 + double getVipPageGridChildAspectRatio(); + + /// 获取VIP页面头部背景图像路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - VIP头部背景图像资源路径 + String getVipPageHeadBackgroundImage(int vipLevel); + + /// 获取VIP页面Tab选中状态图标路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - Tab选中状态图标资源路径 + String getVipPageTabSelectedIcon(int vipLevel); + + /// 获取VIP页面Tab未选中状态图标路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - Tab未选中状态图标资源路径 + String getVipPageTabUnselectedIcon(int vipLevel); + + /// 获取VIP页面大图标路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - 大图标资源路径 + String getVipPageLargeIcon(int vipLevel); + + /// 获取VIP页面标题图像路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - 标题图像资源路径 + String getVipPageTitleImage(int vipLevel); + + /// 获取VIP页面标签图像路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 参数: tagIndex - 标签索引 (1或2) + /// 返回: String - 标签图像资源路径 + String getVipPageTagImage(int vipLevel, int tagIndex); + + /// 获取VIP页面功能项背景图像路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - 功能项背景图像资源路径 + String getVipPageItemBackgroundImage(int vipLevel); + + /// 获取VIP页面特权图标路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 参数: privilegeIndex - 特权索引 (1-14) + /// 返回: String - 特权图标资源路径 + String getVipPagePrivilegeIcon(int vipLevel, int privilegeIndex); + + /// 获取VIP页面功能图标路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 参数: featureName - 功能名称 ('badge', 'profile_frame', 'profile_card', 'entrance_effect', 'special_gift_tassel', 'mic_rippl_theme', 'chatbox', 'room_cover_headdress', 'exclusive_vehicles') + /// 返回: String - 功能图标资源路径 + String getVipPageFeatureIcon(int vipLevel, String featureName); + + /// 获取VIP页面预览图像路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 参数: previewType - 预览类型 ('rev', 'pre') + /// 参数: featureName - 功能名称 + /// 返回: String - 预览图像资源路径 + String getVipPagePreviewImage( + int vipLevel, + String previewType, + String featureName, + ); + + /// 获取VIP页面购买背景图像路径 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: String - 购买背景图像资源路径 + String getVipPagePurchaseBackgroundImage(int vipLevel); + + /// 获取VIP页面购买按钮图像路径 + /// 返回: String - 购买按钮图像资源路径 + String getVipPagePurchaseButtonImage(); + + /// 获取VIP页面文本颜色 + /// 参数: vipLevel - VIP等级 (1-9) + /// 返回: Color - 文本颜色 + Color getVipPageTextColor(int vipLevel); + + /// === 通用页面差异化方法 === + + /// 获取通用页面排名背景图像路径模式 + /// 参数: pageType - 页面类型 ('top', 'local', 'global', 'today') + /// 参数: rank - 排名 (1-based) + /// 返回: String - 排名背景图像资源路径 + String getCommonRankBackgroundImage(String pageType, int rank); + + /// 获取通用页面排名图标路径模式 + /// 参数: pageType - 页面类型 ('top', 'local', 'global', 'today') + /// 参数: rank - 排名 (1-based) + /// 返回: String - 排名图标资源路径 + String getCommonRankIcon(String pageType, int rank); + + /// 获取通用页面网格列数配置 + /// 参数: pageType - 页面类型 ('top', 'local', 'global', 'today') + /// 返回: int - 网格列数 + int getCommonPageGridCrossAxisCount(String pageType); + + /// 获取通用页面网格高度配置 + /// 参数: pageType - 页面类型 ('top', 'local', 'global', 'today') + /// 返回: double - 网格高度比例 + double getCommonPageGridChildAspectRatio(String pageType); + + /// === 个人主页2 (MePage2) 差异化方法 === + + /// 获取MePage2钱包区域背景配置 + /// 返回: Map - 包含渐变颜色、圆角、阴影等配置 + Map getMePage2WalletBackground(); + + /// 获取MePage2统计信息颜色配置 + /// 返回: Map - 包含数字颜色、标签颜色、分割线颜色 + Map getMePage2StatisticsColors(); + + /// 获取MePage2功能卡片图标路径映射 + /// 返回: Map - 功能名称到图标路径的映射 + Map getMePage2FunctionIcons(); + + /// 获取MePage2卡片样式配置 + /// 返回: Map - 包含圆角、阴影、边框等配置 + Map getMePage2CardStyles(); + + /// === Admin编辑页面差异化方法 === + + /// 获取Admin编辑页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getAdminEditingBackgroundImage(); + + /// 获取Admin编辑页面按钮渐变颜色 + /// 参数: buttonType - 按钮类型 ('warning', 'adjust') + /// 返回: List - 渐变颜色列表 + List getAdminEditingButtonGradient(String buttonType); + + /// 获取Admin编辑页面图标路径 + /// 参数: iconType - 图标类型 ('specialIdBg', 'normalIdBg', 'copyId', 'addPic', 'closePic', 'checked') + /// 返回: String - 图标资源路径 + String getAdminEditingIcon(String iconType); + + /// 获取Admin编辑页面输入框装饰 + /// 返回: BoxDecoration - 输入框装饰样式 + BoxDecoration getAdminEditingInputDecoration(); + + /// 获取Admin编辑页面最大图片上传数量 + /// 返回: int - 最大图片上传数量 + int getAdminEditingMaxImageUploadCount(); + + /// 获取Admin编辑页面描述最大长度 + /// 返回: int - 描述文本框最大字符数 + int getAdminEditingDescriptionMaxLength(); + + /// 获取Admin编辑页面违规类型映射 + /// 参数: targetType - 目标类型 ('User', 'Room') + /// 返回: Map - 违规类型名称到类型ID的映射 + Map getAdminEditingViolationTypeMapping(String targetType); + + /// === Admin搜索页面差异化方法 === + + /// 获取Admin搜索页面背景图像路径 + /// 参数: pageType - 页面类型 ('userSearch', 'roomSearch') + /// 返回: String - 背景图像资源路径 + String getAdminSearchBackgroundImage(String pageType); + + /// 获取Admin搜索页面搜索按钮渐变颜色 + /// 参数: pageType - 页面类型 ('userSearch', 'roomSearch') + /// 返回: List - 渐变颜色列表 + List getAdminSearchButtonGradient(String pageType); + + /// 获取Admin搜索页面搜索按钮文本颜色 + /// 参数: pageType - 页面类型 ('userSearch', 'roomSearch') + /// 返回: Color - 文本颜色 + Color getAdminSearchButtonTextColor(String pageType); + + /// 获取Admin搜索页面编辑图标路径 + /// 参数: pageType - 页面类型 ('userSearch', 'roomSearch') + /// 返回: String - 编辑图标资源路径 + String getAdminSearchEditIcon(String pageType); + + /// 获取Admin搜索页面搜索框边框颜色 + /// 参数: pageType - 页面类型 ('userSearch', 'roomSearch') + /// 返回: Color - 边框颜色 + Color getAdminSearchInputBorderColor(String pageType); + + /// 获取Admin搜索页面搜索框文本颜色 + /// 参数: pageType - 页面类型 ('userSearch', 'roomSearch') + /// 返回: Color - 文本颜色 + Color getAdminSearchInputTextColor(String pageType); + + /// 获取Admin搜索页面用户信息图标路径 + /// 参数: iconType - 图标类型 ('specialIdBg', 'normalIdBg', 'copyId') + /// 返回: String - 图标资源路径 + String getAdminSearchUserInfoIcon(String iconType); + + /// === Auth页面差异化方法 === + + /// === 主登录页面 (LoginPage) 方法 === + + /// 获取主登录页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getLoginMainBackgroundImage(); + + /// 获取主登录页面应用图标路径 + /// 返回: String - 应用图标资源路径 + String getLoginMainAppIcon(); + + /// 获取主登录页面Google登录图标路径 + /// 返回: String - Google登录图标资源路径 + String getLoginMainGoogleIcon(); + + /// 获取主登录页面Apple登录图标路径 + /// 返回: String - Apple登录图标资源路径 + String getLoginMainAppleIcon(); + + /// 获取主登录页面账户登录图标路径 + /// 返回: String - 账户登录图标资源路径 + String getLoginMainAccountIcon(); + + /// 获取主登录页面协议选择图标路径 + /// 参数: isSelected - 是否选中状态 + /// 返回: String - 协议选择图标资源路径 + String getLoginMainAgreementIcon(bool isSelected); + + /// 获取主登录页面分隔线颜色 + /// 返回: Color - 分隔线颜色 + Color getLoginMainDividerColor(); + + /// 获取主登录页面按钮边框颜色 + /// 返回: Color - 按钮边框颜色 + Color getLoginMainButtonBorderColor(); + + /// 获取主登录页面按钮背景颜色 + /// 返回: Color - 按钮背景颜色 + Color getLoginMainButtonBackgroundColor(); + + /// 获取主登录页面链接文本颜色 + /// 返回: Color - 链接文本颜色 + Color getLoginMainLinkColor(); + + /// 获取主登录页面按钮文本颜色 + /// 返回: Color - 按钮文本颜色 + Color getLoginMainButtonTextColor(); + + /// === 编辑个人资料页面 (SCEditProfilePage) 方法 === + + /// 获取编辑个人资料页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getEditProfileBackgroundImage(); + + /// 获取编辑个人资料页面默认头像图像路径 + /// 返回: String - 默认头像图像资源路径 + String getEditProfileDefaultAvatarImage(); + + /// 获取编辑个人资料页面性别图标路径 + /// 参数: isMale - 是否为男性图标 + /// 返回: String - 性别图标资源路径 + String getEditProfileGenderIcon(bool isMale); + + /// 获取编辑个人资料页面性别按钮渐变颜色 + /// 参数: isMale - 是否为男性按钮 + /// 参数: isSelected - 是否选中状态 + /// 返回: List - 渐变颜色列表 + List getEditProfileGenderButtonGradient(bool isMale, bool isSelected); + + /// 获取编辑个人资料页面输入框背景颜色 + /// 返回: Color - 输入框背景颜色 + Color getEditProfileInputBackgroundColor(); + + /// 获取编辑个人资料页面日期选择确认按钮颜色 + /// 返回: Color - 日期选择确认按钮颜色 + Color getEditProfileDatePickerConfirmColor(); + + /// 获取编辑个人资料页面继续按钮颜色 + /// 返回: Color - 继续按钮颜色 + Color getEditProfileContinueButtonColor(); + + /// === Country页面差异化方法 === + + /// 获取Country页面选择确认图标路径 + /// 返回: String - 选择确认图标资源路径 + String getCountrySelectOkIcon(); + + /// 获取Country页面选择未确认图标路径 + /// 返回: String - 选择未确认图标资源路径 + String getCountrySelectUnOkIcon(); + + /// 获取Country页面单选选中图标路径 + /// 返回: String - 单选选中图标资源路径 + String getCountryRadioSelectedIcon(); + + /// 获取Country页面单选未选中图标路径 + /// 返回: String - 单选未选中图标资源路径 + String getCountryRadioUnselectedIcon(); + + /// 获取Country页面国家列表项边框颜色 + /// 返回: Color - 国家列表项边框颜色 + Color getCountryItemBorderColor(); + + /// 获取Country页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getCountryPageScaffoldBackgroundColor(); + + /// 获取Country页面图标颜色 + /// 返回: Color - 图标颜色 + Color getCountryPageIconColor(); + + /// 获取Country页面主要文本颜色 + /// 返回: Color - 主要文本颜色 + Color getCountryPagePrimaryTextColor(); + + /// 获取Country页面次要文本颜色 + /// 返回: Color - 次要文本颜色 + Color getCountryPageSecondaryTextColor(); + + /// 获取Country页面容器背景颜色 + /// 返回: Color - 容器背景颜色 + Color getCountryPageContainerBackgroundColor(); + + /// === Chat页面差异化方法 === + + /// === 消息页面 (SCMessagePage) 方法 === + + /// 获取消息页面索引遮罩图标路径 + /// 返回: String - 索引遮罩图标资源路径 + String getMessagePageIndexMaskIcon(); + + /// 获取消息页面公告标签图标路径 + /// 返回: String - 公告标签图标资源路径 + String getMessagePageAnnouncementTagIcon(); + + /// 获取消息页面活动消息图标路径 + /// 返回: String - 活动消息图标资源路径 + String getMessagePageActivityMessageIcon(); + + /// 获取消息页面活动消息标题背景图标路径 + /// 参数: isRtl - 是否为RTL布局(从右到左) + /// 返回: String - 活动消息标题背景图标资源路径 + String getMessagePageActivityTitleBackground(bool isRtl); + + /// 获取消息页面系统消息图标路径 + /// 返回: String - 系统消息图标资源路径 + String getMessagePageSystemMessageIcon(); + + /// 获取消息页面系统消息标题背景图标路径 + /// 参数: isRtl - 是否为RTL布局(从右到左) + /// 返回: String - 系统消息标题背景图标资源路径 + String getMessagePageSystemTitleBackground(bool isRtl); + + /// 获取消息页面通知消息图标路径 + /// 返回: String - 通知消息图标资源路径 + String getMessagePageNotificationMessageIcon(); + + /// 获取消息页面通知消息标题背景图标路径 + /// 参数: isRtl - 是否为RTL布局(从右到左) + /// 返回: String - 通知消息标题背景图标资源路径 + String getMessagePageNotificationTitleBackground(bool isRtl); + + /// === 聊天页面 (SCMessageChatPage) 方法 === + + /// 获取聊天页面房间设置背景图像路径 + /// 返回: String - 房间设置背景图像资源路径 + String getSCMessageChatPageRoomSettingBackground(); + + /// 获取聊天页面表情图标路径 + /// 返回: String - 表情图标资源路径 + String getSCMessageChatPageEmojiIcon(); + + /// 获取聊天页面聊天键盘图标路径 + /// 返回: String - 聊天键盘图标资源路径 + String getSCMessageChatPageChatKeyboardIcon(); + + /// 获取聊天页面添加图标路径 + /// 返回: String - 添加图标资源路径 + String getSCMessageChatPageAddIcon(); + + /// 获取聊天页面发送消息图标路径 + /// 返回: String - 发送消息图标资源路径 + String getSCMessageChatPageSendMessageIcon(); + + /// 获取聊天页面相机图标路径 + /// 返回: String - 相机图标资源路径 + String getSCMessageChatPageCameraIcon(); + + /// 获取聊天页面图片图标路径 + /// 返回: String - 图片图标资源路径 + String getSCMessageChatPagePictureIcon(); + + /// 获取聊天页面红包图标路径 + /// 返回: String - 红包图标资源路径 + String getSCMessageChatPageRedEnvelopeIcon(); + + /// 获取聊天页面红包消息背景图像路径 + /// 返回: String - 红包消息背景图像资源路径 + String getSCMessageChatPageRedEnvelopeMessageBackground(); + + /// 获取聊天页面红包配置标签按钮图标路径 + /// 返回: String - 红包配置标签按钮图标资源路径 + String getSCMessageChatPageRedEnvelopeConfigTabButtonIcon(); + + /// 获取聊天页面红包消息项背景图像路径 + /// 返回: String - 红包消息项背景图像资源路径 + String getSCMessageChatPageRedEnvelopeMessageItemBackground(); + + /// 获取聊天页面红包接收包图标路径 + /// 返回: String - 红包接收包图标资源路径 + String getSCMessageChatPageRedEnvelopeReceivePackageIcon(); + + /// 获取聊天页面红包打开背景图像路径 + /// 返回: String - 红包打开背景图像资源路径 + String getSCMessageChatPageRedEnvelopeOpenBackground(); + + /// 获取聊天页面红包已打开背景图像路径 + /// 返回: String - 红包已打开背景图像资源路径 + String getSCMessageChatPageRedEnvelopeOpenedBackground(); + + /// 获取聊天页面金币图标路径 + /// 返回: String - 金币图标资源路径 + String getSCMessageChatPageGoldCoinIcon(); + + /// 获取聊天页面消息菜单删除图标路径 + /// 返回: String - 消息菜单删除图标资源路径 + String getSCMessageChatPageMessageMenuDeleteIcon(); + + /// 获取聊天页面消息菜单复制图标路径 + /// 返回: String - 消息菜单复制图标资源路径 + String getSCMessageChatPageMessageMenuCopyIcon(); + + /// 获取聊天页面消息菜单撤回图标路径 + /// 返回: String - 消息菜单撤回图标资源路径 + String getSCMessageChatPageMessageMenuRecallIcon(); + + /// 获取聊天页面加载图标路径 + /// 返回: String - 加载图标资源路径 + String getSCMessageChatPageLoadingIcon(); + + /// 获取聊天页面系统头像图像路径 + /// 返回: String - 系统头像图像资源路径 + String getSCMessageChatPageSystemHeadImage(); + + /// === 通用图标差异化方法 === + + /// 获取性别背景图像路径 + /// 参数: isFemale - 是否为女性 + /// 返回: String - 性别背景图像资源路径 + String getGenderBackgroundImage(bool isFemale); + + /// 获取通用ID背景图像路径 + /// 参数: hasSpecialId - 是否有特殊ID + /// 返回: String - ID背景图像资源路径 + String getIdBackgroundImage(bool hasSpecialId); + + /// 获取通用复制ID图标路径 + /// 返回: String - 复制ID图标资源路径 + String getCopyIdIcon(); + + /// 获取通用选择图标路径 + /// 返回: String - 选择图标资源路径 + String getCommonSelectIcon(); + + /// 获取通用未选择图标路径 + /// 返回: String - 未选择图标资源路径 + String getCommonUnselectIcon(); + + /// === Gift页面差异化方法 === + + /// 获取礼物页面首充房间标签图标路径 + /// 返回: String - 首充房间标签图标资源路径 + String getGiftPageFirstRechargeRoomTagIcon(); + + /// 获取礼物页面金币图标路径 + /// 返回: String - 金币图标资源路径 + String getGiftPageGoldCoinIcon(); + + /// 获取礼物页面赠送类型背景图像路径 + /// 返回: String - 赠送类型背景图像资源路径 + String getGiftPageGiveTypeBackground(); + + /// 获取礼物页面活动礼物头部背景图像路径 + /// 参数: giftType - 礼物类型 ('ACTIVITY', 'LUCK', 'CP', 'MAGIC') + /// 返回: String - 活动礼物头部背景图像资源路径 + String getGiftPageActivityGiftHeadBackground(String giftType); + + /// 获取礼物页面自定义规则图标路径 + /// 返回: String - 自定义规则图标资源路径 + String getGiftPageCustomizedRuleIcon(); + + /// 获取礼物页面特效礼物图标路径 + /// 参数: giftType - 礼物类型标识符 + /// 返回: String - 特效礼物图标资源路径 + String getGiftPageGiftEffectIcon(String giftType); + + /// 获取礼物页面音乐礼物图标路径 + /// 参数: giftType - 礼物类型标识符 + /// 返回: String - 音乐礼物图标资源路径 + String getGiftPageGiftMusicIcon(String giftType); + + /// 获取礼物页面幸运礼物图标路径 + /// 参数: giftType - 礼物类型标识符 + /// 返回: String - 幸运礼物图标资源路径 + String getGiftPageGiftLuckIcon(String giftType); + + /// 获取礼物页面CP礼物图标路径 + /// 参数: giftType - 礼物类型标识符 + /// 返回: String - CP礼物图标资源路径 + String getGiftPageGiftCpIcon(String giftType); + + /// 获取礼物页面首充文本图标路径 + /// 参数: languageCode - 语言代码 ('ar'为阿拉伯语, 'en'为英语) + /// 返回: String - 首充文本图标资源路径 + String getGiftPageFirstRechargeTextIcon(String languageCode); + + /// 获取礼物页面"全开麦"图标路径 + /// 返回: String - "全开麦"图标资源路径 + String getGiftPageAllOnMicrophoneIcon(); + + /// 获取礼物页面"用户开麦"图标路径 + /// 返回: String - "用户开麦"图标资源路径 + String getGiftPageUsersOnMicrophoneIcon(); + + /// 获取礼物页面"房间所有人"图标路径 + /// 返回: String - "房间所有人"图标资源路径 + String getGiftPageAllInTheRoomIcon(); + + /// === Media页面差异化方法 === + + /// 获取图片预览页面背景颜色 + /// 返回: Color - 页面背景颜色 + Color getImagePreviewBackgroundColor(); + + /// 获取图片预览画廊背景颜色 + /// 返回: Color - 画廊背景颜色 + Color getImagePreviewGalleryBackgroundColor(); + + /// 获取图片预览AppBar背景颜色 + /// 返回: Color - AppBar背景颜色 + Color getImagePreviewAppBarBackgroundColor(); + + /// 获取图片预览返回图标颜色 + /// 返回: Color - 返回图标颜色 + Color getImagePreviewBackIconColor(); + + /// 获取图片预览文本颜色 + /// 返回: Color - 文本颜色 + Color getImagePreviewTextColor(); + + /// 获取图片预览加载指示器颜色 + /// 返回: Color - 加载指示器颜色 + Color getImagePreviewLoadingIndicatorColor(); + + /// 获取图片预览AppBar透明度 + /// 返回: double - AppBar透明度值 + double getImagePreviewAppBarOpacity(); + + /// 获取视频播放页面背景颜色 + /// 返回: Color - 页面背景颜色 + Color getVideoPlayerBackgroundColor(); + + /// 获取视频播放控制界面背景颜色 + /// 返回: Color - 控制界面背景颜色 + Color getVideoPlayerControlsBackgroundColor(); + + /// 获取视频播放图标颜色 + /// 返回: Color - 图标颜色 + Color getVideoPlayerIconColor(); + + /// 获取视频播放底部控制栏背景颜色 + /// 返回: Color - 底部控制栏背景颜色 + Color getVideoPlayerBottomControlsBackgroundColor(); + + /// 获取视频播放进度条活动颜色 + /// 返回: Color - 进度条活动部分颜色 + Color getVideoPlayerProgressBarActiveColor(); + + /// 获取视频播放进度条非活动颜色 + /// 返回: Color - 进度条非活动部分颜色 + Color getVideoPlayerProgressBarInactiveColor(); + + /// 获取视频播放文本颜色 + /// 返回: Color - 文本颜色 + Color getVideoPlayerTextColor(); + + /// 获取视频播放加载指示器颜色 + /// 返回: Color - 加载指示器颜色 + Color getVideoPlayerLoadingIndicatorColor(); + + /// 获取视频播放控制界面透明度 + /// 返回: double - 控制界面透明度值 + double getVideoPlayerControlsOpacity(); + + /// 获取视频播放底部控制栏透明度 + /// 返回: double - 底部控制栏透明度值 + double getVideoPlayerBottomControlsOpacity(); + + /// 获取视频播放控制界面显示持续时间(秒) + /// 返回: int - 控制界面显示持续时间(秒) + int getVideoPlayerControlsDisplayDuration(); + + /// === 举报页面差异化方法 === + + /// 获取举报页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getReportPageBackgroundImage(); + + /// 获取举报页面提示文本颜色 + /// 返回: Color - 提示文本颜色 + Color getReportPageHintTextColor(); + + /// 获取举报页面容器背景颜色 + /// 返回: Color - 容器背景颜色 + Color getReportPageContainerBackgroundColor(); + + /// 获取举报页面边框颜色 + /// 返回: Color - 边框颜色 + Color getReportPageBorderColor(); + + /// 获取举报页面主要文本颜色 + /// 返回: Color - 主要文本颜色 + Color getReportPagePrimaryTextColor(); + + /// 获取举报页面次级提示文本颜色 + /// 返回: Color - 次级提示文本颜色 + Color getReportPageSecondaryHintTextColor(); + + /// 获取举报页面未选中边框颜色 + /// 返回: Color - 未选中边框颜色 + Color getReportPageUnselectedBorderColor(); + + /// 获取举报页面提交按钮背景颜色 + /// 返回: Color - 提交按钮背景颜色 + Color getReportPageSubmitButtonBackgroundColor(); + + /// 获取举报页面提交按钮文本颜色 + /// 返回: Color - 提交按钮文本颜色 + Color getReportPageSubmitButtonTextColor(); + + /// 获取举报页面图标路径 + /// 参数: iconType - 图标类型 ('addPic', 'closePic', 'checked') + /// 返回: String - 图标资源路径 + String getReportPageIcon(String iconType); + + /// === 语音房间页面差异化方法 === + + /// 获取语音房间页面背景颜色 + /// 返回: Color - 页面背景颜色 + Color getVoiceRoomBackgroundColor(); + + /// 获取语音房间页面Tab标签选中颜色 + /// 返回: Color - Tab标签选中颜色 + Color getVoiceRoomTabLabelColor(); + + /// 获取语音房间页面Tab标签未选中颜色 + /// 返回: Color - Tab标签未选中颜色 + Color getVoiceRoomTabUnselectedLabelColor(); + + /// 获取语音房间页面Tab指示器颜色 + /// 返回: Color - Tab指示器颜色 + Color getVoiceRoomTabIndicatorColor(); + + /// 获取语音房间页面Tab分割线颜色 + /// 返回: Color - Tab分割线颜色 + Color getVoiceRoomTabDividerColor(); + + /// 获取语音房间页面Tab标签内边距 + /// 返回: EdgeInsets - Tab标签内边距 + EdgeInsets getVoiceRoomTabLabelPadding(); + + /// 获取语音房间页面聊天容器外边距 + /// 返回: EdgeInsets - 聊天容器外边距 + EdgeInsetsDirectional getVoiceRoomChatContainerMargin(); + + /// 获取语音房间页面Tab标签选中文本样式 + /// 返回: TextStyle - Tab标签选中文本样式 + TextStyle getVoiceRoomTabLabelStyle(); + + /// 获取语音房间页面Tab标签未选中文本样式 + /// 返回: TextStyle - Tab标签未选中文本样式 + TextStyle getVoiceRoomTabUnselectedLabelStyle(); + + /// 获取语音房间默认背景图像路径 + /// 返回: String - 默认背景图像资源路径 + String getVoiceRoomDefaultBackgroundImage(); + + /// === 钱包页面差异化方法 === + + /// === 充值页面差异化方法 === + + /// 获取充值页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getRechargePageBackgroundImage(); + + /// 获取充值页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getRechargePageScaffoldBackgroundColor(); + + /// 获取充值页面对话框屏障颜色 + /// 返回: Color - 对话框屏障颜色 + Color getRechargePageDialogBarrierColor(); + + /// 获取充值页面对话框容器背景颜色 + /// 返回: Color - 对话框容器背景颜色 + Color getRechargePageDialogContainerBackgroundColor(); + + /// 获取充值页面对话框文本颜色 + /// 返回: Color - 对话框文本颜色 + Color getRechargePageDialogTextColor(); + + /// 获取充值页面主容器背景颜色 + /// 返回: Color - 主容器背景颜色 + Color getRechargePageMainContainerBackgroundColor(); + + /// 获取充值页面按钮背景颜色 + /// 返回: Color - 按钮背景颜色 + Color getRechargePageButtonBackgroundColor(); + + /// 获取充值页面按钮文本颜色 + /// 返回: Color - 按钮文本颜色 + Color getRechargePageButtonTextColor(); + + /// 获取充值页面钱包背景图像路径 + /// 返回: String - 钱包背景图像资源路径 + String getRechargePageWalletBackgroundImage(); + + /// 获取充值页面钱包图标路径 + /// 返回: String - 钱包图标资源路径 + String getRechargePageWalletIcon(); + + /// 获取充值页面钱包文本颜色 + /// 返回: Color - 钱包文本颜色 + Color getRechargePageWalletTextColor(); + + /// 获取充值页面金币图标路径(默认图标) + /// 返回: String - 金币图标资源路径 + String getRechargePageGoldIcon(); + + /// 获取充值页面选中项背景颜色 + /// 返回: Color - 选中项背景颜色 + Color getRechargePageSelectedItemBackgroundColor(); + + /// 获取充值页面选中项边框颜色 + /// 返回: Color - 选中项边框颜色 + Color getRechargePageSelectedItemBorderColor(); + + /// 获取充值页面未选中项边框颜色 + /// 返回: Color - 未选中项边框颜色 + Color getRechargePageUnselectedItemBorderColor(); + + /// 获取充值页面未选中项背景颜色 + /// 返回: Color - 未选中项背景颜色 + Color getRechargePageUnselectedItemBackgroundColor(); + + /// 获取充值页面项目文本颜色 + /// 返回: Color - 项目文本颜色 + Color getRechargePageItemTextColor(); + + /// 获取充值页面价格文本颜色 + /// 返回: Color - 价格文本颜色 + Color getRechargePageItemPriceTextColor(); + + /// 根据索引获取充值页面金币图标路径 + /// 参数: index - 项目索引 (0, 1, 2) + /// 返回: String - 金币图标资源路径 + String getRechargePageGoldIconByIndex(int index); + + /// 获取充值页面Apple产品项背景颜色 + /// 返回: Color - Apple产品项背景颜色 + Color getRechargePageAppleItemBackgroundColor(); + + /// === 金币记录页面差异化方法 === + + /// 获取金币记录页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getGoldRecordPageBackgroundImage(); + + /// 获取金币记录页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getGoldRecordPageScaffoldBackgroundColor(); + + /// 获取金币记录页面列表背景颜色 + /// 返回: Color - 列表背景颜色 + Color getGoldRecordSCPageListBackgroundColor(); + + /// 获取金币记录页面容器背景颜色 + /// 返回: Color - 容器背景颜色 + Color getGoldRecordPageContainerBackgroundColor(); + + /// 获取金币记录页面边框颜色 + /// 返回: Color - 边框颜色 + Color getGoldRecordPageBorderColor(); + + /// 获取金币记录页面主要文本颜色 + /// 返回: Color - 主要文本颜色 + Color getGoldRecordPagePrimaryTextColor(); + + /// 获取金币记录页面次要文本颜色 + /// 返回: Color - 次要文本颜色 + Color getGoldRecordPageSecondaryTextColor(); + + /// 获取充值页面记录图标路径 + /// 返回: String - 充值记录图标资源路径 + String getRechargePageRecordIcon(); + + /// === Store页面差异化方法 === + + /// 获取Store页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getStorePageBackgroundImage(); + + /// 获取Store页面购物袋图标路径 + /// 返回: String - 购物袋图标资源路径 + String getStorePageShoppingBagIcon(); + + /// 获取Store页面购物袋图标外边距 + /// 返回: EdgeInsetsDirectional - 购物袋图标外边距 + EdgeInsetsDirectional getStorePageShoppingBagMargin(); + + /// 获取Store页面Tab标签内边距 + /// 返回: EdgeInsets - Tab标签内边距 + EdgeInsets getStorePageTabLabelPadding(); + + /// 获取Store页面Tab标签选中文本样式 + /// 返回: TextStyle - Tab标签选中文本样式 + TextStyle getStorePageTabLabelStyle(); + + /// 获取Store页面Tab标签未选中文本样式 + /// 返回: TextStyle - Tab标签未选中文本样式 + TextStyle getStorePageTabUnselectedLabelStyle(); + + /// 获取Store页面Tab指示器颜色 + /// 返回: Color - Tab指示器颜色 + Color getStorePageTabIndicatorColor(); + + /// 获取Store页面Tab分割线颜色 + /// 返回: Color - Tab分割线颜色 + Color getStorePageTabDividerColor(); + + /// 获取Store页面底部Divider颜色 + /// 返回: Color - 底部Divider颜色 + Color getStorePageBottomDividerColor(); + + /// 获取Store页面底部Divider厚度 + /// 返回: double - 底部Divider厚度 + double getStorePageBottomDividerThickness(); + + /// 获取Store页面底部Divider高度 + /// 返回: double - 底部Divider高度 + double getStorePageBottomDividerHeight(); + + /// 获取Store页面金币图标路径 + /// 返回: String - 金币图标资源路径 + String getStorePageGoldIcon(); + + /// 获取Store页面金币文本颜色 + /// 返回: Color - 金币文本颜色 + Color getStorePageGoldTextColor(); + + /// 获取Store页面金币图标颜色 + /// 返回: Color - 金币图标颜色 + Color getStorePageGoldIconColor(); + + /// === Store子页面(商品项)差异化方法 === + + /// 获取Store商品项背景颜色 + /// 返回: Color - 商品项背景颜色 + Color getStoreItemBackgroundColor(); + + /// 获取Store商品项未选中边框颜色 + /// 返回: Color - 未选中边框颜色 + Color getStoreItemUnselectedBorderColor(); + + /// 获取Store商品项选中边框颜色 + /// 返回: Color - 选中边框颜色 + Color getStoreItemSelectedBorderColor(); + + /// 获取Store商品项金币图标路径 + /// 返回: String - 金币图标资源路径 + String getStoreItemGoldIcon(); + + /// 获取Store商品项价格文本颜色 + /// 返回: Color - 价格文本颜色 + Color getStoreItemPriceTextColor(); + + /// 获取Store商品项购买按钮背景颜色 + /// 返回: Color - 购买按钮背景颜色 + Color getStoreItemBuyButtonColor(); + + /// 获取Store商品项购买按钮文本颜色 + /// 返回: Color - 购买按钮文本颜色 + Color getStoreItemBuyButtonTextColor(); + + /// 获取Store商品项操作图标路径 + /// 参数: itemType - 商品类型 ('headdress', 'mountains', 'theme', 'chatbox') + /// 返回: String - 操作图标资源路径 + String getStoreItemActionIcon(String itemType); + + /// === 动态页面差异化方法 === + + /// 获取动态页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getDynamicPageBackgroundImage(); + + /// 获取动态页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getDynamicPageScaffoldBackgroundColor(); + + /// 获取动态页面AppBar背景颜色 + /// 返回: Color - AppBar背景颜色 + Color getDynamicPageAppBarBackgroundColor(); + + /// 获取动态页面Tab标签选中颜色 + /// 返回: Color - Tab标签选中颜色 + Color getDynamicPageTabLabelColor(); + + /// 获取动态页面Tab标签未选中颜色 + /// 返回: Color - Tab标签未选中颜色 + Color getDynamicPageTabUnselectedLabelColor(); + + /// 获取动态页面Tab指示器颜色 + /// 返回: Color - Tab指示器颜色 + Color getDynamicPageTabIndicatorColor(); + + /// 获取动态页面Tab分割线颜色 + /// 返回: Color - Tab分割线颜色 + Color getDynamicPageTabDividerColor(); + + /// 获取动态页面添加动态按钮图标路径 + /// 返回: String - 添加动态按钮图标资源路径 + String getDynamicPageAddButtonIcon(); + + /// 是否启用动态页面Tab标签滚动 + /// 返回: bool - 是否启用Tab标签滚动 + bool shouldDynamicPageTabScrollable(); + + /// 获取动态页面Tab标签选中文本样式 + /// 返回: TextStyle - Tab标签选中文本样式 + TextStyle getDynamicPageTabLabelStyle(); + + /// 获取动态页面Tab标签未选中文本样式 + /// 返回: TextStyle - Tab标签未选中文本样式 + TextStyle getDynamicPageTabUnselectedLabelStyle(); + + /// === 启动页面差异化方法 === + + /// 获取启动页面背景图像路径 + /// 返回: String - 启动背景图像资源路径 + String getSplashPageBackgroundImage(); + + /// 获取启动页面图标路径 + /// 返回: String - 启动图标资源路径 + String getSplashPageIcon(); + + /// 获取启动页面跳过按钮背景图像路径 + /// 返回: String - 跳过按钮背景图像资源路径 + String getSplashPageSkipButtonBackground(); + + /// 获取启动页面跳过按钮文本颜色 + /// 返回: Color - 跳过按钮文本颜色 + Color getSplashPageSkipButtonTextColor(); + + /// 获取启动页面游戏名称背景图像路径 + /// 返回: String - 游戏名称背景图像资源路径 + String getSplashPageKingGamesNameBackground(); + + /// 获取启动页面游戏名称文本颜色 + /// 返回: Color - 游戏名称文本颜色 + Color getSplashPageKingGamesTextColor(); + + /// 获取启动页面CP名称背景图像路径 + /// 返回: String - CP名称背景图像资源路径 + String getSplashPageCpNameBackground(); + + /// 获取启动页面CP名称文本颜色 + /// 返回: Color - CP名称文本颜色 + Color getSplashPageCpTextColor(); + + /// === 搜索页面差异化方法 === + + /// 获取搜索页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getSearchPageScaffoldBackgroundColor(); + + /// 获取搜索页面返回图标颜色 + /// 返回: Color - 返回图标颜色 + Color getSearchPageBackIconColor(); + + /// 获取搜索页面搜索框边框颜色 + /// 返回: Color - 搜索框边框颜色 + Color getSearchPageInputBorderColor(); + + /// 获取搜索页面搜索框文本颜色 + /// 返回: Color - 搜索框文本颜色 + Color getSearchPageInputTextColor(); + + /// 获取搜索页面搜索按钮文本颜色 + /// 返回: Color - 搜索按钮文本颜色 + Color getSearchPageButtonTextColor(); + + /// 获取搜索页面搜索按钮渐变颜色 + /// 返回: List - 搜索按钮渐变颜色列表 + List getSearchPageButtonGradient(); + + /// 获取搜索页面历史记录标题文本颜色 + /// 返回: Color - 历史记录标题文本颜色 + Color getSearchPageHistoryTitleTextColor(); + + /// 获取搜索页面历史项文本颜色 + /// 返回: Color - 历史项文本颜色 + Color getSearchPageHistoryItemTextColor(); + + /// 获取搜索页面历史项背景颜色 + /// 返回: Color - 历史项背景颜色 + Color getSearchPageHistoryItemBackgroundColor(); + + /// 获取搜索页面Tab指示器渐变开始颜色 + /// 返回: Color - Tab指示器渐变开始颜色 + Color getSearchPageTabIndicatorGradientStartColor(); + + /// 获取搜索页面Tab指示器渐变结束颜色 + /// 返回: Color - Tab指示器渐变结束颜色 + Color getSearchPageTabIndicatorGradientEndColor(); + + /// 获取搜索页面Tab标签选中颜色 + /// 返回: Color - Tab标签选中颜色 + Color getSearchPageTabSelectedLabelColor(); + + /// 获取搜索页面Tab分割线颜色 + /// 返回: Color - Tab分割线颜色 + Color getSearchPageTabDividerColor(); + + /// 获取搜索页面Tab标签未选中颜色 + /// 返回: Color - Tab标签未选中颜色 + Color getSearchPageTabUnselectedLabelColor(); + + /// 获取搜索页面分隔容器背景颜色 + /// 返回: Color - 分隔容器背景颜色 + Color getSearchPageDividerContainerBackgroundColor(); + + /// 获取搜索页面Tab标签选中文本样式 + /// 返回: TextStyle - Tab标签选中文本样式 + TextStyle getSearchPageTabSelectedLabelStyle(); + + /// 获取搜索页面Tab标签未选中文本样式 + /// 返回: TextStyle - Tab标签未选中文本样式 + TextStyle getSearchPageTabUnselectedLabelStyle(); + + /// 获取搜索页面搜索结果Tab标签内边距 + /// 返回: EdgeInsets - Tab标签内边距 + EdgeInsets getSearchPageResultTabLabelPadding(); + + /// 获取搜索页面搜索结果Tab标签选中颜色 + /// 返回: Color - Tab标签选中颜色 + Color getSearchPageResultTabLabelColor(); + + /// 获取搜索页面搜索结果Tab标签未选中颜色 + /// 返回: Color - Tab标签未选中颜色 + Color getSearchPageResultTabUnselectedLabelColor(); + + /// 获取搜索页面搜索结果Tab标签未选中文本样式 + /// 返回: TextStyle - Tab标签未选中文本样式 + TextStyle getSearchPageResultTabUnselectedLabelStyle(); + + /// 获取搜索页面搜索结果Tab标签选中文本样式 + /// 返回: TextStyle - Tab标签选中文本样式 + TextStyle getSearchPageResultTabLabelStyle(); + + /// 获取搜索页面搜索结果Tab指示器颜色(渐变) + /// 返回: LinearGradient - Tab指示器渐变颜色 + LinearGradient getSearchPageResultTabIndicatorColor(); + + /// 获取搜索页面搜索结果Tab指示器宽度 + /// 返回: double - Tab指示器宽度 + double getSearchPageResultTabIndicatorWidth(); + + /// 获取搜索页面搜索结果背景颜色 + /// 返回: Color - 搜索结果背景颜色 + Color getSearchPageResultBackgroundColor(); + + /// 获取搜索页面清除历史记录图标路径 + /// 返回: String - 清除历史记录图标资源路径 + String getSearchPageClearHistoryIcon(); + + /// 获取搜索页面空数据图标路径 + /// 返回: String - 空数据图标资源路径 + String getSearchPageEmptyDataIcon(); + + /// === 任务页面差异化方法 === + + /// 获取任务页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getTaskPageBackgroundImage(); + + /// 获取任务页面返回按钮颜色 + /// 返回: Color - 返回按钮颜色 + Color getTaskPageBackButtonColor(); + + /// 获取任务页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getTaskPageScaffoldBackgroundColor(); + + /// 获取任务页面容器背景颜色 + /// 返回: Color - 容器背景颜色 + Color getTaskPageContainerBackgroundColor(); + + /// 获取任务页面边框颜色(普通边框) + /// 返回: Color - 普通边框颜色 + Color getTaskPageBorderColor(); + + /// 获取任务页面边框颜色(特殊边框,如任务项边框) + /// 返回: Color - 特殊边框颜色 + Color getTaskPageSpecialBorderColor(); + + /// 获取任务页面主要文本颜色 + /// 返回: Color - 主要文本颜色 + Color getTaskPagePrimaryTextColor(); + + /// 获取任务页面金币文本颜色(折扣前价格) + /// 返回: Color - 金币文本颜色 + Color getTaskPageGoldTextColor(); + + /// 获取任务页面经验值文本颜色 + /// 返回: Color - 经验值文本颜色 + Color getTaskPageExpTextColor(); + + /// 获取任务页面主题颜色(用于SocialChatTheme.primaryColor) + /// 返回: Color - 主题颜色 + Color getTaskPageThemeColor(); + + /// 获取任务页面主题浅色(用于SocialChatTheme.primaryLight) + /// 返回: Color - 主题浅色 + Color getTaskPageThemeLightColor(); + + /// 获取任务页面可领取按钮渐变颜色 + /// 返回: List - 可领取按钮渐变颜色列表 + List getTaskPageReceivableButtonGradient(); + + /// 获取任务页面已完成按钮渐变颜色 + /// 返回: List - 已完成按钮渐变颜色列表 + List getTaskPageCompletedButtonGradient(); + + /// 获取任务页面按钮文本颜色 + /// 返回: Color - 按钮文本颜色 + Color getTaskPageButtonTextColor(); + + /// 获取任务页面遮罩颜色 + /// 返回: Color - 遮罩颜色 + Color getTaskPageMaskColor(); + + /// 获取任务页面头部背景图像路径 + /// 返回: String - 头部背景图像资源路径 + String getTaskPageHeadBackgroundImage(); + + /// 获取任务页面金币图标路径 + /// 返回: String - 金币图标资源路径 + String getTaskPageGoldIcon(); + + /// 获取任务页面经验值图标路径 + /// 返回: String - 经验值图标资源路径 + String getTaskPageExpIcon(); + + /// 获取任务页面邀请奖励背景图像路径 + /// 返回: String - 邀请奖励背景图像资源路径 + String getTaskPageInvitationRewardBackgroundImage(); + + /// 获取任务页面礼品袋文本颜色 + /// 返回: Color - 礼品袋文本颜色 + Color getTaskPageGiftBagTextColor(); + + /// 获取任务页面AppBar背景颜色 + /// 返回: Color - AppBar背景颜色 + Color getTaskPageAppBarBackgroundColor(); + + /// 获取任务页面透明容器颜色 + /// 返回: Color - 透明容器颜色 + Color getTaskPageTransparentContainerColor(); + + /// === WebView页面差异化方法 === + + /// === 普通WebView页面方法 === + + /// 获取WebView页面AppBar背景颜色 + /// 返回: Color - AppBar背景颜色 + Color getWebViewPageAppBarBackgroundColor(); + + /// 获取WebView页面标题文本颜色 + /// 返回: Color - 标题文本颜色 + Color getWebViewPageTitleTextColor(); + + /// 获取WebView页面返回箭头图标颜色 + /// 返回: Color - 返回箭头图标颜色 + Color getWebViewPageBackArrowColor(); + + /// 获取WebView页面进度条背景颜色 + /// 返回: Color - 进度条背景颜色 + Color getWebViewPageProgressBarBackgroundColor(); + + /// 获取WebView页面进度条活动颜色 + /// 返回: Color - 进度条活动颜色 + Color getWebViewPageProgressBarActiveColor(); + + /// === 游戏WebView页面方法 === + + /// 获取游戏WebView页面关闭图标颜色 + /// 返回: Color - 关闭图标颜色 + Color getGameWebViewCloseIconColor(); + + /// 获取游戏WebView页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getGameWebViewScaffoldBackgroundColor(); + + /// 获取游戏WebView页面加载指示器颜色 + /// 返回: Color - 加载指示器颜色 + Color getGameWebViewLoadingIndicatorColor(); + + /// 获取游戏WebView页面进度条背景颜色 + /// 返回: Color - 进度条背景颜色 + Color getGameWebViewProgressBarBackgroundColor(); + + /// === 设置页面差异化方法 === + + /// 获取设置页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getSettingsPageBackgroundImage(); + + /// 获取设置页面主容器背景颜色 + /// 返回: Color - 主容器背景颜色 + Color getSettingsPageMainContainerBackgroundColor(); + + /// 获取设置页面主容器边框颜色 + /// 返回: Color - 主容器边框颜色 + Color getSettingsPageMainContainerBorderColor(); + + /// 获取设置页面主文本颜色 + /// 返回: Color - 主文本颜色 + Color getSettingsPagePrimaryTextColor(); + + /// 获取设置页面次要文本颜色 + /// 返回: Color - 次要文本颜色 + Color getSettingsPageSecondaryTextColor(); + + /// 获取设置页面图标颜色 + /// 返回: Color - 图标颜色 + Color getSettingsPageIconColor(); + + /// 获取设置页面常见文本颜色 + /// 返回: Color - 常见文本颜色 + Color getSettingsPageCommonTextColor(); + + /// 获取设置页面关于文本颜色 + /// 返回: Color - 关于文本颜色 + Color getSettingsPageAboutTextColor(); + + /// 获取设置页面容器边框颜色 + /// 返回: Color - 容器边框颜色 + Color getSettingsPageContainerBorderColor(); + + /// 获取设置页面容器背景颜色 + /// 返回: Color - 容器背景颜色 + Color getSettingsPageContainerBackgroundColor(); + + /// 获取设置页面按钮背景颜色 + /// 返回: Color - 按钮背景颜色 + Color getSettingsPageButtonBackgroundColor(); + + /// 获取设置页面按钮文本颜色 + /// 返回: Color - 按钮文本颜色 + Color getSettingsPageButtonTextColor(); + + /// === 语言页面差异化方法 === + + /// 获取语言页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getLanguagePageBackgroundImage(); + + /// 获取语言页面复选框选中颜色 + /// 返回: Color - 复选框选中颜色 + Color getLanguagePageCheckboxActiveColor(); + + /// 获取语言页面复选框边框颜色 + /// 返回: Color - 复选框边框颜色 + Color getLanguagePageCheckboxBorderColor(); + + /// 获取语言页面主文本颜色 + /// 返回: Color - 主文本颜色 + Color getLanguagePagePrimaryTextColor(); + + /// 获取语言页面复选框对勾颜色 + /// 返回: Color - 复选框对勾颜色 + Color getLanguagePageCheckColor(); + + /// 获取语言页面复选框边框圆角 + /// 返回: double - 边框圆角值 + double getLanguagePageCheckboxBorderRadius(); + + /// 获取语言页面复选框边框宽度 + /// 返回: double - 边框宽度值 + double getLanguagePageCheckboxBorderWidth(); + + /// 获取语言页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getLanguagePageScaffoldBackgroundColor(); + + /// === 账户页面差异化方法 === + + /// 获取账户页面背景图像路径 + /// 返回: String - 背景图像资源路径 + String getAccountPageBackgroundImage(); + + /// 获取账户页面主容器背景颜色 + /// 返回: Color - 主容器背景颜色 + Color getAccountPageMainContainerBackgroundColor(); + + /// 获取账户页面主容器边框颜色 + /// 返回: Color - 主容器边框颜色 + Color getAccountPageMainContainerBorderColor(); + + /// 获取账户页面主文本颜色 + /// 返回: Color - 主文本颜色 + Color getAccountPagePrimaryTextColor(); + + /// 获取账户页面次要文本颜色 + /// 返回: Color - 次要文本颜色 + Color getAccountPageSecondaryTextColor(); + + /// 获取账户页面图标颜色 + /// 返回: Color - 图标颜色 + Color getAccountPageIconColor(); + + /// 获取账户页面分隔线颜色 + /// 返回: Color - 分隔线颜色 + Color getAccountPageDividerColor(); + + /// 获取账户页面Scaffold背景颜色 + /// 返回: Color - Scaffold背景颜色 + Color getAccountPageScaffoldBackgroundColor(); + + Color getGoldRecordPageListBackgroundColor(); +} diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart new file mode 100644 index 0000000..259d8db --- /dev/null +++ b/lib/app/config/configs/sc_variant1_config.dart @@ -0,0 +1,170 @@ +import 'package:flutter/foundation.dart'; +import 'package:yumi/app/config/app_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/app/config/strategies/variant1_business_logic_strategy.dart'; + +/// 马甲包配置 +class SCVariant1Config implements AppConfig { + BusinessLogicStrategy? _businessLogicStrategy; + + @override + String get appName => 'yumi'; // 马甲包应用名称 + + @override + String get packageName => 'com.org.yumi'; + + @override + String get apiHost => 'http://10.0.2.2:1100/'; // Android 模拟器访问宿主机本地网关 + + @override + String get imgHost => 'https://img.atuchat.com/'; // 测试图片服务器,上架前需替换为正式域名 + + @override + String get privacyAgreementUrl => 'https://www.atuchat.com/privacy.html'; // 测试隐私政策页面,上架前需替换 + + @override + String get userAgreementUrl => 'https://www.atuchat.com/service.html'; // 测试用户协议页面,上架前需替换 + + @override + String get appDownloadUrlGoogle => 'https://play.google.com/store/apps/details?id=$packageName'; + + @override + String get appDownloadUrlApple => 'https://apps.apple.com/us/app/atuchat/id1234567890'; // 需要更新为真实App Store ID + + @override + String get anchorAgentUrl => 'https://h5.atuchat.com/#/apply'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get hostCenterUrl => 'https://h5.atuchat.com/#/host-center'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get bdCenterUrl => 'https://h5.atuchat.com/#/bd-center'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get bdLeaderUrl => 'http://h5.atuchat.com/#/bd-leader-center'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get coinSellerUrl => 'https://h5.atuchat.com/#/coin-seller'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get adminUrl => 'https://h5.atuchat.com/#/admin-center'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get agencyCenterUrl => 'https://h5.atuchat.com/#/agency-center'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get gamesKingUrl => 'https://h5.atuchat.com/#/games-king'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get wealthRankUrl => 'https://h5.atuchat.com/#/ranking?first=Wealth'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get charmRankUrl => 'https://h5.atuchat.com/#/ranking?first=Charm'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get roomRankUrl => 'https://h5.atuchat.com/#/ranking?first=Room'; // 测试H5页面,上架前需替换为正式域名 + + @override + String get inviteNewUserUrl => 'https://h5.atuchat.com/#/invitation/invite-new-user'; // 测试H5页面,上架前需替换为正式域名 + + @override + int get primaryColor => 0xffFF5722; // 不同主色(橙色) + + @override + String get tencentImAppid => '00000000'; // 需要注册新的腾讯云IM账户并获取独立App ID + + @override + String get agoraRtcAppid => '00000000'; // 需要创建新的Agora项目并获取独立App ID + + @override + num get gameAppid => 9999999999; // 需要注册新的游戏服务账户并获取独立App ID + + @override + String get gameAppChannel => 'yumi'; + + @override + String get bigBroadcastGroup => '@TGS#YUMI_BROADCAST_GROUP_ID'; // 需要创建独立的腾讯IM广播群组 + + @override + String get imAdmin => 'c2c_atyou-admin'; // 独立的管理账号,需要在实际部署中配置 + + @override + bool get isReview => true; + + @override + bool get isGiftSpecialEffects => true; + + @override + bool get isEntryVehicleAnimation => true; + + @override + bool get isFloatingAnimationInGlobal => true; + + @override + bool get isLuckGiftSpecialEffects => true; + + @override + Map get featureFlags => { + 'giftSpecialEffects': isGiftSpecialEffects, + 'entryVehicleAnimation': isEntryVehicleAnimation, + 'floatingAnimationInGlobal': isFloatingAnimationInGlobal, + 'luckGiftSpecialEffects': isLuckGiftSpecialEffects, + }; + + @override + BusinessLogicStrategy get businessLogicStrategy { + return _businessLogicStrategy ??= Variant1BusinessLogicStrategy(); + } + + @override + Map toMap() { + return { + 'appName': appName, + 'packageName': packageName, + 'apiHost': apiHost, + 'imgHost': imgHost, + 'privacyAgreementUrl': privacyAgreementUrl, + 'userAgreementUrl': userAgreementUrl, + 'primaryColor': primaryColor, + 'tencentImAppid': tencentImAppid, + 'agoraRtcAppid': agoraRtcAppid, + 'gameAppid': gameAppid, + 'gameAppChannel': gameAppChannel, + 'bigBroadcastGroup': bigBroadcastGroup, + 'imAdmin': imAdmin, + 'isReview': isReview, + 'isGiftSpecialEffects': isGiftSpecialEffects, + 'isEntryVehicleAnimation': isEntryVehicleAnimation, + 'isFloatingAnimationInGlobal': isFloatingAnimationInGlobal, + 'isLuckGiftSpecialEffects': isLuckGiftSpecialEffects, + 'featureFlags': featureFlags, + 'businessLogicStrategy': 'Variant1BusinessLogicStrategy', + }; + } + + @override + void validate() { + final errors = []; + + // 检查API主机地址 + if (apiHost.isEmpty || !apiHost.startsWith('http')) { + errors.add('API主机地址无效或未设置'); + } + + // 如果有错误,根据模式处理 + if (errors.isNotEmpty) { + final errorMessage = '应用配置验证失败:\n${errors.join('\n')}'; + + if (kDebugMode) { + // 调试模式下只输出警告,不阻止应用启动(方便开发) + debugPrint('⚠️ 警告: $errorMessage'); + debugPrint('⚠️ 在调试模式下,应用将继续启动,但某些功能可能无法正常工作'); + } else { + // 发布模式下抛出异常,阻止应用启动 + throw StateError(errorMessage); + } + } else { + debugPrint('应用配置验证通过'); + } + } +} diff --git a/lib/app/config/strategies/base_business_logic_strategy.dart b/lib/app/config/strategies/base_business_logic_strategy.dart new file mode 100644 index 0000000..9e64c71 --- /dev/null +++ b/lib/app/config/strategies/base_business_logic_strategy.dart @@ -0,0 +1,2576 @@ +import 'package:flutter/material.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/shared/tools/sc_dialog_utils.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:provider/provider.dart'; + +/// 基础业务逻辑策略实现 +/// 提供原始应用的默认业务逻辑 +class BaseBusinessLogicStrategy implements BusinessLogicStrategy { + + @override + List getHomeTabPages(BuildContext context) { + final List pages = []; + return pages; + } + + @override + List getHomeTabLabels(BuildContext context) { + final List tabs = []; + tabs.add(Tab(text: SCAppLocalizations.of(context)!.popular)); + tabs.add(Tab(text: SCAppLocalizations.of(context)!.explore)); + return tabs; + } + + + @override + int getHomeInitialTabIndex() { + return 0; + } + + @override + void onAvatarTap(BuildContext context) { + Provider.of(context, listen: false).openDrawer(context); + } + + @override + bool shouldShowFirstRechargePrompt() { + return UserManager().getCurrentUser()?.userProfile?.firstRecharge ?? false; + } + + @override + Map getFirstRechargePosition() { + return {'bottom': 40.0, 'end': 15.0, 'top': null, 'start': null}; + } + + @override + void onFirstRechargeTap(BuildContext context) { + SCDialogUtils.showFirstRechargeDialog(context); + } + + /// === 登录页面差异化方法实现 === + + @override + String getLoginBackgroundImage() { + // 原始应用默认背景图片 + return "sc_images/login/login_account.png"; + } + + @override + String getLoginAppIcon() { + // 原始应用默认图标 + return "sc_images/splash/sc_icon_splash_icon.png"; + } + + @override + Color getLoginButtonColor() { + // 原始应用默认按钮颜色 - 使用马甲包主题的primaryLight + return SocialChatTheme.primaryLight; + } + + @override + List? getLoginButtonGradient() { + // 原始应用默认不使用渐变 + return null; + } + + @override + Color getLoginButtonTextColor() { + // 原始应用默认按钮文本颜色(白色) + return Colors.white; + } + + @override + Color getLoginLabelTextColor() { + // 原始应用默认标签文本颜色(黑色) + return Colors.black; + } + + @override + BoxDecoration getLoginInputDecoration() { + // 原始应用输入框装饰:透明背景,黑色边框 + return BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.all(Radius.circular(8)), + border: Border.all(color: Colors.black45, width: 0.5), + ); + } + + @override + EdgeInsets getLoginPagePadding() { + // 原始应用默认内边距 + return const EdgeInsets.only(top: 100); + } + + @override + bool shouldShowLoginStatusBar() { + // 原始应用默认显示状态栏 + return true; + } + + @override + Color getLoginHintTextColor() { + // 原始应用默认提示文本颜色(黑色45%透明度) + return Colors.black45; + } + + @override + Color getLoginInputTextColor() { + // 原始应用默认输入文本颜色(黑色) + return Colors.black; + } + + /// === 家族页面差异化方法实现 === + + + + + + + @override + bool shouldShowPasswordRoomIcon() { + // 原始应用:显示密码房间图标 + return true; + } + + /// === 探索页面差异化方法实现 === + + @override + int getExploreRoomDisplayThreshold() { + // 原始应用:显示前6个房间在抽屉上方 + return 6; + } + + @override + String getExploreGridIcon() { + // 原始应用:网格视图图标 + return "sc_images/index/sc_icon_index_room_model_1.png"; + } + + @override + String getExploreListIcon() { + // 原始应用:列表视图图标 + return "sc_images/index/sc_icon_index_room_model_2.png"; + } + + @override + String getExploreRankIconPattern(bool isGrid, int rank) { + // 原始应用:排名图标路径模式 + if (isGrid) { + return "sc_images/index/sc_icon_explore_room_model_1_rank_$rank.png"; + } else { + return "sc_images/index/sc_icon_explore_room_model_2_rank_$rank.png"; + } + } + + @override + Color getExploreRoomBorderColor() { + // 原始应用:使用主题色作为边框颜色 + return SocialChatTheme.primaryLight; + } + + @override + double getExploreRoomBorderWidth() { + // 原始应用:边框宽度 + return 1.3; + } + + @override + int getExploreDrawerGridCrossAxisCount() { + // 原始应用:抽屉内容网格列数 + return 3; + } + + @override + int getExploreRoomNameMarqueeThreshold() { + // 原始应用:房间名称长度超过10时显示滚动效果 + return 10; + } + + @override + int getExploreRoomDescMarqueeThreshold() { + // 原始应用:房间描述长度超过12时显示滚动效果 + return 12; + } + + /// === 个人主页差异化方法实现 === + + @override + String getMePageDefaultBackgroundImage() { + // 原始应用默认背景图片 + return "sc_images/person/sc_icon_my_head_bg_defalt.png"; + } + + @override + String getMePageGenderBackgroundImage(bool isFemale) { + // 原始应用性别背景图片 + return isFemale + ? "sc_images/person/sc_icon_my_head_bg_woman.png" + : "sc_images/person/sc_icon_my_head_bg_man.png"; + } + + @override + String getMePageCpDialogHeadImage() { + // 原始应用CP对话框头像图片 + return "sc_images/person/sc_icon_send_cp_requst_dialog_head.png"; + } + + @override + String getMePageDefaultAvatarImage() { + // 原始应用默认头像图片 + return "sc_images/general/sc_icon_avar_defalt.png"; + } + + @override + String getMePageIdBackgroundImage(bool hasSpecialId) { + // 原始应用ID背景图片 + return hasSpecialId + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png"; + } + + @override + String getMePageCopyIdIcon() { + // 原始应用复制ID图标 + return "sc_images/room/sc_icon_user_card_copy_id.png"; + } + + @override + String getMePageGenderAgeBackgroundImage(bool isFemale) { + // 原始应用性别年龄背景图片 + return isFemale + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png"; + } + + @override + String getMePageVisitorsFollowFansBackgroundImage(bool isFemale) { + // 原始应用访客关注粉丝背景图片 + return isFemale + ? "sc_images/person/sc_icon_vistors_follow_fans_bg_woman.png" + : "sc_images/person/sc_icon_vistors_follow_fans_bg_man.png"; + } + + @override + String getMePageEditUserInfoIcon() { + // 原始应用编辑用户信息图标 + return "sc_images/person/sc_icon_edit_user_info2.png"; + } + + @override + Color getMePageShineColor() { + // 原始应用闪耀文本颜色 + return const Color(0xffe6e6e6); + } + + @override + List getMePageGradientColors(bool isFemale) { + // 原始应用渐变颜色 + if (isFemale) { + return [ + const Color(0xFF3F1016), + const Color(0xFF1E0B0E), + const Color(0xFF3F0810), + ]; + } else { + return [ + const Color(0xFF666666), + const Color(0xFF000000), + ]; + } + } + + @override + List getMePageTabIndicatorGradient(bool isFemale) { + // 原始应用Tab指示器渐变 + if (isFemale) { + return [ + const Color(0xffC62548), + const Color(0xff9C0322), + ]; + } else { + return [ + const Color(0xff141414), + const Color(0xffE4E4E4), + ]; + } + } + + @override + Color getMePageVisitorsFollowFansTextColor() { + // 原始应用访客关注粉丝文本颜色 + return const Color(0xffD0D0D0); + } + + @override + int getMePageInitialTabIndex() { + // 原始应用初始Tab索引 + return 1; // profile页 + } + + @override + double getMePageScrollOpacityThreshold() { + // 原始应用滚动透明度计算阈值 + return 320.0; + } + + @override + int getMePageNicknameScrollThreshold() { + // 原始应用昵称滚动显示阈值 + return 13; + } + + @override + int getMePageAgeDisplayWidthThreshold() { + // 原始应用年龄显示宽度阈值 + return 999; + } + + @override + double getMePageOpacityThresholdForAvatarDisplay() { + // 原始应用头像显示透明度阈值 + return 0.5; + } + + /// === 热门页面差异化方法实现 === + + @override + String getPopularLeaderboardBackgroundImage(String leaderboardType) { + // 原始应用:使用现有的图像资源路径 + switch (leaderboardType) { + case 'room': + return "sc_images/index/sc_icon_leader_spinner_room_bg.png"; + case 'wealth': + return "sc_images/index/sc_icon_leader_spinner_wealth_bg.png"; + case 'charm': + return "sc_images/index/sc_icon_leader_spinner_charm_bg.png"; + default: + return "sc_images/index/sc_icon_leader_spinner_room_bg.png"; + } + } + + /// === 游戏页面差异化方法实现 === + + @override + String getGameRankBackgroundImage(int rank) { + // 原始应用:使用现有的排名背景图像模式 + return "sc_images/game/sc_icon_game_list_item_bg_$rank.png"; + } + + @override + String getGameRankIcon(int rank) { + // 原始应用:使用现有的排名图标模式 + return "sc_images/game/sc_icon_game_rank_$rank.png"; + } + + @override + Color getGamePageThemeColor() { + // 原始应用:默认主题颜色 + return const Color(0xffFF5722); + } + + @override + Color getGamePageBackgroundColor() { + // 原始应用:默认背景颜色 + return const Color(0xfff5f5f5); + } + + @override + int getGamePageGridCrossAxisCount() { + // 原始应用:3列网格 + return 3; + } + + @override + double getGamePageGridChildAspectRatio() { + // 原始应用:默认高度比例 + return 1.3; + } + + @override + Color getGameRankTextColor(int rank) { + // 原始应用:根据排名返回不同颜色 + switch (rank) { + case 1: + return const Color(0xffFFD700); // 金色 + case 2: + return const Color(0xffC0C0C0); // 银色 + case 3: + return const Color(0xffCD7F32); // 铜色 + default: + return Colors.black87; + } + } + + @override + Color getGamePageGradientSecondColor() { + // 原始应用:渐变第二颜色 + return const Color(0xffEAD4FF); + } + + @override + String getGameHotTagBackgroundImage() { + // 原始应用:热门游戏标签背景图片 + return "sc_images/index/sc_icon_hotgames_tag_bg.png"; + } + + @override + String getGameNewTagImage() { + // 原始应用:新游戏标签图片 + return "sc_images/index/sc_icon_game_new_tag.png"; + } + + @override + String getGamePageMaskImage() { + // 原始应用:游戏页面遮罩图片 + return "sc_images/index/sc_icon_index_mask.png"; + } + + /// === VIP页面差异化方法实现 === + + @override + String getVipPageBackgroundImage(int vipLevel) { + // 原始应用:使用现有的VIP背景图像模式 + return "sc_images/vip/sc_icon_vip_level_bg_$vipLevel.png"; + } + + @override + String getVipPageIcon(int vipLevel) { + // 原始应用:使用现有的VIP图标模式 + return "sc_images/vip/sc_icon_vip_level_$vipLevel.png"; + } + + @override + List getVipPageButtonGradient() { + // 原始应用:VIP按钮渐变颜色 + return [ + const Color(0xffFF5722), + const Color(0xffFEB219), + ]; + } + + @override + int getVipPageGridCrossAxisCount() { + // 原始应用:3列网格 + return 3; + } + + @override + double getVipPageGridChildAspectRatio() { + // 原始应用:默认高度比例 + return 1.5; + } + + @override + String getVipPageHeadBackgroundImage(int vipLevel) { + // 原始应用:VIP头部背景图像模式 + return "sc_images/vip/sc_icon_vip_head_bg_v$vipLevel.png"; + } + + @override + String getVipPageTabSelectedIcon(int vipLevel) { + // 原始应用:Tab选中状态图标模式 + return "sc_images/vip/sc_icon_tab_vip_text_on_v$vipLevel.png"; + } + + @override + String getVipPageTabUnselectedIcon(int vipLevel) { + // 原始应用:Tab未选中状态图标模式 + return "sc_images/vip/sc_icon_tab_vip_text_no_v$vipLevel.png"; + } + + @override + String getVipPageLargeIcon(int vipLevel) { + // 原始应用:VIP页面大图标路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_ic.webp"; + } + + @override + String getVipPageTitleImage(int vipLevel) { + // 原始应用:VIP页面标题图像路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_t1.png"; + } + + @override + String getVipPageTagImage(int vipLevel, int tagIndex) { + // 原始应用:VIP页面标签图像路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_tag$tagIndex.png"; + } + + @override + String getVipPageItemBackgroundImage(int vipLevel) { + // 原始应用:VIP页面功能项背景图像路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_item_bg.png"; + } + + @override + String getVipPagePrivilegeIcon(int vipLevel, int privilegeIndex) { + // 原始应用:VIP页面特权图标路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_privilege$privilegeIndex.png"; + } + + @override + String getVipPageFeatureIcon(int vipLevel, String featureName) { + // 原始应用:VIP页面功能图标路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_$featureName.png"; + } + + @override + String getVipPagePreviewImage(int vipLevel, String previewType, String featureName) { + // 原始应用:VIP页面预览图像路径模式 + if (featureName == 'profile_frame') { + return "sc_images/vip/sc_icon_vip${vipLevel}_profile_rev.png"; + } else if (featureName == 'profile_card') { + return "sc_images/vip/sc_icon_vip${vipLevel}_profile_card_rev.png"; + } else if (featureName == 'room_cover_headdress') { + return "sc_images/vip/sc_icon_vip${vipLevel}_room_cover_border_rev.png"; + } else if (featureName == 'mic_rippl_theme') { + return "sc_images/vip/sc_icon_vip${vipLevel}_sesc_rev.png"; + } else if (featureName == 'chatbox') { + return "sc_images/vip/sc_icon_vip${vipLevel}_chatbox_pre.png"; + } else { + return "sc_images/vip/sc_icon_vip${vipLevel}_${featureName}_$previewType.png"; + } + } + + @override + String getVipPagePurchaseBackgroundImage(int vipLevel) { + // 原始应用:VIP页面购买背景图像路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_buy_bg.png"; + } + + @override + String getVipPagePurchaseButtonImage() { + // 原始应用:VIP页面购买按钮图像路径 + return "sc_images/vip/sc_icon_vip_buy_btn.png"; + } + + @override + Color getVipPageTextColor(int vipLevel) { + // 原始应用:根据VIP等级返回不同的文本颜色 + switch (vipLevel) { + case 1: + return const Color(0xFF71D671); + case 2: + return const Color(0xFF7179D6); + case 3: + return const Color(0xFFAA71D6); + case 4: + return const Color(0xFF71D6B3); + case 5: + return const Color(0xFFD67171); + case 6: + return const Color(0xFFFEE5BB); + default: + return Colors.white; + } + } + + /// === 通用页面差异化方法实现 === + + @override + String getCommonRankBackgroundImage(String pageType, int rank) { + // 原始应用:通用的排名背景图像模式 + return "sc_images/$pageType/sc_icon_${pageType}_rank_bg_$rank.png"; + } + + @override + String getCommonRankIcon(String pageType, int rank) { + // 原始应用:通用的排名图标模式 + return "sc_images/$pageType/sc_icon_${pageType}_rank_$rank.png"; + } + + @override + int getCommonPageGridCrossAxisCount(String pageType) { + // 原始应用:通用的2列网格 + return 2; + } + + @override + double getCommonPageGridChildAspectRatio(String pageType) { + // 原始应用:通用的高度比例 + return 1.4; + } + + /// === 个人主页2 (MePage2) 差异化方法实现 === + + @override + Map getMePage2WalletBackground() { + // 原始应用默认钱包背景配置 + return { + 'gradient': [ + Color(0xFFFF9500), + Color(0xCCFF9500), // withOpacity(0.8) + ], + 'borderRadius': 12.0, + 'shadow': { + 'color': Color(0x1A000000), // Colors.black.withOpacity(0.1) + 'blurRadius': 12.0, + 'offset': Offset(0, 4.0), + }, + }; + } + + @override + Map getMePage2StatisticsColors() { + // 原始应用统计信息颜色配置 + return { + 'numberColor': Color(0xFF333333), + 'labelColor': Color(0xFFB1B1B1), + 'dividerColor': Color(0xFFE6E6E6), + }; + } + + @override + Map getMePage2FunctionIcons() { + // 原始应用功能卡片图标路径映射 + return { + 'invite': 'sc_images/general/sc_icon_invite.png', + 'task': 'sc_images/general/sc_icon_task.png', + 'store': 'sc_images/general/sc_icon_shop.png', + 'items': 'sc_images/general/sc_icon_items.png', + 'level': 'sc_images/general/sc_icon_level.png', + 'badge': 'sc_images/general/sc_icon_badge.png', + 'host': 'sc_images/general/sc_icon_host.png', + 'bd': 'sc_images/general/sc_icon_bd.png', + 'feedback': 'sc_images/general/sc_icon_feedback.png', + 'settings': 'sc_images/general/sc_icon_settings_card.png', + }; + } + + @override + Map getMePage2CardStyles() { + // 原始应用卡片样式配置 + return { + 'borderRadius': 8.0, + 'shadow': { + 'color': Color(0x0D000000), // Colors.black.withOpacity(0.05) + 'blurRadius': 8.0, + 'offset': Offset(0, 2.0), + }, + 'border': null, // 无边框 + }; + } + + /// === Admin编辑页面差异化方法实现 === + + @override + String getAdminEditingBackgroundImage() { + // 原始应用:编辑页面背景图片 + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + List getAdminEditingButtonGradient(String buttonType) { + // 原始应用:编辑页面按钮渐变颜色,两个按钮使用相同的渐变 + return [ + const Color(0xffC670FF), + const Color(0xff7726FF), + ]; + } + + @override + String getAdminEditingIcon(String iconType) { + // 原始应用:编辑页面图标路径 + switch (iconType) { + case 'specialIdBg': + return "sc_images/general/sc_icon_special_id_bg.png"; + case 'normalIdBg': + return "sc_images/general/sc_icon_id_bg.png"; + case 'copyId': + return "sc_images/room/sc_icon_user_card_copy_id.png"; + case 'addPic': + return "sc_images/general/sc_icon_add_pic.png"; + case 'closePic': + return "sc_images/general/sc_icon_pic_close.png"; + case 'checked': + return "sc_images/general/sc_icon_checked.png"; + default: + return ""; + } + } + + @override + BoxDecoration getAdminEditingInputDecoration() { + // 原始应用:编辑页面输入框装饰样式 + return BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xffE6E6E6), + width: 1.0, + ), + ); + } + + @override + int getAdminEditingMaxImageUploadCount() { + // 原始应用:最大图片上传数量(pic01, pic02, pic03) + return 3; + } + + @override + int getAdminEditingDescriptionMaxLength() { + // 原始应用:描述文本框最大字符数 + return 200; + } + + @override + Map getAdminEditingViolationTypeMapping(String targetType) { + // 原始应用:违规类型ID映射 + if (targetType == 'User') { + return { + 'Pornography': 1, + 'Illegal information': 2, + }; + } else if (targetType == 'Room') { + return { + 'Pornography': 3, + 'Illegal information': 4, + 'Fraud': 5, + 'Other': 6, + }; + } + return {}; + } + + /// === Admin搜索页面差异化方法实现 === + + @override + String getAdminSearchBackgroundImage(String pageType) { + // 原始应用:搜索页面背景图片 + return "sc_images/index/sc_icon_coupon_head_bg.png"; + } + + @override + List getAdminSearchButtonGradient(String pageType) { + // 原始应用:搜索按钮渐变颜色 - 透明渐变 + return [ + Colors.transparent, + Colors.transparent, + ]; + } + + @override + Color getAdminSearchButtonTextColor(String pageType) { + // 原始应用:搜索按钮文本颜色 + return Colors.black; + } + + @override + String getAdminSearchEditIcon(String pageType) { + // 原始应用:编辑图标路径 + return "sc_images/room/sc_icon_room_edit.png"; + } + + @override + Color getAdminSearchInputBorderColor(String pageType) { + // 原始应用:搜索框边框颜色 + return Colors.black12; + } + + @override + Color getAdminSearchInputTextColor(String pageType) { + // 原始应用:搜索框文本颜色 + return Colors.grey; + } + + @override + String getAdminSearchUserInfoIcon(String iconType) { + // 原始应用:用户信息图标路径 + switch (iconType) { + case 'specialIdBg': + return "sc_images/general/sc_icon_special_id_bg.png"; + case 'normalIdBg': + return "sc_images/general/sc_icon_id_bg.png"; + case 'copyId': + return "sc_images/room/sc_icon_user_card_copy_id.png"; + default: + return ""; + } + } + + /// === 主登录页面差异化方法实现 === + + @override + String getLoginMainBackgroundImage() { + // 原始应用默认背景图片 + return "sc_images/login/sc_login.png"; + } + + @override + String getLoginMainAppIcon() { + // 原始应用默认应用图标 + return "sc_images/splash/sc_icon_splash_icon.png"; + } + + @override + String getLoginMainGoogleIcon() { + // 原始应用默认Google图标 + return "sc_images/login/sc_icon_google.png"; + } + + @override + String getLoginMainAppleIcon() { + // 原始应用默认Apple图标 + return "sc_images/login/sc_icon_iphone.png"; + } + + @override + String getLoginMainAccountIcon() { + // 原始应用默认账号图标 + return "sc_images/login/sc_icon_account.png"; + } + + @override + String getLoginMainAgreementIcon(bool isSelected) { + // 原始应用默认协议图标 + return isSelected + ? "sc_images/login/sc_icon_login_ser_select.png" + : "sc_images/login/sc_icon_login_ser_select_un.png"; + } + + @override + Color getLoginMainDividerColor() { + // 原始应用默认分割线颜色 + return const Color(0xFFCCCCCC); + } + + @override + Color getLoginMainButtonBorderColor() { + // 原始应用默认按钮边框颜色 + return const Color(0xFFE6E6E6); + } + + @override + Color getLoginMainButtonBackgroundColor() { + // 原始应用默认按钮背景颜色 + return Colors.white; + } + + @override + Color getLoginMainLinkColor() { + // 原始应用默认链接颜色 + return const Color(0xFFFF9500); + } + + @override + Color getLoginMainButtonTextColor() { + // 原始应用默认按钮文本颜色 + return Colors.black; + } + + /// === 编辑个人资料页面差异化方法实现 === + + @override + String getEditProfileBackgroundImage() { + // 原始应用默认背景图片 + return "sc_images/login/edit_profile_bg.png"; + } + + @override + String getEditProfileDefaultAvatarImage() { + // 原始应用默认头像图片 + return "sc_images/login/sc_icon_profile_head.png"; + } + + @override + String getEditProfileGenderIcon(bool isMale) { + // 原始应用默认性别图标 + return isMale + ? "sc_images/login/sc_icon_sex_man.png" + : "sc_images/login/sc_icon_sex_woman.png"; + } + + @override + List getEditProfileGenderButtonGradient(bool isMale, bool isSelected) { + // 原始应用默认性别按钮渐变颜色 + return [ + const Color(0xffFF9326), + const Color(0xffFEB219), + ]; + } + + @override + Color getEditProfileInputBackgroundColor() { + // 原始应用默认输入框背景颜色 + return Colors.white38; + } + + @override + Color getEditProfileDatePickerConfirmColor() { + // 原始应用默认日期选择器确认按钮颜色 + return const Color(0xff7726FF); + } + + @override + Color getEditProfileContinueButtonColor() { + // 原始应用默认继续按钮颜色 + return const Color(0xff7726FF); + } + + /// === Country页面差异化方法实现 === + + @override + String getCountrySelectOkIcon() { + // 原始应用默认选择确认图标 + return "sc_images/general/sc_icon_select_ok.png"; + } + + @override + String getCountrySelectUnOkIcon() { + // 原始应用默认选择未确认图标 + return "sc_images/general/sc_icon_select_un_ok.png"; + } + + @override + String getCountryRadioSelectedIcon() { + // 原始应用默认单选选中图标 + return "sc_images/login/sc_icon_login_ser_select.png"; + } + + @override + String getCountryRadioUnselectedIcon() { + // 原始应用默认单选未选中图标 + return "sc_images/login/sc_icon_login_ser_select_un.png"; + } + + @override + Color getCountryItemBorderColor() { + // 原始应用默认国家列表项边框颜色 + return const Color(0xffE6E6E6); + } + + @override + Color getCountryPageScaffoldBackgroundColor() { + // 原始应用默认Scaffold背景颜色(白色) + return Colors.white; + } + + @override + Color getCountryPageIconColor() { + // 原始应用默认图标颜色(黑色) + return Colors.black; + } + + @override + Color getCountryPagePrimaryTextColor() { + // 原始应用默认主要文本颜色(黑色) + return Colors.black; + } + + @override + Color getCountryPageSecondaryTextColor() { + // 原始应用默认次要文本颜色(黑色45%透明度) + return Colors.black45; + } + + @override + Color getCountryPageContainerBackgroundColor() { + // 原始应用默认容器背景颜色(白色) + return Colors.white; + } + + /// === Chat页面差异化方法实现 === + + /// === 消息页面 (SCMessagePage) 方法实现 === + + @override + String getMessagePageIndexMaskIcon() { + // 原始应用:消息页面索引遮罩图标 + return "sc_images/index/sc_icon_index_mask.png"; + } + + @override + String getMessagePageAnnouncementTagIcon() { + // 原始应用:消息页面公告标签图标 + return "sc_images/msg/sc_icon_message_activity.png"; + } + + @override + String getMessagePageActivityMessageIcon() { + // 原始应用:消息页面活动消息图标 + return "sc_images/msg/sc_icon_message_activity.png"; + } + + @override + String getMessagePageActivityTitleBackground(bool isRtl) { + // 原始应用:消息页面活动消息标题背景图标 + return isRtl + ? "sc_images/msg/sc_icon_activity_title_bg_rtl.png" + : "sc_images/msg/sc_icon_message_activity.png"; + } + + @override + String getMessagePageSystemMessageIcon() { + // 原始应用:消息页面系统消息图标 + return "sc_images/msg/sc_icon_message_system.png"; + } + + @override + String getMessagePageSystemTitleBackground(bool isRtl) { + // 原始应用:消息页面系统消息标题背景图标 + return isRtl + ? "sc_images/msg/sc_icon_system_title_bg_rtl.png" + : "sc_images/msg/sc_icon_system_title_bg.png"; + } + + @override + String getMessagePageNotificationMessageIcon() { + // 原始应用:消息页面通知消息图标 + return "sc_images/msg/sc_icon_message_noti.png"; + } + + @override + String getMessagePageNotificationTitleBackground(bool isRtl) { + // 原始应用:消息页面通知消息标题背景图标 + return isRtl + ? "sc_images/msg/sc_icon_notifcation_title_bg_rtl.png" + : "sc_images/msg/sc_icon_notifcation_title_bg.png"; + } + + /// === 聊天页面 (SCMessageChatPage) 方法实现 === + + @override + String getSCMessageChatPageRoomSettingBackground() { + // 原始应用:聊天页面房间设置背景图像 + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + String getSCMessageChatPageEmojiIcon() { + // 原始应用:聊天页面表情图标 + return "sc_images/msg/sc_icon_emoji.png"; + } + + @override + String getSCMessageChatPageChatKeyboardIcon() { + // 原始应用:聊天页面聊天键盘图标 + return "sc_images/msg/sc_icon_chsc_key.png"; + } + + @override + String getSCMessageChatPageAddIcon() { + // 原始应用:聊天页面添加图标 + return "sc_images/msg/sc_icon_add.png"; + } + + @override + String getSCMessageChatPageSendMessageIcon() { + // 原始应用:聊天页面发送消息图标 + return "sc_images/msg/sc_icon_chsc_message_send.png"; + } + + @override + String getSCMessageChatPageCameraIcon() { + // 原始应用:聊天页面相机图标 + return "sc_images/msg/sc_icon_xiangji.png"; + } + + @override + String getSCMessageChatPagePictureIcon() { + // 原始应用:聊天页面图片图标 + return "sc_images/msg/sc_icon_tupian.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeIcon() { + // 原始应用:聊天页面红包图标 + return "sc_images/msg/sc_icon_hongbao.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeMessageBackground() { + // 原始应用:聊天页面红包消息背景图像 + return "sc_images/msg/sc_icon_msg_send_hongbao_bg.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeConfigTabButtonIcon() { + // 原始应用:聊天页面红包配置标签按钮图标 + return "sc_images/room/sc_icon_red_envelope_config_tab_btn.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeMessageItemBackground() { + // 原始应用:聊天页面红包消息项背景图像 + return "sc_images/msg/sc_icon_red_envelopes_msg_item_bg.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeReceivePackageIcon() { + // 原始应用:聊天页面红包接收包图标 + return "sc_images/room/sc_icon_red_envelope_rec_pack.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeOpenBackground() { + // 原始应用:聊天页面红包打开背景图像 + return "sc_images/msg/sc_icon_red_envelopes_msg_open_bg.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeOpenedBackground() { + // 原始应用:聊天页面红包已打开背景图像 + return "sc_images/msg/sc_icon_red_envelopes_msg_opened_bg.png"; + } + + @override + String getSCMessageChatPageGoldCoinIcon() { + // 原始应用:聊天页面金币图标 + return "sc_images/general/sc_icon_jb.png"; + } + + @override + String getSCMessageChatPageMessageMenuDeleteIcon() { + // 原始应用:聊天页面消息菜单删除图标 + return "sc_images/msg/sc_icon_msg_menu_delete.png"; + } + + @override + String getSCMessageChatPageMessageMenuCopyIcon() { + // 原始应用:聊天页面消息菜单复制图标 + return "sc_images/msg/sc_icon_msg_menu_copy.png"; + } + + @override + String getSCMessageChatPageMessageMenuRecallIcon() { + // 原始应用:聊天页面消息菜单撤回图标 + return "sc_images/msg/sc_icon_msg_menu_recall.png"; + } + + @override + String getSCMessageChatPageLoadingIcon() { + // 原始应用:聊天页面加载图标 + return "sc_images/general/sc_icon_loading.png"; + } + + @override + String getSCMessageChatPageSystemHeadImage() { + // 原始应用:聊天页面系统头像图像 + return "sc_images/msg/head_system.png"; + } + + @override + String getGenderBackgroundImage(bool isFemale) { + // 原始应用:性别背景图片 + return isFemale + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png"; + } + + @override + String getIdBackgroundImage(bool hasSpecialId) { + // 原始应用:ID背景图片 + return hasSpecialId + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png"; + } + + @override + String getCopyIdIcon() { + // 原始应用:复制ID图标 + return "sc_images/room/sc_icon_user_card_copy_id.png"; + } + + /// === Gift页面差异化方法实现 === + + @override + String getGiftPageFirstRechargeRoomTagIcon() { + // 原始应用:首充房间标签图标 + return "sc_images/room/sc_icon_first_recharge_room_tag.png"; + } + + @override + String getGiftPageGoldCoinIcon() { + // 原始应用:金币图标 + return "sc_images/general/sc_icon_jb.png"; + } + + @override + String getGiftPageGiveTypeBackground() { + // 原始应用:赠送类型背景图像 + return "sc_images/room/sc_icon_give_type_bg.png"; + } + + @override + String getGiftPageActivityGiftHeadBackground(String giftType) { + // 原始应用:活动礼物头部背景图像,根据礼物类型返回不同路径 + switch (giftType) { + case 'ACTIVITY': + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; + case 'LUCK': + return "sc_images/room/sc_icon_luck_gift_head_bg.png"; + case 'CP': + return "sc_images/room/sc_icon_cp_gift_head_bg.png"; + case 'MAGIC': + return "sc_images/room/sc_icon_magic_gift_head_bg.png"; + default: + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; + } + } + + @override + String getGiftPageCustomizedRuleIcon() { + // 原始应用:自定义规则图标 + return "sc_images/room/sc_icon_customized_rule.png"; + } + + @override + String getGiftPageGiftEffectIcon(String giftType) { + // 原始应用:特效礼物图标 + return "sc_images/room/sc_icon_gift_effect.png"; + } + + @override + String getGiftPageGiftMusicIcon(String giftType) { + // 原始应用:音乐礼物图标 + return "sc_images/room/sc_icon_gift_music.png"; + } + + @override + String getGiftPageGiftLuckIcon(String giftType) { + // 原始应用:幸运礼物图标 + return "sc_images/room/sc_icon_gift_luck.png"; + } + + @override + String getGiftPageGiftCpIcon(String giftType) { + // 原始应用:CP礼物图标 + return "sc_images/room/sc_icon_gift_cp.png"; + } + + @override + String getGiftPageFirstRechargeTextIcon(String languageCode) { + // 原始应用:首充文本图标(根据语言显示不同图像) + if (languageCode == 'ar') { + return "sc_images/index/sc_icon_first_recharge_ar_text.png"; + } else { + // 默认为英语版本 + return "sc_images/index/sc_icon_first_recharge_en_text.png"; + } + } + + @override + String getGiftPageAllOnMicrophoneIcon() { + // 原始应用:"全开麦"图标 + return "sc_images/room/sc_icon_all_on_microphone.png"; + } + + @override + String getGiftPageUsersOnMicrophoneIcon() { + // 原始应用:"用户开麦"图标 + return "sc_images/room/sc_icon_userson_microphone.png"; + } + + @override + String getGiftPageAllInTheRoomIcon() { + // 原始应用:"房间所有人"图标 + return "sc_images/room/sc_icon_all_in_the_room.png"; + } + + @override + String getCommonSelectIcon() { + // 原始应用:通用选择图标 + return "sc_images/general/sc_icon_checked.png"; + } + + @override + String getCommonUnselectIcon() { + // 原始应用:通用未选择图标 + return "sc_images/general/sc_icon_unchecked.png"; + } + + /// === Media页面差异化方法实现 === + + @override + Color getImagePreviewBackgroundColor() { + // 原始应用:图片预览页面背景颜色 - 黑色 + return Colors.black; + } + + @override + Color getImagePreviewGalleryBackgroundColor() { + // 原始应用:图片预览画廊背景颜色 - 黑色 + return Colors.black; + } + + @override + Color getImagePreviewAppBarBackgroundColor() { + // 原始应用:图片预览AppBar背景颜色 - 黑色54%透明度 + return Colors.black54; + } + + @override + Color getImagePreviewBackIconColor() { + // 原始应用:图片预览返回图标颜色 - 白色 + return Colors.white; + } + + @override + Color getImagePreviewTextColor() { + // 原始应用:图片预览文本颜色 - 白色 + return Colors.white; + } + + @override + Color getImagePreviewLoadingIndicatorColor() { + // 原始应用:图片预览加载指示器颜色 - 默认主题色 + // CircularProgressIndicator默认使用主题色 + return const Color(0xFFFF9500); // 应用主色调 + } + + @override + double getImagePreviewAppBarOpacity() { + // 原始应用:图片预览AppBar透明度 - 0.54 (Colors.black54) + return 0.54; + } + + @override + Color getVideoPlayerBackgroundColor() { + // 原始应用:视频播放页面背景颜色 - 黑色 + return Colors.black; + } + + @override + Color getVideoPlayerControlsBackgroundColor() { + // 原始应用:视频播放控制界面背景颜色 - 黑色38%透明度 + return Colors.black38; + } + + @override + Color getVideoPlayerIconColor() { + // 原始应用:视频播放图标颜色 - 白色 + return Colors.white; + } + + @override + Color getVideoPlayerBottomControlsBackgroundColor() { + // 原始应用:视频播放底部控制栏背景颜色 - 黑色54%透明度 + return Colors.black54; + } + + @override + Color getVideoPlayerProgressBarActiveColor() { + // 原始应用:视频播放进度条活动颜色 - 红色 + return Colors.red; + } + + @override + Color getVideoPlayerProgressBarInactiveColor() { + // 原始应用:视频播放进度条非活动颜色 - 灰色 + return Colors.grey; + } + + @override + Color getVideoPlayerTextColor() { + // 原始应用:视频播放文本颜色 - 白色 + return Colors.white; + } + + @override + Color getVideoPlayerLoadingIndicatorColor() { + // 原始应用:视频播放加载指示器颜色 - 红色 + return Colors.red; + } + + @override + double getVideoPlayerControlsOpacity() { + // 原始应用:视频播放控制界面透明度 - 0.38 (Colors.black38) + return 0.38; + } + + @override + double getVideoPlayerBottomControlsOpacity() { + // 原始应用:视频播放底部控制栏透明度 - 0.54 (Colors.black54) + return 0.54; + } + + @override + int getVideoPlayerControlsDisplayDuration() { + // 原始应用:视频播放控制界面显示持续时间 - 3秒 + return 3; + } + + /// === 举报页面差异化方法实现 === + + @override + String getReportPageBackgroundImage() { + // 原始应用:举报页面背景图片 - 与房间设置背景相同 + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + Color getReportPageHintTextColor() { + // 原始应用:举报页面提示文本颜色 - Colors.black38 + return Colors.black38; + } + + @override + Color getReportPageContainerBackgroundColor() { + // 原始应用:举报页面容器背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getReportPageBorderColor() { + // 原始应用:举报页面边框颜色 - Color(0xffE6E6E6) + return const Color(0xffE6E6E6); + } + + @override + Color getReportPagePrimaryTextColor() { + // 原始应用:举报页面主要文本颜色 - Colors.black + return Colors.black; + } + + @override + Color getReportPageSecondaryHintTextColor() { + // 原始应用:举报页面次级提示文本颜色 - Colors.black45 + return Colors.black45; + } + + @override + Color getReportPageUnselectedBorderColor() { + // 原始应用:举报页面未选中边框颜色 - Colors.grey + return Colors.grey; + } + + @override + Color getReportPageSubmitButtonBackgroundColor() { + // 原始应用:举报页面提交按钮背景颜色 - 使用主题主色 + // 注意:在实际使用时,页面应通过SocialChatTheme.primaryColor获取 + // 这里返回Colors.blue作为占位符,实际页面应该使用主题颜色 + // 或者可以考虑返回null,让页面使用默认主题颜色 + return Colors.blue; // 临时值,需要从主题获取 + } + + @override + Color getReportPageSubmitButtonTextColor() { + // 原始应用:举报页面提交按钮文本颜色 - Colors.white + return Colors.white; + } + + @override + String getReportPageIcon(String iconType) { + // 原始应用:举报页面图标路径 + switch (iconType) { + case 'addPic': + return "sc_images/general/sc_icon_add_pic.png"; + case 'closePic': + return "sc_images/general/sc_icon_pic_close.png"; + case 'checked': + return "sc_images/general/sc_icon_checked.png"; + default: + return ""; // 未知图标类型返回空字符串 + } + } + + /// === 语音房间页面差异化方法实现 === + + @override + Color getVoiceRoomBackgroundColor() { + // 原始应用:语音房间页面背景颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getVoiceRoomTabLabelColor() { + // 原始应用:语音房间页面Tab标签选中颜色 - Colors.white + return Colors.white; + } + + @override + Color getVoiceRoomTabUnselectedLabelColor() { + // 原始应用:语音房间页面Tab标签未选中颜色 - Colors.white54 + return Colors.white54; + } + + @override + Color getVoiceRoomTabIndicatorColor() { + // 原始应用:语音房间页面Tab指示器颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getVoiceRoomTabDividerColor() { + // 原始应用:语音房间页面Tab分割线颜色 - Colors.transparent + return Colors.transparent; + } + + @override + EdgeInsets getVoiceRoomTabLabelPadding() { + // 原始应用:语音房间页面Tab标签内边距 - EdgeInsets.symmetric(horizontal: 12) + // 注意:页面需要将数值乘以ScreenUtil().w + return const EdgeInsets.symmetric(horizontal: 12); + } + + @override + EdgeInsetsDirectional getVoiceRoomChatContainerMargin() { + // 原始应用:语音房间页面聊天容器外边距 - EdgeInsets.only(right: 52) + // 注意:页面需要将数值乘以ScreenUtil().w + return const EdgeInsetsDirectional.only(end: 52); + } + + @override + TextStyle getVoiceRoomTabLabelStyle() { + // 原始应用:语音房间页面Tab标签选中文本样式 + // 字体大小15,字体族"MyCustomFont" + // 注意:页面需要将字体大小乘以ScreenUtil().sp + return const TextStyle( + fontSize: 15, + fontFamily: "MyCustomFont", + ); + } + + @override + TextStyle getVoiceRoomTabUnselectedLabelStyle() { + // 原始应用:语音房间页面Tab标签未选中文本样式 + // 字体大小13,字体族"MyCustomFont" + // 注意:页面需要将字体大小乘以ScreenUtil().sp + return const TextStyle( + fontSize: 13, + fontFamily: "MyCustomFont", + ); + } + + @override + String getVoiceRoomDefaultBackgroundImage() { + // 原始应用:语音房间默认背景图像路径 + return "sc_images/room/sc_icon_room_defaut_bg.png"; + } + + /// === 钱包页面差异化方法实现 === + + /// === 充值页面差异化方法实现 === + + @override + String getRechargePageBackgroundImage() { + // 原始应用:充值页面背景图像路径 - "sc_images/room/sc_icon_room_settig_bg.png" + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + Color getRechargePageScaffoldBackgroundColor() { + // 原始应用:充值页面Scaffold背景颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getRechargePageDialogBarrierColor() { + // 原始应用:充值页面对话框屏障颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getRechargePageDialogContainerBackgroundColor() { + // 原始应用:充值页面对话框容器背景颜色 - Colors.white30 + return Colors.white30; + } + + @override + Color getRechargePageDialogTextColor() { + // 原始应用:充值页面对话框文本颜色 - Colors.black38 + return Colors.black38; + } + + @override + Color getRechargePageMainContainerBackgroundColor() { + // 原始应用:充值页面主容器背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getRechargePageButtonBackgroundColor() { + // 原始应用:充值页面按钮背景颜色 - 使用主题颜色 + // 注意:页面应使用SocialChatTheme.primaryLight + return const Color(0xFFE6F7FF); // 近似主题浅色 + } + + @override + Color getRechargePageButtonTextColor() { + // 原始应用:充值页面按钮文本颜色 - Colors.white + return Colors.white; + } + + @override + String getRechargePageWalletBackgroundImage() { + // 原始应用:充值页面钱包背景图像路径 - 'sc_images/index/sc_icon_wallet_bg.png' + return "sc_images/index/sc_icon_wallet_bg.png"; + } + + @override + String getRechargePageWalletIcon() { + // 原始应用:充值页面钱包图标路径 - 'sc_images/index/sc_icon_wallet_icon.png' + return "sc_images/index/sc_icon_wallet_icon.png"; + } + + @override + Color getRechargePageWalletTextColor() { + // 原始应用:充值页面钱包文本颜色 - Colors.white + return Colors.white; + } + + @override + String getRechargePageGoldIcon() { + // 原始应用:充值页面金币图标路径(默认图标)- 'sc_images/general/sc_icon_jb.png' + return "sc_images/general/sc_icon_jb.png"; + } + + @override + Color getRechargePageSelectedItemBackgroundColor() { + // 原始应用:充值页面选中项背景颜色 - 使用主题浅色 + // 注意:页面应使用SocialChatTheme.primaryLight + return const Color(0xFFE6F7FF); // 近似主题浅色 + } + + @override + Color getRechargePageSelectedItemBorderColor() { + // 原始应用:充值页面选中项边框颜色 - 使用主题主色 + // 注意:页面应使用SocialChatTheme.primaryColor + return const Color(0xFF1890FF); // 近似主题主色 + } + + @override + Color getRechargePageUnselectedItemBorderColor() { + // 原始应用:充值页面未选中项边框颜色 - Color(0xffE6E6E6) + return const Color(0xffE6E6E6); + } + + @override + Color getRechargePageUnselectedItemBackgroundColor() { + // 原始应用:充值页面未选中项背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getRechargePageItemTextColor() { + // 原始应用:充值页面项目文本颜色 - Colors.black54 + return Colors.black54; + } + + @override + Color getRechargePageItemPriceTextColor() { + // 原始应用:充值页面价格文本颜色 - Colors.black + return Colors.black; + } + + @override + String getRechargePageGoldIconByIndex(int index) { + // 原始应用:根据索引获取充值页面金币图标路径 + switch (index) { + case 0: + return "sc_images/general/sc_icon_jb.png"; + case 1: + return "sc_images/general/sc_icon_jb2.png"; + case 2: + return "sc_images/general/sc_icon_jb3.png"; + default: + return "sc_images/general/sc_icon_jb.png"; // 默认图标 + } + } + + @override + String getRechargePageRecordIcon() { + // 原始应用:充值页面记录图标路径 - "sc_images/index/sc_icon_recharge_recod.png" + return "sc_images/index/sc_icon_recharge_recod.png"; + } + + @override + Color getRechargePageAppleItemBackgroundColor() { + // 原始应用:充值页面Apple产品项背景颜色 - Colors.white + return Colors.white; + } + + /// === 金币记录页面差异化方法实现 === + + @override + String getGoldRecordPageBackgroundImage() { + // 原始应用:金币记录页面背景图像路径 - "sc_images/room/sc_icon_room_settig_bg.png" + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + Color getGoldRecordPageScaffoldBackgroundColor() { + // 原始应用:金币记录页面Scaffold背景颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getGoldRecordSCPageListBackgroundColor() { + // 原始应用:金币记录页面列表背景颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getGoldRecordPageContainerBackgroundColor() { + // 原始应用:金币记录页面容器背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getGoldRecordPageBorderColor() { + // 原始应用:金币记录页面边框颜色 - Color(0xffE6E6E6) + return const Color(0xffE6E6E6); + } + + @override + Color getGoldRecordPagePrimaryTextColor() { + // 原始应用:金币记录页面主要文本颜色 - Colors.black + return Colors.black; + } + + @override + Color getGoldRecordPageSecondaryTextColor() { + // 原始应用:金币记录页面次要文本颜色 - Colors.black38 + return Colors.black38; + } + + /// === Store页面差异化方法实现 === + + @override + String getStorePageBackgroundImage() { + // 原始应用:Store页面背景图像路径 - "sc_images/room/sc_icon_room_settig_bg.png" + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + String getStorePageShoppingBagIcon() { + // 原始应用:Store页面购物袋图标路径 - "sc_images/store/sc_icon_shop_bag.png" + return "sc_images/store/sc_icon_shop_bag.png"; + } + + @override + EdgeInsetsDirectional getStorePageShoppingBagMargin() { + // 原始应用:Store页面购物袋图标外边距 - EdgeInsetsDirectional.only(end: 10.w) + // 注意:这里无法使用.w单位,需要在调用处使用ScreenUtil() + return const EdgeInsetsDirectional.only(end: 10); + } + + @override + EdgeInsets getStorePageTabLabelPadding() { + // 原始应用:Store页面Tab标签内边距 - EdgeInsets.symmetric(horizontal: 12.w) + // 注意:这里无法使用.w单位,需要在调用处使用ScreenUtil() + return const EdgeInsets.symmetric(horizontal: 12); + } + + @override + TextStyle getStorePageTabLabelStyle() { + // 原始应用:Store页面Tab标签选中文本样式 + // 原始值:TextStyle(fontSize: 15.sp, color: Colors.black87, fontWeight: FontWeight.w600) + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return const TextStyle( + fontSize: 15, + color: Color(0xDD000000), // Colors.black87 对应 #DD000000 + fontWeight: FontWeight.w600, + ); + } + + @override + TextStyle getStorePageTabUnselectedLabelStyle() { + // 原始应用:Store页面Tab标签未选中文本样式 + // 原始值:TextStyle(fontSize: 13.sp, color: Colors.black38, fontWeight: FontWeight.w500) + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return const TextStyle( + fontSize: 13, + color: Color(0x61000000), // Colors.black38 对应 #61000000 + fontWeight: FontWeight.w500, + ); + } + + @override + Color getStorePageTabIndicatorColor() { + // 原始应用:Store页面Tab指示器颜色 - SocialChatTheme.primaryColor + // 需要查看SocialChatTheme.primaryColor的值 + // 从socialchat_theme.dart中查看到primaryColor = Color(0xffFF5722); + return const Color(0xffFF5722); // SocialChatTheme.primaryColor + } + + @override + Color getStorePageTabDividerColor() { + // 原始应用:Store页面Tab分割线颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getStorePageBottomDividerColor() { + // 原始应用:Store页面底部Divider颜色 - Colors.black12 + return const Color(0x1F000000); // Colors.black12 对应 #1F000000 + } + + @override + double getStorePageBottomDividerThickness() { + // 原始应用:Store页面底部Divider厚度 - 0.5 + // 注意:原始代码中使用0.5,没有.w单位 + return 0.5; + } + + @override + double getStorePageBottomDividerHeight() { + // 原始应用:Store页面底部Divider高度 - 24.w + // 注意:这里无法使用.w单位,需要在调用处使用ScreenUtil() + return 24; + } + + @override + String getStorePageGoldIcon() { + // 原始应用:Store页面金币图标路径 - "sc_images/general/sc_icon_jb.png" + return "sc_images/general/sc_icon_jb.png"; + } + + @override + Color getStorePageGoldTextColor() { + // 原始应用:Store页面金币文本颜色 - Color(0xFFFFE134) + return const Color(0xFFFFE134); + } + + @override + Color getStorePageGoldIconColor() { + // 原始应用:Store页面金币图标颜色 - Color(0xFFFFE134) + return const Color(0xFFFFE134); + } + + /// === Store子页面(商品项)差异化方法实现 === + + @override + Color getStoreItemBackgroundColor() { + // 原始应用:Store商品项背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getStoreItemUnselectedBorderColor() { + // 原始应用:Store商品项未选中边框颜色 - Colors.black12 + return const Color(0x1F000000); // Colors.black12 对应 #1F000000 + } + + @override + Color getStoreItemSelectedBorderColor() { + // 原始应用:Store商品项选中边框颜色 - SocialChatTheme.primaryLight (Color(0xffFF9500)) + return const Color(0xffFF9500); + } + + @override + String getStoreItemGoldIcon() { + // 原始应用:Store商品项金币图标路径 - "sc_images/general/sc_icon_jb.png" + return "sc_images/general/sc_icon_jb.png"; + } + + @override + Color getStoreItemPriceTextColor() { + // 原始应用:Store商品项价格文本颜色 - Colors.black + return Colors.black; + } + + @override + Color getStoreItemBuyButtonColor() { + // 原始应用:Store商品项购买按钮背景颜色 - SocialChatTheme.primaryLight (Color(0xffFF9500)) + return const Color(0xffFF9500); + } + + @override + Color getStoreItemBuyButtonTextColor() { + // 原始应用:Store商品项购买按钮文本颜色 - Colors.white + return Colors.white; + } + + @override + String getStoreItemActionIcon(String itemType) { + // 原始应用:Store商品项操作图标路径 + // 根据商品类型返回不同的图标路径 + switch (itemType) { + case 'headdress': + return "sc_images/store/sc_icon_shop_item_search.png"; // 头饰页面使用搜索图标 + case 'mountains': + return "sc_images/store/sc_icon_shop_item_play.png"; // 山脉页面使用播放图标 + case 'theme': + return "sc_images/store/sc_icon_store_theme_rev.png"; // 主题页面使用主题图标 + case 'chatbox': + return "sc_images/store/sc_icon_shop_item_search.png"; // 聊天气泡页面使用搜索图标 + default: + return "sc_images/store/sc_icon_store_theme_rev.png"; // 默认返回theme图标 + } + } + + /// === 动态页面差异化方法实现 === + + @override + String getDynamicPageBackgroundImage() { + // 原始应用:动态页面背景图像路径 - "sc_images/index/sc_icon_index_mask.png" + return "sc_images/index/sc_icon_index_mask.png"; + } + + @override + Color getDynamicPageScaffoldBackgroundColor() { + // 原始应用:动态页面Scaffold背景颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getDynamicPageAppBarBackgroundColor() { + // 原始应用:动态页面AppBar背景颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getDynamicPageTabLabelColor() { + // 原始应用:动态页面Tab标签选中颜色 - Colors.black87 + return Colors.black87; + } + + @override + Color getDynamicPageTabUnselectedLabelColor() { + // 原始应用:动态页面Tab标签未选中颜色 - Colors.black38 + return Colors.black38; + } + + @override + Color getDynamicPageTabIndicatorColor() { + // 原始应用:动态页面Tab指示器颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getDynamicPageTabDividerColor() { + // 原始应用:动态页面Tab分割线颜色 - Colors.transparent + return Colors.transparent; + } + + @override + String getDynamicPageAddButtonIcon() { + // 原始应用:动态页面添加动态按钮图标路径 - "sc_images/dynamic/sc_icon_add_dynamic_btn.png" + return "sc_images/dynamic/sc_icon_add_dynamic_btn.png"; + } + + @override + bool shouldDynamicPageTabScrollable() { + // 原始应用:动态页面Tab标签是否可滚动 - true + return true; + } + + @override + TextStyle getDynamicPageTabLabelStyle() { + // 原始应用:动态页面Tab标签选中文本样式 - TextStyle(fontWeight: FontWeight.bold, fontSize: 16.sp) + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ); + } + + @override + TextStyle getDynamicPageTabUnselectedLabelStyle() { + // 原始应用:动态页面Tab标签未选中文本样式 - TextStyle(fontWeight: FontWeight.normal, fontSize: 15.sp) + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 15, + ); + } + + /// === 启动页面差异化方法实现 === + + @override + String getSplashPageBackgroundImage() { + // 原始应用:启动页面背景图像路径 - "sc_images/splash/splash.png" + return "sc_images/splash/sc_splash.png"; + } + + @override + String getSplashPageIcon() { + // 原始应用:启动页面图标路径 - "sc_images/splash/sc_icon_splash_icon.png" + return "sc_images/splash/sc_icon_splash_icon.png"; + } + + @override + String getSplashPageSkipButtonBackground() { + // 原始应用:启动页面跳过按钮背景图像路径 - "sc_images/splash/sc_icon_splash_skip_bg.png" + return "sc_images/splash/sc_icon_splash_skip_bg.png"; + } + + @override + Color getSplashPageSkipButtonTextColor() { + // 原始应用:启动页面跳过按钮文本颜色 - Colors.white + return Colors.white; + } + + @override + String getSplashPageKingGamesNameBackground() { + // 原始应用:启动页面游戏名称背景图像路径 - "sc_images/index/sc_icon_splash_king_games_name_bg.png" + return "sc_images/index/sc_icon_splash_king_games_name_bg.png"; + } + + @override + Color getSplashPageKingGamesTextColor() { + // 原始应用:启动页面游戏名称文本颜色 - Colors.white + return Colors.white; + } + + @override + String getSplashPageCpNameBackground() { + // 原始应用:启动页面CP名称背景图像路径 - "sc_images/index/sc_icon_splash_cp_name_bg.png" + return "sc_images/index/sc_icon_splash_cp_name_bg.png"; + } + + @override + Color getSplashPageCpTextColor() { + // 原始应用:启动页面CP名称文本颜色 - Colors.white + return Colors.white; + } + + /// === WebView页面差异化方法实现 === + + @override + Color getWebViewPageAppBarBackgroundColor() { + // 原始应用:WebView页面AppBar背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getWebViewPageTitleTextColor() { + // 原始应用:WebView页面标题文本颜色 - Color(0xff333333) + return const Color(0xff333333); + } + + @override + Color getWebViewPageBackArrowColor() { + // 原始应用:WebView页面返回箭头图标颜色 - 使用默认图标颜色 + return const Color(0xff333333); + } + + @override + Color getWebViewPageProgressBarBackgroundColor() { + // 原始应用:WebView页面进度条背景颜色 - Colors.white70.withOpacity(0) + return Colors.white70.withOpacity(0); + } + + @override + Color getWebViewPageProgressBarActiveColor() { + // 原始应用:WebView页面进度条活动颜色 - Color(0xffFF6000) + return const Color(0xffFF6000); + } + + @override + Color getGameWebViewCloseIconColor() { + // 原始应用:游戏WebView页面关闭图标颜色 - Colors.white + return Colors.white; + } + + @override + Color getGameWebViewScaffoldBackgroundColor() { + // 原始应用:游戏WebView页面Scaffold背景颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getGameWebViewProgressBarBackgroundColor() { + // 原始应用:游戏WebView页面进度条背景颜色 - Colors.white70.withOpacity(0) + return Colors.white70.withOpacity(0); + } + + @override + Color getGameWebViewLoadingIndicatorColor() { + // 原始应用:游戏WebView页面加载指示器颜色 - Color(0xffFF6000) + return const Color(0xffFF6000); + } + + /// === 搜索页面差异化方法实现 === + + @override + Color getSearchPageScaffoldBackgroundColor() { + // 原始应用:搜索页面Scaffold背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getSearchPageBackIconColor() { + // 原始应用:搜索页面返回图标颜色 - Color(0xffBBBBBB) + return const Color(0xffBBBBBB); + } + + @override + Color getSearchPageInputBorderColor() { + // 原始应用:搜索页面搜索框边框颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getSearchPageInputTextColor() { + // 原始应用:搜索页面搜索框文本颜色 - Colors.grey + return Colors.grey; + } + + @override + Color getSearchPageButtonTextColor() { + // 原始应用:搜索页面搜索按钮文本颜色 - Colors.black + return Colors.black; + } + + @override + List getSearchPageButtonGradient() { + // 原始应用:搜索页面搜索按钮渐变颜色 - [Colors.transparent, Colors.transparent] + return [Colors.transparent, Colors.transparent]; + } + + @override + Color getSearchPageHistoryTitleTextColor() { + // 原始应用:搜索页面历史记录标题文本颜色 - Colors.black + return Colors.black; + } + + @override + Color getSearchPageHistoryItemTextColor() { + // 原始应用:搜索页面历史项文本颜色 - Colors.white + return Colors.white; + } + + @override + Color getSearchPageHistoryItemBackgroundColor() { + // 原始应用:搜索页面历史项背景颜色 - SocialChatTheme.primaryLight (Color(0xffFF9500)) + return const Color(0xffFF9500); + } + + @override + Color getSearchPageTabIndicatorGradientStartColor() { + // 原始应用:搜索页面Tab指示器渐变开始颜色 - Color(0xffFFA500) + return const Color(0xffFFA500); + } + + @override + Color getSearchPageTabIndicatorGradientEndColor() { + // 原始应用:搜索页面Tab指示器渐变结束颜色 - Color(0xffFFD700) + return const Color(0xffFFD700); + } + + @override + Color getSearchPageTabSelectedLabelColor() { + // 原始应用:搜索页面Tab标签选中颜色 - Colors.black + return Colors.black; + } + + @override + Color getSearchPageTabDividerColor() { + // 原始应用:搜索页面Tab分割线颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getSearchPageTabUnselectedLabelColor() { + // 原始应用:搜索页面Tab标签未选中颜色 - Colors.black26 + return Colors.black26; + } + + @override + Color getSearchPageDividerContainerBackgroundColor() { + // 原始应用:搜索页面分隔容器背景颜色 - Color(0xffFFF8DA) + return const Color(0xffFFF8DA); + } + + @override + TextStyle getSearchPageTabSelectedLabelStyle() { + // 原始应用:搜索页面Tab标签选中文本样式 - TextStyle(fontSize: 18.sp, fontWeight: FontWeight.w600) + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, + ); + } + + @override + TextStyle getSearchPageTabUnselectedLabelStyle() { + // 原始应用:搜索页面Tab标签未选中文本样式 - TextStyle(fontSize: 17.sp, fontWeight: FontWeight.w400) + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w400, + color: Colors.black26, + ); + } + + @override + EdgeInsets getSearchPageResultTabLabelPadding() { + // 原始应用:搜索页面搜索结果Tab标签内边距 - EdgeInsets.symmetric(horizontal: 15) + return const EdgeInsets.symmetric(horizontal: 15); + } + + @override + Color getSearchPageResultTabLabelColor() { + // 原始应用:搜索页面搜索结果Tab标签选中颜色 - Colors.black + return Colors.black; + } + + @override + Color getSearchPageResultTabUnselectedLabelColor() { + // 原始应用:搜索页面搜索结果Tab标签未选中颜色 - Colors.black26 + return Colors.black26; + } + + @override + TextStyle getSearchPageResultTabUnselectedLabelStyle() { + // 原始应用:搜索页面搜索结果Tab标签未选中文本样式 - TextStyle(fontSize: 17, fontWeight: FontWeight.w400, color: Colors.black26) + return const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w400, + color: Colors.black26, + ); + } + + @override + TextStyle getSearchPageResultTabLabelStyle() { + // 原始应用:搜索页面搜索结果Tab标签选中文本样式 - TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.black87) + return const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, + ); + } + + @override + LinearGradient getSearchPageResultTabIndicatorColor() { + // 原始应用:搜索页面搜索结果Tab指示器颜色 - LinearGradient(colors: [Colors.transparent, Colors.transparent]) + return const LinearGradient( + colors: [Colors.transparent, Colors.transparent], + ); + } + + @override + double getSearchPageResultTabIndicatorWidth() { + // 原始应用:搜索页面搜索结果Tab指示器宽度 - 15.0 + return 15.0; + } + + @override + Color getSearchPageResultBackgroundColor() { + // 原始应用:搜索页面搜索结果背景颜色 - Color(0xffFFF8DA) + return const Color(0xffFFF8DA); + } + + @override + String getSearchPageClearHistoryIcon() { + // 原始应用:搜索页面清除历史记录图标路径 + return "sc_images/general/sc_icon_delete.png"; + } + + @override + String getSearchPageEmptyDataIcon() { + // 原始应用:搜索页面空数据图标路径 + return "sc_images/room/sc_icon_room_defaut_bg.png"; + } + + // === 任务页面差异化方法 === + + @override + String getTaskPageBackgroundImage() { + // 原始应用:任务页面背景图像 - "sc_images/room/sc_icon_room_settig_bg.png" + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + Color getTaskPageBackButtonColor() { + // 原始应用:任务页面返回按钮颜色 - Colors.black + return Colors.black; + } + + @override + Color getTaskPageScaffoldBackgroundColor() { + // 原始应用:任务页面Scaffold背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getTaskPageContainerBackgroundColor() { + // 原始应用:任务页面容器背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getTaskPageBorderColor() { + // 原始应用:任务页面边框颜色 - Color(0xffE6E6E6) + return const Color(0xffE6E6E6); + } + + @override + Color getTaskPageSpecialBorderColor() { + // 原始应用:任务页面特殊边框颜色 - Color(0xffBB92FF) + return const Color(0xffBB92FF); + } + + @override + Color getTaskPagePrimaryTextColor() { + // 原始应用:任务页面主要文本颜色 - Colors.white + return Colors.white; + } + + @override + Color getTaskPageGoldTextColor() { + // 原始应用:任务页面金币文本颜色 - Color(0xffFFB627) + return const Color(0xffFFB627); + } + + @override + Color getTaskPageExpTextColor() { + // 原始应用:任务页面经验文本颜色 - Color(0xff52FF90) + return const Color(0xff52FF90); + } + + @override + Color getTaskPageThemeColor() { + // 原始应用:任务页面主题颜色 - SocialChatTheme.primaryColor + return const Color(0xffFF5722); // SocialChatTheme.primaryColor + } + + @override + Color getTaskPageThemeLightColor() { + // 原始应用:任务页面浅主题颜色 - SocialChatTheme.primaryLight + return const Color(0xffFF9500); // SocialChatTheme.primaryLight + } + + @override + List getTaskPageReceivableButtonGradient() { + // 原始应用:任务页面可领取按钮渐变颜色 - [Color(0xffBB92FF), Color(0xff8B45FF)] + return [const Color(0xffBB92FF), const Color(0xff8B45FF)]; + } + + @override + List getTaskPageCompletedButtonGradient() { + // 原始应用:任务页面已完成按钮渐变颜色 - [Color(0xffDADADA), Color(0xff585859)] + return [const Color(0xffDADADA), const Color(0xff585859)]; + } + + @override + Color getTaskPageButtonTextColor() { + // 原始应用:任务页面按钮文本颜色 - Colors.white + return Colors.white; + } + + @override + Color getTaskPageMaskColor() { + // 原始应用:任务页面遮罩颜色 - Colors.black + return Colors.black; + } + + @override + String getTaskPageHeadBackgroundImage() { + // 原始应用:任务页面头部背景图像 - "sc_images/index/sc_icon_task_head_bg.png" + return "sc_images/index/sc_icon_task_head_bg.png"; + } + + @override + String getTaskPageGoldIcon() { + // 原始应用:任务页面金币图标 - "sc_images/general/sc_icon_jb.png" + return "sc_images/general/sc_icon_jb.png"; + } + + @override + String getTaskPageExpIcon() { + // 原始应用:任务页面经验图标 - "sc_images/index/sc_icon_task_exp.png" + return "sc_images/index/sc_icon_task_exp.png"; + } + + @override + String getTaskPageInvitationRewardBackgroundImage() { + // 原始应用:任务页面邀请奖励背景图像 - "sc_images/index/sc_icon_invitation_bg.png" + return "sc_images/index/sc_icon_invitation_bg.png"; + } + + @override + Color getTaskPageGiftBagTextColor() { + // 原始应用:任务页面礼包文本颜色 - Colors.white + return Colors.white; + } + + @override + Color getTaskPageAppBarBackgroundColor() { + // 原始应用:任务页面AppBar背景颜色 - Colors.transparent + return Colors.transparent; + } + + @override + Color getTaskPageTransparentContainerColor() { + // 原始应用:任务页面透明容器颜色 - Colors.transparent + return Colors.transparent; + } + + /// === 设置页面差异化方法实现 === + + @override + String getSettingsPageBackgroundImage() { + // 原始应用:设置页面背景图像路径 - "sc_images/person/sc_icon_edit_userinfo_bg.png" + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getSettingsPageMainContainerBackgroundColor() { + // 原始应用:设置页面主容器背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getSettingsPageMainContainerBorderColor() { + // 原始应用:设置页面主容器边框颜色 - Color(0xffE6E6E6) + return const Color(0xffE6E6E6); + } + + @override + Color getSettingsPagePrimaryTextColor() { + // 原始应用:设置页面主文本颜色 - Colors.black + return Colors.black; + } + + @override + Color getSettingsPageSecondaryTextColor() { + // 原始应用:设置页面次要文本颜色 - Colors.black38 + return Colors.black38; + } + + @override + Color getSettingsPageIconColor() { + // 原始应用:设置页面图标颜色 - Colors.black26 + return Colors.black26; + } + + @override + Color getSettingsPageCommonTextColor() { + // 原始应用:设置页面常见文本颜色 - Colors.black38 + return Colors.black38; + } + + @override + Color getSettingsPageAboutTextColor() { + // 原始应用:设置页面关于文本颜色 - Colors.black38 + return Colors.black38; + } + + @override + Color getSettingsPageContainerBorderColor() { + // 原始应用:设置页面容器边框颜色 - Color(0xffE6E6E6) + return const Color(0xffE6E6E6); + } + + @override + Color getSettingsPageContainerBackgroundColor() { + // 原始应用:设置页面容器背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getSettingsPageButtonBackgroundColor() { + // 原始应用:设置页面按钮背景颜色 - Color(0xffBBBBBB) + return const Color(0xffBBBBBB); + } + + @override + Color getSettingsPageButtonTextColor() { + // 原始应用:设置页面按钮文本颜色 - Colors.white + return Colors.white; + } + + /// === 语言页面差异化方法实现 === + + @override + String getLanguagePageBackgroundImage() { + // 原始应用:语言页面背景图像路径 - "sc_images/person/sc_icon_edit_userinfo_bg.png" + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getLanguagePageCheckboxActiveColor() { + // 原始应用:语言页面复选框选中颜色 - SocialChatTheme.primaryColor + return const Color(0xffFF5722); // SocialChatTheme.primaryColor + } + + @override + Color getLanguagePageCheckboxBorderColor() { + // 原始应用:语言页面复选框边框颜色 - Color(0xffD4D4D4) + return const Color(0xffD4D4D4); + } + + @override + Color getLanguagePagePrimaryTextColor() { + // 原始应用:语言页面主文本颜色 - Colors.black + return Colors.black; + } + + @override + Color getLanguagePageCheckColor() { + // 原始应用:语言页面复选框对勾颜色 - Colors.transparent + return Colors.transparent; + } + + @override + double getLanguagePageCheckboxBorderRadius() { + // 原始应用:语言页面复选框边框圆角 - 15 + return 15; + } + + @override + double getLanguagePageCheckboxBorderWidth() { + // 原始应用:语言页面复选框边框宽度 - 2 + return 2; + } + + @override + Color getLanguagePageScaffoldBackgroundColor() { + // 原始应用:语言页面Scaffold背景颜色 - Colors.transparent + return Colors.transparent; + } + + /// === 账户页面差异化方法实现 === + + @override + String getAccountPageBackgroundImage() { + // 原始应用:账户页面背景图像路径 - "sc_images/person/sc_icon_edit_userinfo_bg.png" + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getAccountPageMainContainerBackgroundColor() { + // 原始应用:账户页面主容器背景颜色 - Colors.white + return Colors.white; + } + + @override + Color getAccountPageMainContainerBorderColor() { + // 原始应用:账户页面主容器边框颜色 - Color(0xffE6E6E6) + return const Color(0xffE6E6E6); + } + + @override + Color getAccountPagePrimaryTextColor() { + // 原始应用:账户页面主文本颜色 - Colors.black + return Colors.black; + } + + @override + Color getAccountPageSecondaryTextColor() { + // 原始应用:账户页面次要文本颜色 - Colors.black38 + return Colors.black38; + } + + @override + Color getAccountPageIconColor() { + // 原始应用:账户页面图标颜色 - Colors.black26 + return Colors.black26; + } + + @override + Color getAccountPageDividerColor() { + // 原始应用:账户页面分隔线颜色 - Colors.black12 + return Colors.black12; + } + + @override + Color getAccountPageScaffoldBackgroundColor() { + // 原始应用:账户页面Scaffold背景颜色 - Colors.transparent + return Colors.transparent; + } + @override + Color getGoldRecordPageListBackgroundColor() { + // 马甲包策略:金币记录页面列表背景颜色 - 深灰色 + return const Color(0xff1a1a1a); // #1a1a1a + } +} \ No newline at end of file diff --git a/lib/app/config/strategies/variant1_business_logic_strategy.dart b/lib/app/config/strategies/variant1_business_logic_strategy.dart new file mode 100644 index 0000000..c26eab7 --- /dev/null +++ b/lib/app/config/strategies/variant1_business_logic_strategy.dart @@ -0,0 +1,2565 @@ +import 'package:flutter/material.dart'; +import 'package:yumi/app/config/strategies/base_business_logic_strategy.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/tools/sc_dialog_utils.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +import '../../../modules/home/popular/event/home_event_page.dart'; +import '../../../modules/home/popular/mine/sc_home_mine_page.dart'; +import '../../../modules/home/popular/party/sc_home_party_page.dart'; + +/// 马甲包业务逻辑策略实现 +/// 提供与原始应用不同的业务逻辑 +class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { + + /// 马甲包版本:调整Tab顺序,家族放在最后 + @override + List getHomeTabPages(BuildContext context) { + final List pages = []; + pages.add(SCHomePartyPage()); + pages.add(SCHomeMinePage()); + // pages.add(HomeEventPage()); + return pages; + } + + @override + List getHomeTabLabels(BuildContext context) { + final List tabs = []; + tabs.add(Tab(text: SCAppLocalizations.of(context)!.party)); + tabs.add(Tab(text: SCAppLocalizations.of(context)!.me)); + // 马甲包:家族标签放在最后 + // tabs.add(Tab(text: SCAppLocalizations.of(context)!.event)); + return tabs; + } + + @override + int getHomeInitialTabIndex() { + // 马甲包:从探索页开始 + return 0; // explore页 + } + + /// 马甲包版本:不同的首充提示位置 + @override + Map getFirstRechargePosition() { + // 马甲包:调整到左上角,提供完整的四个键值对 + return {'top': 100.0, 'start': 15.0, 'bottom': null, 'end': null}; + } + + /// 马甲包版本:增强的首充提示点击处理 + @override + void onFirstRechargeTap(BuildContext context) { + // 马甲包:显示更详细的首充对话框 + SCDialogUtils.showFirstRechargeDialog(context); + // 额外逻辑:记录点击事件或触发其他行为 + debugPrint('马甲包首充提示被点击'); + } + + /// === 登录页面差异化方法实现(马甲包专属样式)=== + + @override + String getLoginBackgroundImage() { + // 马甲包专属背景图片 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/splash/sc_splash.png"; + } + + @override + String getLoginAppIcon() { + // 马甲包专属应用图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/splash/sc_icon_splash_icon.png"; + } + + @override + Color getLoginButtonColor() { + // 马甲包按钮颜色 - 使用主题色 + return const Color(0xffFF5722); // SocialChatTheme.primaryColor + } + + @override + List? getLoginButtonGradient() { + // 马甲包按钮渐变:从Figma设计中提取的橙色渐变 + return [ + const Color(0xffB4FCCE), // rgb(254, 178, 25) + const Color(0xff5AF492), // rgb(254, 178, 25) + const Color(0xff077142), // rgb(255, 147, 38) + ]; + } + + @override + Color getLoginButtonTextColor() { + // 马甲包按钮文本颜色(白色,与渐变背景形成对比) + return Colors.white; + } + + @override + Color getLoginLabelTextColor() { + // 马甲包标签文本颜色(黑色) + return Colors.white; + } + + @override + BoxDecoration getLoginInputDecoration() { + // 马甲包输入框装饰:浅灰色背景,圆形边框 + return BoxDecoration( + color: Colors.white24, // #f2f2f2 + borderRadius: BorderRadius.circular(12), // 56px圆角 + border: Border.all(color: Colors.transparent, width: 0), + ); + } + + @override + EdgeInsets getLoginPagePadding() { + // 马甲包页面内边距:根据Figma设计调整 + return const EdgeInsets.only(top: 120); // 顶部319px间距 + } + + @override + bool shouldShowLoginStatusBar() { + // 马甲包:显示黑色状态栏 + return true; + } + + @override + Color getLoginHintTextColor() { + // 马甲包:提示文本颜色(黑色45%透明度) + return Colors.white; + } + + @override + Color getLoginInputTextColor() { + // 马甲包:输入文本颜色(黑色) + return Colors.white; + } + + /// === 家族页面差异化方法实现(马甲包专属) === + + + + + + + @override + bool shouldShowPasswordRoomIcon() { + // 马甲包策略:不显示密码房间图标,简化UI + return false; + } + + /// === 探索页面差异化方法实现(马甲包专属) === + + @override + int getExploreRoomDisplayThreshold() { + // 马甲包策略:显示更多房间在抽屉上方,减少抽屉使用 + return 9; + } + + @override + String getExploreGridIcon() { + // 马甲包策略:使用马甲包专属网格视图图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/index/sc_icon_index_room_model_1.png"; + } + + @override + String getExploreListIcon() { + // 马甲包策略:使用马甲包专属列表视图图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/index/sc_icon_index_room_model_2.png"; + } + + @override + String getExploreRankIconPattern(bool isGrid, int rank) { + // 马甲包策略:使用统一的排名图标,不区分网格/列表视图 + // TODO: 根据Figma设计添加马甲包专属排名图标 + if (isGrid) { + return "sc_images/index/sc_icon_explore_room_model_1_rank_$rank.png"; + } else { + return "sc_images/index/sc_icon_explore_room_model_2_rank_$rank.png"; + } + } + + @override + Color getExploreRoomBorderColor() { + // 马甲包策略:使用马甲包主题色作为边框颜色 + return SocialChatTheme.primaryLight; // 马甲包主题色 + } + + @override + double getExploreRoomBorderWidth() { + // 马甲包策略:更细的边框,更简洁的UI + return 1.0; + } + + @override + int getExploreDrawerGridCrossAxisCount() { + // 马甲包策略:4列网格,更紧凑的布局 + return 4; + } + + @override + int getExploreRoomNameMarqueeThreshold() { + // 马甲包策略:更长的房间名称才显示滚动效果,减少不必要的动画 + return 15; + } + + @override + int getExploreRoomDescMarqueeThreshold() { + // 马甲包策略:更长的房间描述才显示滚动效果 + return 18; + } + + /// === 个人主页差异化方法实现(马甲包专属) === + + @override + String getMePageDefaultBackgroundImage() { + // 马甲包策略:使用马甲包专属默认背景图片 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/person/sc_icon_my_head_bg_defalt.png"; + } + + @override + String getMePageGenderBackgroundImage(bool isFemale) { + // 马甲包策略:使用统一的背景图片,不区分性别 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/person/sc_icon_my_head_bg_defalt.png"; + } + + @override + String getMePageCpDialogHeadImage() { + // 马甲包策略:使用马甲包专属CP对话框头像图片 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/person/sc_icon_send_cp_requst_dialog_head.png"; + } + + @override + String getMePageDefaultAvatarImage() { + // 马甲包策略:使用马甲包专属默认头像图片 + // TODO: 根据Figma设计添加马甲包专属头像图标 + return "sc_images/general/sc_icon_avar_defalt.png"; + } + + @override + String getMePageIdBackgroundImage(bool hasSpecialId) { + // 马甲包策略:使用统一的ID背景图片,简化UI + // TODO: 根据Figma设计添加马甲包专属ID背景图片 + return "sc_images/general/sc_icon_id_bg.png"; + } + + @override + String getMePageCopyIdIcon() { + // 马甲包策略:使用马甲包专属复制ID图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_user_card_copy_id.png"; + } + + @override + String getMePageGenderAgeBackgroundImage(bool isFemale) { + // 马甲包策略:使用统一的性别年龄背景图片,不区分性别 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/login/sc_icon_sex_woman_bg.png"; + } + + @override + String getMePageVisitorsFollowFansBackgroundImage(bool isFemale) { + // 马甲包策略:使用统一的访客关注粉丝背景图片,不区分性别 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/person/sc_icon_vistors_follow_fans_bg_woman.png"; + } + + @override + String getMePageEditUserInfoIcon() { + // 马甲包策略:使用马甲包专属编辑用户信息图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/person/sc_icon_edit_user_info2.png"; + } + + @override + Color getMePageShineColor() { + // 马甲包策略:使用马甲包主题色作为闪耀文本颜色 + return const Color(0xffFF5722); // 马甲包主题色 + } + + @override + List getMePageGradientColors(bool isFemale) { + // 马甲包策略:使用统一的渐变颜色,不区分性别 + return [ + const Color(0xffFF5722), // 马甲包主题色 + const Color(0xffFEB219), // 马甲包辅助色 + const Color(0xffFF9326), // 马甲包辅助色 + ]; + } + + @override + List getMePageTabIndicatorGradient(bool isFemale) { + // 马甲包策略:使用统一的Tab指示器渐变,不区分性别 + return [ + const Color(0xffFF5722), // 马甲包主题色 + const Color(0xffFEB219), // 马甲包辅助色 + ]; + } + + @override + Color getMePageVisitorsFollowFansTextColor() { + // 马甲包策略:使用更亮的文本颜色,提高可读性 + return const Color(0xffFFFFFF); // 白色 + } + + @override + int getMePageInitialTabIndex() { + // 马甲包策略:从动态页开始,鼓励用户发布内容 + return 0; // dynamic页 + } + + @override + double getMePageScrollOpacityThreshold() { + // 马甲包策略:降低滚动阈值,更快显示AppBar效果 + return 200.0; + } + + @override + int getMePageNicknameScrollThreshold() { + // 马甲包策略:提高昵称滚动阈值,减少不必要的动画 + return 18; + } + + @override + int getMePageAgeDisplayWidthThreshold() { + // 马甲包策略:降低年龄显示宽度阈值,更早调整布局 + return 99; + } + + @override + double getMePageOpacityThresholdForAvatarDisplay() { + // 马甲包策略:降低头像显示透明度阈值,更早显示头像 + return 0.3; + } + + /// === 热门页面差异化方法实现(马甲包专属) === + + @override + String getPopularLeaderboardBackgroundImage(String leaderboardType) { + // 马甲包策略:使用马甲包专属的排行榜背景图片 + // 原始应用:使用现有的图像资源路径 + switch (leaderboardType) { + case 'room': + return "sc_images/index/sc_icon_leader_spinner_room_bg.png"; + case 'wealth': + return "sc_images/index/sc_icon_leader_spinner_wealth_bg.png"; + case 'charm': + return "sc_images/index/sc_icon_leader_spinner_charm_bg.png"; + default: + return "sc_images/index/sc_icon_leader_spinner_room_bg.png"; + } + } + + /// === 游戏页面差异化方法实现(马甲包专属) === + + @override + String getGameRankBackgroundImage(int rank) { + // 马甲包策略:使用统一的背景图片,简化UI + return "sc_images/game/sc_icon_game_list_item_bg_variant1.png"; + } + + @override + String getGameRankIcon(int rank) { + // 马甲包策略:使用统一的排名图标 + return "sc_images/game/sc_icon_game_rank_variant1.png"; + } + + @override + Color getGamePageThemeColor() { + // 马甲包策略:使用马甲包主题色 + return const Color(0xffFF9326); + } + + @override + Color getGamePageBackgroundColor() { + // 马甲包策略:更亮的背景颜色 + return const Color(0xfffafafa); + } + + @override + int getGamePageGridCrossAxisCount() { + // 马甲包策略:4列网格,更紧凑的布局 + return 4; + } + + @override + double getGamePageGridChildAspectRatio() { + // 马甲包策略:更高的高度比例 + return 1.5; + } + + @override + Color getGameRankTextColor(int rank) { + // 马甲包策略:使用统一的文本颜色,简化UI + return const Color(0xffFF5722); + } + + @override + Color getGamePageGradientSecondColor() { + // 马甲包策略:使用马甲包风格的渐变第二颜色 + return const Color(0xffFFE0B2); + } + + @override + String getGameHotTagBackgroundImage() { + // 马甲包策略:使用马甲包风格的热门游戏标签背景图片 + return "sc_images/index/sc_icon_hotgames_tag_bg_variant1.png"; + } + + @override + String getGameNewTagImage() { + // 马甲包策略:使用马甲包风格的新游戏标签图片 + return "sc_images/index/sc_icon_game_new_tag_variant1.png"; + } + + @override + String getGamePageMaskImage() { + // 马甲包策略:使用马甲包风格的游戏页面遮罩图片 + return "sc_images/index/sc_icon_index_mask_variant1.png"; + } + + /// === VIP页面差异化方法实现(马甲包专属) === + + @override + String getVipPageBackgroundImage(int vipLevel) { + // 原始应用:使用现有的VIP背景图像模式 + return "sc_images/vip/sc_icon_vip_level_bg_$vipLevel.png"; + } + + @override + String getVipPageIcon(int vipLevel) { + // 原始应用:使用现有的VIP图标模式 + return "sc_images/vip/sc_icon_vip_level_$vipLevel.png"; + } + + @override + List getVipPageButtonGradient() { + // 原始应用:VIP按钮渐变颜色 + return [const Color(0xffFF5722), const Color(0xffFEB219)]; + } + + @override + int getVipPageGridCrossAxisCount() { + // 原始应用:3列网格 + return 3; + } + + @override + double getVipPageGridChildAspectRatio() { + // 原始应用:默认高度比例 + return 1.5; + } + + @override + String getVipPageHeadBackgroundImage(int vipLevel) { + // 原始应用:VIP头部背景图像模式 + return "sc_images/vip/sc_icon_vip_head_bg_v$vipLevel.png"; + } + + @override + String getVipPageTabSelectedIcon(int vipLevel) { + // 原始应用:Tab选中状态图标模式 + return "sc_images/vip/sc_icon_tab_vip_text_on_v$vipLevel.png"; + } + + @override + String getVipPageTabUnselectedIcon(int vipLevel) { + // 原始应用:Tab未选中状态图标模式 + return "sc_images/vip/sc_icon_tab_vip_text_no_v$vipLevel.png"; + } + + @override + String getVipPageLargeIcon(int vipLevel) { + // 原始应用:VIP页面大图标路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_ic.webp"; + } + + @override + String getVipPageTitleImage(int vipLevel) { + // 原始应用:VIP页面标题图像路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_t1.png"; + } + + @override + String getVipPageTagImage(int vipLevel, int tagIndex) { + // 原始应用:VIP页面标签图像路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_tag$tagIndex.png"; + } + + @override + String getVipPageItemBackgroundImage(int vipLevel) { + // 原始应用:VIP页面功能项背景图像路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_item_bg.png"; + } + + @override + String getVipPagePrivilegeIcon(int vipLevel, int privilegeIndex) { + // 原始应用:VIP页面特权图标路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_privilege$privilegeIndex.png"; + } + + @override + String getVipPageFeatureIcon(int vipLevel, String featureName) { + // 原始应用:VIP页面功能图标路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_$featureName.png"; + } + + @override + String getVipPagePreviewImage( + int vipLevel, + String previewType, + String featureName, + ) { + // 原始应用:VIP页面预览图像路径模式 + if (featureName == 'profile_frame') { + return "sc_images/vip/sc_icon_vip${vipLevel}_profile_rev.png"; + } else if (featureName == 'profile_card') { + return "sc_images/vip/sc_icon_vip${vipLevel}_profile_card_rev.png"; + } else if (featureName == 'room_cover_headdress') { + return "sc_images/vip/sc_icon_vip${vipLevel}_room_cover_border_rev.png"; + } else if (featureName == 'mic_rippl_theme') { + return "sc_images/vip/sc_icon_vip${vipLevel}_sesc_rev.png"; + } else if (featureName == 'chatbox') { + return "sc_images/vip/sc_icon_vip${vipLevel}_chatbox_pre.png"; + } else { + return "sc_images/vip/sc_icon_vip${vipLevel}_${featureName}_$previewType.png"; + } + } + + @override + String getVipPagePurchaseBackgroundImage(int vipLevel) { + // 原始应用:VIP页面购买背景图像路径模式 + return "sc_images/vip/sc_icon_vip${vipLevel}_buy_bg.png"; + } + + @override + String getVipPagePurchaseButtonImage() { + // 原始应用:VIP页面购买按钮图像路径 + return "sc_images/vip/sc_icon_vip_buy_btn.png"; + } + + @override + Color getVipPageTextColor(int vipLevel) { + // 原始应用:根据VIP等级返回不同的文本颜色 + switch (vipLevel) { + case 1: + return const Color(0xFF71D671); + case 2: + return const Color(0xFF7179D6); + case 3: + return const Color(0xFFAA71D6); + case 4: + return const Color(0xFF71D6B3); + case 5: + return const Color(0xFFD67171); + case 6: + return const Color(0xFFFEE5BB); + default: + return Colors.white; + } + } + + /// === 通用页面差异化方法实现 === + + @override + String getCommonRankBackgroundImage(String pageType, int rank) { + // 原始应用:通用的排名背景图像模式 + return "sc_images/$pageType/sc_icon_${pageType}_rank_bg_$rank.png"; + } + + @override + String getCommonRankIcon(String pageType, int rank) { + // 原始应用:通用的排名图标模式 + return "sc_images/$pageType/sc_icon_${pageType}_rank_$rank.png"; + } + + @override + int getCommonPageGridCrossAxisCount(String pageType) { + // 原始应用:通用的2列网格 + return 2; + } + + @override + double getCommonPageGridChildAspectRatio(String pageType) { + // 原始应用:通用的高度比例 + return 1.4; + } + + /// === Admin编辑页面差异化方法实现(马甲包专属) === + + @override + String getAdminEditingBackgroundImage() { + // 马甲包策略:使用马甲包专属背景图片 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + List getAdminEditingButtonGradient(String buttonType) { + // 马甲包策略:使用马甲包主题渐变 + return [ + const Color(0xffFF9326), // 马甲包主题色 + const Color(0xffFEB219), // 马甲包辅助色 + ]; + } + + @override + String getAdminEditingIcon(String iconType) { + // 马甲包策略:使用统一的图标,简化UI + // TODO: 根据Figma设计添加马甲包专属图标 + switch (iconType) { + case 'specialIdBg': + return "sc_images/general/sc_icon_special_id_bg.png"; + case 'normalIdBg': + return "sc_images/general/sc_icon_id_bg.png"; + case 'copyId': + return "sc_images/room/sc_icon_user_card_copy_id.png"; + case 'addPic': + return "sc_images/general/sc_icon_add_pic.png"; + case 'closePic': + return "sc_images/general/sc_icon_pic_close.png"; + case 'checked': + return "sc_images/general/sc_icon_checked.png"; + default: + return ""; + } + } + + @override + BoxDecoration getAdminEditingInputDecoration() { + // 马甲包策略:使用马甲包风格的输入框装饰 + return BoxDecoration( + color: const Color(0xfff2f2f2), // #f2f2f2 + borderRadius: BorderRadius.circular(56), // 56px圆角,与登录页保持一致 + border: Border.all(color: Colors.transparent, width: 0), + ); + } + + @override + int getAdminEditingMaxImageUploadCount() { + // 马甲包策略:增加图片上传数量,支持更多图片 + return 5; // 允许上传最多5张图片 + } + + @override + int getAdminEditingDescriptionMaxLength() { + // 马甲包策略:增加描述最大长度 + return 300; // 允许更长的描述 + } + + @override + Map getAdminEditingViolationTypeMapping(String targetType) { + // 马甲包策略:使用相同的违规类型映射,或可自定义 + // 暂时使用原始应用的映射,后续可根据需求调整 + if (targetType == 'User') { + return {'Pornography': 1, 'Illegal information': 2}; + } else if (targetType == 'Room') { + return { + 'Pornography': 3, + 'Illegal information': 4, + 'Fraud': 5, + 'Other': 6, + }; + } + return {}; + } + + /// === Admin搜索页面差异化方法实现(马甲包专属) === + + @override + String getAdminSearchBackgroundImage(String pageType) { + // 马甲包策略:使用马甲包专属背景图片 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/index/sc_icon_coupon_head_bg.png"; + } + + @override + List getAdminSearchButtonGradient(String pageType) { + // 马甲包策略:使用马甲包主题渐变 + return [ + const Color(0xffFF9326), // 马甲包主题色 + const Color(0xffFEB219), // 马甲包辅助色 + ]; + } + + @override + Color getAdminSearchButtonTextColor(String pageType) { + // 马甲包策略:使用白色文本,提高可读性 + return Colors.white; + } + + @override + String getAdminSearchEditIcon(String pageType) { + // 马甲包策略:使用马甲包专属编辑图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_room_edit.png"; + } + + @override + Color getAdminSearchInputBorderColor(String pageType) { + // 马甲包策略:使用马甲包主题色作为边框 + return const Color(0xffFF5722); // 马甲包主题色 + } + + @override + Color getAdminSearchInputTextColor(String pageType) { + // 马甲包策略:使用更深的文本颜色 + return Colors.black87; + } + + @override + String getAdminSearchUserInfoIcon(String iconType) { + // 马甲包策略:使用统一的图标 + // TODO: 根据Figma设计添加马甲包专属图标 + switch (iconType) { + case 'specialIdBg': + return "sc_images/general/sc_icon_special_id_bg.png"; + case 'normalIdBg': + return "sc_images/general/sc_icon_id_bg.png"; + case 'copyId': + return "sc_images/room/sc_icon_user_card_copy_id.png"; + default: + return ""; + } + } + + /// === 主登录页面差异化方法实现(马甲包专属) === + + @override + String getLoginMainBackgroundImage() { + // 马甲包策略:使用马甲包专属背景图片 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/splash/sc_splash.png"; + } + + @override + String getLoginMainAppIcon() { + // 马甲包策略:使用马甲包专属应用图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/splash/sc_icon_splash_icon.png"; + } + + @override + String getLoginMainGoogleIcon() { + // 马甲包策略:使用马甲包专属Google图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/login/sc_icon_google.png"; + } + + @override + String getLoginMainAppleIcon() { + // 马甲包策略:使用马甲包专属Apple图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/login/sc_icon_iphone.png"; + } + + @override + String getLoginMainAccountIcon() { + // 马甲包策略:使用马甲包专属账号图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/login/sc_icon_account.png"; + } + + @override + String getLoginMainAgreementIcon(bool isSelected) { + // 马甲包策略:使用马甲包专属协议图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return isSelected + ? "sc_images/login/sc_icon_login_ser_select.png" + : "sc_images/login/sc_icon_login_ser_select_un.png"; + } + + @override + Color getLoginMainDividerColor() { + // 马甲包策略:使用马甲包主题色作为分割线颜色 + return const Color(0xffE6E6E6); // 马甲包主题色 + } + + @override + Color getLoginMainButtonBorderColor() { + // 马甲包策略:使用马甲包主题色作为按钮边框颜色 + return Colors.white24; // 马甲包主题色 + } + + @override + Color getLoginMainButtonBackgroundColor() { + // 马甲包策略:使用浅色背景,提高可读性 + return Colors.white24; // 浅灰色 + } + + @override + Color getLoginMainButtonTextColor() { + // 马甲包策略:按钮文本颜色(黑色) + return Colors.white; + } + + @override + Color getLoginMainLinkColor() { + // 马甲包策略:使用马甲包主题色作为链接颜色 + return const Color(0xff18F2B1); // 马甲包主题色 + } + + /// === 编辑个人资料页面差异化方法实现(马甲包专属) === + + @override + String getEditProfileBackgroundImage() { + // 马甲包策略:使用马甲包专属背景图片 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/splash/sc_splash.png"; + } + + @override + String getEditProfileDefaultAvatarImage() { + // 马甲包策略:使用马甲包专属默认头像图片 + // TODO: 根据Figma设计添加马甲包专属头像图标 + return "sc_images/general/sc_icon_avar_defalt.png"; + } + + @override + String getEditProfileGenderIcon(bool isMale) { + // 马甲包策略:使用马甲包专属性别图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return isMale + ? "sc_images/login/sc_icon_avar_sex_man.png" + : "sc_images/login/sc_icon_avar_sex_woman.png"; + } + + @override + List getEditProfileGenderButtonGradient(bool isMale, bool isSelected) { + // 马甲包策略:使用马甲包主题渐变 + return [ + const Color(0xff18F2B1), // 马甲包主题色 + const Color(0xff18F2B1), // 马甲包辅助色 + ]; + } + + @override + Color getEditProfileInputBackgroundColor() { + // 马甲包策略:使用更亮的背景颜色,提高可读性 + return Colors.white24; + } + + @override + Color getEditProfileDatePickerConfirmColor() { + // 马甲包策略:使用马甲包主题色 + return const Color(0xff18F2B1); // 马甲包主题色 + } + + @override + Color getEditProfileContinueButtonColor() { + // 马甲包策略:使用马甲包主题色 + return const Color(0xff18F2B1); // 马甲包主题色 + } + + /// === Country页面差异化方法实现(马甲包专属) === + + @override + String getCountrySelectOkIcon() { + // 马甲包策略:使用马甲包专属选择确认图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/general/sc_icon_select_ok.png"; + } + + @override + String getCountrySelectUnOkIcon() { + // 马甲包策略:使用马甲包专属选择未确认图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/general/sc_icon_select_un_ok.png"; + } + + @override + String getCountryRadioSelectedIcon() { + // 马甲包策略:使用马甲包专属单选选中图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/login/sc_icon_login_ser_select.png"; + } + + @override + String getCountryRadioUnselectedIcon() { + // 马甲包策略:使用马甲包专属单选未选中图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/login/sc_icon_login_ser_select_un.png"; + } + + @override + Color getCountryItemBorderColor() { + // 马甲包策略:使用马甲包主题色作为边框颜色 + return SocialChatTheme.primaryLight; // 马甲包主题色 + } + + @override + Color getCountryPageScaffoldBackgroundColor() { + // 马甲包策略:使用浅灰色背景 + return const Color(0xfff5f5f5); + } + + @override + Color getCountryPageIconColor() { + // 马甲包策略:使用深灰色图标 + return const Color(0xff333333); + } + + @override + Color getCountryPagePrimaryTextColor() { + // 马甲包策略:使用深灰色文本 + return const Color(0xff212121); + } + + @override + Color getCountryPageSecondaryTextColor() { + // 马甲包策略:使用中等灰色文本 + return const Color(0xff757575); + } + + @override + Color getCountryPageContainerBackgroundColor() { + // 马甲包策略:使用白色容器背景 + return Colors.white; + } + + /// === Chat页面差异化方法实现(马甲包专属) === + + /// === 消息页面 (SCMessagePage) 方法实现 === + + @override + String getMessagePageIndexMaskIcon() { + // 马甲包策略:使用马甲包专属索引遮罩图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/index/sc_icon_index_bg.png"; + } + + @override + String getMessagePageAnnouncementTagIcon() { + // 马甲包策略:使用马甲包专属公告标签图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_message_activity.png"; + } + + @override + String getMessagePageActivityMessageIcon() { + // 马甲包策略:使用马甲包专属活动消息图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_message_activity.png"; + } + + @override + String getMessagePageActivityTitleBackground(bool isRtl) { + // 马甲包策略:使用马甲包专属活动消息标题背景图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return isRtl + ? "sc_images/msg/sc_icon_message_activity.png" + : "sc_images/msg/sc_icon_message_activity.png"; + } + + @override + String getMessagePageSystemMessageIcon() { + // 马甲包策略:使用马甲包专属系统消息图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_message_system.png"; + } + + @override + String getMessagePageSystemTitleBackground(bool isRtl) { + // 马甲包策略:使用马甲包专属系统消息标题背景图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return isRtl + ? "sc_images/msg/sc_icon_system_title_bg_rtl.png" + : "sc_images/msg/sc_icon_system_title_bg.png"; + } + + @override + String getMessagePageNotificationMessageIcon() { + // 马甲包策略:使用马甲包专属通知消息图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_message_noti.png"; + } + + @override + String getMessagePageNotificationTitleBackground(bool isRtl) { + // 马甲包策略:使用马甲包专属通知消息标题背景图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return isRtl + ? "sc_images/msg/sc_icon_notifcation_title_bg_rtl.png" + : "sc_images/msg/sc_icon_notifcation_title_bg.png"; + } + + /// === 聊天页面 (SCMessageChatPage) 方法实现 === + + @override + String getSCMessageChatPageRoomSettingBackground() { + // 马甲包策略:使用马甲包专属房间设置背景图像 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + String getSCMessageChatPageEmojiIcon() { + // 马甲包策略:使用马甲包专属表情图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_msg_emoji.png"; + } + + @override + String getSCMessageChatPageChatKeyboardIcon() { + // 马甲包策略:使用马甲包专属聊天键盘图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_chat_key.png"; + } + + @override + String getSCMessageChatPageAddIcon() { + // 马甲包策略:使用马甲包专属添加图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_add.png"; + } + + @override + String getSCMessageChatPageSendMessageIcon() { + // 马甲包策略:使用马甲包专属发送消息图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_chat_message_send.png"; + } + + @override + String getSCMessageChatPageCameraIcon() { + // 马甲包策略:使用马甲包专属相机图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_xiangji.png"; + } + + @override + String getSCMessageChatPagePictureIcon() { + // 马甲包策略:使用马甲包专属图片图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_tupian.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeIcon() { + // 马甲包策略:使用马甲包专属红包图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_hongbao.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeMessageBackground() { + // 马甲包策略:使用马甲包专属红包消息背景图像 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/msg/sc_icon_msg_send_hongbao_bg.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeConfigTabButtonIcon() { + // 马甲包策略:使用马甲包专属红包配置标签按钮图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_red_envelope_config_tab_btn.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeMessageItemBackground() { + // 马甲包策略:使用马甲包专属红包消息项背景图像 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/msg/sc_icon_red_envelopes_msg_item_bg.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeReceivePackageIcon() { + // 马甲包策略:使用马甲包专属红包接收包图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_red_envelope_rec_pack.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeOpenBackground() { + // 马甲包策略:使用马甲包专属红包打开背景图像 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/msg/sc_icon_red_envelopes_msg_open_bg.png"; + } + + @override + String getSCMessageChatPageRedEnvelopeOpenedBackground() { + // 马甲包策略:使用马甲包专属红包已打开背景图像 + // TODO: 根据Figma设计添加马甲包专属背景图片 + return "sc_images/msg/sc_icon_red_envelopes_msg_opened_bg.png"; + } + + @override + String getSCMessageChatPageGoldCoinIcon() { + // 马甲包策略:使用马甲包专属金币图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/general/sc_icon_jb.png"; + } + + @override + String getSCMessageChatPageMessageMenuDeleteIcon() { + // 马甲包策略:使用马甲包专属消息菜单删除图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_msg_menu_delete.png"; + } + + @override + String getSCMessageChatPageMessageMenuCopyIcon() { + // 马甲包策略:使用马甲包专属消息菜单复制图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_msg_menu_copy.png"; + } + + @override + String getSCMessageChatPageMessageMenuRecallIcon() { + // 马甲包策略:使用马甲包专属消息菜单撤回图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/msg/sc_icon_msg_menu_recall.png"; + } + + @override + String getSCMessageChatPageLoadingIcon() { + // 马甲包策略:使用马甲包专属加载图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/general/sc_icon_loading.png"; + } + + @override + String getGenderBackgroundImage(bool isFemale) { + // 马甲包策略:使用马甲包专属性别背景图片 + // TODO: 根据Figma设计添加马甲包专属性别背景图片 + return isFemale + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png"; + } + + @override + String getIdBackgroundImage(bool hasSpecialId) { + // 马甲包策略:使用马甲包专属ID背景图片 + // TODO: 根据Figma设计添加马甲包专属ID背景图片 + return hasSpecialId + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png"; + } + + @override + String getCopyIdIcon() { + // 马甲包策略:使用马甲包专属复制ID图标 + // TODO: 根据Figma设计添加马甲包专属复制ID图标 + return "sc_images/room/sc_icon_user_card_copy_id.png"; + } + + @override + String getSCMessageChatPageSystemHeadImage() { + // 马甲包策略:使用马甲包专属系统头像图像 + // TODO: 根据Figma设计添加马甲包专属头像图片 + return "sc_images/msg/head_system.png"; + } + + /// === Gift页面差异化方法实现(马甲包专属) === + + @override + String getGiftPageFirstRechargeRoomTagIcon() { + // 马甲包策略:使用马甲包专属首充房间标签图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_first_recharge_room_tag.png"; + } + + @override + String getGiftPageGoldCoinIcon() { + // 马甲包策略:使用马甲包专属金币图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/general/sc_icon_jb.png"; + } + + @override + String getGiftPageGiveTypeBackground() { + // 马甲包策略:使用马甲包专属赠送类型背景图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_give_type_bg.png"; + } + + @override + String getGiftPageActivityGiftHeadBackground(String giftType) { + // 马甲包策略:使用马甲包专属活动礼物头部背景图标 + // TODO: 根据Figma设计添加马甲包专属图标 + switch (giftType) { + case 'ACTIVITY': + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; + case 'LUCK': + return "sc_images/room/sc_icon_luck_gift_head_bg.png"; + case 'CP': + return "sc_images/room/sc_icon_cp_gift_head_bg.png"; + case 'MAGIC': + return "sc_images/room/sc_icon_magic_gift_head_bg.png"; + default: + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; + } + } + + @override + String getGiftPageCustomizedRuleIcon() { + // 马甲包策略:使用马甲包专属定制规则图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_customized_rule.png"; + } + + @override + String getGiftPageGiftEffectIcon(String giftType) { + // 马甲包策略:使用马甲包专属礼物效果图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_gift_effect.png"; + } + + @override + String getGiftPageGiftMusicIcon(String giftType) { + // 马甲包策略:使用马甲包专属礼物音乐图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_gift_music.png"; + } + + @override + String getGiftPageGiftLuckIcon(String giftType) { + // 马甲包策略:使用马甲包专属礼物幸运图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_gift_luck.png"; + } + + @override + String getGiftPageGiftCpIcon(String giftType) { + // 马甲包策略:使用马甲包专属礼物CP图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_gift_cp.png"; + } + + @override + String getGiftPageFirstRechargeTextIcon(String languageCode) { + // 马甲包策略:使用马甲包专属首充文本图标 + // TODO: 根据Figma设计添加马甲包专属图标 + if (languageCode == 'ar') { + return "sc_images/index/sc_icon_first_recharge_ar_text.png"; + } else { + // 默认为英语版本 + return "sc_images/index/sc_icon_first_recharge_en_text.png"; + } + } + + @override + String getGiftPageAllOnMicrophoneIcon() { + // 马甲包策略:使用马甲包专属"全开麦"图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_all_on_microphone.png"; + } + + @override + String getGiftPageUsersOnMicrophoneIcon() { + // 马甲包策略:使用马甲包专属"用户开麦"图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_userson_microphone.png"; + } + + @override + String getGiftPageAllInTheRoomIcon() { + // 马甲包策略:使用马甲包专属"房间所有人"图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/room/sc_icon_all_in_the_room.png"; + } + + @override + String getCommonSelectIcon() { + // 马甲包策略:使用马甲包专属选择图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/general/sc_icon_checked.png"; + } + + @override + String getCommonUnselectIcon() { + // 马甲包策略:使用马甲包专属未选择图标 + // TODO: 根据Figma设计添加马甲包专属图标 + return "sc_images/general/sc_icon_unchecked.png"; + } + + /// === Media页面差异化方法实现(马甲包专属) === + + @override + Color getImagePreviewBackgroundColor() { + // 马甲包策略:图片预览页面背景颜色 - 深灰色 + return const Color(0xff1a1a1a); // #1a1a1a + } + + @override + Color getImagePreviewGalleryBackgroundColor() { + // 马甲包策略:图片预览画廊背景颜色 - 深灰色 + return const Color(0xff1a1a1a); // #1a1a1a + } + + @override + Color getImagePreviewAppBarBackgroundColor() { + // 马甲包策略:图片预览AppBar背景颜色 - 深灰色70%透明度 + return const Color(0xB31a1a1a); // withOpacity(0.7) + } + + @override + Color getImagePreviewBackIconColor() { + // 马甲包策略:图片预览返回图标颜色 - 浅灰色 + return const Color(0xffe6e6e6); // #e6e6e6 + } + + @override + Color getImagePreviewTextColor() { + // 马甲包策略:图片预览文本颜色 - 浅灰色 + return const Color(0xffe6e6e6); // #e6e6e6 + } + + @override + Color getImagePreviewLoadingIndicatorColor() { + // 马甲包策略:图片预览加载指示器颜色 - 马甲包主题色(橙色) + return const Color(0xffFF5722); // 马甲包主题色 + } + + @override + double getImagePreviewAppBarOpacity() { + // 马甲包策略:图片预览AppBar透明度 - 0.7 + return 0.7; + } + + @override + Color getVideoPlayerBackgroundColor() { + // 马甲包策略:视频播放页面背景颜色 - 深灰色 + return const Color(0xff1a1a1a); // #1a1a1a + } + + @override + Color getVideoPlayerControlsBackgroundColor() { + // 马甲包策略:视频播放控制界面背景颜色 - 深灰色50%透明度 + return const Color(0x801a1a1a); // withOpacity(0.5) + } + + @override + Color getVideoPlayerIconColor() { + // 马甲包策略:视频播放图标颜色 - 浅灰色 + return const Color(0xffe6e6e6); // #e6e6e6 + } + + @override + Color getVideoPlayerBottomControlsBackgroundColor() { + // 马甲包策略:视频播放底部控制栏背景颜色 - 深灰色60%透明度 + return const Color(0x991a1a1a); // withOpacity(0.6) + } + + @override + Color getVideoPlayerProgressBarActiveColor() { + // 马甲包策略:视频播放进度条活动颜色 - 马甲包主题色(橙色) + return const Color(0xffFF5722); // 马甲包主题色 + } + + @override + Color getVideoPlayerProgressBarInactiveColor() { + // 马甲包策略:视频播放进度条非活动颜色 - 深灰色 + return const Color(0xff666666); // 深灰色 + } + + @override + Color getVideoPlayerTextColor() { + // 马甲包策略:视频播放文本颜色 - 浅灰色 + return const Color(0xffe6e6e6); // #e6e6e6 + } + + @override + Color getVideoPlayerLoadingIndicatorColor() { + // 马甲包策略:视频播放加载指示器颜色 - 马甲包主题色(橙色) + return const Color(0xffFF5722); // 马甲包主题色 + } + + @override + double getVideoPlayerControlsOpacity() { + // 马甲包策略:视频播放控制界面透明度 - 0.5 + return 0.5; + } + + @override + double getVideoPlayerBottomControlsOpacity() { + // 马甲包策略:视频播放底部控制栏透明度 - 0.6 + return 0.6; + } + + @override + int getVideoPlayerControlsDisplayDuration() { + // 马甲包策略:视频播放控制界面显示持续时间 - 5秒 + return 5; + } + + /// === 举报页面差异化方法实现 === + + @override + String getReportPageBackgroundImage() { + // 马甲包策略:举报页面背景图片 - 与房间设置背景相同(与原始应用相同) + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getReportPageHintTextColor() { + // 马甲包策略:举报页面提示文本颜色 - 深灰色 + return Colors.white54; // 深灰色 + } + + @override + Color getReportPageContainerBackgroundColor() { + // 马甲包策略:举报页面容器背景颜色 - 白色(与原始应用相同) + return Color(0xff09372E).withOpacity(0.5); + } + + @override + Color getReportPageBorderColor() { + // 马甲包策略:举报页面边框颜色 - 浅灰色 + return const Color(0xffCCCCCC); // 浅灰色 + } + + @override + Color getReportPagePrimaryTextColor() { + // 马甲包策略:举报页面主要文本颜色 - 黑色(与原始应用相同) + return Colors.white; // 黑色 + } + + @override + Color getReportPageSecondaryHintTextColor() { + // 马甲包策略:举报页面次级提示文本颜色 - 深灰色 + return Colors.white54; // 深灰色 + } + + @override + Color getReportPageUnselectedBorderColor() { + // 马甲包策略:举报页面未选中边框颜色 - 灰色 + return Colors.grey; // 灰色 + } + + @override + Color getReportPageSubmitButtonBackgroundColor() { + // 马甲包策略:举报页面提交按钮背景颜色 - 马甲包主题色(橙色) + return SocialChatTheme.primaryLight; // 马甲包主题色 + } + + @override + Color getReportPageSubmitButtonTextColor() { + // 马甲包策略:举报页面提交按钮文本颜色 - 白色(与原始应用相同) + return Colors.white; + } + + @override + String getReportPageIcon(String iconType) { + // 马甲包策略:举报页面图标路径(目前使用与原始应用相同的图标) + switch (iconType) { + case 'addPic': + return "sc_images/general/sc_icon_reort_add_pic.png"; + case 'closePic': + return "sc_images/general/sc_icon_pic_close.png"; + case 'checked': + return "sc_images/login/sc_icon_login_ser_select.png"; + default: + return ""; // 未知图标类型返回空字符串 + } + } + + /// === 语音房间页面差异化方法实现 === + + @override + Color getVoiceRoomBackgroundColor() { + // 马甲包策略:语音房间页面背景颜色 - 深灰色 + return const Color(0xff1a1a1a); // #1a1a1a + } + + @override + Color getVoiceRoomTabLabelColor() { + // 马甲包策略:语音房间页面Tab标签选中颜色 - 浅灰色 + return const Color(0xffe6e6e6); // #e6e6e6 + } + + @override + Color getVoiceRoomTabUnselectedLabelColor() { + // 马甲包策略:语音房间页面Tab标签未选中颜色 - 灰色 + return const Color(0xff999999); // 灰色 + } + + @override + Color getVoiceRoomTabIndicatorColor() { + // 马甲包策略:语音房间页面Tab指示器颜色 - 马甲包主题色(橙色) + return const Color(0xffFF5722); // 马甲包主题色 + } + + @override + Color getVoiceRoomTabDividerColor() { + // 马甲包策略:语音房间页面Tab分割线颜色 - 深灰色 + return Colors.transparent; // 深灰色 + } + + @override + EdgeInsets getVoiceRoomTabLabelPadding() { + // 马甲包策略:语音房间页面Tab标签内边距 - 与原始应用相同 + return const EdgeInsets.symmetric(horizontal: 12); + } + + @override + EdgeInsetsDirectional getVoiceRoomChatContainerMargin() { + // 马甲包策略:语音房间页面聊天容器外边距 - 与原始应用相同 + return const EdgeInsetsDirectional.only(end: 52); + } + + @override + TextStyle getVoiceRoomTabLabelStyle() { + // 马甲包策略:语音房间页面Tab标签选中文本样式 - 与原始应用相同 + return const TextStyle(fontSize: 15, fontFamily: "MyCustomFont"); + } + + @override + TextStyle getVoiceRoomTabUnselectedLabelStyle() { + // 马甲包策略:语音房间页面Tab标签未选中文本样式 - 与原始应用相同 + return const TextStyle(fontSize: 13, fontFamily: "MyCustomFont"); + } + + @override + String getVoiceRoomDefaultBackgroundImage() { + // 马甲包策略:语音房间默认背景图像路径 - 与原始应用相同 + return "sc_images/room/sc_icon_room_defaut_bg.png"; + } + + /// === 钱包页面差异化方法实现 === + + /// === 充值页面差异化方法实现 === + + @override + String getRechargePageBackgroundImage() { + // 马甲包策略:充值页面背景图像路径 - 与原始应用相同 + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getRechargePageScaffoldBackgroundColor() { + // 马甲包策略:充值页面Scaffold背景颜色 - 深灰色 + return Colors.transparent; // #1a1a1a + } + + @override + Color getRechargePageDialogBarrierColor() { + // 马甲包策略:充值页面对话框屏障颜色 - 深灰色半透明 + return const Color(0xCC1a1a1a); // withOpacity(0.8) + } + + @override + Color getRechargePageDialogContainerBackgroundColor() { + // 马甲包策略:充值页面对话框容器背景颜色 - 深灰色 + return const Color(0xff333333); // 深灰色 + } + + @override + Color getRechargePageDialogTextColor() { + // 马甲包策略:充值页面对话框文本颜色 - 浅灰色 + return const Color(0xffe6e6e6); // #e6e6e6 + } + + @override + Color getRechargePageMainContainerBackgroundColor() { + // 马甲包策略:充值页面主容器背景颜色 - 深灰色 + return Colors.transparent; // #1a1a1a + } + + @override + Color getRechargePageButtonBackgroundColor() { + // 马甲包策略:充值页面按钮背景颜色 - 马甲包主题色(橙色) + return SocialChatTheme.primaryLight; // 马甲包主题色 + } + + @override + Color getRechargePageButtonTextColor() { + // 马甲包策略:充值页面按钮文本颜色 - 白色(与原始应用相同) + return Colors.white; + } + + @override + String getRechargePageWalletBackgroundImage() { + // 马甲包策略:充值页面钱包背景图像路径 - 与原始应用相同 + return "sc_images/index/sc_icon_wallet_bg.png"; + } + + @override + String getRechargePageWalletIcon() { + // 马甲包策略:充值页面钱包图标路径 - 与原始应用相同 + return "sc_images/index/sc_icon_wallet_icon.png"; + } + + @override + Color getRechargePageWalletTextColor() { + // 马甲包策略:充值页面钱包文本颜色 - 浅灰色 + return Colors.white; // #e6e6e6 + } + + @override + String getRechargePageGoldIcon() { + // 马甲包策略:充值页面金币图标路径(默认图标)- 与原始应用相同 + return "sc_images/general/sc_icon_jb.png"; + } + + @override + Color getRechargePageSelectedItemBackgroundColor() { + // 马甲包策略:充值页面选中项背景颜色 - 马甲包主题色半透明 + return const Color(0x33FF5722); // withOpacity(0.2) + } + + @override + Color getRechargePageSelectedItemBorderColor() { + // 马甲包策略:充值页面选中项边框颜色 - 马甲包主题色 + return SocialChatTheme.primaryLight; // 马甲包主题色 + } + + @override + Color getRechargePageUnselectedItemBorderColor() { + // 马甲包策略:充值页面未选中项边框颜色 - 深灰色 + return const Color(0xff666666); // 深灰色 + } + + @override + Color getRechargePageUnselectedItemBackgroundColor() { + // 马甲包策略:充值页面未选中项背景颜色 - 深灰色 + return const Color(0xff2a2a2a); // 深灰色背景 + } + + @override + Color getRechargePageItemTextColor() { + // 马甲包策略:充值页面项目文本颜色 - 浅灰色 + return const Color(0xffe6e6e6); // #e6e6e6 + } + + @override + Color getRechargePageItemPriceTextColor() { + // 马甲包策略:充值页面价格文本颜色 - 白色 + return Colors.white; + } + + @override + String getRechargePageGoldIconByIndex(int index) { + // 马甲包策略:根据索引获取充值页面金币图标路径 - 与原始应用相同 + switch (index) { + case 0: + return "sc_images/general/sc_icon_jb.png"; + case 1: + return "sc_images/general/sc_icon_jb2.png"; + case 2: + return "sc_images/general/sc_icon_jb3.png"; + default: + return "sc_images/general/sc_icon_jb.png"; // 默认图标 + } + } + + @override + Color getRechargePageAppleItemBackgroundColor() { + // 马甲包策略:充值页面Apple产品项背景颜色 - 深灰色 + return const Color(0xff2a2a2a); // 深灰色 + } + + @override + String getRechargePageRecordIcon() { + // 马甲包策略:充值页面记录图标路径 - 与原始应用相同 + return "sc_images/index/sc_icon_recharge_recod.png"; + } + + /// === 金币记录页面差异化方法实现 === + + @override + String getGoldRecordPageBackgroundImage() { + // 马甲包策略:金币记录页面背景图像路径 - 与原始应用相同 + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getGoldRecordPageScaffoldBackgroundColor() { + // 马甲包策略:金币记录页面Scaffold背景颜色 - 深灰色 + return Colors.transparent; // #1a1a1a + } + + @override + Color getGoldRecordSCPageListBackgroundColor() { + // 马甲包策略:金币记录页面列表背景颜色 - 深灰色 + return const Color(0xff1a1a1a); // #1a1a1a + } + + @override + Color getGoldRecordPageContainerBackgroundColor() { + // 马甲包策略:金币记录页面容器背景颜色 - 深灰色 + return Colors.black38; // #1a1a1a + } + + @override + Color getGoldRecordPageBorderColor() { + // 马甲包策略:金币记录页面边框颜色 - 深灰色 + return Colors.white12; // 深灰色 + } + + @override + Color getGoldRecordPagePrimaryTextColor() { + // 马甲包策略:金币记录页面主要文本颜色 - 浅灰色 + return const Color(0xffe6e6e6); // #e6e6e6 + } + + @override + Color getGoldRecordPageSecondaryTextColor() { + // 马甲包策略:金币记录页面次要文本颜色 - 灰色 + return const Color(0xff999999); // 灰色 + } + + /// === Store页面差异化方法实现 === + + @override + String getStorePageBackgroundImage() { + // 马甲包策略:Store页面背景图像路径 - 使用相同背景图像(保持一致性) + // 注意:如果需要差异化,可以返回不同的背景图像路径 + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + String getStorePageShoppingBagIcon() { + // 马甲包策略:Store页面购物袋图标路径 - 使用相同图标(保持一致性) + // 注意:如果需要差异化,可以返回不同的图标路径 + return "sc_images/store/sc_icon_shop_bag.png"; + } + + @override + EdgeInsetsDirectional getStorePageShoppingBagMargin() { + // 马甲包策略:Store页面购物袋图标外边距 - 保持相同外边距 + return const EdgeInsetsDirectional.only(end: 10); + } + + @override + EdgeInsets getStorePageTabLabelPadding() { + // 马甲包策略:Store页面Tab标签内边距 - 保持相同内边距 + return const EdgeInsets.symmetric(horizontal: 12); + } + + @override + TextStyle getStorePageTabLabelStyle() { + // 马甲包策略:Store页面Tab标签选中文本样式 + // 差异化:使用马甲包主题色作为选中文本颜色 + return const TextStyle( + fontSize: 15, + color: Colors.black87, // 马甲包主题橙色,替代Colors.black87 + fontWeight: FontWeight.w600, + ); + } + + @override + TextStyle getStorePageTabUnselectedLabelStyle() { + // 马甲包策略:Store页面Tab标签未选中文本样式 + // 差异化:调整未选中文本颜色为灰色系 + return const TextStyle( + fontSize: 13, + color: Colors.white54, // 灰色,替代Colors.black38 + fontWeight: FontWeight.w500, + ); + } + + @override + Color getStorePageTabIndicatorColor() { + // 马甲包策略:Store页面Tab指示器颜色 - 使用马甲包主题色 + // 注意:原始应用使用SocialChatTheme.primaryColor,马甲包使用相同值 + return SocialChatTheme.primaryLight; // SocialChatTheme.primaryColor + } + + @override + Color getStorePageTabDividerColor() { + // 马甲包策略:Store页面Tab分割线颜色 - 保持透明 + return Colors.transparent; + } + + @override + Color getStorePageBottomDividerColor() { + // 马甲包策略:Store页面底部Divider颜色 - 调整为浅灰色 + return const Color(0xffEEEEEE); // 浅灰色,替代Colors.black12 + } + + @override + double getStorePageBottomDividerThickness() { + // 马甲包策略:Store页面底部Divider厚度 - 保持相同 + return 0.5; + } + + @override + double getStorePageBottomDividerHeight() { + // 马甲包策略:Store页面底部Divider高度 - 保持相同 + return 24; + } + + @override + String getStorePageGoldIcon() { + // 马甲包策略:Store页面金币图标路径 - 使用相同图标 + return "sc_images/general/sc_icon_jb.png"; + } + + @override + Color getStorePageGoldTextColor() { + // 马甲包策略:Store页面金币文本颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryLight; // 马甲包主题橙色,替代Color(0xFFFFE134) + } + + @override + Color getStorePageGoldIconColor() { + // 马甲包策略:Store页面金币图标颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryLight; // 马甲包主题橙色,替代Color(0xFFFFE134) + } + + /// === Store子页面(商品项)差异化方法实现 === + + @override + Color getStoreItemBackgroundColor() { + // 马甲包策略:Store商品项背景颜色 - 使用马甲包表面色 + return Color(0xff18F2B1).withOpacity(0.1); // 马甲包表面色,替代Colors.white + } + + @override + Color getStoreItemUnselectedBorderColor() { + // 马甲包策略:Store商品项未选中边框颜色 - 调整为浅灰色 + return Colors.transparent; // 浅灰色,替代Colors.black12 + } + + @override + Color getStoreItemSelectedBorderColor() { + // 马甲包策略:Store商品项选中边框颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryLight; // 马甲包主题橙色,替代Color(0xffFF9500) + } + + @override + String getStoreItemGoldIcon() { + // 马甲包策略:Store商品项金币图标路径 - 使用相同图标 + return "sc_images/general/sc_icon_jb.png"; + } + + @override + Color getStoreItemPriceTextColor() { + // 马甲包策略:Store商品项价格文本颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.primaryLight; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getStoreItemBuyButtonColor() { + // 马甲包策略:Store商品项购买按钮背景颜色 - 使用马甲包主题色 + return const Color(0xffFF5722); // 马甲包主题橙色,替代Color(0xffFF9500) + } + + @override + Color getStoreItemBuyButtonTextColor() { + // 马甲包策略:Store商品项购买按钮文本颜色 - 使用马甲包主色上的文本颜色 + return SocialChatTheme.textOnPrimary; // 马甲包主色上的文本颜色,替代Colors.white + } + + @override + String getStoreItemActionIcon(String itemType) { + // 马甲包策略:Store商品项操作图标路径 + // 使用相同的图标路径,确保功能一致 + switch (itemType) { + case 'headdress': + return "sc_images/store/sc_icon_shop_item_search.png"; // 头饰页面使用搜索图标 + case 'mountains': + return "sc_images/store/sc_icon_shop_item_play.png"; // 山脉页面使用播放图标 + case 'theme': + return "sc_images/store/sc_icon_store_theme_rev.png"; // 主题页面使用主题图标 + case 'chatbox': + return "sc_images/store/sc_icon_shop_item_search.png"; // 聊天气泡页面使用搜索图标 + default: + return "sc_images/store/sc_icon_store_theme_rev.png"; // 默认返回theme图标 + } + } + + /// === 动态页面差异化方法实现 === + + @override + String getDynamicPageBackgroundImage() { + // 马甲包策略:动态页面背景图像路径 - 使用相同图像保持功能一致 + return "sc_images/index/sc_icon_index_mask.png"; + } + + @override + Color getDynamicPageScaffoldBackgroundColor() { + // 马甲包策略:动态页面Scaffold背景颜色 - 保持透明以保持视觉效果 + return Colors.transparent; + } + + @override + Color getDynamicPageAppBarBackgroundColor() { + // 马甲包策略:动态页面AppBar背景颜色 - 保持透明以保持视觉效果 + return Colors.transparent; + } + + @override + Color getDynamicPageTabLabelColor() { + // 马甲包策略:动态页面Tab标签选中颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.black87 + } + + @override + Color getDynamicPageTabUnselectedLabelColor() { + // 马甲包策略:动态页面Tab标签未选中颜色 - 使用马甲包次要文本颜色 + return SocialChatTheme.textSecondary; // 马甲包次要文本颜色,替代Colors.black38 + } + + @override + Color getDynamicPageTabIndicatorColor() { + // 马甲包策略:动态页面Tab指示器颜色 - 使用马甲包主题色 + return const Color(0xffFF5722); // 马甲包主题橙色,替代Colors.transparent + } + + @override + Color getDynamicPageTabDividerColor() { + // 马甲包策略:动态页面Tab分割线颜色 - 保持透明以保持简洁设计 + return Colors.transparent; + } + + @override + String getDynamicPageAddButtonIcon() { + // 马甲包策略:动态页面添加动态按钮图标路径 - 使用相同图标保持功能一致 + return "sc_images/dynamic/sc_icon_add_dynamic_btn.png"; + } + + @override + bool shouldDynamicPageTabScrollable() { + // 马甲包策略:动态页面Tab标签是否可滚动 - 保持相同行为 + return true; + } + + @override + TextStyle getDynamicPageTabLabelStyle() { + // 马甲包策略:动态页面Tab标签选中文本样式 - 使用相同样式但可能应用主题颜色 + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: SocialChatTheme.textPrimary, // 应用马甲包文本颜色 + ); + } + + @override + TextStyle getDynamicPageTabUnselectedLabelStyle() { + // 马甲包策略:动态页面Tab标签未选中文本样式 - 使用相同样式但可能应用主题颜色 + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return TextStyle( + fontWeight: FontWeight.normal, + fontSize: 15, + color: SocialChatTheme.textSecondary, // 应用马甲包次要文本颜色 + ); + } + + /// === 启动页面差异化方法实现 === + + @override + String getSplashPageBackgroundImage() { + // 马甲包策略:启动页面背景图像路径 - 使用马甲包专属启动背景图像 + // TODO: 根据Figma设计添加马甲包专属启动背景图像 + // 目前先使用相同图像,确保功能正常 + return "sc_images/splash/sc_splash.png"; + } + + @override + String getSplashPageIcon() { + // 马甲包策略:启动页面图标路径 - 使用马甲包专属启动图标 + // TODO: 根据Figma设计添加马甲包专属启动图标 + // 目前先使用相同图标,确保功能正常 + return "sc_images/splash/sc_icon_splash_icon.png"; + } + + @override + String getSplashPageSkipButtonBackground() { + // 马甲包策略:启动页面跳过按钮背景图像路径 - 使用马甲包专属跳过按钮背景 + // TODO: 根据Figma设计添加马甲包专属跳过按钮背景图像 + // 目前先使用相同图像,确保功能正常 + return "sc_images/splash/sc_icon_splash_skip_bg.png"; + } + + @override + Color getSplashPageSkipButtonTextColor() { + // 马甲包策略:启动页面跳过按钮文本颜色 - 使用马甲包主色上的文本颜色 + return SocialChatTheme.textOnPrimary; // 马甲包主色上的文本颜色,替代Colors.white + } + + @override + String getSplashPageKingGamesNameBackground() { + // 马甲包策略:启动页面游戏名称背景图像路径 - 使用马甲包专属背景图像 + // TODO: 根据Figma设计添加马甲包专属游戏名称背景图像 + // 目前先使用相同图像,确保功能正常 + return "sc_images/index/sc_icon_splash_king_games_name_bg.png"; + } + + @override + Color getSplashPageKingGamesTextColor() { + // 马甲包策略:启动页面游戏名称文本颜色 - 使用马甲包主色上的文本颜色 + return SocialChatTheme.textOnPrimary; // 马甲包主色上的文本颜色,替代Colors.white + } + + @override + String getSplashPageCpNameBackground() { + // 马甲包策略:启动页面CP名称背景图像路径 - 使用马甲包专属背景图像 + // TODO: 根据Figma设计添加马甲包专属CP名称背景图像 + // 目前先使用相同图像,确保功能正常 + return "sc_images/index/sc_icon_splash_cp_name_bg.png"; + } + + @override + Color getSplashPageCpTextColor() { + // 马甲包策略:启动页面CP名称文本颜色 - 使用马甲包主色上的文本颜色 + return SocialChatTheme.textOnPrimary; // 马甲包主色上的文本颜色,替代Colors.white + } + + /// === WebView页面差异化方法实现 === + + @override + Color getWebViewPageAppBarBackgroundColor() { + // 马甲包策略:WebView页面AppBar背景颜色 - 使用马甲包背景颜色 + return SocialChatTheme.backgroundColor; // 马甲包背景颜色,替代Colors.white + } + + @override + Color getWebViewPageTitleTextColor() { + // 马甲包策略:WebView页面标题文本颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Color(0xff333333) + } + + @override + Color getWebViewPageBackArrowColor() { + // 马甲包策略:WebView页面返回箭头图标颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Color(0xff333333) + } + + @override + Color getWebViewPageProgressBarBackgroundColor() { + // 马甲包策略:WebView页面进度条背景颜色 - 使用透明背景 + return Colors.transparent; // 透明背景,替代Colors.white70.withOpacity(0) + } + + @override + Color getWebViewPageProgressBarActiveColor() { + // 马甲包策略:WebView页面进度条活动颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryColor; // 马甲包主题色,替代Color(0xffFF6000) + } + + @override + Color getGameWebViewCloseIconColor() { + // 马甲包策略:游戏WebView页面关闭图标颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.white + } + + @override + Color getGameWebViewScaffoldBackgroundColor() { + // 马甲包策略:游戏WebView页面Scaffold背景颜色 - 使用马甲包背景颜色 + return SocialChatTheme.backgroundColor; // 马甲包背景颜色,替代Colors.transparent + } + + @override + Color getGameWebViewProgressBarBackgroundColor() { + // 马甲包策略:游戏WebView页面进度条背景颜色 - 使用马甲包背景颜色(透明) + return SocialChatTheme.backgroundColor.withOpacity(0.3); // 马甲包背景颜色,半透明 + } + + @override + Color getGameWebViewLoadingIndicatorColor() { + // 马甲包策略:游戏WebView页面加载指示器颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryColor; // 马甲包主题色,替代Color(0xffFF6000) + } + + /// === 搜索页面差异化方法实现 === + + @override + Color getSearchPageScaffoldBackgroundColor() { + // 马甲包策略:搜索页面Scaffold背景颜色 - 使用浅灰色背景 + return Colors.transparent; // 马甲包浅灰色背景颜色,替代Colors.white + } + + @override + Color getSearchPageBackIconColor() { + // 马甲包策略:搜索页面返回图标颜色 - 使用深灰色 + return const Color(0xff666666); // 深灰色,替代Color(0xffBBBBBB) + } + + @override + Color getSearchPageInputBorderColor() { + // 马甲包策略:搜索页面搜索框边框颜色 - 使用马甲包主题色 + return SocialChatTheme.transparent; // 马甲包主题色,替代Colors.transparent + } + + @override + Color getSearchPageInputTextColor() { + // 马甲包策略:搜索页面搜索框文本颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.grey + } + + @override + Color getSearchPageButtonTextColor() { + // 马甲包策略:搜索页面搜索按钮文本颜色 - 使用白色 + return Colors.white; // 马甲包使用白色文本,替代Colors.black + } + + @override + List getSearchPageButtonGradient() { + // 马甲包策略:搜索页面搜索按钮渐变颜色 - 使用马甲包主题色渐变 + return [ + SocialChatTheme.transparent, + SocialChatTheme.transparent, + ]; // 马甲包主题色渐变,替代[Colors.transparent, Colors.transparent] + } + + @override + Color getSearchPageHistoryTitleTextColor() { + // 马甲包策略:搜索页面历史记录标题文本颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getSearchPageHistoryItemTextColor() { + // 马甲包策略:搜索页面历史项文本颜色 - 使用白色 + return Colors.white; // 马甲包使用白色文本,替代Colors.white + } + + @override + Color getSearchPageHistoryItemBackgroundColor() { + // 马甲包策略:搜索页面历史项背景颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryColor; // 马甲包主题色,替代Color(0xffFF9500) + } + + @override + Color getSearchPageTabIndicatorGradientStartColor() { + // 马甲包策略:搜索页面Tab指示器渐变开始颜色 - 使用马甲包主题色变体 + return SocialChatTheme.primaryColor; // 马甲包主题色,替代Color(0xffFFA500) + } + + @override + Color getSearchPageTabIndicatorGradientEndColor() { + // 马甲包策略:搜索页面Tab指示器渐变结束颜色 - 使用马甲包主题色变体 + return SocialChatTheme.primaryLight; // 马甲包浅主题色,替代Color(0xffFFD700) + } + + @override + Color getSearchPageTabSelectedLabelColor() { + // 马甲包策略:搜索页面Tab标签选中颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.primaryLight; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getSearchPageTabDividerColor() { + // 马甲包策略:搜索页面Tab分割线颜色 - 使用马甲包分割线颜色 + return SocialChatTheme.transparent; // 马甲包分割线颜色,替代Colors.transparent + } + + @override + Color getSearchPageTabUnselectedLabelColor() { + // 马甲包策略:搜索页面Tab标签未选中颜色 - 使用马甲包次要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包次要文本颜色,替代Colors.black26 + } + + @override + Color getSearchPageDividerContainerBackgroundColor() { + // 马甲包策略:搜索页面分隔容器背景颜色 - 使用马甲包背景颜色 + return Color(0xffFFF8DA); // 马甲包背景颜色,替代Color(0xffFFF8DA) + } + + @override + TextStyle getSearchPageTabSelectedLabelStyle() { + // 马甲包策略:搜索页面Tab标签选中文本样式 - 使用马甲包样式 + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black, // 将在调用处替换为马甲包主要文本颜色 + ).copyWith(color: SocialChatTheme.textPrimary); + } + + @override + TextStyle getSearchPageTabUnselectedLabelStyle() { + // 马甲包策略:搜索页面Tab标签未选中文本样式 - 使用马甲包样式 + // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() + return const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w400, + color: Colors.black26, // 将在调用处替换为马甲包次要文本颜色 + ).copyWith(color: SocialChatTheme.textSecondary); + } + + @override + EdgeInsets getSearchPageResultTabLabelPadding() { + // 马甲包策略:搜索页面搜索结果Tab标签内边距 - 与原始应用相同 + return const EdgeInsets.symmetric(horizontal: 15); + } + + @override + Color getSearchPageResultTabLabelColor() { + // 马甲包策略:搜索页面搜索结果Tab标签选中颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getSearchPageResultTabUnselectedLabelColor() { + // 马甲包策略:搜索页面搜索结果Tab标签未选中颜色 - 使用马甲包次要文本颜色 + return SocialChatTheme.textSecondary; // 马甲包次要文本颜色,替代Colors.black26 + } + + @override + TextStyle getSearchPageResultTabUnselectedLabelStyle() { + // 马甲包策略:搜索页面搜索结果Tab标签未选中文本样式 - 使用马甲包样式 + return const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w400, + color: Colors.black26, // 将在调用处替换为马甲包次要文本颜色 + ).copyWith(color: SocialChatTheme.textSecondary); + } + + @override + TextStyle getSearchPageResultTabLabelStyle() { + // 马甲包策略:搜索页面搜索结果Tab标签选中文本样式 - 使用马甲包样式 + return const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.black87, // 将在调用处替换为马甲包主要文本颜色 + ).copyWith(color: SocialChatTheme.textPrimary); + } + + @override + LinearGradient getSearchPageResultTabIndicatorColor() { + // 马甲包策略:搜索页面搜索结果Tab指示器颜色 - 使用马甲包主题渐变 + return const LinearGradient( + colors: [SocialChatTheme.primaryColor, Color(0xffFF9A00)], + ); + } + + @override + double getSearchPageResultTabIndicatorWidth() { + // 马甲包策略:搜索页面搜索结果Tab指示器宽度 - 与原始应用相同 + return 15.0; + } + + @override + Color getSearchPageResultBackgroundColor() { + // 马甲包策略:搜索页面搜索结果背景颜色 - 使用浅橙色背景 + return const Color(0xffFFECB3); // 马甲包浅橙色背景,替代Color(0xffFFF8DA) + } + + @override + String getSearchPageClearHistoryIcon() { + // 马甲包策略:搜索页面清除历史记录图标路径 - 使用相同图标 + return "sc_images/general/sc_icon_delete.png"; + } + + @override + String getSearchPageEmptyDataIcon() { + // 马甲包策略:搜索页面空数据图标路径 - 使用相同图标 + return "sc_images/room/sc_icon_room_defaut_bg.png"; + } + + // === 任务页面差异化方法 === + + @override + String getTaskPageBackgroundImage() { + // 马甲包策略:任务页面背景图像 - 使用相同图片 + return "sc_images/room/sc_icon_room_settig_bg.png"; + } + + @override + Color getTaskPageBackButtonColor() { + // 马甲包策略:任务页面返回按钮颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getTaskPageScaffoldBackgroundColor() { + // 马甲包策略:任务页面Scaffold背景颜色 - 使用马甲包背景颜色 + return const Color(0xfff5f5f5); // 马甲包背景颜色 + } + + @override + Color getTaskPageContainerBackgroundColor() { + // 马甲包策略:任务页面容器背景颜色 - 使用马甲包表面颜色 + return SocialChatTheme.surfaceColor; // 马甲包表面颜色,替代Color(0xffE6E6E6) + } + + @override + Color getTaskPageBorderColor() { + // 马甲包策略:任务页面边框颜色 - 使用马甲包边框颜色 + return SocialChatTheme.borderColor; // 马甲包边框颜色,替代Color(0xffE6E6E6) + } + + @override + Color getTaskPageSpecialBorderColor() { + // 马甲包策略:任务页面特殊边框颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryColor; // 马甲包主题色,替代Color(0xffBB92FF) + } + + @override + Color getTaskPagePrimaryTextColor() { + // 马甲包策略:任务页面主要文本颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getTaskPageGoldTextColor() { + // 马甲包策略:任务页面金币文本颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryColor; // 马甲包主题色,替代Color(0xffFFB627) + } + + @override + Color getTaskPageExpTextColor() { + // 马甲包策略:任务页面经验文本颜色 - 使用马甲包成功颜色 + return SocialChatTheme.successColor; // 马甲包成功颜色,替代Color(0xff52FF90) + } + + @override + Color getTaskPageThemeColor() { + // 马甲包策略:任务页面主题颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryColor; // 马甲包主题色,替代Color(0xffFF5722) + } + + @override + Color getTaskPageThemeLightColor() { + // 马甲包策略:任务页面浅主题颜色 - 使用马甲包浅主题色 + return SocialChatTheme.primaryLight; // 马甲包浅主题色,替代Color(0xffFF9500) + } + + @override + List getTaskPageReceivableButtonGradient() { + // 马甲包策略:任务页面可领取按钮渐变颜色 - 使用马甲包主题色渐变 + return [ + SocialChatTheme.primaryColor, + const Color(0xffFF9A00), + ]; // 马甲包主题色渐变,替代[Color(0xffBB92FF), Color(0xff8B45FF)] + } + + @override + List getTaskPageCompletedButtonGradient() { + // 马甲包策略:任务页面已完成按钮渐变颜色 - 使用灰色渐变 + return [ + const Color(0xffCCCCCC), + const Color(0xff999999), + ]; // 马甲包灰色渐变,替代[Color(0xffDADADA), Color(0xff585859)] + } + + @override + Color getTaskPageButtonTextColor() { + // 马甲包策略:任务页面按钮文本颜色 - 使用马甲包主色上的文本颜色 + return SocialChatTheme.textOnPrimary; // 马甲包主色上的文本颜色,替代Colors.white + } + + @override + Color getTaskPageMaskColor() { + // 马甲包策略:任务页面遮罩颜色 - 半透明黑色 + return Colors.black.withOpacity(0.5); + } + + @override + String getTaskPageHeadBackgroundImage() { + // 马甲包策略:任务页面头部背景图像 - 使用相同图片 + return "sc_images/index/sc_icon_task_head_bg.png"; + } + + @override + String getTaskPageGoldIcon() { + // 马甲包策略:任务页面金币图标 - 使用相同图标 + return "sc_images/general/sc_icon_jb.png"; + } + + @override + String getTaskPageExpIcon() { + // 马甲包策略:任务页面经验图标 - 使用相同图标 + return "sc_images/index/sc_icon_task_exp.png"; + } + + @override + String getTaskPageInvitationRewardBackgroundImage() { + // 马甲包策略:任务页面邀请奖励背景图像 - 使用相同图片 + return "sc_images/index/sc_icon_invitation_bg.png"; + } + + @override + Color getTaskPageGiftBagTextColor() { + // 马甲包策略:任务页面礼包文本颜色 - 使用马甲包主色上的文本颜色 + return SocialChatTheme.textOnPrimary; // 马甲包主色上的文本颜色,替代Colors.white + } + + @override + Color getTaskPageAppBarBackgroundColor() { + // 马甲包策略:任务页面AppBar背景颜色 - 保持透明,与原始应用一致 + return Colors.transparent; + } + + @override + Color getTaskPageTransparentContainerColor() { + // 马甲包策略:任务页面透明容器颜色 - 保持透明,与原始应用一致 + return Colors.transparent; + } + + /// === 设置页面差异化方法实现 === + + @override + String getSettingsPageBackgroundImage() { + // 马甲包策略:设置页面背景图像路径 - 使用相同图片 + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getSettingsPageMainContainerBackgroundColor() { + // 马甲包策略:设置页面主容器背景颜色 - 使用马甲包背景色 + return Color(0xff18F2B1).withOpacity(0.1); // 马甲包背景色,与原始应用一致 + } + + @override + Color getSettingsPageMainContainerBorderColor() { + // 马甲包策略:设置页面主容器边框颜色 - 使用马甲包边框色 + return SocialChatTheme.borderColor; // 马甲包边框色,替代Color(0xffE6E6E6) + } + + @override + Color getSettingsPagePrimaryTextColor() { + // 马甲包策略:设置页面主文本颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getSettingsPageSecondaryTextColor() { + // 马甲包策略:设置页面次要文本颜色 - 使用马甲包次要文本颜色 + return SocialChatTheme.textSecondary; // 马甲包次要文本颜色,替代Colors.black38 + } + + @override + Color getSettingsPageIconColor() { + // 马甲包策略:设置页面图标颜色 - 使用马甲包次要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包次要文本颜色,替代Colors.black26 + } + + @override + Color getSettingsPageCommonTextColor() { + // 马甲包策略:设置页面常见文本颜色 - 使用深灰色 + return const Color(0xff666666); + } + + @override + Color getSettingsPageAboutTextColor() { + // 马甲包策略:设置页面关于文本颜色 - 使用深灰色 + return const Color(0xff666666); + } + + @override + Color getSettingsPageContainerBorderColor() { + // 马甲包策略:设置页面容器边框颜色 - 使用浅灰色 + return Colors.transparent; + } + + @override + Color getSettingsPageContainerBackgroundColor() { + // 马甲包策略:设置页面容器背景颜色 - 使用白色(与原始应用相同) + return Color(0xff18F2B1).withOpacity(0.1); + } + + @override + Color getSettingsPageButtonBackgroundColor() { + // 马甲包策略:设置页面按钮背景颜色 - 使用马甲包主题色 + return Color(0xff18F2B1).withOpacity(0.1); + } + + @override + Color getSettingsPageButtonTextColor() { + // 马甲包策略:设置页面按钮文本颜色 - 使用白色 + return Colors.white; + } + + /// === 语言页面差异化方法实现 === + + @override + String getLanguagePageBackgroundImage() { + // 马甲包策略:语言页面背景图像路径 - 使用相同图片 + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getLanguagePageCheckboxActiveColor() { + // 马甲包策略:语言页面复选框选中颜色 - 使用马甲包主题色 + return SocialChatTheme.primaryColor; // 马甲包主题色,替代Color(0xffFF5722) + } + + @override + Color getLanguagePageCheckboxBorderColor() { + // 马甲包策略:语言页面复选框边框颜色 - 使用马甲包边框色 + return Colors.white54; // 马甲包边框色,替代Colors.grey + } + + @override + Color getLanguagePagePrimaryTextColor() { + // 马甲包策略:语言页面主文本颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getLanguagePageCheckColor() { + // 马甲包策略:语言页面复选框对勾颜色 - 使用透明色 + return Colors.transparent; + } + + @override + double getLanguagePageCheckboxBorderRadius() { + // 马甲包策略:语言页面复选框边框圆角 - 使用相同的圆角值15 + return 15; + } + + @override + double getLanguagePageCheckboxBorderWidth() { + // 马甲包策略:语言页面复选框边框宽度 - 使用相同的边框宽度2 + return 2; + } + + @override + Color getLanguagePageScaffoldBackgroundColor() { + // 马甲包策略:语言页面Scaffold背景颜色 - 使用透明色 + return Colors.transparent; + } + + /// === 账户页面差异化方法实现 === + + @override + String getAccountPageBackgroundImage() { + // 马甲包策略:账户页面背景图像路径 - 使用相同图片 + return "sc_images/person/sc_icon_edit_userinfo_bg.png"; + } + + @override + Color getAccountPageMainContainerBackgroundColor() { + // 马甲包策略:账户页面主容器背景颜色 - 使用马甲包背景色 + return Color(0xff18F2B1).withOpacity(0.1); + } + + @override + Color getAccountPageMainContainerBorderColor() { + // 马甲包策略:账户页面主容器边框颜色 - 使用马甲包边框色 + return const Color(0xffE0E0E0); // SocialChatTheme.borderColor + } + + @override + Color getAccountPagePrimaryTextColor() { + // 马甲包策略:账户页面主文本颜色 - 使用马甲包主要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包主要文本颜色,替代Colors.black + } + + @override + Color getAccountPageSecondaryTextColor() { + // 马甲包策略:账户页面次要文本颜色 - 使用马甲包次要文本颜色 + return SocialChatTheme.textSecondary; // 马甲包次要文本颜色,替代Colors.black38 + } + + @override + Color getAccountPageIconColor() { + // 马甲包策略:账户页面图标颜色 - 使用马甲包次要文本颜色 + return SocialChatTheme.textPrimary; // 马甲包次要文本颜色,替代Colors.black26 + } + + @override + Color getAccountPageDividerColor() { + // 马甲包策略:账户页面分隔线颜色 - 使用马甲包边框颜色 + return SocialChatTheme.borderColor; // 马甲包边框颜色,替代Colors.black12 + } + + @override + Color getAccountPageScaffoldBackgroundColor() { + // 马甲包策略:账户页面Scaffold背景颜色 - 使用透明色 + return Colors.transparent; + } + + @override + Color getGoldRecordPageListBackgroundColor() { + // 马甲包策略:金币记录页面列表背景颜色 - 深灰色 + return Colors.transparent; // #1a1a1a + } +} diff --git a/lib/app/constants/sc_app_colors.dart b/lib/app/constants/sc_app_colors.dart new file mode 100644 index 0000000..60e937a --- /dev/null +++ b/lib/app/constants/sc_app_colors.dart @@ -0,0 +1,40 @@ +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:yumi/app/config/app_config.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +/// 主题颜色配置(兼容层) + +///主题色 - 从AppConfig获取主色 +@deprecated +Color get themeColor => SocialChatTheme.primaryColor; + +/// 次要主题色 - 基于主色的浅色版本 +@deprecated +Color get themeColor2 => SocialChatTheme.primaryLight; + +/// 欢迎页面颜色 +@deprecated +const int welcomeColorValue = 0xFFD0D3B5; +@deprecated +const Color color999 = Color(0xFF999999); + +/// 主题色值(兼容旧代码) +@deprecated +int get THEME_COLOR_VALUE => SocialChatTheme.primaryColor.value; +@deprecated +const int WELCOME_COLOR_VALUE = 0xFFD0D3B5; + +/// 分隔线颜色 - 使用SocialChat主题系统的颜色 +@deprecated +Color get dividerColor => SocialChatTheme.dividerColor; + +@deprecated +LinearGradient getLinearGradient(List colorValues, {bool vertical = false, double opacity = 1}){ + if(vertical){ + return LinearGradient(colors: colorValues.map((e) => Color(e).withOpacity(opacity)).toList(), begin: Alignment.topCenter, end: Alignment.bottomCenter); + } else{ + return LinearGradient(colors: colorValues.map((e) => Color(e).withOpacity(opacity)).toList(), begin: Alignment.centerLeft, end: Alignment.centerRight); + } +} \ No newline at end of file diff --git a/lib/app/constants/sc_emoji_datas.dart b/lib/app/constants/sc_emoji_datas.dart new file mode 100644 index 0000000..d580d0a --- /dev/null +++ b/lib/app/constants/sc_emoji_datas.dart @@ -0,0 +1,34 @@ +class SCEmojiDatas{ + static const List smileys = [ + '😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', + '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', + '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩', + '🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '☹️', '😣', + '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬', + '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', + '🤔', '🤭', '🤫', '🤥', '😶', '😐', '😑', '😬', '🙄', '😯', + '😦', '😧', '😮', '😲', '🥱', '😴', '🤤', '😪', '😵', '🤐', + '🥴', '🤢', '🤮', '🤧', '😷', '🤒', '🤕', '🤑', '🤠', '😈', + '👿', '👹', '👺', '🤡', '💩', '👻', '💀', '☠️', '👽', '👾', + '🤖', '🎃', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿', + '😾','🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', + '🦁', '🐮', '🐷', '🐽', '🐸', '🐵', '🙈', '🙉', '🙊', '🐒', + '🐔', '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉', '🦇', + '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋', '🐌', '🐞', '🐜', + '🦟', '🦗', '🕷️', '🕸️', '🦂', '🐢', '🐍', '🦎', '🦖', '🦕', + '🐙', '🦑', '🦐', '🦞', '🦀', '🐡', '🐠', '🐟', '🐬', '🐳', + '🐋', '🦈', '🐊', '🐅', '🐆', '🦓', '🦍', '🦧', '🐘', + '🦛', '🦏', '🐪', '🐫', '🦒', '🦘', '🐃', '🐂', '🐄', + '🐎', '🐖', '🐏', '🐑', '🦙', '🐐', '🦌', '🐕', '🐩', '🦮', + '🐕‍🦺', '🐈', '🐓', '🦃', '🦚', '🦜', '🦢', + '🦩', '🕊️', '🐇', '🦝', '🦨', '🦡', '🦦', '🦥', '🐁', + '🐀', '🐿️', '🦔', '🐾', '🐉', '🐲', '🥬', '🥒', '🌶️', '🌽', '🥕', '🧄', '🧅', '🥔', + '🍠', '🥐', '🥯', '🍞', '🥖', '🥨', '🧀', '🥚', '🍳', '🧈', + '🥞', '🧇', '🥓', '🥩', '🍗', '🍖', '🦴', '🌭', '🍔', '🍟', + '🍕', '🥪', '🥙', '🧆', '🌮', '🌯', '🥗', '🥘', + '🥫', '🍝', '🍜', '🍲', '🍛', '🍣', '🍱', '🥟', '🦪', '🏍️', '🛺', '🚨', '🚔', '🚍', '🚘', '🚖', '🚡', '🚠', '🚟', + '🚃', '🚋', '🚞', '🚝', '🚄', '🚅', '🚈', '🚂', '🚆', '🚇', + '🚊', '🚉', '✈️', '🛫', '🛬', '🛩️', '💺', '🛰️', '🚀', '🛸', '⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', + '🪀', '🏓', '🏸', '🏒', '🏑', '🥍', '🏏', + ]; +} \ No newline at end of file diff --git a/lib/app/constants/sc_global_config.dart b/lib/app/constants/sc_global_config.dart new file mode 100644 index 0000000..bbf2aef --- /dev/null +++ b/lib/app/constants/sc_global_config.dart @@ -0,0 +1,97 @@ +import 'package:yumi/app/config/app_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; + +class SCGlobalConfig { + static String get apiHost => AppConfig.current.apiHost; + static String get imgHost => AppConfig.current.imgHost; + static String get privacyAgreementUrl => AppConfig.current.privacyAgreementUrl; + static String get userAgreementUrl => AppConfig.current.userAgreementUrl; + + // 以下URL从AppConfig获取 + static String get anchorAgentUrl => AppConfig.current.anchorAgentUrl; + static String get hostCenterUrl => AppConfig.current.hostCenterUrl; + static String get bdCenterUrl => AppConfig.current.bdCenterUrl; + static String get bdLeaderUrl => AppConfig.current.bdLeaderUrl; + static String get coinSellerUrl => AppConfig.current.coinSellerUrl; + static String get adminUrl => AppConfig.current.adminUrl; + static String get agencyCenterUrl => AppConfig.current.agencyCenterUrl; + static String get gamesKingUrl => AppConfig.current.gamesKingUrl; + + // 应用下载链接需要根据Flavor调整 + static String get appDownloadUrlGoogle => AppConfig.current.appDownloadUrlGoogle; + static String get appDownloadUrlApple => AppConfig.current.appDownloadUrlApple; + + ///语言 + static String lang = "en"; + + ///设备号 + static String imei = ""; + + ///版本 + static String version = "1.0.0"; + static String build = "1"; + static String model = "SM-G9550"; + static String sysVersion = "9"; + static int sdkInt = 28; + + ///高于这个值才播放特效,避免低性能手机卡顿 + static int maxSdkNoAnim = 27; + + ///渠道SocialChat + static String channel = "Google"; + static String origin = "ATYOU"; + static String originChild = "ATYOU"; + + static String get tencentImAppid => AppConfig.current.tencentImAppid; + static String get agoraRtcAppid => AppConfig.current.agoraRtcAppid; + + static num get gameAppid => AppConfig.current.gameAppid; + static String get gameAppChannel => AppConfig.current.gameAppChannel; + + ///全服广播大群 + static String get bigBroadcastGroup => AppConfig.current.bigBroadcastGroup; + + static String get imAdmin => AppConfig.current.imAdmin; + + ///财富榜单 + static String get wealthRankUrl => AppConfig.current.wealthRankUrl; + + ///魅力榜 + static String get charmRankUrl => AppConfig.current.charmRankUrl; + + ///房间榜单 + static String get roomRankUrl => AppConfig.current.roomRankUrl; + + ///邀请新用户活动链接 + static String get inviteNewUserUrl => AppConfig.current.inviteNewUserUrl; + + ///是否在审核 + static bool? _isReviewOverride; + static bool get isReview => _isReviewOverride ?? AppConfig.current.isReview; + static set isReview(bool value) => _isReviewOverride = value; + + ///礼物特效开关 + static bool _isGiftSpecialEffects = true; + static bool get isGiftSpecialEffects => _isGiftSpecialEffects; + static set isGiftSpecialEffects(bool value) => _isGiftSpecialEffects = value; + + ///入场秀 + static bool _isEntryVehicleAnimation = true; + static bool get isEntryVehicleAnimation => _isEntryVehicleAnimation; + static set isEntryVehicleAnimation(bool value) => _isEntryVehicleAnimation = value; + + ///全局飘屏 + static bool _isFloatingAnimationInGlobal = true; + static bool get isFloatingAnimationInGlobal => _isFloatingAnimationInGlobal; + static set isFloatingAnimationInGlobal(bool value) => _isFloatingAnimationInGlobal = value; + + ///幸运礼物特效开关 + static bool _isLuckGiftSpecialEffects = true; + static bool get isLuckGiftSpecialEffects => _isLuckGiftSpecialEffects; + static set isLuckGiftSpecialEffects(bool value) => _isLuckGiftSpecialEffects = value; + + /// 获取当前业务逻辑策略 + static BusinessLogicStrategy get businessLogicStrategy { + return AppConfig.current.businessLogicStrategy; + } +} diff --git a/lib/app/constants/sc_room_msg_type.dart b/lib/app/constants/sc_room_msg_type.dart new file mode 100644 index 0000000..d6cb82e --- /dev/null +++ b/lib/app/constants/sc_room_msg_type.dart @@ -0,0 +1,113 @@ +class SCRoomMsgType { + static const String text = "TEXT"; + static const String image = "IMAGE"; + static const String joinRoom = "JOIN_ROOM"; + static const String systemTips = "SYSTEM_TIPS"; + + ///表情包 + static const String emoticons = "EMOTICONS"; + + + ///新用户 + static const String newUser = "NEW_USER"; + + ///上麦 + static const String shangMai = "SHANG_MAI"; + + ///下麦 + static const String xiaMai = "XIA_MAI"; + + ///踢下麦 + static const String killXiaMai = "KILL_XIA_MAI"; + + ///请出房间 + static const String qcfj = "QCFJ"; + + ///封麦 + static const String fengMai = "FENG_MAI"; + + ///解封 + static const String jieFeng = "JIE_FENG"; + + ///禁麦 + static const String jinMai = "JIN_MAI"; + + ///解禁 + static const String jieJin = "JIE_JIN"; + + ///送礼 + static const String gift = "GIFT"; + + ///幸运礼物飘向麦位 + static const String luckGiftAnimOther = "LUCK_GIFT_ANIM_OTHER"; + + ///送礼 + static const String allGift = "ALL_GIFT"; + + ///收到礼物 + static const String reciverGift = "RECIVER_GIFT"; + + ///抱上麦 + static const String bsm = "BSM"; + + ///欢迎语 + static const String welcome = "WELCOME"; + + ///禁言变动 + static const String jinyanList = "JINYAN_LIST"; + + ///麦位变动 + static const String micChange = "MIC_CHANGE"; + + ///房间身份变动 + static const String roomRoleChange = "ROOM_ROLE_CHANGE"; + + ///火箭进度条更新 + static const String rocketEnergyUpdate = "ROCKET_ENERGY_UPDSCE"; + + ///房间红包 + static const String roomRedPacket = "ROOM_RED_PACKET"; + + ///邀请进入房间 + static const String inviteRoom = "INVITE_ROOM"; + + ///用户火箭中奖 + static const String rocketRewardUser = "ROCKET_REWARD_USER"; + + ///管理变动 + static const String managerList = "MANAGER_LIST"; + + ///房间设置更新 + static const String roomSettingUpdate = "ROOM_SETTING_UPDSCE"; + + ///掷骰子 + static const String roomDice = "ROOM_DICE"; + + ///石头剪刀布 + static const String roomRPS = "ROOM_RPS"; + + ///幸运数字 + static const String roomLuckNumber = "ROOM_LUCK_NUMBER"; + + ///房间背景更新 + static const String roomBGUpdate = "ROOM_BG_UPDSCE"; + + ///幸运礼物 + static const String gameLuckyGift = "GAME_LUCKY_GIFT"; + + ///幸运礼物中奖5倍以及以上 + static const String gameLuckyGift_5 = "GAME_LUCKY_GIFT_5"; + + ///房间多人游戏关闭 + static const String roomGameClose = "ROOM_GAME_CLOSE"; + + ///房间游戏创建 + static const String roomGameCreate = "ROOM_GAME_CRESCE"; + + ///暂时不用监听这个 + static const String sendGift = "SEND_GIFT"; + static const String gameBurstCrystalSprint = "GAME_BURST_CRYSTAL_SPRINT"; + static const String gameBurstCrystalBox = "GAME_BURST_CRYSTAL_BOX"; + static const String refreshOnlineUser = "REFRESH_ONLINE_USER"; + +} diff --git a/lib/app/constants/sc_screen.dart b/lib/app/constants/sc_screen.dart new file mode 100644 index 0000000..1a34769 --- /dev/null +++ b/lib/app/constants/sc_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class SCScreen{ + static double designWidth = 375; + static double designHeight = 667; + static double width = ScreenUtil().setWidth(designWidth); + static double height = ScreenUtil().setHeight(designHeight); +} + +double width(double width) { + return ScreenUtil().setWidth(width); +} + +double height(double height) { + return ScreenUtil().setHeight(height); +} +double sp(double sp) { + return ScreenUtil().setSp(sp); +} \ No newline at end of file diff --git a/lib/app/routes/sc_404_page.dart b/lib/app/routes/sc_404_page.dart new file mode 100644 index 0000000..95076c7 --- /dev/null +++ b/lib/app/routes/sc_404_page.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class SCWidgetNotFound extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('页面不存在'), + ), + body: Center( + child: Text('页面不存在'), + ), + ); + } +} diff --git a/lib/app/routes/sc_fluro_navigator.dart b/lib/app/routes/sc_fluro_navigator.dart new file mode 100644 index 0000000..9b4d97e --- /dev/null +++ b/lib/app/routes/sc_fluro_navigator.dart @@ -0,0 +1,141 @@ +import 'dart:io'; + +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; +import 'package:yumi/modules/chat/chat_route.dart'; +import 'package:yumi/modules/auth/login_route.dart'; +import 'package:yumi/app/routes/sc_lk_application.dart'; + +/// 安卓页面切换时长 +const Duration kAndroidTransitionDuration = Duration(milliseconds: 375); + +/// 进入房间页动画时长 +const Duration kOpenRoomTransitionDuration = Duration(milliseconds: 325); + + +/// 进入房间页过渡动画 +SlideTransition kOpenRoomTransitionBuilder( BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child,) { + return SlideTransition( + position: Tween( + begin: const Offset(0.0, 1.0), + end: const Offset(0.0, 0.0), + ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)), + child: child, + ); +} + +/// fluro的路由跳转工具类 +class SCNavigatorUtils { + static bool inChatPage = false; + static bool inLoginPage = false; + //不需要页面返回值的跳转 + static Future push(BuildContext context, String path, + {bool replace = false, + bool clearStack = false, + TransitionType? transition, + Duration? transitionDuration, + RouteTransitionsBuilder? transitionBuilder}) async { + FocusScope.of(context).unfocus(); + inLoginPage = path.startsWith("${LoginRouter.login}"); + inChatPage = path.startsWith("${SCChatRouter.chat}"); + final result = await SCLkApplication.router.navigateTo(context, path, + replace: replace, + clearStack: clearStack, + ///在系统语言为ar语会有问题 + transition: (Platform.isAndroid + ? null + : TransitionType.cupertino), + // transitionDuration: transitionDuration ?? + // (Platform.isAndroid ? kAndroidTransitionDuration : null), + // transitionBuilder: transitionBuilder ?? + // (Platform.isAndroid ? (context, animation, secondaryAnimation, child) { + // return ScaleTransition( + // scale: Tween(begin: 1.0,end: 0.9).animate(CurvedAnimation(parent: secondaryAnimation, curve: Curves.ease)), + // child: SlideTransition( + // position: Tween( + // begin: const Offset(1.0, 0.0), + // end: const Offset(0.0, 0.0), + // ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)), + // child: child, + // ), + // ); + // } : null) + ); + + return result; + } + + //需要页面返回值的跳转 + static pushResult( + BuildContext context, String path, Function(Object) function, + {bool replace = false, + bool clearStack = false, + TransitionType? transition}) { + FocusScope.of(context).unfocus(); + inLoginPage = path == LoginRouter.login; + inChatPage = path.startsWith("${SCChatRouter.chat}"); + SCLkApplication.router + .navigateTo(context, path, + replace: replace, + clearStack: clearStack, + // transition: transition ?? + // (Platform.isAndroid + // ? TransitionType.custom + // : TransitionType.cupertino), + // transitionDuration: + // Platform.isAndroid ? kAndroidTransitionDuration : null, + // transitionBuilder: + // Platform.isAndroid ? (context, animation, secondaryAnimation, child) { + // return ScaleTransition( + // scale: Tween(begin: 1.0,end: 0.9).animate(CurvedAnimation(parent: secondaryAnimation, curve: Curves.ease)), + // child: SlideTransition( + // position: Tween( + // begin: const Offset(1.0, 0.0), + // end: const Offset(0.0, 0.0), + // ).animate(CurvedAnimation(parent: animation, curve: Curves.ease)), + // child: child, + // ), + // ); + // } : null + ) + .then((result) { + // 页面返回result为null + if (result == null) { + return; + } + function(result); + }).catchError((error) { + print('$error'); + }); + } + + /// 直接返回 + static void goBack(BuildContext context) { + if(inChatPage){ + inChatPage = false; + } + FocusScope.of(context).unfocus(); + Navigator.pop(context); + } + + /// 带参数返回 + static void goBackWithParams(BuildContext context, result) { + if(inChatPage){ + inChatPage = false; + } + FocusScope.of(context).unfocus(); + Navigator.pop(context, result); + } + + static void popUntil(BuildContext context, RoutePredicate predicate) { + if(inChatPage){ + inChatPage = false; + } + FocusScope.of(context).unfocus(); + Navigator.popUntil(context, predicate); + } +} diff --git a/lib/app/routes/sc_lk_application.dart b/lib/app/routes/sc_lk_application.dart new file mode 100644 index 0000000..e625013 --- /dev/null +++ b/lib/app/routes/sc_lk_application.dart @@ -0,0 +1,5 @@ +import 'package:fluro/fluro.dart' as fluro; + +class SCLkApplication { + static fluro.FluroRouter router = fluro.FluroRouter(); +} diff --git a/lib/app/routes/sc_router_init.dart b/lib/app/routes/sc_router_init.dart new file mode 100644 index 0000000..0517434 --- /dev/null +++ b/lib/app/routes/sc_router_init.dart @@ -0,0 +1,7 @@ + +import 'package:fluro/fluro.dart'; + +abstract class SCIRouterProvider{ + + void initRouter(FluroRouter router); +} \ No newline at end of file diff --git a/lib/app/routes/sc_routes.dart b/lib/app/routes/sc_routes.dart new file mode 100644 index 0000000..113b1f7 --- /dev/null +++ b/lib/app/routes/sc_routes.dart @@ -0,0 +1,75 @@ +import 'package:fluro/fluro.dart' as fluro; +import 'package:flutter/material.dart'; +import 'package:yumi/app/routes/sc_router_init.dart'; +import 'package:yumi/modules/country/country_route.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/modules/chat/chat_route.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/modules/index/index_page.dart'; +import 'package:yumi/modules/user/settings/settings_route.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/modules/auth/login_route.dart'; +import 'package:yumi/modules/room/voice_room_route.dart'; +import 'package:yumi/app/routes/sc_404_page.dart'; + +class SCRoutes { + static String home = '/'; + + static List _listRouter = []; + + static void configureRoutes(fluro.FluroRouter router) { + /// 指定路由跳转错误返回页 + router.notFoundHandler = fluro.Handler(handlerFunc: (BuildContext? context, Map> params) { + debugPrint('未找到目标页'); + return SCWidgetNotFound(); + }); + + router.define(home, + handler: fluro.Handler(handlerFunc: (BuildContext? context, Map> params) => SCIndexPage())); + + _listRouter.clear(); + + /// 各自路由由各自模块管理,统一在此添加初始化 + + /// 登陆路由组 + _listRouter.add(LoginRouter()); + _listRouter.add(CountryRoute()); + _listRouter.add(SettingsRoute()); + _listRouter.add(VoiceRoomRoute()); + _listRouter.add(SCMainRoute()); + _listRouter.add(WalletRoute()); + _listRouter.add(StoreRoute()); + _listRouter.add(SCChatRouter()); + + /// 初始化路由 + for (var routerProvider in _listRouter) { + routerProvider.initRouter(router); + } + } +} + +// PS:路由使用方法 + +// 1、不需要传参的 替换所有历史记录 +/// SCNavigatorUtils.push(context, LoginRouter.loginPage, replace: true); +/// 2、不需要传参的 不替换历史记录 +/// SCNavigatorUtils.push(context, LoginRouter.loginPage, replace: false); + +// 需要传参的 +// 3、SCNavigatorUtils.push(context,'${SCRoutes.webViewPage}?param1=${Uri.encodeComponent(content1)}¶m2=${Uri.encodeComponent(content2)}', replace: true); +// 4、SCNavigatorUtils.push(context, '${MarketRouter.hotCoin}?id=xxxxxxx'); + +// 有返回值跳转 +/// 5、SCNavigatorUtils.pushResult(context, MarketRouter.hotCoin, (result){ +// setState(() { +// //result是返回的结果 +// TestModel model = result; +// _name = model.name; +// }); +// }); + +// 返回上一级 +// 6、SCNavigatorUtils.goBack(context); + +// 带参数返回上一级 +// 7、SCNavigatorUtils.goBackWithParams(context, result); diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart new file mode 100644 index 0000000..1747257 --- /dev/null +++ b/lib/app_localizations.dart @@ -0,0 +1,1601 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; + +class SCAppLocalizations { + final Locale locale; + + SCAppLocalizations(this.locale); + + static SCAppLocalizations? of(BuildContext context) { + return Localizations.of(context, SCAppLocalizations); + } + + static const LocalizationsDelegate delegate = + _SCAppLocalizationsDelegate(); + + Map? _localizedStrings; + + Future load() async { + final jsonString = await rootBundle.loadString( + 'assets/l10n/intl_${locale.languageCode}.json', + ); + + final Map jsonMap = json.decode(jsonString); + + _localizedStrings = jsonMap.map((key, value) { + return MapEntry(key, value.toString()); + }); + + return true; + } + + String translate(String key) { + return _localizedStrings?[key] ?? key; + } + + // 添加具体翻译键的getter方法 + String get signInWithGoogle => translate('signInWithGoogle'); + + String get or => translate('or'); + + String get yes => translate('yes'); + + String get sound2 => translate('sound2'); + + String get recent => translate('recent'); + + String get shop => translate('shop'); + + String get startYourBrandNewJourney => translate('startYourBrandNewJourney'); + + String get other => translate('other'); + + String get maliciousHarassment => translate('maliciousHarassment'); + + String get roomEdit => translate('roomEdit'); + + String get roomOwner => translate('roomOwner'); + + String get searchNoDataTips => translate('searchNoDataTips'); + + String get preventBeingBlocked => translate('preventBeingBlocked'); + + String get enableRankIncognitoMode => translate('enableRankIncognitoMode'); + + String get vipPrivilege => translate('vipPrivilege'); + + String get expirationTime => translate('expirationTime'); + + String get inviteNewUsersToEarnCoins => translate('inviteNewUsersToEarnCoins'); + + String get avoidBeingKicked => translate('avoidBeingKicked'); + + String get vipEntranceEffect => translate('vipEntranceEffect'); + + String get vipSpecialGiftTassel => translate('vipSpecialGiftTassel'); + + String get youHaveNotHadVIPYet => translate('youHaveNotHadVIPYet'); + + String get games => translate('games'); + + String get popularEvents => translate('popularEvents'); + + String get pleaseEnterContent => translate('pleaseEnterContent'); + + String get profilePhoto => translate('profilePhoto'); + + String get aboutMe => translate('aboutMe'); + + String get numberOfMic => translate('numberOfMic'); + + String get noHistoricalRecordsAvailable => translate('noHistoricalRecordsAvailable'); + + String get myRoom => translate('myRoom'); + + String get cancelRoomPassword => translate('cancelRoomPassword'); + + String get roomMemberFee => translate('roomMemberFee'); + + String get roomPassword => translate('roomPassword'); + + String get roomTheme2 => translate('roomTheme2'); + + String get blockedList2 => translate('blockedList2'); + + String get crateMyRoom => translate('crateMyRoom'); + + String get exclusiveEmojiWillBeReleasedAfterBecoming => translate('exclusiveEmojiWillBeReleasedAfterBecoming'); + + String get socialPrivilege => translate('socialPrivilege'); + + String get mysteriousInvisibility => translate('mysteriousInvisibility'); + + String get antiBlock => translate('antiBlock'); + + String get goToUpgrade => translate('goToUpgrade'); + + String get createFamilyForFree => translate('createFamilyForFree'); + + String get privateChat => translate('privateChat'); + + String get everyone => translate('everyone'); + + String get basicPermissions => translate('basicPermissions'); + + String get vipBirthdayGift => translate('vipBirthdayGift'); + + String get pleaseUpgradeYourVipLevel => translate('pleaseUpgradeYourVipLevel'); + + String get membershipFreeChatSpeak => translate('membershipFreeChatSpeak'); + + String get priorityRoomSorting => translate('priorityRoomSorting'); + + String get userColoredID => translate('userColoredID'); + + String get casualInteraction => translate('casualInteraction'); + + String get haveGamePlayingTips => translate('haveGamePlayingTips'); + + String get historicalTour => translate('historicalTour'); + + String get gameCenter => translate('gameCenter'); + + String get returnToVoiceChat => translate('returnToVoiceChat'); + + String get exitGameMode => translate('exitGameMode'); + + String get enterTheRoom => translate('enterTheRoom'); + + String get taskNamePersonalGameConsume => translate('taskNamePersonalGameConsume'); + + String get taskNamePersonalMicInRoom => translate('taskNamePersonalMicInRoom'); + + String get taskNamePersonalActiveInRoom => translate('taskNamePersonalActiveInRoom'); + + String get taskNameRoomOwnerMicTime => translate('taskNameRoomOwnerMicTime'); + + String get taskNamePersonalLuckyGiftGold => translate('taskNamePersonalLuckyGiftGold'); + + String get taskNamePersonalMagicGiftGold => translate('taskNamePersonalMagicGiftGold'); + + String get taskNameRoomOwnerSendGiftGold => translate('taskNameRoomOwnerSendGiftGold'); + + String get taskNameRoomUserSendGiftGold => translate('taskNameRoomUserSendGiftGold'); + + String get taskNameRoomOwnerInviteMic => translate('taskNameRoomOwnerInviteMic'); + + String get taskNameRoomOnlineUserCount => translate('taskNameRoomOnlineUserCount'); + + String get taskNamePersonalSendGift => translate('taskNamePersonalSendGift'); + + String get taskNameRoomMicUser30Min => translate('taskNameRoomMicUser30Min'); + + String get taskNameRoomMicUser60Min => translate('taskNameRoomMicUser60Min'); + + String get taskNameRoomMicUser120Min => translate('taskNameRoomMicUser120Min'); + + String get taskNameRoomOwnerSendGiftUser => translate('taskNameRoomOwnerSendGiftUser'); + + String get taskNameRoomOwnerSendRedPacket => translate('taskNameRoomOwnerSendRedPacket'); + + String get taskNameRoomNewMember => translate('taskNameRoomNewMember'); + + String get copyLink => translate('copyLink'); + + String get shareTo => translate('shareTo'); + + String get invite => translate('invite'); + + String get faceBook => translate('faceBook'); + + String get whatsApp => translate('whatsApp'); + + String get snapChat => translate('snapChat'); + + String get complete => translate('complete'); + + String get claim => translate('claim'); + + String get inviteGoRoomTips => translate('inviteGoRoomTips'); + + String get dailyCoinBonanzaRules => translate('dailyCoinBonanzaRules'); + + String get dailyCoinBonanzaRulesDetail => translate('dailyCoinBonanzaRulesDetail'); + + String get personalTasks => translate('personalTasks'); + + String get roomOwnerTasks => translate('roomOwnerTasks'); + + String get getPaidToRefer => translate('getPaidToRefer'); + + String get weekStart => translate('weekStart'); + + String get kingQuuen => translate('kingQuuen'); + + String get coinsReceived => translate('coinsReceived'); + + String get operationFail => translate('operationFail'); + + String get forMoreRewardsPleaseCheckTheTaskCenter => translate('forMoreRewardsPleaseCheckTheTaskCenter'); + + String get likedYourComment => translate('likedYourComment'); + + String get deleteAccountTips2 => translate('deleteAccountTips2'); + + String get likedYourDynamic => translate('likedYourDynamic'); + + String get doYouWantToKeepTheDraft => translate('doYouWantToKeepTheDraft'); + + String get createDynamicSuccess => translate('createDynamicSuccess'); + + String get like => translate('like'); + + String get noPromptsToday => translate('noPromptsToday'); + + String get currentVip => translate('currentVip'); + + String get ramadan => translate('ramadan'); + + String get deleteDynamicTips => translate('deleteDynamicTips'); + + String get deleteCommentTips => translate('deleteCommentTips'); + + String get deleteSuccessful => translate('deleteSuccessful'); + + String get itemsLeft => translate('itemsLeft'); + + String get replySucc => translate('replySucc'); + + String get basicFeatures => translate('basicFeatures'); + + String get areYouSureYouWantToDeleteYourAccount => translate('areYouSureYouWantToDeleteYourAccount'); + + String get game => translate('game'); + + String get accountDeletionNotice => translate('accountDeletionNotice'); + + String get deleteAccountTips => translate('deleteAccountTips'); + + String get thisUserHasBeenBlacklisted => translate('thisUserHasBeenBlacklisted'); + + String get comment => translate('comment'); + + String get cantSendDynamicTips => translate('cantSendDynamicTips'); + + String get customizedGiftRules => translate('customizedGiftRules'); + + String get customizedGiftRulesContent => translate('customizedGiftRulesContent'); + + String get clearCache => translate('clearCache'); + + String get saySomething => translate('saySomething'); + + String get catchFirstComment => translate('catchFirstComment'); + + String get dynamicT => translate('dynamic'); + + String get more => translate('more'); + + String get roomAnnouncement => translate('roomAnnouncement'); + + String get reply => translate('reply'); + + String get importantReminder => translate('importantReminder'); + + String get enterThisVoiceChatRoom => translate('enterThisVoiceChatRoom'); + + String get swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt => translate('swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt'); + + String get areYouSureYouWantToClearLocalCache => translate('areYouSureYouWantToClearLocalCache'); + + String get multiple => translate('multiple'); + + String get posting => translate('posting'); + + String get showMore => translate('showMore'); + + String get showLess => translate('showLess'); + + String get clearCacheSuccessfully => translate('clearCacheSuccessfully'); + + String get successfullyAddedToTheBlacklist => translate('successfullyAddedToTheBlacklist'); + + String get successfullyRemovedFromTheBlacklist => translate('successfullyRemovedFromTheBlacklist'); + + String get successfullyRemovedFromTheDynamicBlacklist => translate('successfullyRemovedFromTheDynamicBlacklist'); + + String get successfullyAddedToTheDynamicBlacklist => translate('successfullyAddedToTheDynamicBlacklist'); + + String get areYouSureToCancelBlacklist => + translate('areYouSureToCancelBlacklist'); + + String get areYouSureYouWantToBlockThisUser => + translate('areYouSureYouWantToBlockThisUser'); + + String get areYouSureYouWantToDynamicBlockThisUser => + translate('areYouSureYouWantToDynamicBlockThisUser'); + + String get removeFromBlacklist => translate('removeFromBlacklist'); + + String get unblockUserDynamic => translate('unblockUserDynamic'); + + String get blockUserDynamic => translate('blockUserDynamic'); + + String get moveToBlacklist => translate('moveToBlacklist'); + + String get userBlacklist => translate('userBlacklist'); + + String get areYouSureToCancelDynamicBlacklist => translate('areYouSureToCancelDynamicBlacklist'); + + String get thisFeatureIsCurrentlyUnavailable => + translate('thisFeatureIsCurrentlyUnavailable'); + + String get pleaseSelectTheRecipient => translate('pleaseSelectTheRecipient'); + + String get searchMemberIdHint => translate('searchMemberIdHint'); + + String get win => translate('win'); + + String get luckGiftSpecialEffects => translate('luckGiftSpecialEffects'); + + String get receivedFromALuckyGift => translate('receivedFromALuckyGift'); + + String get entryVehicleAnimation => translate('entryVehicleAnimation'); + + String get entryVehicleAnimation2 => translate('entryVehicleAnimation2'); + + String get floatingAnimationInGlobal => + translate('floatingAnimationInGlobal'); + + String get theRedEnvelopeHasExpired => translate('theRedEnvelopeHasExpired'); + + String get wishingYouHappinessEveryDay => + translate('wishingYouHappinessEveryDay'); + + String get newMessage => translate('newMessage'); + + String get familyNews => translate('familyNews'); + + String get familyName => translate('familyName'); + + String get help => translate('help'); + + String get open => translate('open'); + + String get canSendMsgTips => translate('canSendMsgTips'); + + String get systemAnnouncement => translate('systemAnnouncement'); + + String get systemAnnouncementTips1 => translate('systemAnnouncementTips1'); + + String get contactUs => translate('contactUs'); + + String get sentARedEnvelope => translate('sentARedEnvelope'); + + String get systemAnnouncementTips => translate('systemAnnouncementTips'); + + String get doNotClickUnfamiliarTips => translate('doNotClickUnfamiliarTips'); + + String get disbandTheFamilyTips => translate('disbandTheFamilyTips'); + + String get familyHelpTips => translate('familyHelpTips'); + + String get familyInfo => translate('familyInfo'); + + String get joinRequest => translate('joinRequest'); + + String get createRoomSuccsess => translate('createRoomSuccsess'); + + String get enterFamilyInfo => translate('enterFamilyInfo'); + + String get searchFamilyIdHint => translate('searchFamilyIdHint'); + + String get enterFamilyName => translate('enterFamilyName'); + + String get cpList => translate('cpList'); + + String get redEnvelopeNotYetClaimed => translate('redEnvelopeNotYetClaimed'); + + String get refuse => translate('refuse'); + + String get sayHi2 => translate('sayHi2'); + + String get rulesUpload => translate('rulesUpload'); + + String get agree => translate('agree'); + + String get coins3 => translate('coins3'); + + String get trend => translate('trend'); + + String get discard => translate('discard'); + + String get atTag => translate('atTag'); + + String get keep => translate('keep'); + + String get magic => translate('magic'); + + String get customized => translate('customized'); + + String get unclaimedRedEnvelopes => translate('unclaimedRedEnvelopes'); + + String get boxContributeTips => translate('boxContributeTips'); + + String get supporterWeeklyRank => translate('supporterWeeklyRank'); + + String get hostWeeklyRank => translate('hostWeeklyRank'); + + String get noMatchedCP => translate('noMatchedCP'); + + String get giftSpecialEffects => translate('giftSpecialEffects'); + + String get familyLevel => translate('familyLevel'); + + String get applyToJoin => translate('applyToJoin'); + + String get msgSendRedEnvelopeTips => translate('msgSendRedEnvelopeTips'); + + String get ownerSendTheRedEnvelope => translate('ownerSendTheRedEnvelope'); + + String get hotGames => translate('hotGames'); + + String get levelPrivileges => translate('levelPrivileges'); + + String get cpRequest => translate('cpRequest'); + + String get areYouSureYouWantToSpend3 => + translate('areYouSureYouWantToSpend3'); + + String get areYouSureYouWantToSpend => translate('areYouSureYouWantToSpend'); + + String get areYouSureYouWantToSpend2 => + translate('areYouSureYouWantToSpend2'); + + String get allGames => translate('allGames'); + + String get restorePurchases => translate('restorePurchases'); + + String get createFamily => translate('createFamily'); + + String get agreeJoinFamilyTips => translate('agreeJoinFamilyTips'); + + String get refuseJoinFamilyTips => translate('refuseJoinFamilyTips'); + + String get welcomeToMyFamily => translate('welcomeToMyFamily'); + + String get slotsClass => translate('slotsClass'); + + String get greedyClass => translate('greedyClass'); + + String get raceSeries => translate('raceSeries'); + + String get fishClass => translate('fishClass'); + + String get supporterList => translate('supporterList'); + + String get hostList => translate('hostList'); + + String get pending => translate('pending'); + + String get approved => translate('approved'); + + String get restorePurchasesTips => translate('restorePurchasesTips'); + + String get rejected => translate('rejected'); + + String get applicationRecord => translate('applicationRecord'); + + String get joinFamily => translate('joinFamily'); + + String get received => translate('received'); + + String get others => translate('others'); + + String get cpSexTips => translate('cpSexTips'); + + String get editProfile => translate('editProfile'); + + String get myPhoto => translate('myPhoto'); + + String get balanceNotEnough => translate('balanceNotEnough'); + + String get chooseFromAblum => translate('chooseFromAblum'); + + String get information => translate('information'); + + String get memberList => translate('memberList'); + + String get supporter => translate('supporter'); + + String get family => translate('family'); + + String get spaceBackground => translate('spaceBackground'); + + String get currentProgress => translate('currentProgress'); + + String get redEnvelopeTips2 => translate('redEnvelopeTips2'); + + String get openTheTreasureChest => translate('openTheTreasureChest'); + + String get number2 => translate('number2'); + + String get underReview => translate('underReview'); + + String get createAFamily => translate('createAFamily'); + + String get doYouWantToDeleteIt => translate('doYouWantToDeleteIt'); + + String get lastWeekProgress => translate('lastWeekProgress'); + + String get roomReward => translate('roomReward'); + + String get goToRecharge => translate('goToRecharge'); + + String get insufhcientGoldsGoToRecharge => + translate('insufhcientGoldsGoToRecharge'); + + String get redEnvelopeRecTips2 => translate('redEnvelopeRecTips2'); + + String get redEnvelopeRecTips3 => translate('redEnvelopeRecTips3'); + + String get sendARedEnvelope => translate('sendARedEnvelope'); + + String get redEnvelopeSendingRecords => + translate('redEnvelopeSendingRecords'); + + String get countdownMinutes => translate('countdownMinutes'); + + String get spam => translate('spam'); + + String get sendRedPackConfirmTips => translate('sendRedPackConfirmTips'); + + String get redEnvelopeTips1 => translate('redEnvelopeTips1'); + + String get roomTools => translate('roomTools'); + + String get fraud => translate('fraud'); + + String get redEnvelope => translate('redEnvelope'); + + String get dice => translate('dice'); + + String get rps => translate('rps'); + + String get operationsAreTooFrequent => translate('operationsAreTooFrequent'); + + String get luckNumber => translate('luckNumber'); + + String get entertainment => translate('entertainment'); + + String get reportSucc => translate('reportSucc'); + + String get reportInputTips => translate('reportInputTips'); + + String get pornography => translate('pornography'); + + String get redEnvelopeRecTips1 => translate('redEnvelopeRecTips1'); + + String get inappropriateContent => translate('inappropriateContent'); + + String get personalAttack => translate('personalAttack'); + + String get illegalInformation => translate('illegalInformation'); + + String get pleaseSelectTheTypeContent => + translate('pleaseSelectTheTypeContent'); + + String get searchUserId => translate('searchUserId'); + + String get sendUser => translate('sendUser'); + + String get userName => translate('userName'); + + String get identity => translate('identity'); + + String get roomProfilePicture => translate('roomProfilePicture'); + + String get userProfilePicture => translate('userProfilePicture'); + + String get roomNotice => translate('roomNotice'); + + String get roomTheme => translate('roomTheme'); + + String get warning => translate('warning'); + + String get adjust => translate('adjust'); + + String get inputDesHint => translate('inputDesHint'); + + String get screenshotTips => translate('screenshotTips'); + + String get description => translate('description'); + + String get pleaseSelectTheTypeToProcess => + translate('pleaseSelectTheTypeToProcess'); + + String get enterTheRoomId => translate('enterTheRoomId'); + + String get enterTheUserId => translate('enterTheUserId'); + + String get roomEditing => translate('roomEditing'); + + String get userEditing => translate('userEditing'); + + String get wearHonor => translate('wearHonor'); + + String get youDontHaveAnyHonorYet => translate('youDontHaveAnyHonorYet'); + + String get activityHonor => translate('activityHonor'); + + String get achievementHonor => translate('achievementHonor'); + + String get letGoToWatch => translate('letGoToWatch'); + + String get launchedARocket => translate('launchedARocket'); + + String get useCoupontips => translate('useCoupontips'); + + String get joinMemberTips2 => translate('joinMemberTips2'); + + String get leaveRoomIdentityTips => translate('leaveRoomIdentityTips'); + + String get followSucc => translate('followSucc'); + + String get searchCouponHint => translate('searchCouponHint'); + + String get signInWithYourAccount => translate('signInWithYourAccount'); + + String get signInWithApple => translate('signInWithApple'); + + String get deleteConversationTips => translate('deleteConversationTips'); + + String get loginRepresentsAgreementTo => + translate('loginRepresentsAgreementTo'); + + String get termsofService => translate('termsofService'); + + String get theImageSizeCannotExceed => translate('theImageSizeCannotExceed'); + + String get privaceyPolicy => translate('privaceyPolicy'); + + String get youHaventFollowed => translate('youHaventFollowed'); + + String get tips => translate('tips'); + + String get inviteYouToBecomeBD => translate('inviteYouToBecomeBD'); + + String get bDLeaderInviteYouToBecomeBDLeader => + translate('bDLeaderInviteYouToBecomeBDLeader'); + + String get adminInviteRechargeAgent => translate('adminInviteRechargeAgent'); + + String get task => translate('task'); + + String get honor => translate('honor'); + + String get bio => translate('bio'); + + String get hobby => translate('hobby'); + + String get search => translate('search'); + + String get sendCoupontips => translate('sendCoupontips'); + + String get coupon => translate('coupon'); + + String get done => translate('done'); + + String get join => translate('join'); + + String get roomRocketHelpTips => translate('roomRocketHelpTips'); + + String get giveUpIdentity => translate('giveUpIdentity'); + + String get bdLeader => translate('bdLeader'); + + String get picture => translate('picture'); + + String get signedin => translate('signedin'); + + String get couponRecord => translate('couponRecord'); + + String get improvementTasks => translate('improvementTasks'); + + String get image => translate('image'); + + String get inRocket => translate('inRocket'); + + String get video => translate('video'); + + String get get => translate('get'); + + String get sound => translate('sound'); + + String get wearMedal => translate('wearMedal'); + + String get sureUnfollowThisRoom => translate('sureUnfollowThisRoom'); + + String get camera => translate('camera'); + + String get crop => translate('crop'); + + String get inRoom => translate('inRoom'); + + String get youDontHaveAnyCouponsYet => translate('youDontHaveAnyCouponsYet'); + + String get finish => translate('finish'); + + String get visitorList => translate('visitorList'); + + String get gift2 => translate('gift2'); + + String get receiveSucc => translate('receiveSucc'); + + String get album => translate('album'); + + String get dailyTasks => translate('dailyTasks'); + + String get reject => translate('reject'); + + String get messageHasBeenRecalled => translate('messageHasBeenRecalled'); + + String get accept => translate('accept'); + + String get go => translate('go'); + + String get copy => translate('copy'); + + String get openUserProfleCard => translate('openUserProfleCard'); + + String get deleteOnAllDevices => translate('deleteOnAllDevices'); + + String get deleteFromMyDevice => translate('deleteFromMyDevice'); + + String get recallThisMessage => translate('recallThisMessage'); + + String get recall => translate('recall'); + + String get followedYou => translate('followedYou'); + + String get confirmAcceptTheInvitation => + translate('confirmAcceptTheInvitation'); + + String get confirmDeclineTheInvitation => + translate('confirmDeclineTheInvitation'); + + String get propMessagePrompt => translate('propMessagePrompt'); + + String get chats => translate('chats'); + + String get delete => translate('delete'); + + String get enterRoomConfirmTips => translate('enterRoomConfirmTips'); + + String get inputUserId => translate('inputUserId'); + + String get enterNickname => translate('enterNickname'); + + String get inviteCode => translate('inviteCode'); + + String get notifcation => translate('notifcation'); + + String get gameRules => translate('gameRules'); + + String get male => translate('male'); + + String get familyNotifcations => translate('familyNotifcations'); + + String get female => translate('female'); + + String get pullToLoadMore => translate('pullToLoadMore'); + + String get loadingFailedClickToRetry => + translate('loadingFailedClickToRetry'); + + String get releaseToLoadMore => translate('releaseToLoadMore'); + + String get haveMyLimits => translate('haveMyLimits'); + + String get becomeAgent => translate('becomeAgent'); + + String get setAccount => translate('setAccount'); + + String get deleteAccount => translate('deleteAccount'); + + String get backTheRoom => translate('backTheRoom'); + + String get spendCoinsToGainExperiencePoints => + translate('spendCoinsToGainExperiencePoints'); + + String get enterRoomName => translate('enterRoomName'); + + String get selectYourCountry => translate('selectYourCountry'); + + String get theMembershipFee => translate('theMembershipFee'); + + String get termsOfServicePrivacyPolicyTips => + translate('termsOfServicePrivacyPolicyTips'); + + String get and => translate('and'); + + String get inviteYouToBecomeHost => translate('inviteYouToBecomeHost'); + + String get inviteYouToBecomeAgent => translate('inviteYouToBecomeAgent'); + + String get theVideoSizeCannotExceed => translate('theVideoSizeCannotExceed'); + + String get clickHereToStartChatting => translate('clickHereToStartChatting'); + + String get music => translate('music'); + + String get unread => translate('unread'); + + String get checkInSuccessful => translate('checkInSuccessful'); + + String get sginTips => translate('sginTips'); + + String get adminCenter => translate('adminCenter'); + + String get confirm => translate('confirm'); + + String get cancel => translate('cancel'); + + String get reapply => translate('reapply'); + + String get takeTheMic => translate('takeTheMic'); + + String get unlockTheMic => translate('unlockTheMic'); + + String get lockTheMic => translate('lockTheMic'); + + String get removeTheMic => translate('removeTheMic'); + + String get leavelTheMic => translate('leavelTheMic'); + + String get muteTheMic => translate('muteTheMic'); + + String get openTheMic => translate('openTheMic'); + + String get leavFamilyTips => translate('leavFamilyTips'); + + String get leavingTheFamily => translate('leavingTheFamily'); + + String get inviteToTheMicrophone => translate('inviteToTheMicrophone'); + + String get toConsume => translate('toConsume'); + + String get userLevel => translate('userLevel'); + + String get settings => translate('settings'); + + String get receive => translate('receive'); + + String get sendTheCpRequest => translate('sendTheCpRequest'); + + String get addCp => translate('addCp'); + + String get scrollToTheBottom => translate('scrollToTheBottom'); + + String get host => translate('host'); + + String get read => translate('read'); + + String get system => translate('system'); + + String get receivedAMessage => translate('receivedAMessage'); + + String get stop => translate('stop'); + + String get familyOwner2 => translate('familyOwner2'); + + String get agent => translate('agent'); + + String get obtain => translate('obtain'); + + String get apple => translate('apple'); + + String get google => translate('google'); + + String get wins => translate('wins'); + + String get ra => translate('ra'); + + String get bd => translate('bd'); + + String get charm => translate('charm'); + + String get specialEffectsManagement => translate('specialEffectsManagement'); + + String get giftCounter => translate('giftCounter'); + + String get resetLoginPasswordtTips2 => translate('resetLoginPasswordtTips2'); + + String get enterYourOldPassword => translate('enterYourOldPassword'); + + String get inputYourOldPassword => translate('inputYourOldPassword'); + + String get localMusic => translate('localMusic'); + + String get pleaseUpgradeYourVipLevelFirst => + translate('pleaseUpgradeYourVipLevelFirst'); + + String get howToUpgrade => translate('howToUpgrade'); + + String get higherLevelFancierAvatarFrame => + translate('higherLevelFancierAvatarFrame'); + + String get touristsTakeToTheMic => translate('touristsTakeToTheMic'); + + String get pleaseChatFfriendly => translate('pleaseChatFfriendly'); + + String get startVoiceParty => translate('startVoiceParty'); + + String get mountains => translate('mountains'); + + String get account => translate('account'); + + String get fromLuckyGifts => translate('fromLuckyGifts'); + + String get setLoginPassword => translate('setLoginPassword'); + + String get theTwoPasswordsDoNotMatch => + translate('theTwoPasswordsDoNotMatch'); + + String get confirmYourPassword => translate('confirmYourPassword'); + + String get resetLoginPasswordtTips1 => translate('resetLoginPasswordtTips1'); + + String get medalAndAvatarFrameRewards => + translate('medalAndAvatarFrameRewards'); + + String get theModificationsMade => translate('theModificationsMade'); + + String get touristsSendText => translate('touristsSendText'); + + String get membershipFeeTips1 => translate('membershipFeeTips1'); + + String get membershipFeeTips2 => translate('membershipFeeTips2'); + + String get membershipFee => translate('membershipFee'); + + String get setYourPassword => translate('setYourPassword'); + + String get vip => translate('vip'); + + String get vip1 => translate('vip1'); + + String get vip2 => translate('vip2'); + + String get vip3 => translate('vip3'); + + String get vip4 => translate('vip4'); + + String get vip5 => translate('vip5'); + + String get vip6 => translate('vip6'); + + String get vipChatBox => translate('vipChatBox'); + + String get sendTo => translate('sendTo'); + + String get level => translate('level'); + + String get add => translate('add'); + + String get free => translate('free'); + + String get openRedPackDialogTip => translate('openRedPackDialogTip'); + + String get start => translate('start'); + + String get vipUseThisThemeTips => translate('vipUseThisThemeTips'); + + String get charmGameRulesTips => translate('charmGameRulesTips'); + + + String get enterYourNewPassword => translate('enterYourNewPassword'); + + String get confirmSwitchMicThemeTips => + translate('confirmSwitchMicThemeTips'); + + String get confirmSwitchMicModelTips => + translate('confirmSwitchMicModelTips'); + + String get micTheme => translate('micTheme'); + + String get conntinue => translate('conntinue'); + + String get renewVip => translate('renewVip'); + + String get buyVip => translate('buyVip'); + + String get couple2 => translate('couple2'); + + String get personal2 => translate('personal2'); + + String get family2 => translate('family2'); + + String get password => translate('password'); + + String get days => translate('days'); + + String get vipMicTheme => translate('vipMicTheme'); + + String get myMusic => translate('myMusic'); + + String get partWays => translate('partWays'); + + String get reconcileInvitationTips => translate('reconcileInvitationTips'); + + String get reconcileInvitation => translate('reconcileInvitation'); + + String get reconcile => translate('reconcile'); + + String get areYouSureYouWantToSpend4 => + translate('areYouSureYouWantToSpend4'); + + String get partWaysTips => translate('partWaysTips'); + + String get areYouSureYouWantToPartWaysWithYourCP => + translate('areYouSureYouWantToPartWaysWithYourCP'); + + String get separated => translate('separated'); + + String classicMic(String name) => + translate('classicMic').replaceAll('{1}', name); + + String redEnvelopeAmount(String name) => + translate('redEnvelopeAmount').replaceAll('{1}', name); + + String yesterday(String name) => + translate('yesterday').replaceAll('{1}', name); + + String monday(String name) => translate('monday').replaceAll('{1}', name); + + String tuesday(String name) => translate('tuesday').replaceAll('{1}', name); + + String wednesday(String name) => + translate('wednesday').replaceAll('{1}', name); + + String vipEmoticon(String name) => + translate('vipEmoticon').replaceAll('{1}', name); + + String family3(String name) => + translate('family3').replaceAll('{1}', name); + + String onlineUsers(String name1, String name2) => + translate( + 'onlineUsers', + ).replaceAll('{1}', name1).replaceAll('{2}', name2); + + String timeSpentTogether(String name) => + translate('timeSpentTogether').replaceAll('{1}', name); + + String firstDay(String name) => translate('firstDay').replaceAll('{1}', name); + + String thursday(String name) => translate('thursday').replaceAll('{1}', name); + + String friday(String name) => translate('friday').replaceAll('{1}', name); + + String saturday(String name) => translate('saturday').replaceAll('{1}', name); + + String sunday(String name) => translate('sunday').replaceAll('{1}', name); + + String win2(String name) => translate('win2').replaceAll('{1}', name); + + String numberOfMyCPs(String name1, String name2) => + translate( + 'numberOfMyCPs', + ).replaceAll('{1}', name1).replaceAll('{2}', name2); + + String sendUserId(String name) => + translate('sendUserId').replaceAll('{1}', name); + + String youAccepted(String name) => + translate('youAccepted').replaceAll('{1}', name); + + String acceptedYour(String name) => + translate('acceptedYour').replaceAll('{1}', name); + + String couple(String name) => translate('couple').replaceAll('{1}', name); + + String areYouSureYouWantToSpend5(String name) => + translate('areYouSureYouWantToSpend5').replaceAll('{1}', name); + + String areYouSureYouWantToSpend6(String name) => + translate('areYouSureYouWantToSpend6').replaceAll('{1}', name); + + String get micManagement => translate('micManagement'); + + String get logIn => translate('logIn'); + + String get sayHi => translate('sayHi'); + + String get permanent => translate('permanent'); + + String get enterAccount => translate('enterAccount'); + + String get cantBuyVip => translate('cantBuyVip'); + + String get vipRoomCoverBorder => translate('vipRoomCoverBorder'); + + String get bdCenter => translate('bdCenter'); + + String get number => translate('number'); + + String get successfulWear => translate('successfulWear'); + + String get successfullyUnloaded => translate('successfullyUnloaded'); + + String get common => translate('common'); + + String get wealthLevel => translate('wealthLevel'); + + String get roomName => translate('roomName'); + + String get enterPassword => translate('enterPassword'); + + String get goToUpload => translate('goToUpload'); + + String get language => translate('language'); + + String get feedback => translate('feedback'); + + String get about => translate('about'); + + String get unFollow => translate('unFollow'); + + String get vipBadge => translate('vipBadge'); + + String get vipProfileFrame => translate('vipProfileFrame'); + + String get vipProfileCard => translate('vipProfileCard'); + + String get profile => translate('profile'); + + String get props => translate('props'); + + String get relationShip => translate('relationShip'); + + String get medal => translate('medal'); + + String get blockedList => translate('blockedList'); + + String get renewal => translate('renewal'); + + String get daily => translate('daily'); + + String get vipExclusiveVehicles => translate('vipExclusiveVehicles'); + + String get kickPrevention => translate('kickPrevention'); + + String get freeChatSpeak => translate('freeChatSpeak'); + + String get weekly => translate('weekly'); + + String get monthly => translate('monthly'); + + String get resetLoginPassword => translate('resetLoginPassword'); + + String get confirmUseTips => translate('confirmUseTips'); + + String get confirmUnUseTips => translate('confirmUnUseTips'); + + String get inUse => translate('inUse'); + + String get themeGoToUploadTips => translate('themeGoToUploadTips'); + + String get myItems => translate('myItems'); + + String get items => translate('items'); + + String get admin => translate('admin'); + + String get medals => translate('medals'); + + String get fansList => translate('fansList'); + + String get message => translate('message'); + + String get friends => translate('friends'); + + String get rechargeAgency => translate('rechargeAgency'); + + String get announcement => translate('announcement'); + + String get aboutUs => translate('aboutUs'); + + String get logout => translate('logout'); + + String get home => translate('home'); + + String get followList => translate('followList'); + + String get country2 => translate('country2'); + + String get giftwall => translate('giftwall'); + + String get purchaseIsSuccessful => translate('purchaseIsSuccessful'); + + String get member => translate('member'); + + String get explore => translate('explore'); + + String get me => translate('me'); + + String get expired => translate('expired'); + + String get confirmBuyTips => translate('confirmBuyTips'); + + String get buy => translate('buy'); + + String get use => translate('use'); + + String get unUse => translate('unUse'); + + String get day => translate('day'); + + String get popular => translate('popular'); + + String get recommend => translate('recommend'); + + String get follow => translate('follow'); + + String get followed => translate('followed'); + + String get following => translate('following'); + + String get history => translate('history'); + + String get hotRooms => translate('hotRooms'); + + String get viewMore => translate('viewMore'); + + String get noData => translate('noData'); + + String get freePrice => translate('freePrice'); + + String get purchase => translate('purchase'); + + String get wallet => translate('wallet'); + + String get viewFrame => translate('viewFrame'); + + String get touristsAreNotAllowedToGoOnTheMic => + translate('touristsAreNotAllowedToGoOnTheMic'); + + String get inputRoomPassword => translate('inputRoomPassword'); + + String get users => translate('users'); + + String get rooms => translate('rooms'); + + String get store => translate('store'); + + String get bag => translate('bag'); + + String get coins => translate('coins'); + + String get coins4 => translate('coins4'); + + String get custom => translate('custom'); + + String get touristsCannotSendMessages => + translate('touristsCannotSendMessages'); + + String get setRoomPassword => translate('setRoomPassword'); + + String get goldList => translate('goldList'); + + String get submit => translate('submit'); + + String get theme => translate('theme'); + + String get chatBox => translate('chatBox'); + + String get pleaseUploadUserAvatar => translate('pleaseUploadUserAvatar'); + + String get headdress => translate('headdress'); + + String get searchInputHint => translate('searchInputHint'); + + String get kickRoomTips => translate('kickRoomTips'); + + String get joinRoomTips => translate('joinRoomTips'); + + String get youAreCurrentlyCPRelationshipPleaseDissolve => + translate('youAreCurrentlyCPRelationshipPleaseDissolve'); + + String get joinMemberTips => translate('joinMemberTips'); + + String get systemRoomTips => translate('systemRoomTips'); + + String get roomSetting => translate('roomSetting'); + + String get roomDetails => translate('roomDetails'); + + String get copiedToClipboard => translate('copiedToClipboard'); + + String get recharge => translate('recharge'); + + String get enter => translate('enter'); + + String get roomMember => translate('roomMember'); + + String get alreadyAnAdministrator => translate('alreadyAnAdministrator'); + + String get alreadyAnMember => translate('alreadyAnMember'); + + String get alreadyAnTourist => translate('alreadyAnTourist'); + + String get operationSuccessful => translate('operationSuccessful'); + + String get setUpAnIdentity => translate('setUpAnIdentity'); + + String get kickedOutOfRoom => translate('kickedOutOfRoom'); + + String get playGiftMusicAndDynamicMusic => + translate('playGiftMusicAndDynamicMusic'); + + String get agentCenter => translate('agentCenter'); + + String get becomeHost => translate('becomeHost'); + + String get report => translate('report'); + + String get goldListort => translate('goldListort'); + + String get rechargeList => translate('rechargeList'); + + String get edit => translate('edit'); + + String get save => translate('save'); + + String get guest => translate('guest'); + + String get nickName => translate('nickName'); + + String get gender => translate('gender'); + + String get superFans => translate('superFans'); + + String get dateOfBirth => translate('dateOfBirth'); + + String get country => translate('country'); + + String get activity => translate('activity'); + + String get luck => translate('luck'); + + String get cp => translate('cp'); + + String get treasureChest => translate('treasureChest'); + + String get countryRegion => translate('countryRegion'); + + String get birthday => translate('birthday'); + + String get man => translate('man'); + + String get woman => translate('woman'); + + String get mute => translate('mute'); + + String get exit => translate('exit'); + + String get owner => translate('owner'); + + String get adminByHomeowner => translate('adminByHomeowner'); + + String get memberByHomeowner => translate('memberByHomeowner'); + + String get touristByHomeowner => translate('touristByHomeowner'); + + String get allInTheRoom => translate('allInTheRoom'); + + String get mInimize => translate('mInimize'); + + String get hostCenter => translate('hostCenter'); + + String get allOnMicrophone => translate('allOnMicrophone'); + + String get pleaseEnterNickname => translate('pleaseEnterNickname'); + + String get pleaseSelectYourCountry => translate('pleaseSelectYourCountry'); + + String get pleaseSelectYourGender => translate('pleaseSelectYourGender'); + + String get usersOnMicrophone => translate('usersOnMicrophone'); + + String get send => translate('send'); + + String get familyAnnouncement => translate('familyAnnouncement'); + + String get enterFamilyAnnouncement => translate('enterFamilyAnnouncement'); + + String get disbandTheFamily => translate('disbandTheFamily'); + + String get editFamily => translate('editFamily'); + + String get createFamilySuccess => translate('createFamilySuccess'); + + String get sent => translate('sent'); + + String get mine => translate('mine'); + + String get party => translate('party'); + + String get event => translate('event'); + + String get vistors => translate('vistors'); + + String get fans => translate('fans'); + + String get lockTheRoom => translate('lockTheRoom'); + + String get unLockTheRoom => translate('unLockTheRoom'); + + String get all => translate('all'); + + String get chat => translate('chat'); + + String get gift => translate('gift'); + + String get activityMedal => translate('activityMedal'); + + String get achievementMedal => translate('achievementMedal'); + + String get knapsack => translate('knapsack'); + + String get cancelRequest => translate('cancelRequest'); + + String get kickOutOfFamily => translate('kickOutOfFamily'); + + String get kickFamilyUserTips => translate('kickFamilyUserTips'); + + String get cancelFamilyAdmin => translate('cancelFamilyAdmin'); + + String get setAsFamilyAdmin => translate('setAsFamilyAdmin'); + + String get pleaseGetOnTheMicFirst => translate('pleaseGetOnTheMicFirst'); + + String get special => translate('special'); + + String get skip2 => translate('skip2'); + + String get updateNow => translate('updateNow'); + + String get clearMessage => translate('clearMessage'); + + String get pleaseSelectaItem => translate('pleaseSelectaItem'); + + String get areYouRureRoRecharge => translate('areYouRureRoRecharge'); + + String get giftGivingSuccessful => translate('giftGivingSuccessful'); + + String get theAccountPasswordCannotBeEmpty => + translate('theAccountPasswordCannotBeEmpty'); + + String invitesYouToTheMicrophone(String name) => + translate('invitesYouToTheMicrophone').replaceAll('{1}', name); + + String enterRoomTips(String name) => + translate('enterRoomTips').replaceAll('{1}', name); + + String userLevelXPBoost(String name) => + translate('userLevelXPBoost').replaceAll('{1}', name); + + String dailyTaskRewardBonus(String name) => + translate('dailyTaskRewardBonus').replaceAll('{1}', name); + + String storeDiscount(String name) => + translate('storeDiscount').replaceAll('{1}', name); + + String confirmInviteThisUserToTheRoom(String name) => + translate('confirmInviteThisUserToTheRoom').replaceAll('{1}', name); + + String upToAdmins(String name) => + translate('upToAdmins').replaceAll('{1}', name); + + String upToMembers(String name) => + translate('upToMembers').replaceAll('{1}', name); + + String xxfamily(String name) => translate('xxfamily').replaceAll('{1}', name); + + String collectionTimeTips(String time, + String remainCount, + String totalCount,) => + translate('collectionTimeTips') + .replaceAll('{1}', time) + .replaceAll("{2}", remainCount) + .replaceAll("{3}", totalCount); + + String yourVipWillExpire(String name) => + translate('yourVipWillExpire').replaceAll('{1}', name); + + String andAboveUsers(String name) => + translate('andAboveUsers').replaceAll('{1}', name); + + String privileges(String name) => + translate('privileges').replaceAll('{1}', name); + + String remainingNumberTips(String name1, String name2) => + translate( + 'remainingNumberTips', + ).replaceAll('{1}', name1).replaceAll("{2}", name2); + + String coins2(String name) => translate('coins2').replaceAll('{1}', name); + + String credits(String name) => translate('credits').replaceAll('{1}', name); + + String currentStage(String name) => + translate('currentStage').replaceAll('{1}', name); + + String roomReward2(String name) => + translate('roomReward2').replaceAll('{1}', name); + + String ownerIncomeCoins(String name) => + translate('ownerIncomeCoins').replaceAll('{1}', name); + + String rewardCoins(String name) => + translate('rewardCoins').replaceAll('{1}', name); + + String deleteAccount2(String name) => + translate('deleteAccount2').replaceAll('{1}', name); + + String skip(String name) => translate('skip').replaceAll('{1}', name); + + String familyMember2(String name1, String name2) => + translate( + 'familyMember2', + ).replaceAll('{1}', name1).replaceAll('{2}', name2); + + String familyMember3(String name1) => + translate('familyMember3').replaceAll('{1}', name1); + + String familyAdmin2(String name1, String name2) => + translate( + 'familyAdmin2', + ).replaceAll('{1}', name1).replaceAll('{2}', name2); + + String follow2(String name) => translate('follow2').replaceAll('{1}', name); + + String fans2(String name) => translate('fans2').replaceAll('{1}', name); + + String vistors2(String name) => translate('vistors2').replaceAll('{1}', name); + + String duration2(String name) => + translate('duration2').replaceAll('{1}', name); + + String cancelRequestFamilyMsg(String name) => + translate('cancelRequestFamilyMsg').replaceAll('{1}', name); + + String appUpdateTip(String name) => + translate('appUpdateTip').replaceAll('{1}', name); + + String numberOfSign(String name) => + translate('numberOfSign').replaceAll('{1}', name); +} + +class _SCAppLocalizationsDelegate + extends LocalizationsDelegate { + const _SCAppLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => + ['en', 'zh', 'ar','bn','tr'].contains(locale.languageCode); + + @override + Future load(Locale locale) async { + final localizations = SCAppLocalizations(locale); + await localizations.load(); + return localizations; + } + + @override + bool shouldReload(covariant LocalizationsDelegate old) => + false; +} diff --git a/lib/config/pickImage.dart b/lib/config/pickImage.dart new file mode 100644 index 0000000..a1bfb5d --- /dev/null +++ b/lib/config/pickImage.dart @@ -0,0 +1,107 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../shared/tools/sc_path_utils.dart'; +import '../ui_kit/components/sc_tts.dart'; + +class ImagePick { + static final ImagePicker _picker = ImagePicker(); + + /// 选择图片和视频 + static Future?> pickPicAndVideoFromGallery( + BuildContext context, + ) async { + try { + // 调用 pickImage 方法,source 设置为 gallery 即可调用相册 + final XFile? pickedFile = await _picker.pickMedia( + // 以下为可选参数,用于控制输出图像 + maxWidth: 1800, // 限制图片最大宽度 + maxHeight: 1800, // 限制图片最大高度 + imageQuality: 85, // 图片压缩质量 (0-100) + ); + if (pickedFile != null) { + if (!SCPathUtils.fileTypeIsPicAndMP4(pickedFile.path)) { + SCTts.show( "Please select sc_images in .jpg, .jpeg, .png format."); + return null; + } + return [File(pickedFile.path)]; + } + return null; + } catch (e) { + // 处理异常,例如权限被拒绝或选择器被取消 + print("图片选择出错: $e"); + // 可以在这里添加用户友好的提示信息 + return null; + } + } + + /// 从相册选择多张图片 + static Future?> pickMultipleFromGallery( + BuildContext context, + ) async { + try { + final List pickedFiles = await _picker.pickMultiImage( + maxWidth: 1800, + maxHeight: 1800, + imageQuality: 85, + limit: 9, + ); + if (pickedFiles.isNotEmpty) { + return pickedFiles + .where((file) { + return file.mimeType?.startsWith('image/') == true; + }) + .map((xFile) => File(xFile.path)) + .toList(); + } + return null; + } catch (e) { + print("多图选择出错: $e"); + return null; + } + } + + /// 从相册选择单张图片 + static Future pickSingleFromGallery(BuildContext context) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.gallery, + maxWidth: 1800, + maxHeight: 1800, + imageQuality: 85, + ); + if (pickedFile != null) { + if (!SCPathUtils.fileTypeIsPic(pickedFile.path)) { + SCTts.show( "Please select sc_images in .jpg, .jpeg, .png format."); + return null; + } + return File(pickedFile.path); + } + return null; + } catch (e) { + print("图片选择出错: $e"); + return null; + } + } + + /// 使用相机拍摄照片 + static Future pickFromCamera(BuildContext context) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.camera, + maxWidth: 1800, + maxHeight: 1800, + imageQuality: 85, + ); + if (pickedFile != null) { + return File(pickedFile.path); + } + return null; + } catch (e) { + print("相机拍照出错: $e"); + return null; + } + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..c392c59 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,381 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:fluro/fluro.dart' as fluro; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/shared/tools/sc_keybord_util.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:provider/provider.dart'; +import 'package:provider/single_child_widget.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import 'app_localizations.dart'; +import 'app/config/app_config.dart'; +import 'app/constants/sc_global_config.dart'; +import 'app/constants/sc_screen.dart'; +import 'app/routes/sc_routes.dart'; +import 'app/routes/sc_lk_application.dart'; +import 'shared/tools/sc_deep_link_handler.dart'; +import 'shared/tools/sc_deviceId_utils.dart'; +import 'modules/splash/splash_page.dart'; +import 'services/general/sc_app_general_manager.dart'; +import 'services/payment/apple_payment_manager.dart'; +import 'services/auth/authentication_manager.dart'; +import 'services/gift/gift_animation_manager.dart'; +import 'services/gift/gift_system_manager.dart'; +import 'services/payment/google_payment_manager.dart'; +import 'services/localization/localization_manager.dart'; +import 'services/room/rc_room_manager.dart'; +import 'services/audio/rtc_manager.dart'; +import 'services/audio/rtm_manager.dart'; +import 'services/shop/shop_manager.dart'; +import 'services/theme/theme_manager.dart'; +import 'services/auth/user_profile_manager.dart'; +import 'ui_kit/theme/socialchat_theme.dart'; + + +void main() async { + + // 初始化应用配置 + AppConfig.initialize(); + + + // 2. 使用 runZonedGuarded 创建一个隔离区域来捕获所有异步错误 + runZonedGuarded( + () async { + // 1. 确保 Widget 绑定初始化 + WidgetsFlutterBinding.ensureInitialized(); + // 性能优化:启用手势重采样 + GestureBinding.instance.resamplingEnabled = true; + // 阶段1:初始化绝对必要的服务(不阻塞UI) + await _setupA(); + // 使用重试机制初始化存储 + await _initStore(); + // 立即运行应用,不等待非必要服务初始化 + runApp(const RootAppWithProviders()); + }, + // 9. runZonedGuarded 的错误回调(这是捕获异步错误的主要方式) + (error, stackTrace) { + // 这是捕获 runZonedGuarded 区域内所有未捕获异步错误的地方 + FirebaseCrashlytics.instance.recordError(error, stackTrace, fatal: true); + debugPrint('Zoned Error: $error\nStack: $stackTrace'); + }, + ); +} + +/// 设置核心服务(重命名自_initializeEssentialServices) +Future _setupA() async { + // 配置设备方向:仅竖屏 + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + + // Android平台状态栏样式设置 + if (Platform.isAndroid) { + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + ), + ); + } + + // 键盘工具初始化 + SCKeybordUtil.init(); + + // Firebase核心初始化(必需组件) + try { + await Firebase.initializeApp(); + if (kDebugMode) { + debugPrint('Firebase初始化完成'); + } + } catch (e, stackTrace) { + debugPrint('Firebase初始化异常: $e\n$stackTrace'); + // 非阻塞错误处理:记录但继续运行 + } + + // Flutter错误拦截器配置 + FlutterError.onError = (details) { + FirebaseCrashlytics.instance.recordFlutterFatalError(details); + FlutterError.presentError(details); + }; + + // 平台层错误处理器 + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + return true; // 错误已处理 + }; + + // 启用崩溃收集 + await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); +} + + +/// 带重试的数据存储初始化(重命名自_initializeStorageWithRetry) +Future _initStore() async { + const maxAttempts = 3; + int attemptCount = 0; + bool success = false; + + while (attemptCount < maxAttempts && !success) { + attemptCount++; + try { + await DataPersistence.initialize(); + success = true; + if (kDebugMode) { + debugPrint('数据存储初始化成功(尝试次数: $attemptCount)'); + } + } catch (e) { + debugPrint('数据存储初始化尝试 $attemptCount 失败: $e'); + + if (attemptCount >= maxAttempts) { + debugPrint('数据存储初始化失败,已达最大重试次数: $maxAttempts'); + // 不抛出异常,允许应用继续运行 + return; + } + + // 递增延迟:500ms, 1000ms, 1500ms... + int delayMs = 500 * attemptCount; + await Future.delayed(Duration(milliseconds: delayMs)); + } + } +} + +/// 带有所有Provider的根组件 +class RootAppWithProviders extends StatelessWidget { + const RootAppWithProviders({super.key}); + + @override + Widget build(BuildContext context) { + return MultiProvider(providers: _buildP(), child: const YumiApplication()); + } + + /// 构建应用Provider列表 - 重新排序以优化性能 + List _buildP() { + return [ + // 核心用户相关Provider - 必须立即初始化 + ChangeNotifierProvider( + lazy: false, + create: (context) => SocialChatAuthenticationManager(), + ), + ChangeNotifierProvider( + lazy: false, + create: (context) => SocialChatUserProfileManager(), + ), + + // UI与主题相关Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => ThemeManager(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => LocalizationManager(), + ), + + // 实时通信Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => RealTimeCommunicationManager(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => RealTimeMessagingManager(), + ), + + // 房间与社交功能Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => SocialChatRoomManager(), + ), + + // 礼物与动画系统Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => SocialChatGiftSystemManager(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => GiftAnimationManager(), + ), + + // 支付与商店Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => AndroidPaymentProcessor(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => IOSPaymentProcessor(), + ), + ChangeNotifierProvider( + lazy: true, + create: (context) => ShopManager(), + ), + + + // 通用应用管理Provider - 懒加载 + ChangeNotifierProvider( + lazy: true, + create: (context) => SCAppGeneralManager(), + ), + ]; + } +} + +final GlobalKey navigatorKey = GlobalKey(); +final RouteObserver routeObserver = RouteObserver(); + +class YumiApplication extends StatefulWidget { + const YumiApplication({super.key}); + + @override + State createState() => _YumiApplicationState(); +} + +class _YumiApplicationState extends State { + late fluro.FluroRouter _router; + final SCDeepLinkHandler _deepLinkHandler = SCDeepLinkHandler(); + + @override + void initState() { + super.initState(); + // 初始化深层链接处理 + _initLink(); + // 延迟初始化路由和设备信息 + WidgetsBinding.instance.addPostFrameCallback((_) { + _initRouter(); + SCDeviceIdUtils.initDeviceinfo(); + }); + } + + @override + dispose() { + _deepLinkHandler.dispose(); + super.dispose(); + } + + Future _initLink() async { + // 初始化,并传递一个回调函数用于处理链接 + await _deepLinkHandler.initDeepLinks( + onLinkReceived: (Uri uri) { + // 当收到链接时,无论应用在哪个页面,都可以进行路由 + _handleLink(uri); + }, + ); + } + + void _handleLink(Uri uri) { + // 这是处理链接的核心路由逻辑 + print('App 根层收到链接: $uri'); + String path = uri.path; + String id = uri.queryParameters['id'] ?? ''; + } + + void _initRouter() { + _router = fluro.FluroRouter(); + SCRoutes.configureRoutes(_router); + SCLkApplication.router = _router; + } + + @override + Widget build(BuildContext context) { + return _buildUI(); + } + + // 构建下拉刷新配置的页脚部件 + Widget Function() _buildFooter() { + return () => CustomFooter( + builder: (BuildContext context, LoadStatus? mode) { + Widget body; + if (mode == LoadStatus.idle) { + body = Text( + SCAppLocalizations.of(context)!.pullToLoadMore, + ); + } else if (mode == LoadStatus.loading) { + body = CupertinoActivityIndicator(color: Colors.white24); + } else if (mode == LoadStatus.failed) { + body = Text( + SCAppLocalizations.of(context)!.loadingFailedClickToRetry, + ); + } else if (mode == LoadStatus.canLoading) { + body = Text( + SCAppLocalizations.of(context)!.releaseToLoadMore, + ); + } else { + body = Text( + "", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xff999999), + ), + ); + } + return Container( + height: kTextTabBarHeight + ScreenUtil().statusBarHeight, + child: Center(child: body), + ); + }, + ); + } + + + Widget _buildUI() { + return Consumer( + builder: (context, localeProvider, child) { + // 只有在LocaleProvider可用时才设置语言 + SCGlobalConfig.lang = localeProvider.locale?.languageCode ?? "en"; + return Consumer( + builder: (context, themeManager, child) { + return ScreenUtilInit( + designSize: Size(SCScreen.designWidth, SCScreen.designHeight), + splitScreenMode: false, + minTextAdapt: true, + builder: (context, child) { + return AnnotatedRegion( + value: SystemUiOverlayStyle.dark, + child: RefreshConfiguration( + headerBuilder: () => MaterialClassicHeader(color: SocialChatTheme.primaryColor), + footerBuilder: _buildFooter(), + child: MaterialApp( + title: 'Yumi', + locale: Provider.of(context).locale, + localizationsDelegates: [ + SCAppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: [ + const Locale('en', ''), + const Locale('zh', ''), + const Locale('ar', ''), + const Locale('tr', ''), + const Locale('bn', ''), + ], + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + onGenerateRoute: SCLkApplication.router.generator, + theme: themeManager.currentTheme, + home: SplashPage(), + builder: FlutterSmartDialog.init(), + navigatorObservers: [routeObserver], + ), + ), + ); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/modules/admin/editing/sc_editing_user_room_page.dart b/lib/modules/admin/editing/sc_editing_user_room_page.dart new file mode 100644 index 0000000..faa4149 --- /dev/null +++ b/lib/modules/admin/editing/sc_editing_user_room_page.dart @@ -0,0 +1,701 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +class SCEditingUserRoomPage extends StatefulWidget { + final String type; + + ///被举报的id(用户id或者房间id) + final SocialChatUserProfile? userProfile; + final SocialChatRoomRes? roomProfile; + + const SCEditingUserRoomPage({ + Key? key, + required this.type, + this.userProfile, + this.roomProfile, + }) : super(key: key); + + @override + _SCEditingUserRoomPageState createState() => _SCEditingUserRoomPageState(); +} + +class _SCEditingUserRoomPageState extends State { + // 业务逻辑策略访问器 + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + num selectedIndex = 0; + String reportReason = ""; + List imageUrls = []; // 动态图片URL列表 + + int violationType = 1; + final TextEditingController _descriptionController = TextEditingController(); + Debouncer debouncer = Debouncer(); + + @override + void initState() { + super.initState(); + final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(widget.type); + if (violationTypeMapping.isNotEmpty) { + // 获取第一个值作为默认违规类型 + violationType = violationTypeMapping.values.first; + } + // 初始化图片URL列表,长度为最大上传数量 + final maxImageCount = _strategy.getAdminEditingMaxImageUploadCount(); + imageUrls = List.generate(maxImageCount, (index) => ''); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + _strategy.getAdminEditingBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + actions: [], + title: + widget.type == "User" + ? SCAppLocalizations.of(context)!.userEditing + : SCAppLocalizations.of(context)!.roomEditing, + backgroundColor: Colors.transparent, + ), + body: SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.type == "User" + ? Container( + padding: EdgeInsets.symmetric( + vertical: 8.w, + horizontal: 5.w, + ), + margin: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black12, width: 1.w), + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == widget.userProfile?.id}&tageId=${widget.userProfile?.id}", + ); + }, + behavior: HitTestBehavior.opaque, + child: head( + url: widget.userProfile?.userAvatar ?? "", + width: 55.w, + ), + ), + SizedBox(width: 2.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + xb( + widget.userProfile?.userSex, + color: + widget.userProfile?.userSex == 1 + ? Colors.blue + : Colors.purpleAccent, + ), + SizedBox(width: 5.w), + // richText( + // txt: data.userNickname ?? "", + // key: (widget as SearchUserList).text, + // highlight: SocialChatTheme.primaryColor, + // defaultColor: Colors.black, + // ), + Expanded( + child: text( + widget.userProfile?.userNickname ?? + "", + fontSize: 15.sp, + fontWeight: FontWeight.w600, + textColor: Colors.black, + ), + ), + SizedBox(width: 5.w), + // dj(data.level.toString()), + ], + ), + Row( + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric( + vertical: 8.w, + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + (widget.userProfile + ?.hasSpecialId() ?? + false) + ? _strategy.getAdminEditingIcon('specialIdBg') + : _strategy.getAdminEditingIcon('normalIdBg'), + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + widget.userProfile?.getID() ?? + "", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + _strategy.getAdminEditingIcon('copyId'), + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData( + text: + widget.userProfile?.getID() ?? + "", + ), + ); + SCTts.show( + SCAppLocalizations.of( + context, + )!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + ), + SizedBox(width: 5.w), + ], + ), + ) + : Container( + padding: EdgeInsets.symmetric( + vertical: 8.w, + horizontal: 5.w, + ), + margin: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black12, width: 1.w), + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + SCRoomUtils.goRoom( + widget.roomProfile?.id ?? "", + context, + ); + }, + behavior: HitTestBehavior.opaque, + child: head( + url: widget.roomProfile?.roomCover ?? "", + width: 55.w, + ), + ), + SizedBox(width: 2.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: text( + widget.roomProfile?.roomName ?? "", + fontSize: 15.sp, + fontWeight: FontWeight.w600, + textColor: Colors.black, + ), + ), + SizedBox(width: 5.w), + // dj(data.level.toString()), + ], + ), + Row( + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric( + vertical: 8.w, + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 5.w), + text( + "ID:${widget.roomProfile?.roomAccount}", + fontSize: 12.sp, + textColor: Colors.black26, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + _strategy.getAdminEditingIcon('copyId'), + width: 12.w, + height: 12.w, + color: Colors.black26, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData( + text: + widget + .roomProfile + ?.roomAccount ?? + "", + ), + ); + SCTts.show( + SCAppLocalizations.of( + context, + )!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + ), + SizedBox(width: 5.w), + ], + ), + ), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of( + context, + )!.pleaseSelectTheTypeToProcess, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.black, + ), + SizedBox(width: 15.w), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + border: Border.all(color: Color(0xffE6E6E6), width: 1.w), + ), + margin: EdgeInsets.only(top: 10.w, left: 15.w, right: 15.w), + padding: EdgeInsets.symmetric(horizontal: 15.w), + child: Column( + children: [ + // 动态生成违规类型选项 + ..._buildViolationTypeOptions(context), + SizedBox(height: 20.w,), + Row( + children: [ + text( + SCAppLocalizations.of(context)!.description, + fontSize: 14.sp, + fontWeight: FontWeight.w500, + textColor: Colors.black, + ), + Spacer(), + ], + ), + SizedBox(height: 8.w), + Container( + padding: EdgeInsets.all(5.w), + decoration: _strategy.getAdminEditingInputDecoration(), + child: TextField( + controller: _descriptionController, + onChanged: (text) {}, + maxLength: _strategy.getAdminEditingDescriptionMaxLength(), + maxLines: 5, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.inputDesHint, + hintStyle: TextStyle( + color: Colors.black45, + fontSize: 14.sp, + ), + contentPadding: EdgeInsets.only(top: 0.w), + + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + fillColor: Colors.black45, + ), + style: TextStyle( + fontSize: sp(15), + color: Colors.black, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + SizedBox(height: 5.w), + Row( + children: [ + text( + SCAppLocalizations.of(context)!.screenshotTips, + fontSize: 14.sp, + textColor: Colors.black, + fontWeight: FontWeight.w500, + ), + Spacer(), + ], + ), + SizedBox(height: 8.w), + Row( + children: [ + // 动态生成图片上传组件 + ...List.generate(_strategy.getAdminEditingMaxImageUploadCount(), (index) { + return Expanded( + child: GestureDetector( + child: Stack( + children: [ + imageUrls[index].isNotEmpty + ? netImage(url: imageUrls[index], height: 100.w) + : Image.asset( + _strategy.getAdminEditingIcon('addPic'), + height: 100.w, + ), + imageUrls[index].isNotEmpty + ? Positioned( + top: 5.w, + right: 5.w, + child: GestureDetector( + child: Image.asset( + _strategy.getAdminEditingIcon('closePic'), + width: 14.w, + height: 14.w, + ), + onTap: () { + setState(() { + imageUrls[index] = ""; + }); + }, + ), + ) + : Container(), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + imageUrls[index] = url; + }); + } + }); + }, + ), + ); + }).expand((widget) => [widget, if (_strategy.getAdminEditingMaxImageUploadCount() > 1 && widget != Expanded) SizedBox(width: 5.w)]).toList(), + ], + ), + SizedBox(height: 15.w), + ], + ), + ), + SizedBox(height: 45.w), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + List uploadedImages = imageUrls.where((url) => url.isNotEmpty).toList(); + if (widget.type == "User") { + SCAccountRepository().userViolationHandle( + widget.userProfile?.id ?? "", + violationType, + 1, + _descriptionController.text, + imageUrls: imageUrls, + ).then((b){ + SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); + SCNavigatorUtils.goBack(context); + }).catchError((e){ + + }); + } else { + SCChatRoomRepository().roomViolationHandle( + widget.roomProfile?.id ?? "", + violationType, + 1, + _descriptionController.text, + imageUrls: imageUrls, + ).then((b){ + SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); + SCNavigatorUtils.goBack(context); + }).catchError((e){ + + }); + } + }, + ); + }, + child: Container( + height: 40.w, + width: double.infinity, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 45.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: AlignmentDirectional.topCenter, + end: AlignmentDirectional.bottomCenter, + colors: _strategy.getAdminEditingButtonGradient('warning'), + ), + borderRadius: BorderRadius.circular(25.w), + ), + child: Text( + SCAppLocalizations.of(context)!.warning, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + SizedBox(height: 20.w), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + List uploadedImages = imageUrls.where((url) => url.isNotEmpty).toList(); + if (widget.type == "User") { + SCAccountRepository().userViolationHandle( + widget.userProfile?.id ?? "", + violationType, + 2, + _descriptionController.text, + imageUrls: imageUrls, + ).then((b){ + SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); + SCNavigatorUtils.goBack(context); + }).catchError((e){ + + }); + } else { + SCChatRoomRepository().roomViolationHandle( + widget.roomProfile?.id ?? "", + violationType, + 2, + _descriptionController.text, + imageUrls: imageUrls, + ).then((b){ + SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); + SCNavigatorUtils.goBack(context); + }).catchError((e){ + + }); + } + }, + ); + }, + child: Container( + height: 40.w, + width: double.infinity, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 45.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: AlignmentDirectional.topCenter, + end: AlignmentDirectional.bottomCenter, + colors: _strategy.getAdminEditingButtonGradient('adjust'), + ), + borderRadius: BorderRadius.circular(25.w), + ), + child: Text( + SCAppLocalizations.of(context)!.adjust, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + SizedBox(height: 35.w), + ], + ), + ), + ), + ), + ], + ); + } + + List _buildViolationTypeOptions(BuildContext context) { + final violationTypeMapping = _strategy.getAdminEditingViolationTypeMapping(widget.type); + final List options = []; + + if (violationTypeMapping.isEmpty) { + // 如果没有映射,返回默认选项 + if (widget.type == "User") { + options.add(_item(SCAppLocalizations.of(context)!.userName, 0, 1)); + options.add(_item(SCAppLocalizations.of(context)!.userProfilePicture, 1, 2)); + } else { + options.add(_item(SCAppLocalizations.of(context)!.roomName, 0, 3)); + options.add(_item(SCAppLocalizations.of(context)!.roomProfilePicture, 1, 4)); + options.add(_item(SCAppLocalizations.of(context)!.roomNotice, 2, 5)); + options.add(_item(SCAppLocalizations.of(context)!.roomTheme, 3, 6)); + } + return options; + } + + // 根据映射动态生成选项 + int index = 0; + violationTypeMapping.forEach((displayName, typeId) { + // 需要将英文显示名称转换为本地化文本 + String localizedName; + switch (displayName) { + case 'Pornography': + localizedName = SCAppLocalizations.of(context)!.pornography; + break; + case 'Illegal information': + localizedName = SCAppLocalizations.of(context)!.illegalInformation; + break; + case 'Fraud': + localizedName = SCAppLocalizations.of(context)!.fraud; + break; + case 'Other': + localizedName = SCAppLocalizations.of(context)!.others; + break; + case 'User Name': + localizedName = SCAppLocalizations.of(context)!.userName; + break; + case 'User Profile Picture': + localizedName = SCAppLocalizations.of(context)!.userProfilePicture; + break; + case 'Room Name': + localizedName = SCAppLocalizations.of(context)!.roomName; + break; + case 'Room Profile Picture': + localizedName = SCAppLocalizations.of(context)!.roomProfilePicture; + break; + case 'Room Notice': + localizedName = SCAppLocalizations.of(context)!.roomNotice; + break; + case 'Room Theme': + localizedName = SCAppLocalizations.of(context)!.roomTheme; + break; + default: + localizedName = displayName; + } + options.add(_item(localizedName, index, typeId)); + index++; + }); + + return options; + } + + Widget _item(String str, int index, int vtype) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + reportReason = str; + selectedIndex = index; + violationType = vtype; + }); + }, + child: Container( + width: double.infinity, + height: 35.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + text(str, fontSize: 13.sp, textColor: Colors.black,fontWeight: FontWeight.w500), + Spacer(), + selectedIndex == index + ? Image.asset( + _strategy.getAdminEditingIcon('checked'), + width: 20.w, + fit: BoxFit.fitWidth, + ) + : Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.grey, width: 2.w), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/admin/search/sc_edit_room_search_admin_page.dart b/lib/modules/admin/search/sc_edit_room_search_admin_page.dart new file mode 100644 index 0000000..ae1fc93 --- /dev/null +++ b/lib/modules/admin/search/sc_edit_room_search_admin_page.dart @@ -0,0 +1,232 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/socialchat_gradient_button.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; + +class SCEditRoomSearchAdminPage extends StatefulWidget { + @override + _SCEditRoomSearchAdminPageState createState() => + _SCEditRoomSearchAdminPageState(); +} + +class _SCEditRoomSearchAdminPageState extends State + with SingleTickerProviderStateMixin { + final TextEditingController _textEditingController = TextEditingController(); + List rooms = []; + bool canEdit = false; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + _strategy.getAdminSearchBackgroundImage('roomSearch'), + width: ScreenUtil().screenWidth, + height: 150.w, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.roomEditing, + actions: [], + ), + body: SafeArea( + top: false, + child: Container( + margin: EdgeInsetsDirectional.only(top: 15.w), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(35.w), + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: searchWidget( + hint: SCAppLocalizations.of(context)!.enterTheRoomId, + controller: _textEditingController, + borderColor: _strategy.getAdminSearchInputBorderColor('roomSearch'), + textColor: _strategy.getAdminSearchInputTextColor('roomSearch'), + ), + ), + socialchatGradientButton( + text: SCAppLocalizations.of(context)!.search, + radius: 25, + textSize: 14.sp, + textColor: _strategy.getAdminSearchButtonTextColor('roomSearch'), + gradient: LinearGradient( + colors: _strategy.getAdminSearchButtonGradient('roomSearch'), + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + onPress: () { + if (_textEditingController.text.isNotEmpty) { + _searchUser(); + } + }, + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 10.w), + rooms.isNotEmpty + ? Expanded( + child: ListView.separated( + itemBuilder: (context, i) => buildItem(rooms[i]), + itemCount: rooms.length, + padding: EdgeInsets.zero, + separatorBuilder: + (context, i) => Divider( + height: height(1), + color: SocialChatTheme.dividerColor, + indent: width(28), //25+50+10 + endIndent: width(15), + ), + ), + ) + : mainEmpty(), + ], + ), + ), + ), + ), + ], + ); + } + + Widget buildItem(SocialChatRoomRes data) { + return Container( + padding: EdgeInsets.symmetric(vertical: 8.w, horizontal: 5.w), + margin: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black12, width: 1.w), + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + SCRoomUtils.goRoom(data.id ?? "", context); + }, + behavior: HitTestBehavior.opaque, + child: head(url: data.roomCover ?? "", width: 55.w), + ), + SizedBox(width: 2.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // richText( + // txt: data.userNickname ?? "", + // key: (widget as SearchUserList).text, + // highlight: SocialChatTheme.primaryColor, + // defaultColor: Colors.black, + // ), + Expanded( + child: text( + data.roomName ?? "", + fontSize: 15.sp, + textColor: Colors.black, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 5.w), + // dj(data.level.toString()), + ], + ), + Row( + children: [ + text( + "ID:${data.roomAccount}", + fontSize: 12.sp, + textColor: Colors.black26, + fontWeight: FontWeight.w600, + ), + ], + ), + ], + ), + ), + canEdit + ? GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + width: 20.w, + height: 20.w, + child: Image.asset( + _strategy.getAdminSearchEditIcon('roomSearch'), + color: Colors.black, + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.editingUserRoomAdmin}?type=Room&profile=${Uri.encodeComponent(jsonEncode(data.toJson()))}", + ); + }, + ) + : Container(), + SizedBox(width: 5.w), + ], + ), + ); + } + + void _searchUser() async { + try { + SCLoadingManager.show(); + rooms.clear(); + rooms = await SCChatRoomRepository().searchRoom( + _textEditingController.text, + ); + var userIdentity = await SCAccountRepository().userIdentity( + userId: rooms.first.userId ?? "", + ); + if (!(userIdentity.admin ?? false) && + !(userIdentity.superAdmin ?? false)) { + canEdit = true; + } + SCLoadingManager.hide(); + setState(() {}); + } catch (e) { + SCLoadingManager.hide(); + } + } +} diff --git a/lib/modules/admin/search/sc_edit_user_search_admin_page.dart b/lib/modules/admin/search/sc_edit_user_search_admin_page.dart new file mode 100644 index 0000000..d8b14cc --- /dev/null +++ b/lib/modules/admin/search/sc_edit_user_search_admin_page.dart @@ -0,0 +1,277 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/socialchat_gradient_button.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; + +class SCEditUserSearchAdminPage extends StatefulWidget { + @override + _SCEditUserSearchAdminPageState createState() => + _SCEditUserSearchAdminPageState(); +} + +class _SCEditUserSearchAdminPageState extends State + with SingleTickerProviderStateMixin { + final TextEditingController _textEditingController = TextEditingController(); + List users = []; + + bool canEdit = false; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + _strategy.getAdminSearchBackgroundImage('userSearch'), + width: ScreenUtil().screenWidth, + height: 150.w, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.userEditing, + actions: [], + ), + body: SafeArea( + top: false, + child: Container( + margin: EdgeInsetsDirectional.only(top: 15.w), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(35.w), + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: searchWidget( + hint: SCAppLocalizations.of(context)!.enterTheUserId, + controller: _textEditingController, + borderColor: _strategy.getAdminSearchInputBorderColor('userSearch'), + textColor: _strategy.getAdminSearchInputTextColor('userSearch'), + ), + ), + socialchatGradientButton( + text: SCAppLocalizations.of(context)!.search, + radius: 25, + textSize: 14.sp, + textColor: _strategy.getAdminSearchButtonTextColor('userSearch'), + gradient: LinearGradient( + colors: _strategy.getAdminSearchButtonGradient('userSearch'), + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + onPress: () { + if (_textEditingController.text.isNotEmpty) { + _searchUser(); + } + }, + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 10.w), + users.isNotEmpty + ? Expanded( + child: ListView.separated( + itemBuilder: (context, i) => buildItem(users[i]), + itemCount: users.length, + padding: EdgeInsets.zero, + separatorBuilder: + (context, i) => Divider( + height: height(1), + color: SocialChatTheme.dividerColor, + indent: width(28), //25+50+10 + endIndent: width(15), + ), + ), + ) + : mainEmpty(), + ], + ), + ), + ), + ), + ], + ); + } + + Widget buildItem(SocialChatUserProfile data) { + return Container( + padding: EdgeInsets.symmetric(vertical: 8.w, horizontal: 5.w), + margin: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black12, width: 1.w), + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == data.id}&tageId=${data.id}", + ); + }, + behavior: HitTestBehavior.opaque, + child: head(url: data.userAvatar ?? "", width: 55.w), + ), + SizedBox(width: 2.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + xb( + data.userSex, + color: + data.userSex == 1 ? Colors.blue : Colors.purpleAccent, + ), + SizedBox(width: 5.w), + // richText( + // txt: data.userNickname ?? "", + // key: (widget as SearchUserList).text, + // highlight: SocialChatTheme.primaryColor, + // defaultColor: Colors.black, + // ), + Expanded( + child: text( + data.userNickname ?? "", + fontSize: 15.sp, + textColor: Colors.black, + ), + ), + SizedBox(width: 5.w), + // dj(data.level.toString()), + ], + ), + Row( + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + (data.hasSpecialId() ?? false) + ? _strategy.getAdminSearchUserInfoIcon('specialIdBg') + : _strategy.getAdminSearchUserInfoIcon('normalIdBg'), + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + data.getID(), + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + _strategy.getAdminSearchUserInfoIcon('copyId'), + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData(ClipboardData(text: data.getID())); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + ), + canEdit + ? GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + width: 20.w, + height: 20.w, + child: Image.asset( + _strategy.getAdminSearchEditIcon('userSearch'), + color: Colors.black, + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.editingUserRoomAdmin}?type=User&profile=${Uri.encodeComponent(jsonEncode(data.toJson()))}", + ); + }, + ) + : Container(), + SizedBox(width: 5.w), + ], + ), + ); + } + + void _searchUser() async { + try { + SCLoadingManager.show(); + users.clear(); + var result = await SCAccountRepository().searchUser( + _textEditingController.text, + ); + var userIdentity = await SCAccountRepository().userIdentity( + userId: result.id, + ); + if (!(userIdentity.admin ?? false) && + !(userIdentity.superAdmin ?? false)) { + canEdit = true; + } + users.add(result); + SCLoadingManager.hide(); + setState(() {}); + } catch (e) { + SCLoadingManager.hide(); + } + } +} diff --git a/lib/modules/auth/account/sc_login_with_account_page.dart b/lib/modules/auth/account/sc_login_with_account_page.dart new file mode 100644 index 0000000..dc0f1f2 --- /dev/null +++ b/lib/modules/auth/account/sc_login_with_account_page.dart @@ -0,0 +1,371 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_version_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +///密码账户登录 +class SCLoginWithAccountPage extends StatefulWidget { + const SCLoginWithAccountPage({super.key}); + + @override + SCLoginWithAccountPageState createState() => SCLoginWithAccountPageState(); +} + +class SCLoginWithAccountPageState extends State + with WidgetsBindingObserver { + ///显示密码 + bool showPass = false; + String account = ""; + + ///账户控制器 + TextEditingController accountController = TextEditingController(); + + ///密码控制器 + TextEditingController passController = TextEditingController(); + + @override + void initState() { + super.initState(); + String account = DataPersistence.getString( + "Login_Account", + defaultValue: "", + ); + String pwd = DataPersistence.getString("Login_Pwd", defaultValue: ""); + accountController.text = account; + passController.text = pwd; + } + + @override + Widget build(BuildContext context) { + final businessLogicStrategy = SCGlobalConfig.businessLogicStrategy; + final pagePadding = businessLogicStrategy.getLoginPagePadding(); + + return Scaffold( + resizeToAvoidBottomInset: false, + body: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Stack( + alignment: Alignment.topCenter, + children: [ + Image.asset( + businessLogicStrategy.getLoginBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + + SingleChildScrollView( + // 添加滚动视图 + padding: EdgeInsets.only( + bottom: + MediaQuery.of(context).viewInsets.bottom + 20, // 添加底部内边距 + ), + child: Column( + children: [ + SizedBox(height: pagePadding.top), + Image.asset( + businessLogicStrategy.getLoginAppIcon(), + width: 107.w, + height: 159.w, + ), + SizedBox(height: 35.w), + Container( + margin: EdgeInsetsDirectional.only(start: 65.w), + alignment: AlignmentDirectional.centerStart, + child: text( + SCAppLocalizations.of(context)!.account, + fontSize: 16.sp, + textColor: businessLogicStrategy.getLoginLabelTextColor(), + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 10.w), + _buildAccountInput(businessLogicStrategy), + SizedBox(height: 10.w), + Container( + margin: EdgeInsetsDirectional.only(start: 65.w), + alignment: AlignmentDirectional.centerStart, + child: text( + SCAppLocalizations.of(context)!.password, + fontSize: 16.sp, + textColor: businessLogicStrategy.getLoginLabelTextColor(), + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 10.w), + _buildPassInput(businessLogicStrategy), + SizedBox(height: 80.w), + _buildLoginBtn(businessLogicStrategy), + ], + ), + ), + ], + ), + ), + ); + } + + ///登录按钮 + Widget _buildLoginBtn(BusinessLogicStrategy strategy) { + final buttonColor = strategy.getLoginButtonColor(); + final buttonGradient = strategy.getLoginButtonGradient(); + + BoxDecoration buttonDecoration; + if (buttonGradient != null && buttonGradient.isNotEmpty) { + // 使用渐变 + buttonDecoration = BoxDecoration( + gradient: LinearGradient( + colors: buttonGradient, + begin: Alignment.centerRight, + end: Alignment.centerLeft, + ), + border: Border.all(color: Color(0xff077142), width: 1.w), + borderRadius: BorderRadius.circular(12), // 马甲包设计使用70px圆角 + ); + } else { + // 使用纯色 + buttonDecoration = BoxDecoration( + color: buttonColor, + borderRadius: BorderRadius.all(Radius.circular(height(8))), // 原始应用8px圆角 + ); + } + + return Row( + children: [ + Expanded( + child: SCDebounceWidget( + child: Container( + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 60.w), + height: 42.w, + decoration: buttonDecoration, + child: text( + SCAppLocalizations.of(context)!.logIn, + textColor: strategy.getLoginButtonTextColor(), + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + _login(); + }, + ), + ), + ], + ); + } + + ///账户输入框 + Widget _buildAccountInput(BusinessLogicStrategy strategy) { + return Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 60.w), + padding: EdgeInsetsDirectional.only( + start: width(12), + end: width(12), + ), + alignment: AlignmentDirectional.centerStart, + height: 45.w, + width: ScreenUtil().screenWidth, + decoration: strategy.getLoginInputDecoration(), + child: TextField( + controller: accountController, + onChanged: (text) { + setState(() { + account = text; + }); + }, + maxLength: 30, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[0-9]")), + ], + decoration: InputDecoration( + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + hintText: SCAppLocalizations.of(context)!.enterAccount, + hintStyle: TextStyle( + color: strategy.getLoginHintTextColor(), + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + contentPadding: EdgeInsets.only(top: 0.w), + counterText: '', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + suffix: _buildPhoneCancel(), + ), + style: TextStyle( + fontSize: 14.sp, + color: strategy.getLoginInputTextColor(), + fontWeight: FontWeight.w600, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + ), + ], + ); + } + + _buildPhoneCancel() { + return Offstage( + offstage: accountController.text == "", + child: GestureDetector( + onTap: () { + setState(() { + accountController.text = ''; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset("sc_images/login/sc_icon_sc.png", width: 15.w,color: Colors.white,), + ), + ), + ); + } + + ///密码输入框 + Widget _buildPassInput(BusinessLogicStrategy strategy) { + return Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 60.w), + padding: REdgeInsetsDirectional.only( + start: width(12), + end: width(12), + ), + alignment: AlignmentDirectional.centerStart, + height: 45.w, + width: ScreenUtil().screenWidth, + decoration: strategy.getLoginInputDecoration(), + child: TextField( + controller: passController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny("[\u4e00-\u9fa5]"), + ], + + obscureText: !showPass, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + hintText: SCAppLocalizations.of(context)!.enterPassword, + hintStyle: TextStyle( + color: strategy.getLoginHintTextColor(), + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_pwd.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass = !showPass; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + color: Colors.white, + width: 15.w, + ), + ), + ), + ), + style: TextStyle( + fontSize: 14.sp, + color: strategy.getLoginInputTextColor(), + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ); + } + + ///登录 + _login() async { + String account = accountController.text; + String pass = passController.text; + if (account.isEmpty || pass.isEmpty) { + SCTts.show( + SCAppLocalizations.of(context)!.theAccountPasswordCannotBeEmpty, + ); + return; + } + SCLoadingManager.show(context: context); + try { + final results = await Future.wait([ + SCAccountRepository().loginForAccount(account, pass), + SCVersionUtils.checkReview(), + ]); + var user = (results[0] as SocialChatLoginRes); + AccountStorage().setCurrentUser(user); + SCLoadingManager.hide(); + DataPersistence.setString("Login_Account", account); + DataPersistence.setString("Login_Pwd", pass); + if (mounted) { + SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true); + } + } catch (e) { + SCLoadingManager.hide(); + // 可以添加错误处理逻辑 + rethrow; + } + } +} diff --git a/lib/modules/auth/edit/sc_edit_profile_page.dart b/lib/modules/auth/edit/sc_edit_profile_page.dart new file mode 100644 index 0000000..1c05ee2 --- /dev/null +++ b/lib/modules/auth/edit/sc_edit_profile_page.dart @@ -0,0 +1,549 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/services/auth/authentication_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/custom_cached_image.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +import '../../../shared/business_logic/usecases/sc_custom_filtering_textinput_formatter.dart'; +import '../../country/country_page.dart'; +import '../../country/country_route.dart'; + +///编辑个人信息 +class SCEditProfilePage extends StatefulWidget { + const SCEditProfilePage({super.key}); + + @override + _SCEditProfilePageState createState() => _SCEditProfilePageState(); +} + +class _SCEditProfilePageState extends State { + ///默认女 + int type = 0; + DateTime birthdayDate = DateTime(2006); + TextEditingController nicknameController = TextEditingController(); + SocialChatUserProfileManager? userProvider; + + @override + void initState() { + super.initState(); + userProvider = Provider.of( + context, + listen: false, + ); + Provider.of(context, listen: false).selectCountryInfo = + null; + } + + @override + void dispose() { + userProvider?.resetEditUserData(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final businessLogicStrategy = SCGlobalConfig.businessLogicStrategy; + return Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + alignment: Alignment.topCenter, + children: [ + Image.asset( + businessLogicStrategy.getEditProfileBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + Column( + children: [ + SizedBox(height: 120.w), + GestureDetector( + child: Consumer( + builder: (_, provider, __) { + return provider.editUser != null && + provider.editUser!.userAvatar != null + ? head( + url: provider.editUser!.userAvatar ?? "", + width: 75.w, + ) + : Image.asset( + businessLogicStrategy + .getEditProfileDefaultAvatarImage(), + width: 75.w, + height: 75.w, + ); + }, + ), + onTap: () { + SCPickUtils.pickImage(context, (bool success, String url) { + if (success) { + userProvider?.updateUserAvatar(url); + } + }); + }, + ), + SizedBox(height: 15.w), + Container( + padding: EdgeInsets.only(left: width(12), right: width(12)), + alignment: Alignment.center, + height: 46.w, + width: 300.w, + decoration: BoxDecoration( + color: + businessLogicStrategy + .getEditProfileInputBackgroundColor(), + borderRadius: BorderRadius.all(Radius.circular(height(8))), + ), + child: TextField( + textAlign: TextAlign.center, + controller: nicknameController, + onChanged: (text) { + setState(() {}); + }, + inputFormatters: [SCCustomFilteringTextInputFormatter()], + maxLength: 38, + decoration: InputDecoration( + hintText: SCAppLocalizations.of(context)!.enterNickname, + hintStyle: TextStyle(color: Colors.white, fontSize: 14.sp), + contentPadding: EdgeInsets.only(top: 0.w), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + fillColor: Colors.black45, + ), + style: TextStyle( + fontSize: sp(15), + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + SizedBox(height: 15.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + child: Container( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 2.w, + color: + type == 1 + ? Color(0xff18F2B1) + : Colors.transparent, + ), + ), + child: Image.asset( + businessLogicStrategy.getEditProfileGenderIcon( + true, + ), + width: 55.w, + height: 55.w, + ), + ), + SizedBox(height: 8.w), + text( + SCAppLocalizations.of(context)!.male, + textColor: type == 1 ? Colors.white : Colors.white, + fontWeight: + type == 1 ? FontWeight.w600 : FontWeight.w400, + fontSize: 13.sp, + ), + ], + ), + ), + onTap: () { + if (type == 0) { + setState(() { + type = 1; + userProvider?.updateUserSex(type); + }); + } + }, + ), + SizedBox(width: 99.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 2.w, + color: + type == 1 + ? Colors.transparent + : Color(0xff18F2B1), + ), + ), + child: Image.asset( + businessLogicStrategy.getEditProfileGenderIcon( + false, + ), + width: 55.w, + height: 55.w, + ), + ), + SizedBox(height: 8.w), + text( + SCAppLocalizations.of(context)!.female, + textColor: type == 1 ? Colors.white : Colors.white, + fontWeight: + type == 1 ? FontWeight.w400 : FontWeight.w600, + fontSize: 13.sp, + ), + ], + ), + ), + onTap: () { + if (type == 1) { + setState(() { + type = 0; + userProvider?.updateUserSex(type); + }); + } + }, + ), + ], + ), + SizedBox(height: 12.w), + GestureDetector( + child: Container( + padding: EdgeInsets.only(left: width(12), right: width(12)), + alignment: Alignment.center, + height: 46.w, + width: 300.w, + decoration: BoxDecoration( + color: + businessLogicStrategy + .getEditProfileInputBackgroundColor(), + borderRadius: BorderRadius.all(Radius.circular(height(12))), + ), + child: Row( + children: [ + text( + SCAppLocalizations.of(context)!.birthday, + fontSize: 15.sp, + textColor: Colors.white, + ), + Spacer(), + text( + SCMDateUtils.formatDateTime(birthdayDate), + textColor: Colors.white54, + fontSize: 15.sp, + ), + Icon( + Icons.chevron_right_outlined, + color: Colors.white54, + size: 20.w, + ), + ], + ), + ), + onTap: () { + _selectDate(); + }, + ), + SizedBox(height: 12.w), + GestureDetector( + child: Consumer( + builder: (_, provider, __) { + return Container( + padding: EdgeInsets.only( + left: width(12), + right: width(12), + ), + alignment: Alignment.center, + height: 46.w, + width: 300.w, + decoration: BoxDecoration( + color: + businessLogicStrategy + .getEditProfileInputBackgroundColor(), + borderRadius: BorderRadius.all( + Radius.circular(height(12)), + ), + ), + child: + provider.selectCountryInfo != null + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + text( + SCAppLocalizations.of( + context, + )!.countryRegion, + fontSize: 15.sp, + textColor: Colors.white, + ), + Spacer(), + + netImage( + url: + provider + .selectCountryInfo! + .nationalFlag ?? + "", + borderRadius: BorderRadius.circular(3), + width: 26.w, + height: 16.w, + ), + SizedBox(width: 3.w), + text( + provider.selectCountryInfo!.aliasName ?? "", + textColor: Colors.white54, + fontSize: 14.sp, + ), + Icon( + Icons.chevron_right_outlined, + color: Colors.white54, + size: 20.w, + ), + ], + ) + : Row( + children: [ + text( + SCAppLocalizations.of( + context, + )!.countryRegion, + fontSize: 15.sp, + textColor: Colors.white, + ), + Spacer(), + text( + SCAppLocalizations.of( + context, + )!.selectYourCountry, + textColor: Colors.white54, + fontSize: 15.sp, + ), + Icon( + Icons.chevron_right_outlined, + color: Colors.white54, + size: 20.w, + ), + ], + ), + ); + }, + ), + onTap: () { + SCNavigatorUtils.push( + context, + CountryRoute.country, + replace: false, + ); + }, + ), + SizedBox(height: 45.w), + GestureDetector( + child: Container( + padding: EdgeInsets.only(left: width(12), right: width(12)), + alignment: Alignment.center, + height: 46.w, + width: 270.w, + decoration: BoxDecoration( + color: + businessLogicStrategy + .getEditProfileContinueButtonColor(), + borderRadius: BorderRadius.all(Radius.circular(height(12))), + ), + child: text( + SCAppLocalizations.of(context)!.conntinue, + textColor: Colors.white, + fontSize: 15.sp, + ), + ), + onTap: () { + _submitUserProfile(); + }, + ), + ], + ), + ], + ), + ); + } + + Future _selectDate() async { + showCupertinoModalPopup( + context: context, + builder: + (context) => SafeArea( + top: false, + child: Container( + height: 240.w, + padding: EdgeInsets.only(top: 6), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/login/sc_icon_login_edit_data_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Container( + alignment: Alignment.topCenter, + child: text( + SCAppLocalizations.of(context)!.cancel, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + Navigator.of(context).pop(); + }, + ), + Spacer(), + GestureDetector( + child: Container( + alignment: Alignment.topCenter, + child: text( + SCAppLocalizations.of(context)!.confirm, + textColor: + SCGlobalConfig.businessLogicStrategy + .getEditProfileDatePickerConfirmColor(), + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + setState(() {}); + Navigator.of(context).pop(); + }, + ), + SizedBox(width: 10.w), + ], + ), + Expanded( + child: CupertinoTheme( + data: CupertinoThemeData( + textTheme: CupertinoTextThemeData( + // 在这里设置字体颜色、大小等样式 + dateTimePickerTextStyle: TextStyle( + color: Colors.white, // 设置字体颜色 + fontSize: 16.sp, // 设置字体大小 + ), + ), + ), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: _getBefor18(), + maximumDate: _getBefor18(), + onDateTimeChanged: (date) => birthdayDate = date, + ), + ), + ), + ], + ), + ), + ), + ); + } + + DateTime _getBefor18() { + DateTime currentDate = DateTime.now(); + DateTime eighteenYearsAgo = DateTime( + currentDate.year - 18, + currentDate.month, + currentDate.day, + currentDate.hour, + currentDate.minute, + currentDate.second, + currentDate.millisecond, + currentDate.microsecond, + ); + return eighteenYearsAgo; + } + + ///提交数据 + void _submitUserProfile() async { + if (nicknameController.text.isEmpty) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseEnterNickname); + return; + } + if (userProvider?.editUser?.userAvatar == null) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseUploadUserAvatar); + return; + } + if (Provider.of( + context, + listen: false, + ).selectCountryInfo == + null) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectYourCountry); + return; + } + + SCLoadingManager.show(context: context); + userProvider?.updateUserNickname(nicknameController.text); + userProvider?.updateBornYear(birthdayDate.year); + userProvider?.updateBornMonth(birthdayDate.month); + userProvider?.updateBornDay(birthdayDate.day); + num age = DateTime.now().year - birthdayDate.year; + userProvider?.updateAge(age.abs()); + String authType = + Provider.of( + context, + listen: false, + ).authType; + String idToken = + Provider.of( + context, + listen: false, + ).uid; + SocialChatLoginRes user = await SCAccountRepository().regist( + authType, + idToken, + userProvider!.editUser!, + ); + AccountStorage().setCurrentUser(user); + SCLoadingManager.hide(); + SCNavigatorUtils.push(context, SCRoutes.home, clearStack: true); + } +} diff --git a/lib/modules/auth/login_page.dart b/lib/modules/auth/login_page.dart new file mode 100644 index 0000000..6d71362 --- /dev/null +++ b/lib/modules/auth/login_page.dart @@ -0,0 +1,435 @@ +import 'dart:io'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/services/auth/authentication_manager.dart'; +import 'package:yumi/ui_kit/widgets/pop/pop_route.dart'; +import 'package:yumi/modules/webview/webview_page.dart'; +import 'package:yumi/modules/auth/login_route.dart'; + +import '../../shared/data_sources/models/enum/sc_auth_type.dart'; + +/// 登录 +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + LoginPageState createState() => LoginPageState(); +} + +class LoginPageState extends State with WidgetsBindingObserver { + bool isAgreement = false; + SocialChatAuthenticationManager? authProvider; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + authProvider = Provider.of( + context, + listen: false, + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final businessLogicStrategy = SCGlobalConfig.businessLogicStrategy; + + return Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + alignment: Alignment.topCenter, + children: [ + Image.asset( + businessLogicStrategy.getLoginMainBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + Positioned( + top: 120, + child: Image.asset( + businessLogicStrategy.getLoginMainAppIcon(), + width: 107.w, + height: 159.w, + ), + ), + Positioned( + top: 390.w, + left: 27.w, + right: 27.w, + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Platform.isAndroid + ? SCDebounceWidget( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: + businessLogicStrategy + .getLoginMainButtonBackgroundColor(), + ), + padding: EdgeInsets.symmetric( + vertical: 12.w, + horizontal: 24.w, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + businessLogicStrategy + .getLoginMainGoogleIcon(), + width: 30.w, + height: 30.w, + ), + SizedBox(width: 20.w), + Expanded( + child: text( + SCAppLocalizations.of( + context, + )!.signInWithGoogle, + textColor: + businessLogicStrategy + .getLoginMainButtonTextColor(), + fontSize: 16.sp, + fontWeight: FontWeight.bold, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + onTap: () { + _loginByGoogle(); + }, + ) + : Container(), + Platform.isIOS ? SizedBox(height: 20.w) : Container(), + Platform.isIOS + ? SCDebounceWidget( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: + businessLogicStrategy + .getLoginMainButtonBackgroundColor(), + ), + padding: EdgeInsets.symmetric( + vertical: 12.w, + horizontal: 24.w, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + businessLogicStrategy + .getLoginMainAppleIcon(), + width: 30.w, + height: 30.w, + ), + SizedBox(width: 20.w), + Expanded( + child: text( + SCAppLocalizations.of( + context, + )!.signInWithApple, + textColor: + businessLogicStrategy + .getLoginMainButtonTextColor(), + fontSize: 16.sp, + fontWeight: FontWeight.bold, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + onTap: () { + _loginByApple(); + }, + ) + : Container(), + SizedBox(height: 8.w), // 从5.w改为8.w + Container( + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: + businessLogicStrategy + .getLoginMainDividerColor(), + height: 0.5.w, + width: 82.w, + ), + SizedBox(width: 20.w), + text( + SCAppLocalizations.of(context)!.or, + textColor: + businessLogicStrategy + .getLoginMainDividerColor(), + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 20.w), + Container( + color: + businessLogicStrategy + .getLoginMainDividerColor(), + height: 0.5.w, + width: 82.w, + ), + ], + ), + ), + SizedBox(height: 12.w), // 从8.w改为12.w + SCDebounceWidget( + onTap: () { + _goLoginWithAccount(); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: + businessLogicStrategy + .getLoginMainButtonBackgroundColor(), + ), + padding: EdgeInsets.symmetric( + vertical: 12.w, + horizontal: 24.w, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + businessLogicStrategy.getLoginMainAccountIcon(), + width: 30.w, + height: 30.w, + ), + SizedBox(width: 20.w), + Expanded( + child: text( + SCAppLocalizations.of( + context, + )!.signInWithYourAccount, + textColor: + businessLogicStrategy + .getLoginMainButtonTextColor(), + fontSize: 16.sp, + fontWeight: FontWeight.bold, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ], + ), + SizedBox(height: 8.w), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 8.w), + Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + isAgreement = !isAgreement; + }); + }, + child: Container( + padding: EdgeInsets.all(3.w), + child: Image.asset( + businessLogicStrategy.getLoginMainAgreementIcon( + isAgreement, + ), + width: 16.w, + height: 16.w, + ), + ), + ), + ], + ), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 3.w), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: + SCAppLocalizations.of( + context, + )!.loginRepresentsAgreementTo, + style: TextStyle( + color: Colors.white, + fontSize: sp(12), + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: + SCAppLocalizations.of( + context, + )!.termsofService, + style: TextStyle( + color: + businessLogicStrategy + .getLoginMainLinkColor(), + fontSize: sp(12), + fontWeight: FontWeight.w500, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () async { + Navigator.push( + context, + PopRoute( + child: WebViewPage( + title: + SCAppLocalizations.of( + context, + )!.termsofService, + url: + SCGlobalConfig + .userAgreementUrl, + ), + ), + ); + }, + ), + TextSpan( + text: SCAppLocalizations.of(context)!.and, + style: TextStyle( + color: Colors.white, + fontSize: sp(12), + fontWeight: FontWeight.w400, + ), + ), + TextSpan( + text: + SCAppLocalizations.of( + context, + )!.privaceyPolicy, + style: TextStyle( + color: + businessLogicStrategy + .getLoginMainLinkColor(), + fontSize: sp(12), + fontWeight: FontWeight.w500, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () async { + Navigator.push( + context, + PopRoute( + child: WebViewPage( + title: + SCAppLocalizations.of( + context, + )!.privaceyPolicy, + url: + SCGlobalConfig + .privacyAgreementUrl, + ), + ), + ); + }, + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 45.w), + ], + ), + ), + ), + ], + ), + ); + } + + void _loginByGoogle() async { + if (!isAgreement) { + //弹出勾选确认 + showPolicyConfirm(); + return; + } + await authProvider?.authenticateUser(context, SCAuthType.GOOGLE.name); + } + + void showPolicyConfirm() { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.termsOfServicePrivacyPolicyTips, + btnText: SCAppLocalizations.of(context)!.yes, + onEnsure: () { + isAgreement = true; + setState(() {}); + }, + ); + }, + ); + } + + void _goLoginWithAccount() async { + if (!isAgreement) { + //弹出勾选确认 + showPolicyConfirm(); + return; + } + SCNavigatorUtils.push( + context, + LoginRouter.loginWithAccount, + replace: false, + ); + } + + void _loginByApple() { + if (!isAgreement) { + //弹出勾选确认 + showPolicyConfirm(); + return; + } + authProvider?.authenticateUser(context, SCAuthType.APPLE.name); + } +} diff --git a/lib/modules/auth/login_route.dart b/lib/modules/auth/login_route.dart new file mode 100644 index 0000000..3df0de2 --- /dev/null +++ b/lib/modules/auth/login_route.dart @@ -0,0 +1,23 @@ +import 'package:fluro/fluro.dart'; +import 'package:yumi/modules/auth/account/sc_login_with_account_page.dart'; + +import 'package:yumi/app/routes/sc_router_init.dart'; +import 'package:yumi/modules/auth/edit/sc_edit_profile_page.dart'; +import 'package:yumi/modules/auth/login_page.dart'; + +class LoginRouter implements SCIRouterProvider { + static String login = '/login'; + static String editProfile = '/login/edit_profile'; + static String loginWithAccount = '/login/login_account'; + + @override + void initRouter(FluroRouter router) { + router.define(login, + handler: Handler(handlerFunc: (_, params) => LoginPage())); + router.define(editProfile, + handler: Handler(handlerFunc: (_, params) => SCEditProfilePage())); + router.define(loginWithAccount, + handler: Handler( + handlerFunc: (_, params) => SCLoginWithAccountPage())); + } +} diff --git a/lib/modules/chat/activity/message_activity_page.dart b/lib/modules/chat/activity/message_activity_page.dart new file mode 100644 index 0000000..d83e9e8 --- /dev/null +++ b/lib/modules/chat/activity/message_activity_page.dart @@ -0,0 +1,282 @@ +import 'package:extended_text/extended_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; + +class MessageActivityPage extends StatefulWidget { + @override + _MessageActivityPageState createState() => _MessageActivityPageState(); +} + +class _MessageActivityPageState extends State { + List records = []; + num index = 1; + final RefreshController _refreshController = RefreshController(); + final ScrollController _scrollController = ScrollController(); + RtmProvider? rtmProvider; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + @override + void initState() { + super.initState(); + rtmProvider = Provider.of(context, listen: false); + rtmProvider?.onNewActivityMessageCurrentConversationListener = + _onNewMessage; + loadData(); + } + + _onNewMessage(Records? message) async { + if (message == null) { + return; + } + + int? index; + for (var element in records) { + if (element.id == message.id) { + index = records.indexOf(element); + continue; + } + } + if (index != null) { + records.removeAt(index); + records.insert(index, message); + } else { + records.insert(0, message); + } + _scrollController.jumpTo(0.0); + setState(() {}); + } + + @override + void dispose() { + rtmProvider?.onNewActivityMessageCurrentConversationListener = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + _strategy.getSCMessageChatPageRoomSettingBackground(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.activity, + actions: [], + ), + body: SafeArea( + top: false, + child: SmartRefresher( + enablePullDown: false, + enablePullUp: true, + onLoading: () async { + index = index + 1; + loadData(); + }, + footer: CustomFooter( + height: 1, + builder: (context, mode) { + return SizedBox( + height: 1, + width: 1, + child: SizedBox(height: 1, width: 1), + ); + }, + ), + controller: _refreshController, + child: + records.isNotEmpty + ? Container( + child: CustomScrollView( + shrinkWrap: records.length > 3 ? false : true, + controller: _scrollController, + reverse: true, + physics: const ClampingScrollPhysics(), + // 禁用回弹效果 + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (c, i) => _MessageItem(message: records[i]), + childCount: records.length, + ), + ), + ], + ), + ) + : mainEmpty(), + ), + ), + ), + ], + ); + } + + void loadData() { + SCAccountRepository().publicMessagePage("activity", index, 30).then(( + result, + ) { + var list = result.records ?? []; + if (list.isNotEmpty) { + records.addAll(list); + if (list.length == 30) { + _refreshController.loadComplete(); + } else { + _refreshController.loadNoData(); + } + } else { + _refreshController.loadNoData(); + } + setState(() {}); + }); + } +} + +class _MessageItem extends StatelessWidget { + final Records message; + late BuildContext context; + + _MessageItem({required this.message}); + + @override + Widget build(BuildContext context) { + this.context = context; + print('status:${message.status}'); + String time = SCMDateUtils.formatMessageTime( + context, + DateTime.fromMillisecondsSinceEpoch(int.parse(message.createdAt ?? "0")), + ); + return Container( + margin: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 12.w), + child: Row( + children: [ + Spacer(), + Container( + decoration: BoxDecoration( + color: Colors.black12, + borderRadius: BorderRadius.all(Radius.circular(height(13))), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + horizontal: width(15), + vertical: 8.w, + ), + child: Text( + time, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ), + Spacer(), + ], + ), + ), + _msg(message), + ], + ), + ); + } + + _msg(Records message) { + return GestureDetector( + child: Container( + alignment: AlignmentDirectional.centerStart, + decoration: BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 5.w), + Expanded( + child: ExtendedText( + message.title ?? "", + overflow: TextOverflow.ellipsis, + maxLines: 1, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle( + fontSize: sp(16), + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(width: 5.w), + ], + ), + message.imageUrl != null && message.imageUrl!.isNotEmpty + ? Container( + margin: EdgeInsets.all(5.w), + child: netImage( + url: message.imageUrl ?? "", + height: 110.w, + fit: BoxFit.contain, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ) + : Container(), + Row( + children: [ + SizedBox(width: 5.w), + Expanded( + child: ExtendedText( + message.content ?? "", + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: sp(14), color: Colors.white), + ), + ), + SizedBox(width: 5.w), + ], + ), + + ], + ), + ), + onTap: () { + if (message.extraData != null && message.extraData?.link != null) { + if (message.extraData!.link!.startsWith("http") || + message.extraData!.link!.startsWith("https")) { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(message.extraData?.link ?? "")}&showTitle=false", + replace: false, + ); + } + } + }, + ); + } +} diff --git a/lib/modules/chat/chat_route.dart b/lib/modules/chat/chat_route.dart new file mode 100644 index 0000000..9c05086 --- /dev/null +++ b/lib/modules/chat/chat_route.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:fluro/fluro.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:yumi/modules/chat/noti/message_notifcation_page.dart'; +import 'package:yumi/modules/chat/system/message_system_page.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:yumi/app/routes/sc_router_init.dart'; +import 'package:yumi/modules/chat/activity/message_activity_page.dart'; + +import 'message_chat_page.dart'; + +class SCChatRouter implements SCIRouterProvider { + static String chat = '/chat'; + static String systemChat = '/systemChat'; + static String dynamicMsg = '/dynamicMsg'; + static String notifcation = '/chat/notifcation'; + static String activity = '/chat/activity'; + + @override + void initRouter(FluroRouter router) { + router.define( + chat, + handler: Handler( + handlerFunc: + (_, params) => SCMessageChatPage( + conversation: V2TimConversation.fromJson( + jsonDecode(params['conversation']!.first), + ), + ), + ), + ); + + router.define( + systemChat, + handler: Handler( + handlerFunc: + (_, params) => MessageSystemPage( + conversation: V2TimConversation.fromJson( + jsonDecode(params['conversation']!.first), + ), + ), + ), + ); + router.define( + dynamicMsg, + handler: Handler(handlerFunc: (_, params) => Container()), + ); + + router.define( + notifcation, + handler: Handler(handlerFunc: (_, params) => MessageNotifcationPage()), + ); + router.define( + activity, + handler: Handler(handlerFunc: (_, params) => MessageActivityPage()), + ); + } +} diff --git a/lib/modules/chat/message/sc_message_friends_page.dart b/lib/modules/chat/message/sc_message_friends_page.dart new file mode 100644 index 0000000..58f1a03 --- /dev/null +++ b/lib/modules/chat/message/sc_message_friends_page.dart @@ -0,0 +1,320 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/message_friend_user_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; + +import '../../../ui_kit/components/sc_compontent.dart'; +import '../../../ui_kit/components/sc_page_list.dart'; +import '../../../ui_kit/components/text/sc_text.dart'; +import '../chat_route.dart'; + +class MessageFriendsPage extends SCPageList { + @override + _MessageFriendsPageState createState() => _MessageFriendsPageState(); +} + +class _MessageFriendsPageState + extends SCPageListState { + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + String? lastId; + late TextEditingController _textEditingController; + String search = ''; + Timer? _searchTimer; + + @override + void initState() { + super.initState(); + _textEditingController = TextEditingController(); + _textEditingController.addListener(() { + search = _textEditingController.text; + if (search.trim().isNotEmpty) { + _searchTimer?.cancel(); + _searchTimer = Timer(Duration(milliseconds: 550), () { + if (search.trim().isNotEmpty) { + _search(); + } + }); + } + }); + enablePullUp = true; + backgroundColor = Colors.transparent; + isShowDivider = true; + loadData(1); + } + + _search() { + SCAccountRepository() + .friendSearch(search) + .then((result) { + items = result; + loadComplete(); + setState(() {}); + }) + .catchError((e) { + loadComplete(); + setState(() {}); + }); + } + + @override + void dispose() { + _searchTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + Container( + height: kToolbarHeight + ScreenUtil().statusBarHeight, + alignment: Alignment.centerLeft, + child: Row( + children: [ + Expanded( + child: searchWidget( + hint: SCAppLocalizations.of(context)!.inputUserId, + borderColor: Colors.white, + controller: _textEditingController, + ), + ), + SizedBox(width: 10.w), + GestureDetector( + child: text( + SCAppLocalizations.of(context)!.cancel, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.black45, + ), + onTap: () { + _textEditingController.text = ""; + search = ""; + loadData(1); + }, + ), + SizedBox(width: 10.w), + ], + ), + ), + Expanded(child: buildList(context)), + ], + ), + ); + } + + @override + Widget buildItem(MessageFriendUserRes res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration(), + child: Row( + children: [ + SizedBox(width: 10.w), + head(url: res?.userProfile?.userAvatar ?? "", width: 62.w), + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + netImage( + url: + Provider.of(context, listen: false) + .findCountryByName( + res?.userProfile?.countryName ?? "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all(Radius.circular(3.w)), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + socialchatNickNameText( + maxWidth: 160.w, + textColor: Colors.black, + res.userProfile + ?.userNickname ?? + "", + fontSize: 14.sp, + type: + res.userProfile + ?.getVIP() + ?.name ?? + "", + needScroll: + (res.userProfile + ?.userNickname + ?.characters.length ?? + 0) > + 14, + ), + SizedBox(width: 3.w), + Container( + width: 47.w, + height: 24.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + _strategy.getGenderBackgroundImage(res?.userProfile?.userSex == 0), + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + xb(res?.userProfile?.userSex), + text( + "${res?.userProfile?.age}", + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ], + ), + ), + ], + ), + SizedBox(height: 4.w), + Row( + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + _strategy.getIdBackgroundImage(res?.userProfile?.hasSpecialId() ?? false), + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + "${res?.userProfile?.getID()}", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + _strategy.getCopyIdIcon(), + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData( + text: res?.userProfile?.getID() ?? "", + ), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + SizedBox(width: 8.w), + getWealthLevel( + res?.userProfile?.wealthLevel ?? 0, + width: 58.w, + height: 30.w, + ), + SizedBox(width: 8.w), + getUserLevel( + res?.userProfile?.charmLevel ?? 0, + width: 58.w, + height: 30.w, + ), + ], + ), + ], + ), + ), + SizedBox(width: 15.w), + ], + ), + ), + onTap: () async { + var conversation = V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: res.userProfile?.id, + conversationID: '', + ); + var bool = await Provider.of( + context, + listen: false, + ).startConversation(conversation); + if (!bool) return; + var json = jsonEncode(conversation.toJson()); + SCNavigatorUtils.push( + context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + }, + ); + } + + @override + builderDivider() { + return Divider( + height: 22.w, + color: Color(0xffF2F2F2), + indent: 15.w, + endIndent: 15.w, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + if (_textEditingController.text.isNotEmpty) { + _search(); + } else { + if (page == 1) { + lastId = null; + } + try { + var followList = await SCAccountRepository().friendList(lastId: lastId); + if (followList.isNotEmpty) { + lastId = followList.last.id; + } + onSuccess(followList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + } +} diff --git a/lib/modules/chat/message/sc_message_page.dart b/lib/modules/chat/message/sc_message_page.dart new file mode 100644 index 0000000..b74f78c --- /dev/null +++ b/lib/modules/chat/message/sc_message_page.dart @@ -0,0 +1,422 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/ui_kit/widgets/msg/message_conversation_list_page.dart'; +import 'package:yumi/modules/chat/message/sc_message_friends_page.dart'; + +import '../../index/main_route.dart'; +import '../chat_route.dart'; + +///消息 +class SCMessagePage extends StatefulWidget { + bool isFromRoom = false; + + SCMessagePage({this.isFromRoom = false}); + + @override + _MessagePageState createState() => _MessagePageState(isFromRoom); +} + +class _MessagePageState extends State { + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + bool isFromRoom = false; + bool showSystemAnnouncementTips = true; + + _MessagePageState(this.isFromRoom); + + @override + void initState() { + super.initState(); + showSystemAnnouncementTips = DataPersistence.getBool( + "${AccountStorage().getCurrentUser()?.userProfile?.account}-ShowSystemAnnouncementTips", + defaultValue: true, + ); + _checkLogin(); + } + + void _checkLogin() async { + V2TimValueCallback getLoginStatusRes = + await TencentImSDKPlugin.v2TIMManager.getLoginStatus(); + if (getLoginStatusRes.code == 0) { + var status = getLoginStatusRes.data; // getLoginStatusRes.data为用户登录状态值 + if (status == 1) { + // 已登录 + ///处理有时候拉不到最后一条消息 + Provider.of( + context, + listen: false, + ).initConversation().then((value) { + Future.delayed(Duration(milliseconds: 550), () { + Provider.of( + context, + listen: false, + ).initConversation().then((value) { + setState(() {}); + }); + }); + }); + } else if (status == 2) { + // 登录中 + } else if (status == 3) { + // 未登录 + Provider.of( + context, + listen: false, + ).loginTencetRtm(context).then((value) { + _checkLogin(); + }); + } + setState(() {}); + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + widget.isFromRoom + ? Image.asset( + _strategy.getMessagePageIndexMaskIcon(), + width: ScreenUtil().screenWidth, + fit: BoxFit.cover, + ) + : Container(), + DefaultTabController( + length: 1, + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + leading: null, + title: TabBar( + tabs: + [SCAppLocalizations.of(context)!.message] + .map( + (e) => Padding( + padding: EdgeInsets.only(bottom: 3.w, top: 4.w), + child: Text(e), + ), + ) + .toList(), + dividerColor: Colors.transparent, + labelStyle: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + ), + unselectedLabelStyle: TextStyle( + fontSize: 15.sp, + color: Colors.white, + fontWeight: FontWeight.normal, + ), + indicator: BoxDecoration(), + //TabBarIndicator(), + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + tabAlignment: TabAlignment.start, + labelColor: SocialChatTheme.primaryLight, + ), + actions: [ + SCDebounceWidget( + child: Container( + padding: EdgeInsetsDirectional.only(end: 10.w), + child: Image.asset( + "sc_images/index/sc_icon_serach.png", + width: 20.w, + height: 20.w, + ), + ), + onTap: () { + SCNavigatorUtils.push(context, SCMainRoute.mainSearch); + }, + ), + ], + ), + body: TabBarView( + children: [ + Consumer( + builder: (_, provider, __) { + return Container( + margin: EdgeInsetsDirectional.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/index/sc_index_msg_content_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + _headMenu(provider), + Expanded( + child: Container( + margin: EdgeInsets.only( + top: 0.w, + left: 8.w, + right: 8.w, + ), + child: MessageConversationListPage(false), + ), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _headMenu(RtmProvider provider) { + return Container( + margin: EdgeInsetsDirectional.symmetric(horizontal: 25.w,).copyWith(top: 10.w), + child: Column( + spacing: 15.w, + children: [ + SCDebounceWidget( + child: Stack( + children: [ + Row( + children: [ + SizedBox(height: 3.w), + Image.asset( + _strategy.getMessagePageActivityMessageIcon(), + width: 45.w, + height: 45.w, + ), + SizedBox(width: 5.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text( + SCAppLocalizations.of(context)!.activity, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + SizedBox(height: 3.w), + text( + "[${SCAppLocalizations.of(context)!.newMessage}]", + fontSize: 12.sp, + textColor: Colors.white54, + ), + ], + ), + ], + ), + PositionedDirectional( + bottom: 0, + end: 10.w, + child: Visibility( + visible: provider.activityUnReadCount > 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 6.w), + alignment: Alignment.center, + height: 16.w, + decoration: BoxDecoration( + //gradient: LinearGradient(colors: [Color(0xffA447FF), Color(0xff623CE9)]), + color: Colors.red, + shape: BoxShape.circle, + //borderRadius: BorderRadius.all(Radius.circular(7.w)), + ), + child: Text( + "${provider.activityUnReadCount > 99 ? '99+' : provider.activityUnReadCount}", + style: TextStyle( + fontSize: 10.sp, + color: Color(0xffffffff), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + ), + ), + ], + ), + onTap: () { + provider.updateActivityCount(0); + SCNavigatorUtils.push( + context, + SCChatRouter.activity, + replace: false, + ); + }, + ), + SCDebounceWidget( + child: Stack( + children: [ + Row( + children: [ + SizedBox(height: 3.w), + Image.asset( + _strategy.getMessagePageSystemMessageIcon(), + width: 45.w, + height: 45.w, + ), + SizedBox(width: 5.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text( + SCAppLocalizations.of(context)!.system, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + SizedBox(height: 3.w), + text( + "[${SCAppLocalizations.of(context)!.newMessage}]", + fontSize: 12.sp, + textColor: Colors.white54, + ), + ], + ), + ], + ), + PositionedDirectional( + bottom: 0, + end: 10.w, + child: Visibility( + visible: provider.systemUnReadCount > 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 6.w), + alignment: Alignment.center, + height: 16.w, + decoration: BoxDecoration( + //gradient: LinearGradient(colors: [Color(0xffA447FF), Color(0xff623CE9)]), + color: Colors.red, + shape: BoxShape.circle, + //borderRadius: BorderRadius.all(Radius.circular(7.w)), + ), + child: Text( + "${provider.systemUnReadCount > 99 ? '99+' : provider.systemUnReadCount}", + style: TextStyle( + fontSize: 10.sp, + color: Color(0xffffffff), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + ), + ), + ], + ), + onTap: () async { + var conversation = V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: "atyou-admin", + conversationID: SCGlobalConfig.imAdmin, + ); + provider.updateSystemCount(0); + var bool = await provider.startConversation(conversation); + if (!bool) return; + var json = jsonEncode(conversation.toJson()); + SCNavigatorUtils.push( + context, + "${SCChatRouter.systemChat}?conversation=${Uri.encodeComponent(json)}", + ); + }, + ), + SCDebounceWidget( + child: Stack( + children: [ + Container( + alignment: Alignment.center, + child: Row( + children: [ + SizedBox(height: 3.w), + Image.asset( + _strategy.getMessagePageNotificationMessageIcon(), + width: 45.w, + height: 45.w, + ), + SizedBox(width: 5.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text( + SCAppLocalizations.of(context)!.notifcation, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + SizedBox(height: 3.w), + text( + "[${SCAppLocalizations.of(context)!.newMessage}]", + fontSize: 12.sp, + textColor: Colors.white54, + ), + ], + ) + ], + ), + ), + PositionedDirectional( + bottom: 0, + end: 10.w, + child: Visibility( + visible: provider.notifcationUnReadCount > 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 6.w), + alignment: Alignment.center, + height: 16.w, + decoration: BoxDecoration( + //gradient: LinearGradient(colors: [Color(0xffA447FF), Color(0xff623CE9)]), + color: Colors.red, + shape: BoxShape.circle, + //borderRadius: BorderRadius.all(Radius.circular(7.w)), + ), + child: Text( + "${provider.notifcationUnReadCount > 99 ? '99+' : provider.notifcationUnReadCount}", + style: TextStyle( + fontSize: 10.sp, + color: Color(0xffffffff), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + ), + ), + ], + ), + onTap: () { + provider.updateNotificationCount(0); + SCNavigatorUtils.push( + context, + SCChatRouter.notifcation, + replace: false, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/modules/chat/message_chat_page.dart b/lib/modules/chat/message_chat_page.dart new file mode 100644 index 0000000..d550eea --- /dev/null +++ b/lib/modules/chat/message_chat_page.dart @@ -0,0 +1,2099 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:extended_image/extended_image.dart' + show ExtendedImage, ExtendedRawImage, ExtendedImageState, LoadState; +import 'package:extended_text/extended_text.dart'; +import 'package:extended_text_field/extended_text_field.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_emoji_datas.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_message_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:marquee/marquee.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_extension.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_online_url.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_text_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_video_elem.dart'; +import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; + +import 'package:yumi/config/pickImage.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_keybord_util.dart'; +import 'package:yumi/shared/tools/sc_message_notifier.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/usecases/custom_tab_selector.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart'; + +class SCMessageChatPage extends StatefulWidget { + final V2TimConversation? conversation; + final bool shrinkWrap; + + //是否是房间内聊天界面。 + final bool inRoom; + + const SCMessageChatPage({ + Key? key, + this.conversation, + this.shrinkWrap = false, + this.inRoom = false, + }) : super(key: key); + + @override + _SCMessageChatPageState createState() => _SCMessageChatPageState(); +} + +class _SCMessageChatPageState extends State { + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + final TextEditingController _textController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + final RefreshController _refreshController = RefreshController(); + + // FlutterSoundPlayer? flutterSound; + RtmProvider? rtmProvider; + V2TimConversation? currentConversation; + List currentConversationMessageList = []; + final FocusNode _focusNode = FocusNode(); + + ///是否显示工具栏 + bool showTools = false; + bool showEmoji = false; + + bool showDoNotClickUnfamiliarTips = true; + + ///互相关注才能互相发送消息 + bool canSendMsg = false; + SocialChatUserProfile? friend; + + ///是否显示发送按钮 + bool showSend = false; + + List coinsTitles = ["100", "1000", "10000", "50000"]; + String selecteCoins = "100"; + + @override + void initState() { + super.initState(); + SCMessageNotifier.canPlay = false; + SCMessageUtils.redPacketFutureCache.clear(); + + // flutterSound = FlutterSoundPlayer(); + + rtmProvider = Provider.of(context, listen: false); + currentConversation = widget.conversation; + rtmProvider?.onMessageRecvC2CReadListener = _onMessageRecvC2CRead; + rtmProvider?.onRevokeMessageListener = _onRevokeMessage; + rtmProvider?.onNewMessageCurrentConversationListener = _onNewMessage; + loadMsg(); + + if (friend == null) { + loadFriend(); + } + + _focusNode.addListener(() { + if (_focusNode.hasFocus) { + setState(() { + showTools = false; + showEmoji = false; + }); + } + }); + } + + @override + void dispose() { + rtmProvider?.onMessageRecvC2CReadListener = null; + rtmProvider?.onRevokeMessageListener = null; + rtmProvider?.onNewMessageCurrentConversationListener = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (_, provider, __) { + return WillPopScope( + onWillPop: () { + if (showEmoji || showTools) { + setState(() { + showEmoji = false; + showTools = false; + }); + return Future.value(false); + } + SCNavigatorUtils.goBack(context); + return Future.value(true); + }, + child: Stack( + children: [ + if (!widget.inRoom) + // 渐变背景 + Positioned( + left: 0, + right: 0, + top: 0, + child: Column( + children: [ + SizedBox( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + child: Image.asset( + _strategy.getSCMessageChatPageRoomSettingBackground(), + fit: BoxFit.fill, + ), + ), + ], + ), + ), + Scaffold( + //backgroundColor: widget.inRoom ? Colors.transparent: Color(0xfff8f8f8), + backgroundColor: Colors.transparent, + // appBar: _appBar(), + appBar: null, + body: SafeArea( + top: widget.shrinkWrap ? false : true, + bottom: false, + child: Column( + children: [ + Row( + children: [ + !widget.inRoom + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.pop(context); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, + ), + child: Icon( + SCGlobalConfig.lang == "ar" + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + size: 28.w, + color: Colors.white, + ), + ), + ) + : Container(), + SizedBox(width: 5.w), + Expanded( + child: Container( + alignment: AlignmentDirectional.center, + child: socialchatNickNameText( + friend?.userNickname ?? "", + fontSize: 14.sp, + fontWeight: FontWeight.w500, + textColor: Colors.white, + type: "", + needScroll: + (friend?.userNickname?.characters.length ?? + 0) > + 22, + ), + ), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SCNavigatorUtils.push( + context, + replace: true, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == friend?.id}&tageId=${friend?.id}", + ); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Icon( + Icons.more_vert, + size: 22.w, + color: Colors.white, + ), + ), + ), + ], + ), + Expanded(child: _msgList()), + Container( + decoration: BoxDecoration( + //color: Colors.white, + //border: Border(top: BorderSide(color: Color(0xfff1f1f1))) + ), + child: SafeArea( + top: false, + child: Column( + children: [ + if (currentConversation?.conversationID != + "administrator") + _input(), + _tools(provider), + _emoji(), + // _fahongbao(), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + void loadFriend() { + SCLoadingManager.show(); + Future.wait([ + SCAccountRepository().loadUserInfo("${currentConversation?.userID}"), + SCAccountRepository().friendRelationCheck( + "${currentConversation?.userID}", + ), + ]) + .then((result) { + SCLoadingManager.hide(); + friend = result[0] as SocialChatUserProfile; + canSendMsg = result[1] as bool; + setState(() {}); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } + + ///消息列表 + Widget _msgList() { + print('xiaoxiliebiao:${currentConversationMessageList.length}'); + return SmartRefresher( + enablePullDown: false, + enablePullUp: true, + onLoading: () async { + print('onLoading'); + if (currentConversationMessageList.isNotEmpty) { + // 拉取单聊历史消息 + // 首次拉取,lastMsgID 设置为 null + // 再次拉取时,lastMsgID 可以使用返回的消息列表中的最后一条消息的id + V2TimValueCallback> v2timValueCallback = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .getC2CHistoryMessageList( + userID: currentConversation!.userID ?? "", + count: 30, + lastMsgID: currentConversationMessageList.last.msgID, + ); + List messages = + v2timValueCallback.data as List; + print('加载前:${currentConversationMessageList.length}'); + currentConversationMessageList.addAll(messages); + print('加载后:${currentConversationMessageList.length}'); + if (messages.length == 30) { + _refreshController.loadComplete(); + } else { + _refreshController.loadNoData(); + } + } else { + _refreshController.loadNoData(); + } + setState(() {}); + }, + footer: CustomFooter( + height: 1, + builder: (context, mode) { + return SizedBox( + height: 1, + width: 1, + child: SizedBox(height: 1, width: 1), + ); + }, + ), + controller: _refreshController, + child: CustomScrollView( + shrinkWrap: true, + controller: _scrollController, + reverse: true, + physics: const ClampingScrollPhysics(), + // 禁用回弹效果 + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (c, i) => _MessageItem( + message: currentConversationMessageList[i], + preMessage: + i < currentConversationMessageList.length - 1 + ? currentConversationMessageList[i + 1] + : null, + isSystem: + currentConversationMessageList[i].sender == "administrator", + friend: friend, + currentConversationMessageList: currentConversationMessageList, + updateCall: () { + setState(() {}); + }, + ), + childCount: currentConversationMessageList.length, + ), + ), + ], + ), + ); + } + + Future loadMsg() async { + // 1. 在初始化时,添加高级消息监听器 + TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( + listener: V2TimAdvancedMsgListener( + // 监听消息扩展更新 + onRecvMessageExtensionsChanged: (msgID, extensions) { + for (var ext in extensions) { + if (ext.extensionKey == "packetID") { + String packetID = ext.extensionValue; + SCMessageUtils.redPacketFutureCache[packetID] = + SCAccountRepository().userRedPacketDetail(packetID); + setState(() {}); + break; + } + } + }, + ), + ); + V2TimValueCallback> v2timValueCallback = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .getC2CHistoryMessageList( + userID: currentConversation!.userID!, + count: 100, + lastMsgID: null, + ); + List messages = v2timValueCallback.data!; + // List messages = await FTIM.getMessageManager().getMessages(conversation: currentConversation); + print("messages : ${messages?.length ?? 0}"); + currentConversationMessageList ??= []; + currentConversationMessageList?.clear(); + + for (var msg in messages) { + if (!msg.isSelf! && msg.isPeerRead!) { + TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessageReadReceipts(messageIDList: [msg.msgID!]); + } + } + currentConversationMessageList.insertAll(0, messages ?? []); + setState(() {}); + } + + ///输入栏 + Widget _input() { + return Consumer( + builder: + (_, provider, __) => Container( + color: Colors.transparent, + height: 56.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 15.w), + Expanded( + child: Container( + height: 34.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(52.w), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: Row( + children: [ + Expanded( + child: ExtendedTextField( + controller: _textController, + focusNode: _focusNode, + onChanged: (s) { + setState(() { + showSend = _textController.text.isNotEmpty; + }); + }, + onSubmitted: (s) { + if (s.isNotEmpty) { + sendMsg(s); + showSend = false; + setState(() {}); + _scrollController.jumpTo(0.0); + _textController.clear(); + } + }, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of( + context, + )!.clickHereToStartChatting, + hintStyle: TextStyle( + color: Color(0xff999999), + fontSize: 14.sp, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 15, + ), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + ), + style: TextStyle( + fontSize: ScreenUtil().setSp(14), + color: Colors.white, + ), + ), + ), + //原来的emoji按钮 + //SizedBox(width: 10.w,), + ], + ), + ), + ), + SizedBox(width: 10.w), + !showEmoji + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SCKeybordUtil.hide(context); + setState(() { + if (showTools) { + showTools = false; + } + showEmoji = !showEmoji; + }); + }, + child: Image.asset( + _strategy.getSCMessageChatPageEmojiIcon(), + width: 24.w, + color: Colors.white, + fit: BoxFit.fill, + ), + ) + : Container(), + showEmoji + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SCKeybordUtil.hide(context); + setState(() { + if (showTools) { + showTools = false; + } + showEmoji = !showEmoji; + }); + }, + child: Image.asset( + _strategy.getSCMessageChatPageChatKeyboardIcon(), + width: 24.w, + color: Colors.white, + fit: BoxFit.fill, + ), + ) + : Container(), + if (!showSend) SizedBox(width: 12.w), + if (showSend) SizedBox(width: 5.w), + Visibility( + visible: !showSend, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SCKeybordUtil.hide(context); + setState(() { + if (showEmoji) { + showEmoji = false; + } + showTools = !showTools; + }); + }, + child: Image.asset( + _strategy.getSCMessageChatPageAddIcon(), + width: 24.w, + color: Colors.white, + fit: BoxFit.fill, + ), + ), + ), + Visibility( + visible: showSend, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + _scrollController.jumpTo(0.0); + if (_textController.text.isNotEmpty) { + SCKeybordUtil.hide(context); + sendMsg(_textController.text); + showSend = false; + setState(() {}); + _textController.clear(); + } + }, + child: Image.asset( + _strategy.getSCMessageChatPageSendMessageIcon(), + height: 22.w, + color: Colors.white, + width: 32.w, + ), + ), + ), + SizedBox(width: 15.w), + ], + ), + ), + ); + } + + ///发消息 + sendMsg(String msg) async { + if (!canSendMsg) { + SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips); + return; + } + Provider.of( + context, + listen: false, + ).sendC2CTextMsg(msg, currentConversation!); + } + + ///发消息 + sendCustomMsg(String msg, String extension) async { + if (!canSendMsg) { + SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips); + return; + } + Provider.of( + context, + listen: false, + ).sendC2CCustomMsg(msg, currentConversation!, extension); + } + + ///工具栏 + Widget _tools(RtmProvider provider) { + return Visibility( + visible: showTools, + child: Container( + color: Colors.white, + padding: EdgeInsets.only(bottom: 18.w, top: 5.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox(width: 20.w), + Expanded( + child: _toolsItem( + _strategy.getSCMessageChatPageCameraIcon(), + SCAppLocalizations.of(context)!.camera, + () async { + if (!canSendMsg) { + SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips); + return; + } + var pick = await ImagePick.pickFromCamera(context); + if (pick != null) { + provider.sendImageMsg( + file: pick, + conversation: currentConversation!, + ); + } + }, + ), + ), + Expanded( + child: _toolsItem( + _strategy.getSCMessageChatPagePictureIcon(), + SCAppLocalizations.of(context)!.album, + () async { + if (!canSendMsg) { + SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips); + return; + } + final List? result = + await ImagePick.pickPicAndVideoFromGallery(context); + + if (result == null) return; // 用户取消选择 + if (result.isNotEmpty) { + provider.sendImageMsg( + selectedList: result, + conversation: currentConversation!, + ); + } + }, + ), + ), + Expanded(flex: 1, child: SizedBox()), + Expanded(flex: 1, child: SizedBox()), + ], + ), + ), + ); + } + + _emoji() { + return Visibility( + visible: showEmoji, + child: Container( + color: Colors.white, + height: 150.w, + child: GridView.builder( + padding: EdgeInsets.symmetric(horizontal: 15.w), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 8, + crossAxisSpacing: 8.w, + mainAxisSpacing: 8.w, + ), + itemBuilder: (c, index) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + var text = SCEmojiDatas.smileys[index]; + final TextEditingValue value = _textController.value; + _textController.value = value.copyWith( + text: value.text + text, + selection: TextSelection.fromPosition( + TextPosition(offset: text.length), + ), + ); + showSend = true; + setState(() {}); + }, + child: Padding( + padding: EdgeInsets.all(4.0.w), + child: ExtendedText( + SCEmojiDatas.smileys[index], + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: sp(15)), + ), + ), + ); + }, + itemCount: SCEmojiDatas.smileys.length, + ), + ), + ); + } + + ///工具项 + _toolsItem(String image, String title, Function() onclick) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onclick, + child: Column( + children: [ + Image.asset(image, width: 50.w, fit: BoxFit.fitWidth), + SizedBox(height: 9.w), + Text(title, style: TextStyle(fontSize: 11.sp, color: Colors.black)), + ], + ), + ); + } + + _onRevokeMessage(String msgId) { + int index = 0; + for (var msg in currentConversationMessageList) { + if (msgId == msg.msgID) { + break; + } + index = index + 1; + } + currentConversationMessageList.removeAt(index); + setState(() {}); + } + + _onMessageRecvC2CRead(List messageIDList) { + for (var msg in currentConversationMessageList) { + for (var id in messageIDList) { + if (id == msg.msgID) { + msg.isPeerRead = true; + } + } + } + setState(() {}); + } + + _onNewMessage(V2TimMessage? message, {String? msgId}) async { + if (message == null) { + return; + } + if (message.userID != currentConversation?.userID) return; + + int? index; + for (var element in currentConversationMessageList) { + if (msgId != null) { + if (msgId == element.msgID) { + element.status = MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL; + setState(() {}); + continue; + } + } else { + if (element.msgID == message.msgID) { + index = currentConversationMessageList.indexOf(element); + continue; + } + } + } + if (msgId != null) { + return; + } + if (index != null) { + currentConversationMessageList.removeAt(index!); + currentConversationMessageList.insert(index!, message); + } else { + currentConversationMessageList.insert(0, message); + } + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .markC2CMessageAsRead(userID: currentConversation!.userID!); + //发送已读回执 + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessageReadReceipts(messageIDList: [message.msgID!]); + setState(() {}); + // await FTIM.getContactManager().setReadMessage(currentConversation); + } +} + +class _MessageItem extends StatelessWidget { + final V2TimMessage message; + final V2TimMessage? preMessage; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + // final FTIMUserProfile userProfile; + final SocialChatUserProfile? friend; + bool isSystem = false; + List currentConversationMessageList = []; + Function updateCall; + + ///上一条 + late BuildContext context; + + _MessageItem({ + required this.message, + this.preMessage, + // this.userProfile, + this.isSystem = false, + required this.currentConversationMessageList, + this.friend, + required this.updateCall, + }); + + @override + Widget build(BuildContext context) { + this.context = context; + SocialChatUserProfile? me = AccountStorage().getCurrentUser()?.userProfile; + bool showTime = true; + int timestamp = (message.timestamp ?? 0) * 1000; + int preTimestamp = (preMessage?.timestamp ?? 0) * 1000; + if (preMessage != null) { + ///5分钟以内 不显示 + // print('timestamp:$timestamp===preTimestamp:${preTimestamp}'); + if (DateTime.fromMillisecondsSinceEpoch(timestamp) + .difference(DateTime.fromMillisecondsSinceEpoch(preTimestamp)) + .inMilliseconds < + 1000 * 60 * 5) { + showTime = false; + } + } + String time = SCMDateUtils.formatMessageTime( + context, + DateTime.fromMillisecondsSinceEpoch(timestamp ?? 0), + ); + if (message.status == MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED) { + return Container( + alignment: AlignmentDirectional.center, + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), + child: text( + SCAppLocalizations.of(context)!.messageHasBeenRecalled, + fontSize: 12.sp, + textColor: Colors.white54, + fontWeight: FontWeight.w400, + ), + ); + } + return Container( + margin: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), + child: Column( + crossAxisAlignment: + !message.isSelf! + ? CrossAxisAlignment.start + : CrossAxisAlignment.end, + // textDirection: message.isSelf ? TextDirection.rtl : TextDirection.ltr, + children: [ + Visibility( + visible: showTime, + child: Container( + margin: EdgeInsets.only(bottom: 12.w), + child: Row( + children: [ + Spacer(), + Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + horizontal: width(15), + vertical: 8.w, + ), + decoration: BoxDecoration( + //color: Color(0xFFEAEFF7), + borderRadius: BorderRadius.all( + Radius.circular(height(13)), + ), + ), + child: Text( + time, + style: TextStyle( + fontSize: sp(12), + color: Colors.white54, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ), + Spacer(), + ], + ), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + textDirection: + message.isSelf! ? TextDirection.rtl : TextDirection.ltr, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (isSystem) { + return; + } + SCNavigatorUtils.push( + context, + replace: + AccountStorage().getCurrentUser()?.userProfile?.id != + message.sender, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == message.sender}&tageId=${message.sender}", + ); + }, + child: + message.sender == "administrator" || + message.sender == "customer" + ? ExtendedImage.asset( + _strategy.getSCMessageChatPageSystemHeadImage(), + width: 45.w, + shape: BoxShape.circle, + fit: BoxFit.cover, + ) + : netImage( + url: + message.isSelf! + ? (me?.userAvatar ?? "") + : (friend?.userAvatar ?? ""), + shape: BoxShape.circle, + width: width(45), + ), + ), + // 他人消息的阴影间隔是10dp + Container(width: 8.w), + Row( + textDirection: + message.isSelf! ? TextDirection.rtl : TextDirection.ltr, + children: [ + Container( + alignment: + message.isSelf! + ? AlignmentDirectional.topEnd + : AlignmentDirectional.topStart, + margin: EdgeInsets.only(top: 4.w), + child: _msg(), + ), + SizedBox(width: 6.w), + Visibility( + visible: + message.status == + MessageStatus.V2TIM_MSG_STATUS_SENDING, + child: CupertinoActivityIndicator(), + ), + + Visibility( + visible: + message.status == + MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL, + child: Icon(Icons.error, color: Colors.red), + ), + ], + ), + Spacer(), + ], + ), + ], + ), + ); + } + + _msg() { + V2TIMElem? elem = message.textElem; + if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) { + V2TimTextElem textElem = elem as V2TimTextElem; + String content = textElem.text ?? ""; + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE) { + V2TimImageElem imageElem = message.imageElem as V2TimImageElem; + var image; + if (imageElem.path!.length > 2 && + File("${imageElem.path}").existsSync()) { + image = Image.file(File(imageElem.path!), fit: BoxFit.cover); + } else { + image = ExtendedImage.network( + '${imageElem.imageList![1]?.url}', + fit: BoxFit.cover, + width: double.infinity, + cache: true, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.all(Radius.circular(width(6))), + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == LoadState.completed) { + return ExtendedRawImage( + image: state.extendedImageInfo?.image, + fit: BoxFit.cover, + ); + } else { + return Image.asset( + _strategy.getSCMessageChatPageLoadingIcon(), + fit: BoxFit.cover, + ); + } + }, + ); + } + return Builder( + builder: (ct) { + return GestureDetector( + onTap: () { + List ims = []; + int initialIndex = 0; + int index = 0; + for (var imageMsg in currentConversationMessageList) { + if (imageMsg.elemType == + MessageElemType.V2TIM_ELEM_TYPE_IMAGE) { + String path = imageMsg.imageElem?.imageList![1]?.url ?? ""; + if (path.isNotEmpty) { + ims.add(imageMsg.imageElem?.imageList![1]?.url ?? ""); + if (imageMsg.msgID == message.msgID) { + initialIndex = index; + } + index = index + 1; + } + } + } + String encodedUrls = Uri.encodeComponent(jsonEncode(ims)); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=$initialIndex", + ); + }, + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + child: Container( + constraints: BoxConstraints(maxWidth: 200.w), + child: ClipRRect( + borderRadius: BorderRadius.circular(6.w), + child: image, + ), + ), + ); + }, + ); + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) { + V2TimVideoElem videoElem = message.videoElem as V2TimVideoElem; + if (videoElem.snapshotPath != null && + videoElem.snapshotPath!.isNotEmpty) { + return Builder( + builder: (ct) { + return GestureDetector( + child: Container( + constraints: BoxConstraints(maxWidth: 200.w), + child: ClipRRect( + borderRadius: BorderRadius.circular(6.w), + child: Stack( + alignment: Alignment.center, + children: [ + Image.file( + File(videoElem.snapshotPath!), + fit: BoxFit.cover, + ), + Icon(Icons.play_circle, color: Colors.white, size: 55.w), + ], + ), + ), + ), + onTap: () { + String encodedVideoUrl = Uri.encodeComponent( + videoElem.videoPath ?? "", + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.videoPlayer}?videoUrl=$encodedVideoUrl", + ); + }, + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + ); + }, + ); + } else { + return FutureBuilder>( + // 假设 getMessageOnlineUrlForMessage 是对 TencentImSDKPlugin.v2TIMManager.getMessageManager().getMessageOnlineUrl 的封装,返回 Future + future: SCMessageUtils().getMessageOnlineUrl( + msgID: message.msgID ?? "", + ), + builder: (context, snapshot) { + // 根据异步任务状态构建UI + if (snapshot.connectionState == ConnectionState.waiting) { + // 数据加载中,显示占位符 + return Container(); + } else if (snapshot.hasError) { + // 数据加载失败,显示错误UI + return Container(); + } else if (snapshot.hasData) { + // 数据加载成功,更新消息的mediaUrl并构建正常消息UI + message.videoElem = snapshot.data?.data?.videoElem!; + return Builder( + builder: (ct) { + return GestureDetector( + child: Container( + constraints: BoxConstraints(maxWidth: 200.w), + child: ClipRRect( + borderRadius: BorderRadius.circular(6.w), + child: Stack( + alignment: Alignment.center, + children: [ + SCPathUtils.getPathType( + message.videoElem?.snapshotPath ?? "", + ) == + PathType.file + ? Image.file( + File(message.videoElem?.snapshotPath ?? ""), + fit: BoxFit.cover, + ) + : netImage( + url: message.videoElem?.snapshotUrl ?? "", + ), + Icon( + Icons.play_circle, + color: Colors.white, + size: 55.w, + ), + ], + ), + ), + ), + onTap: () { + String encodedVideoUrl = Uri.encodeComponent( + message.videoElem?.videoUrl ?? "", + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.videoPlayer}?videoUrl=$encodedVideoUrl", + ); + }, + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + ); + }, + ); + } else { + // 其他情况 + return Container(); + } + }, + ); + } + } + + return _text(); + } + + ///文本消息 + _text() { + String content = ""; + if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) { + V2TimTextElem textElem = message.textElem!; + content = textElem.text ?? ""; + if (isSystem) {} + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE) { + content = SCAppLocalizations.of(context)!.image; + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) { + content = SCAppLocalizations.of(context)!.video; + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND) { + content = SCAppLocalizations.of(context)!.sound; + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) { + V2TimCustomElem customElem = message.customElem!; + content = customElem.data ?? ""; + } + + return Builder( + builder: (ct) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.only( + topLeft: + message.isSelf! ? Radius.circular(8) : Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: + message.isSelf! ? Radius.circular(0) : Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Container( + constraints: BoxConstraints(maxWidth: width(220)), + child: ExtendedText( + content, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle( + fontSize: sp(14), + color: message.isSelf! ? Colors.white : Colors.white, + ), + ), + ), + ), + onLongPress: () { + _showMsgItemMenu(ct, content); + }, + ); + }, + ); + } + + void _showMsgItemMenu(BuildContext ct, String content) { + SmartDialog.showAttach( + tag: "showMsgItemMenu", + targetContext: ct, + alignment: Alignment.topCenter, + maskColor: Colors.transparent, + animationType: SmartAnimationType.fade, + scalePointBuilder: (selfSize) => Offset(selfSize.width, 10), + builder: (_) { + return Container( + margin: EdgeInsets.only(bottom: 10.w), + decoration: BoxDecoration( + color: Color(0xffdcdddf), + borderRadius: BorderRadius.circular(10), + ), + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SmartDialog.dismiss(tag: "showMsgItemMenu"); + _delete(); + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.w), + child: Column( + children: [ + Image.asset( + _strategy.getSCMessageChatPageMessageMenuDeleteIcon(), + width: 20.w, + ), + SizedBox(height: 3.w), + text( + SCAppLocalizations.of(context)!.delete, + fontSize: 12.sp, + textColor: Color(0xff777777), + ), + ], + ), + ), + ), + content.isNotEmpty + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SmartDialog.dismiss(tag: "showMsgItemMenu"); + Clipboard.setData(ClipboardData(text: content)); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.w), + child: Column( + children: [ + Image.asset( + _strategy.getSCMessageChatPageMessageMenuCopyIcon(), + width: 20.w, + ), + SizedBox(height: 3.w), + text( + SCAppLocalizations.of(context)!.copy, + fontSize: 12.sp, + textColor: Color(0xff777777), + ), + ], + ), + ), + ) + : Container(), + (message.isSelf ?? false) + ? GestureDetector( + onTap: () { + SmartDialog.dismiss(tag: "showMsgItemMenu"); + _revokeMessage(); + }, + behavior: HitTestBehavior.opaque, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.w), + child: Column( + children: [ + Image.asset( + _strategy + .getSCMessageChatPageMessageMenuRecallIcon(), + width: 20.w, + ), + SizedBox(height: 3.w), + text( + SCAppLocalizations.of(context)!.recall, + fontSize: 12.sp, + textColor: Color(0xff777777), + ), + ], + ), + ), + ) + : Container(), + ], + ), + ); + }, + ); + } + + ///撤回消息 + void _revokeMessage() async { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.recallThisMessage, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () async { + // 撤回指定msgID的消息 + V2TimCallback revokeMessage = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .revokeMessage(msgID: message.msgID ?? ""); + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .deleteMessages(msgIDs: [message.msgID ?? ""]); + if (revokeMessage.code == 0) { + // 撤回成功 + int index = 0; + for (var value in currentConversationMessageList) { + if (value.msgID == message.msgID) { + break; + } + index = index + 1; + } + currentConversationMessageList.removeAt(index); + updateCall(); + } else { + // 撤回失败,可通过 revokeMessage.desc 获取错误描述 + SCTts.show("fail:${revokeMessage.desc}"); + } + }, + ); + }, + ); + } + + ///删除消息 + void _delete() { + SmartDialog.show( + tag: "showDeleteDialog", + alignment: Alignment.bottomCenter, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return SafeArea( + child: Container( + height: 168.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.tips, + fontSize: 16.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + ], + ), + SizedBox(height: 5.w), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.deleteFromMyDevice, + fontSize: 14.sp, + textColor: Colors.black, + ), + ], + ), + ), + onTap: () async { + V2TimCallback deleteMessageFromLocalStorage = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .deleteMessageFromLocalStorage( + msgID: message.msgID ?? "", + ); + if (deleteMessageFromLocalStorage.code == 0) { + //删除成功 + int index = 0; + for (var value in currentConversationMessageList) { + if (value.msgID == message.msgID) { + break; + } + index = index + 1; + } + currentConversationMessageList.removeAt(index); + updateCall(); + SmartDialog.dismiss(tag: "showDeleteDialog"); + } + }, + ), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + GestureDetector( + onTap: () async { + V2TimCallback deleteMessageFromLocalStorage = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .deleteMessages(msgIDs: [message.msgID ?? ""]); + if (deleteMessageFromLocalStorage.code == 0) { + //删除成功 + int index = 0; + for (var value in currentConversationMessageList) { + if (value.msgID == message.msgID) { + break; + } + index = index + 1; + } + currentConversationMessageList.removeAt(index); + updateCall(); + SmartDialog.dismiss(tag: "showDeleteDialog"); + } + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.deleteOnAllDevices, + fontSize: 14.sp, + textColor: Colors.black, + ), + ], + ), + ), + ), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + GestureDetector( + onTap: () async { + SmartDialog.dismiss(tag: "showDeleteDialog"); + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.cancel, + fontSize: 14.sp, + textColor: Colors.black, + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Future updateRedPacketStatus(String messageID, String packetID) async { + // 构建扩展信息,例如标记为已领取,并包含领取者ID + Map extensionMap = {"packetID": packetID}; + + List extensions = + extensionMap.entries + .map( + (e) => V2TimMessageExtension( + extensionKey: e.key, + extensionValue: e.value, + ), + ) + .toList(); + + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .setMessageExtensions( + msgID: messageID, // 原始红包消息的ID + extensions: extensions, + ); + } + + void _showOpenDialog(SCUserRedPacketSendRes resData) { + SmartDialog.show( + tag: "showOpenDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return Container( + height: 400.w, + alignment: Alignment.topCenter, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + _strategy.getSCMessageChatPageRedEnvelopeOpenBackground(), + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 100.w), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 60.w, + height: 60.w, + color: Colors.transparent, + child: text( + SCAppLocalizations.of(context)!.open, + textColor: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w900, + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showOpenDialog"); + SCLoadingManager.show(); + SCAccountRepository() + .userRedPacketGrab(resData.packetId ?? "") + .then((res) { + var future = SCAccountRepository().userRedPacketDetail( + resData.packetId ?? "", + ); + updateRedPacketStatus( + message.msgID ?? "", + resData.packetId ?? "", + ); + SCMessageUtils.redPacketFutureCache[resData.packetId ?? + ""] = + future; + future + .then((res2) { + SCLoadingManager.hide(); + updateCall(); + _showOpenedDialog(res2); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + }, + ), + SizedBox(height: 35.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.openRedPackDialogTip, + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 14.sp, + ), + SizedBox(width: 3.w), + Container( + margin: EdgeInsets.only(bottom: 1.w), + height: 18.w, + constraints: BoxConstraints(maxWidth: 65.w), + child: + (resData.senderInfo?.nickName?.length ?? 0) > 6 + ? Marquee( + text: resData.senderInfo?.nickName ?? "", + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration(milliseconds: 550), + decelerationCurve: Curves.easeOut, + ) + : Text( + resData.senderInfo?.nickName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + SizedBox(width: 3.w), + Container( + child: text( + SCAppLocalizations.of(context)!.sent, + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 14.sp, + ), + height: 18.w, + ), + ], + ), + SizedBox(height: 15.w), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.wishingYouHappinessEveryDay, + fontWeight: FontWeight.w600, + textColor: Colors.white70, + fontSize: 13.sp, + ), + ], + ), + ], + ), + ); + }, + ); + } + + void _showOpenedDialog(SCUserRedPacketSendRes resData) { + SmartDialog.show( + tag: "showOpenedDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 20.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + _strategy.getSCMessageChatPageRedEnvelopeOpenedBackground(), + ), + fit: BoxFit.fill, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 70.w), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + child: Icon(Icons.close, size: 18.w, color: Colors.white), + onTap: () { + SmartDialog.dismiss(tag: "showOpenedDialog"); + }, + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 25.w), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 20.w), + netImage( + url: resData.senderInfo?.avatar ?? "", + shape: BoxShape.circle, + width: 45.w, + height: 45.w, + ), + SizedBox(width: 10.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + height: 18.w, + constraints: BoxConstraints(maxWidth: 85.w), + child: + (resData.senderInfo?.nickName?.length ?? 0) > 8 + ? Marquee( + text: resData.senderInfo?.nickName ?? "", + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + resData.senderInfo?.nickName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + SizedBox(width: 5.w), + Container( + child: text( + SCAppLocalizations.of(context)!.sentARedEnvelope, + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 14.sp, + ), + height: 18.w, + ), + ], + ), + SizedBox(height: 5.w), + Row( + children: [ + text( + SCAppLocalizations.of( + context, + )!.wishingYouHappinessEveryDay, + fontWeight: FontWeight.w600, + textColor: Colors.white70, + fontSize: 13.sp, + ), + ], + ), + ], + ), + ], + ), + SizedBox(height: 10.w), + AccountStorage().getCurrentUser()?.userProfile?.id == + "${resData.receiverInfo?.userId}" + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + _strategy.getSCMessageChatPageGoldCoinIcon(), + height: 35.w, + ), + text( + "${resData.totalAmount ?? 0}", + fontWeight: FontWeight.w900, + textColor: Color(0xffFFF836), + fontSize: 22.sp, + ), + ], + ) + : Container(), + Divider( + color: Color(0xffF3F3F3), + endIndent: 15.w, + indent: 15.w, + thickness: 0.4, + ), + SizedBox(height: 5.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of( + context, + )!.redEnvelopeAmount("${resData.totalAmount}"), + fontWeight: FontWeight.w600, + textColor: Colors.white70, + fontSize: 13.sp, + ), + Spacer(), + resData.status == 2 + ? Row( + children: [ + Image.asset( + _strategy.getSCMessageChatPageGoldCoinIcon(), + height: 15.w, + ), + text( + "${resData.totalAmount ?? 0}", + fontWeight: FontWeight.w400, + textColor: Color(0xffFFF836), + fontSize: 14.sp, + ), + ], + ) + : Container(), + SizedBox(width: 15.w), + ], + ), + SizedBox(height: 8.w), + resData.status == 2 + ? Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 20.w), + netImage( + url: resData.receiverInfo?.avatar ?? "", + shape: BoxShape.circle, + width: 45.w, + height: 45.w, + ), + SizedBox(width: 10.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + height: 18.w, + constraints: BoxConstraints(maxWidth: 120.w), + child: + (resData.receiverInfo?.nickName?.length ?? + 0) > + 12 + ? Marquee( + text: + resData.receiverInfo?.nickName ?? + "", + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + resData.receiverInfo?.nickName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + SizedBox(width: 5.w), + ], + ), + SizedBox(height: 5.w), + Row( + children: [ + text( + SCMDateUtils.formatDateTime3( + DateTime.fromMillisecondsSinceEpoch( + resData.grabbedAt ?? 0, + ), + ), + fontWeight: FontWeight.w600, + textColor: Colors.white70, + fontSize: 13.sp, + ), + ], + ), + ], + ), + ], + ) + : (resData.status == 1 + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of( + context, + )!.redEnvelopeNotYetClaimed, + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 15.sp, + ), + ], + ) + : Container()), + SizedBox(height: 18.w), + ], + ), + ); + }, + ); + } + + _buildPackTips(SCUserRedPacketSendRes? resData) { + //状态:1-待领取 2-已领取 3-已过期 + if (resData?.status == 2) { + if ("${resData?.receiverInfo?.userId}" == + AccountStorage().getCurrentUser()?.userProfile?.id) { + return Container( + width: ScreenUtil().screenWidth * 0.6, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: SCAppLocalizations.of( + context, + )!.youAccepted(resData?.senderInfo?.nickName ?? ""), + style: TextStyle( + fontSize: 12.sp, + color: Colors.black26, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: SCAppLocalizations.of(context)!.redEnvelope, + style: TextStyle( + fontSize: 12.sp, + color: Color(0xffFF7759), + fontWeight: FontWeight.w600, + ), + ), + TextSpan( + text: ".", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xffFF7759), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + } else if ("${resData?.senderInfo?.userId}" == + AccountStorage().getCurrentUser()?.userProfile?.id) { + return Container( + width: ScreenUtil().screenWidth * 0.6, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: SCAppLocalizations.of( + context, + )!.acceptedYour(resData?.receiverInfo?.nickName ?? ""), + style: TextStyle( + fontSize: 12.sp, + color: Colors.black26, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: SCAppLocalizations.of(context)!.redEnvelope, + style: TextStyle( + fontSize: 12.sp, + color: Color(0xffFF7759), + fontWeight: FontWeight.w600, + ), + ), + TextSpan( + text: ".", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xffFF7759), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + } + } + return Container(); + } +} + +// class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder { +// /// whether show background for @somebody +// MySpecialTextSpanBuilder(); +// +// @override +// TextSpan build(String data, {TextStyle? textStyle, onTap}) { +// var textSpan = super.build(data, textStyle: textStyle, onTap: onTap); +// return textSpan; +// } +// +// @override +// SpecialText? createSpecialText( +// String flag, { +// TextStyle? textStyle, +// SpecialTextGestureTapCallback? onTap, +// required int index, +// }) { +// if (flag == null || flag == "") return null; +// +// ///index is end index of start flag, so text start index should be index-(flag.length-1) +// if (textStyle != null) { +// if (isStart(flag, EmojiText.flag)) { +// return EmojiText(textStyle, start: index - (EmojiText.flag.length - 1)); +// } +// } +// return null; +// } +// } + +// class EmojiText extends SpecialText { +// EmojiText(TextStyle textStyle, {required this.start}) +// : super(EmojiText.flag, ']', textStyle); +// static const String flag = '['; +// final int start; +// +// @override +// InlineSpan finishText() { +// String key = toString(); +// key = key.replaceAll("[", ""); +// key = key.replaceAll("]", ""); +// int index; +// try { +// index = int.parse(key); +// } catch (e) { +// return TextSpan(text: toString(), style: textStyle); +// } +// +// ///https://github.com/flutter/flutter/issues/42086 +// /// widget span is not working on web +// if (index <= 113) { +// //fontsize id define image height +// //size = 30.0/26.0 * fontSize +// double size = 16.w; +// +// ///fontSize 26 and text height =30.0 +// //final double fontSize = 26.0; +// return ImageSpan( +// AssetImage("sc_images/msg/emoji/$key.png"), +// actualText: key, +// imageWidth: textStyle!.fontSize!, +// imageHeight: textStyle!.fontSize!, +// start: start, +// fit: BoxFit.fill, +// margin: const EdgeInsets.only(left: 2.0, top: 2.0, right: 2.0), +// ); +// } +// +// return TextSpan(text: toString(), style: textStyle); +// } +// } diff --git a/lib/modules/chat/noti/message_notifcation_page.dart b/lib/modules/chat/noti/message_notifcation_page.dart new file mode 100644 index 0000000..d48cd75 --- /dev/null +++ b/lib/modules/chat/noti/message_notifcation_page.dart @@ -0,0 +1,294 @@ +import 'dart:convert'; + +import 'package:extended_text/extended_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; + +class MessageNotifcationPage extends StatefulWidget { + @override + _MessageNotifcationPageState createState() => _MessageNotifcationPageState(); +} + +class _MessageNotifcationPageState extends State { + List records = []; + num index = 1; + final RefreshController _refreshController = RefreshController(); + final ScrollController _scrollController = ScrollController(); + RtmProvider? rtmProvider; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + @override + void initState() { + super.initState(); + rtmProvider = Provider.of(context, listen: false); + rtmProvider?.onNewNotifcationMessageCurrentConversationListener = + _onNewMessage; + loadData(); + } + + @override + void dispose() { + rtmProvider?.onNewNotifcationMessageCurrentConversationListener = null; + super.dispose(); + } + + _onNewMessage(Records? message) async { + if (message == null) { + return; + } + + int? index; + for (var element in records) { + if (element.id == message.id) { + index = records.indexOf(element); + continue; + } + } + if (index != null) { + records.removeAt(index); + records.insert(index, message); + } else { + records.insert(0, message); + } + _scrollController.jumpTo(0.0); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + _strategy.getSCMessageChatPageRoomSettingBackground(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.notifcation, + actions: [], + ), + body: SafeArea( + top: false, + child: SmartRefresher( + enablePullDown: false, + enablePullUp: true, + onLoading: () async { + index = index + 1; + loadData(); + }, + footer: CustomFooter( + height: 1, + builder: (context, mode) { + return SizedBox( + height: 1, + width: 1, + child: SizedBox(height: 1, width: 1), + ); + }, + ), + controller: _refreshController, + child: + records.isNotEmpty + ? Container( + child: CustomScrollView( + shrinkWrap: records.length > 3 ? false : true, + controller: _scrollController, + reverse: true, + physics: const ClampingScrollPhysics(), + // 禁用回弹效果 + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (c, i) => _MessageItem(message: records[i]), + childCount: records.length, + ), + ), + ], + ), + ) + : mainEmpty(), + ), + ), + ), + ], + ); + } + + void loadData() { + SCAccountRepository().publicMessagePage("notification", index, 30).then(( + result, + ) { + var list = result.records ?? []; + if (list.isNotEmpty) { + records.addAll(list); + if (list.length == 30) { + _refreshController.loadComplete(); + } else { + _refreshController.loadNoData(); + } + } else { + _refreshController.loadNoData(); + } + setState(() {}); + _scrollController.jumpTo(0.0); + }); + } +} + +class _MessageItem extends StatelessWidget { + final Records message; + late BuildContext context; + + _MessageItem({required this.message}); + + @override + Widget build(BuildContext context) { + this.context = context; + print('status:${message.status}'); + String time = SCMDateUtils.formatMessageTime( + context, + DateTime.fromMillisecondsSinceEpoch(int.parse(message.createdAt ?? "0")), + ); + return Container( + margin: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 12.w), + child: Row( + children: [ + Spacer(), + Container( + decoration: BoxDecoration( + color: Colors.black12, + borderRadius: BorderRadius.all(Radius.circular(height(13))), + ), + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + horizontal: width(15), + vertical: 8.w, + ), + child: Text( + time, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ), + Spacer(), + ], + ), + ), + _msg(message), + ], + ), + ); + } + + _msg(Records message) { + return GestureDetector( + child: Container( + alignment: AlignmentDirectional.centerStart, + decoration: BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 5.w), + Expanded( + child: ExtendedText( + message.title ?? "", + overflow: TextOverflow.ellipsis, + maxLines: 2, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle( + fontSize: sp(16), + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(width: 5.w), + ], + ), + message.imageUrl != null && message.imageUrl!.isNotEmpty + ? GestureDetector( + child: Container( + margin: EdgeInsets.all(5.w), + child: netImage( + url: message.imageUrl ?? "", + fit: BoxFit.contain, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + onTap: () { + String encodedUrls = Uri.encodeComponent( + jsonEncode([message.imageUrl]), + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", + ); + }, + ) + : Container(), + Row( + children: [ + SizedBox(width: 5.w), + Expanded( + child: ExtendedText( + message.content ?? "", + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: sp(14), color: Colors.white), + ), + ), + SizedBox(width: 5.w), + ], + ), + ], + ), + ), + onTap: () { + if (message.extraData != null && message.extraData?.link != null) { + if (message.extraData!.link!.startsWith("http") || + message.extraData!.link!.startsWith("https")) { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(message.extraData?.link ?? "")}&showTitle=false", + replace: false, + ); + } + } + }, + ); + } +} diff --git a/lib/modules/chat/system/message_system_page.dart b/lib/modules/chat/system/message_system_page.dart new file mode 100644 index 0000000..eda3470 --- /dev/null +++ b/lib/modules/chat/system/message_system_page.dart @@ -0,0 +1,1172 @@ +import 'dart:convert'; +import 'package:extended_image/extended_image.dart' + show ExtendedImage, ExtendedRawImage, ExtendedImageState, LoadState; +import 'package:extended_text/extended_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_system_message_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_text_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; + +import '../../../shared/data_sources/models/enum/sc_sysytem_message_type.dart'; +import '../../../shared/business_logic/models/res/sc_system_invit_message_res.dart'; + +class MessageSystemPage extends StatefulWidget { + final V2TimConversation? conversation; + final bool shrinkWrap; + + //是否是房间内聊天界面。 + final bool inRoom; + + const MessageSystemPage({ + Key? key, + this.conversation, + this.shrinkWrap = false, + this.inRoom = false, + }) : super(key: key); + + @override + _MessageSystemPageState createState() => _MessageSystemPageState(); +} + +class _MessageSystemPageState extends State { + final ScrollController _scrollController = ScrollController(); + final RefreshController _refreshController = RefreshController(); + RtmProvider? rtmProvider; + V2TimConversation? currentConversation; + List currentConversationMessageList = []; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + @override + void initState() { + super.initState(); + rtmProvider = Provider.of(context, listen: false); + currentConversation = widget.conversation; + rtmProvider?.onNewMessageCurrentConversationListener = _onNewMessage; + loadMsg(); + } + + @override + void dispose() { + rtmProvider?.onNewMessageCurrentConversationListener = null; + super.dispose(); + } + + Future loadMsg() async { + V2TimValueCallback> v2timValueCallback = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .getC2CHistoryMessageList( + userID: currentConversation!.userID!, + count: 100, + lastMsgID: null, + ); + List messages = v2timValueCallback.data!; + // List messages = await FTIM.getMessageManager().getMessages(conversation: currentConversation); + print("messages : ${messages?.length ?? 0}"); + currentConversationMessageList ??= []; + currentConversationMessageList?.clear(); + + for (var msg in messages) { + if (!msg.isSelf! && msg.isPeerRead!) { + TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessageReadReceipts(messageIDList: [msg.msgID!]); + } + } + currentConversationMessageList.insertAll(0, messages ?? []); + setState(() {}); + _scrollController.jumpTo(0.0); + } + + _onNewMessage(V2TimMessage? message, {String? msgId}) async { + if (message == null) { + return; + } + if (message.userID != currentConversation?.userID) return; + + int? index; + for (var element in currentConversationMessageList) { + if (msgId != null) { + if (msgId == element.msgID) { + element.status = MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL; + setState(() {}); + continue; + } + } else { + if (element.msgID == message.msgID) { + index = currentConversationMessageList.indexOf(element); + continue; + } + } + } + if (msgId != null) { + return; + } + if (index != null) { + currentConversationMessageList.removeAt(index!); + currentConversationMessageList.insert(index!, message); + } else { + currentConversationMessageList.insert(0, message); + } + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .markC2CMessageAsRead(userID: currentConversation!.userID!); + //发送已读回执 + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessageReadReceipts(messageIDList: [message.msgID!]); + setState(() {}); + // await FTIM.getContactManager().setReadMessage(currentConversation); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + _strategy.getSCMessageChatPageRoomSettingBackground(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.system, + actions: [], + ), + body: SafeArea(top: false, child: _msgList()), + ), + ], + ); + } + + Widget _msgList() { + print('xiaoxiliebiao:${currentConversationMessageList.length}'); + return SmartRefresher( + enablePullDown: false, + enablePullUp: true, + onLoading: () async { + print('onLoading'); + if (currentConversationMessageList.isNotEmpty) { + // 拉取单聊历史消息 + // 首次拉取,lastMsgID 设置为 null + // 再次拉取时,lastMsgID 可以使用返回的消息列表中的最后一条消息的id + V2TimValueCallback> v2timValueCallback = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .getC2CHistoryMessageList( + userID: currentConversation!.userID ?? "", + count: 30, + lastMsgID: currentConversationMessageList.last.msgID, + ); + List messages = + v2timValueCallback.data as List; + print('加载前:${currentConversationMessageList.length}'); + currentConversationMessageList.addAll(messages); + print('加载后:${currentConversationMessageList.length}'); + if (messages.length == 30) { + _refreshController.loadComplete(); + } else { + _refreshController.loadNoData(); + } + } else { + _refreshController.loadNoData(); + } + setState(() {}); + }, + footer: CustomFooter( + height: 1, + builder: (context, mode) { + return SizedBox( + height: 1, + width: 1, + child: SizedBox(height: 1, width: 1), + ); + }, + ), + controller: _refreshController, + child: Container( + child: CustomScrollView( + shrinkWrap: currentConversationMessageList.length > 10 ? false : true, + controller: _scrollController, + reverse: true, + physics: const ClampingScrollPhysics(), + // 禁用回弹效果 + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (c, i) => _MessageItem( + message: currentConversationMessageList[i], + preMessage: + i < currentConversationMessageList.length - 1 + ? currentConversationMessageList[i + 1] + : null, + currentConversationMessageList: + currentConversationMessageList, + updateCall: () { + setState(() {}); + }, + ), + childCount: currentConversationMessageList.length, + ), + ), + ], + ), + ), + ); + } +} + +class _MessageItem extends StatelessWidget { + final V2TimMessage message; + final V2TimMessage? preMessage; + List currentConversationMessageList = []; + Function updateCall; + + ///上一条 + late BuildContext context; + + _MessageItem({ + required this.message, + this.preMessage, + required this.currentConversationMessageList, + required this.updateCall, + }); + + @override + Widget build(BuildContext context) { + this.context = context; + bool showTime = true; + int timestamp = (message.timestamp ?? 0) * 1000; + int preTimestamp = (preMessage?.timestamp ?? 0) * 1000; + if (preMessage != null) { + ///5分钟以内 不显示 + // print('timestamp:$timestamp===preTimestamp:${preTimestamp}'); + if (DateTime.fromMillisecondsSinceEpoch(timestamp) + .difference(DateTime.fromMillisecondsSinceEpoch(preTimestamp)) + .inMilliseconds < + 1000 * 60 * 5) { + showTime = false; + } + } + String time = SCMDateUtils.formatMessageTime( + context, + DateTime.fromMillisecondsSinceEpoch(timestamp ?? 0), + ); + String noticeType = ""; + String tagHead = ""; + if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) { + V2TimCustomElem customElem = message.customElem!; + String content = customElem.data ?? ""; + final data = jsonDecode(content); + noticeType = data["noticeType"]; + if (noticeType == SCSysytemMessageType.CP_LOVE_LETTER.name) { + var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"])); + tagHead = bean.userAvatar ?? ""; + } + } + print('status:${message.status}'); + return Container( + margin: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + // textDirection: message.isSelf ? TextDirection.rtl : TextDirection.ltr, + children: [ + Visibility( + visible: showTime, + child: Container( + margin: EdgeInsets.only(bottom: 12.w), + child: Row( + children: [ + Spacer(), + Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric( + horizontal: width(15), + vertical: 8.w, + ), + decoration: BoxDecoration( + color: Colors.black12, + borderRadius: BorderRadius.all( + Radius.circular(height(13)), + ), + ), + child: Text( + time, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ), + Spacer(), + ], + ), + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + textDirection: + message.isSelf! ? TextDirection.rtl : TextDirection.ltr, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: + noticeType == SCSysytemMessageType.CP_LOVE_LETTER.name && + tagHead.isNotEmpty + ? netImage( + url: tagHead, + width: 45.w, + shape: BoxShape.circle, + ) + : ExtendedImage.asset( + SCGlobalConfig.businessLogicStrategy.getMessagePageSystemMessageIcon(), + width: 45.w, + shape: BoxShape.circle, + fit: BoxFit.cover, + ), + ), + // 他人消息的阴影间隔是10dp + Container(width: 8.w), + Expanded( + child: Row( + textDirection: TextDirection.ltr, + children: [ + Expanded( + child: Container( + alignment: AlignmentDirectional.topStart, + margin: EdgeInsets.only(top: 4.w), + child: _msg(), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ); + } + + _msg() { + return _text(); + } + + ///文本消息 + _text() { + String content = ""; + if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) { + V2TimTextElem textElem = message.textElem!; + content = textElem.text ?? ""; + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE) { + content = SCAppLocalizations.of(context)!.image; + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) { + content = SCAppLocalizations.of(context)!.video; + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND) { + content = SCAppLocalizations.of(context)!.sound; + } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) { + V2TimCustomElem customElem = message.customElem!; + content = customElem.data ?? ""; + final data = jsonDecode(content); + String type = data["noticeType"]; + if (type == SCSysytemMessageType.GIVE_AWAY_PROPS.name) { + String title = data["title"] ?? ""; + String expand = data["expand"] ?? ""; + return Builder( + builder: (ct) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + text( + SCAppLocalizations.of(context)!.propMessagePrompt, + maxLines: 1, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + fontSize: sp(16), + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + ExtendedText( + data["content"], + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: sp(14), color: Colors.white), + ), + expand.isNotEmpty + ? Row( + children: [ + SizedBox(width: 10.w), + netImage( + url: expand, + width: 45.w, + height: 45.w, + fit: BoxFit.contain, + ), + ], + ) + : Container(), + ], + ), + ), + onTap: () { + if (title.isNotEmpty) { + var dTitle = jsonDecode(title); + String propType = dTitle["propType"] ?? ""; + if ("ACTIVITY" == propType || + "ADMINISTRSCOR" == propType || + "ACHIEVEMENT" == propType) { + + }else { + SCNavigatorUtils.push( + context, + StoreRoute.bags, + replace: false, + ); + } + } else { + SCNavigatorUtils.push(context, StoreRoute.bags, replace: false); + } + }, + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + ); + }, + ); + } else if (type == SCSysytemMessageType.AGENT_SEND_INVITE_HOST.name) { + return SCSystemMessageUtils.buildAgentSendInviteHostMessage( + data, + (ct, content) { + _showMsgItemMenu(ct, content); + }, + (String id) { + rejectOpt(id, type); + }, + (String id) { + acceptOpt(id, type); + }, + context, + ); + } else if (type == SCSysytemMessageType.BD_SEND_INVITE_AGENT.name) { + return SCSystemMessageUtils.buildBdSendInviteAgentMessage( + data, + (ct, content) { + _showMsgItemMenu(ct, content); + }, + (String id) { + rejectOpt(id, type); + }, + (String id) { + acceptOpt(id, type); + }, + context, + ); + } else if (type == SCSysytemMessageType.BD_LEADER_INVITE_BD.name) { + return SCSystemMessageUtils.buildBdLeaderInviteBdMessage( + data, + (ct, content) { + _showMsgItemMenu(ct, content); + }, + (String id) { + rejectOpt(id, type); + }, + (String id) { + acceptOpt(id, type); + }, + context, + ); + } else if (type == SCSysytemMessageType.BD_LEADER_INVITE_BD_LEADER.name) { + return SCSystemMessageUtils.buildBdLeaderInviteBdLeaderMessage( + data, + (ct, content) { + _showMsgItemMenu(ct, content); + }, + (String id) { + rejectOpt(id, type); + }, + (String id) { + acceptOpt(id, type); + }, + context, + ); + } else if (type == SCSysytemMessageType.ADMIN_INVITE_RECHARGE_AGENT.name) { + return SCSystemMessageUtils.buildAdminInviteRechargeAgentMessage( + data, + (ct, content) { + _showMsgItemMenu(ct, content); + }, + (String id) { + rejectOpt(id, type); + }, + (String id) { + acceptOpt(id, type); + }, + context, + ); + } else if (type == SCSysytemMessageType.CP_BUILD.name) { + return SCSystemMessageUtils.buildCPBuildMessage( + data, + (ct, content) { + _showMsgItemMenu(ct, content); + }, + (String id) { + rejectOpt(id, type); + }, + (String id) { + acceptOpt(id, type); + }, + context, + ); + } else if (type == SCSysytemMessageType.CP_LOVE_LETTER.name) { + return SCSystemMessageUtils.buildCPLoveLetterMessage(data, (ct, content) { + _showMsgItemMenu(ct, content); + }, context); + } else if (type == SCSysytemMessageType.USER_COINS_RECEIVED.name) { + return SCSystemMessageUtils.buildUserCoinsReceivedMessage(data, ( + ct, + content, + ) { + _showMsgItemMenu(ct, content); + }, context); + } else if (type == SCSysytemMessageType.COIN_SELLER_COINS_RECEIVED.name) { + return SCSystemMessageUtils.buildUserCoinsReceivedMessage(data, ( + ct, + content, + ) { + _showMsgItemMenu(ct, content); + }, context); + } else if (type == SCSysytemMessageType.USER_SPECIAL_AUDIT_RESULTS.name) { + if (data["content"] != null) { + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow(blurRadius: 2.w, color: Colors.black26), + ], + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: ExtendedText( + data["content"], + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: sp(14), color: Colors.black54), + ), + ), + ); + }, + ); + } + } else if (type == SCSysytemMessageType.USER_FOLLOW.name) { + String expand = data["expand"] ?? ""; + if (data["content"] != null) { + var bean = SCSystemInvitMessageRes.fromJson( + jsonDecode(data["content"]), + ); + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow(blurRadius: 2.w, color: Colors.black26), + ], + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + Row( + children: [ + head(url: bean.userAvatar ?? "", width: 65.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + netImage( + url: + Provider.of( + context, + listen: false, + ) + .findCountryByName( + bean.countryName ?? "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all( + Radius.circular(3.w), + ), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + Container( + constraints: BoxConstraints( + maxWidth: 160.w, + ), + child: text( + bean.userNickname ?? "", + fontWeight: FontWeight.bold, + textColor: Colors.black, + fontSize: 16.sp, + ), + ), + ], + ), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + SCGlobalConfig.businessLogicStrategy.getIdBackgroundImage(bean.account != bean.actualAccount), + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + bean.actualAccount ?? "", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + SCGlobalConfig.businessLogicStrategy.getCopyIdIcon(), + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData( + text: bean.actualAccount ?? "", + ), + ); + SCTts.show( + SCAppLocalizations.of( + context, + )!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + SizedBox(height: 3.w), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xffB464FF), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.follow, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + SCAccountRepository().followNew(expand).then((result) { + SCTts.show(SCAppLocalizations.of(context)!.followSucc); + }); + }, + ), + Divider( + color: Colors.black12, + thickness: 0.5, + height: 28.w, + ), + ExtendedText( + SCAppLocalizations.of(context)!.followedYou, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle( + fontSize: sp(14), + color: Colors.black54, + ), + ), + SizedBox(height: 5.w), + ], + ), + ), + ); + }, + ); + } + } else if (type == SCSysytemMessageType.VIOLSCION_WARNING.name) { + if (data["content"] != null) { + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow(blurRadius: 2.w, color: Colors.black26), + ], + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: ExtendedText( + data["content"], + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: sp(14), color: Colors.black54), + ), + ), + ); + }, + ); + } + } else if (type == SCSysytemMessageType.VIOLSCION_ADJUST.name) { + if (data["content"] != null) { + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow(blurRadius: 2.w, color: Colors.black26), + ], + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: ExtendedText( + data["content"], + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: sp(14), color: Colors.black54), + ), + ), + ); + }, + ); + } + } + if (content.isEmpty) { + if (customElem.extension == 'sendGift') { + content = SCAppLocalizations.of(context)!.gift2; + } else { + content = SCAppLocalizations.of(context)!.receivedAMessage; + } + } + } + + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + _showMsgItemMenu(ct, ""); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(blurRadius: 2.w, color: Colors.black26)], + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: ExtendedText( + content, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: sp(14), color: Colors.black54), + ), + ), + ); + }, + ); + } + + void _showMsgItemMenu(BuildContext ct, String content) { + SmartDialog.showAttach( + tag: "showMsgItemMenu", + targetContext: ct, + alignment: Alignment.topCenter, + maskColor: Colors.transparent, + animationType: SmartAnimationType.fade, + scalePointBuilder: (selfSize) => Offset(selfSize.width, 10), + builder: (_) { + return Container( + margin: EdgeInsets.only(bottom: 10.w), + decoration: BoxDecoration( + color: Color(0xffdcdddf), + borderRadius: BorderRadius.circular(10), + ), + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SmartDialog.dismiss(tag: "showMsgItemMenu"); + _delete(); + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.w), + child: Column( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getSCMessageChatPageMessageMenuDeleteIcon(), + width: 20.w, + ), + SizedBox(height: 3.w), + text( + SCAppLocalizations.of(context)!.delete, + fontSize: 12.sp, + textColor: Color(0xff777777), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + ///删除消息 + void _delete() { + SmartDialog.show( + tag: "showDeleteDialog", + alignment: Alignment.bottomCenter, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return SafeArea( + child: Container( + height: 168.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0), + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.tips, + fontSize: 16.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + ], + ), + SizedBox(height: 5.w), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.deleteFromMyDevice, + fontSize: 14.sp, + textColor: Colors.black, + ), + ], + ), + ), + onTap: () async { + V2TimCallback deleteMessageFromLocalStorage = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .deleteMessageFromLocalStorage( + msgID: message.msgID ?? "", + ); + if (deleteMessageFromLocalStorage.code == 0) { + //删除成功 + int index = 0; + for (var value in currentConversationMessageList) { + if (value.msgID == message.msgID) { + break; + } + index = index + 1; + } + currentConversationMessageList.removeAt(index); + updateCall(); + SmartDialog.dismiss(tag: "showDeleteDialog"); + } + }, + ), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + GestureDetector( + onTap: () async { + V2TimCallback deleteMessageFromLocalStorage = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .deleteMessages(msgIDs: [message.msgID ?? ""]); + if (deleteMessageFromLocalStorage.code == 0) { + //删除成功 + int index = 0; + for (var value in currentConversationMessageList) { + if (value.msgID == message.msgID) { + break; + } + index = index + 1; + } + currentConversationMessageList.removeAt(index); + updateCall(); + SmartDialog.dismiss(tag: "showDeleteDialog"); + } + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.deleteOnAllDevices, + fontSize: 14.sp, + textColor: Colors.black, + ), + ], + ), + ), + ), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + GestureDetector( + onTap: () async { + SmartDialog.dismiss(tag: "showDeleteDialog"); + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.cancel, + fontSize: 14.sp, + textColor: Colors.black, + ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + void acceptOpt(String id, String type) { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.confirmAcceptTheInvitation, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () async { + if (type == SCSysytemMessageType.AGENT_SEND_INVITE_HOST.name) { + Provider.of( + context, + listen: false, + ).inviteHostOpt(context, id, "1"); + } else if (type == SCSysytemMessageType.BD_SEND_INVITE_AGENT.name) { + Provider.of( + context, + listen: false, + ).inviteAgentOpt(context, id, "1"); + } else if (type == SCSysytemMessageType.BD_LEADER_INVITE_BD.name) { + Provider.of( + context, + listen: false, + ).inviteBDOpt(context, id, "1"); + } else if (type == + SCSysytemMessageType.BD_LEADER_INVITE_BD_LEADER.name) { + Provider.of( + context, + listen: false, + ).inviteBDLeader(context, id, "1"); + } else if (type == + SCSysytemMessageType.ADMIN_INVITE_RECHARGE_AGENT.name) { + Provider.of( + context, + listen: false, + ).inviteRechargeAgent(context, id, "1"); + } else if (type == SCSysytemMessageType.CP_BUILD.name) { + Provider.of( + context, + listen: false, + ).cpRlationshipProcessApply(context, id, true); + } + }, + ); + }, + ); + } + + void rejectOpt(String id, String type) { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.confirmDeclineTheInvitation, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () async { + if (type == SCSysytemMessageType.AGENT_SEND_INVITE_HOST.name) { + Provider.of( + context, + listen: false, + ).inviteHostOpt(context, id, "2"); + } else if (type == SCSysytemMessageType.BD_SEND_INVITE_AGENT.name) { + Provider.of( + context, + listen: false, + ).inviteAgentOpt(context, id, "2"); + } else if (type == SCSysytemMessageType.BD_LEADER_INVITE_BD.name) { + Provider.of( + context, + listen: false, + ).inviteBDOpt(context, id, "2"); + } else if (type == + SCSysytemMessageType.BD_LEADER_INVITE_BD_LEADER.name) { + Provider.of( + context, + listen: false, + ).inviteBDLeader(context, id, "2"); + } else if (type == + SCSysytemMessageType.ADMIN_INVITE_RECHARGE_AGENT.name) { + Provider.of( + context, + listen: false, + ).inviteRechargeAgent(context, id, "2"); + } else if (type == SCSysytemMessageType.CP_BUILD.name) { + Provider.of( + context, + listen: false, + ).cpRlationshipProcessApply(context, id, false); + } + }, + ); + }, + ); + } +} diff --git a/lib/modules/country/country_page.dart b/lib/modules/country/country_page.dart new file mode 100644 index 0000000..933a9e5 --- /dev/null +++ b/lib/modules/country/country_page.dart @@ -0,0 +1,305 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/models/country_mode.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/ui_kit/components/custom_cached_image.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/business_logic/models/res/country_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; + +import '../../app/constants/sc_global_config.dart'; +import '../../app/config/business_logic_strategy.dart'; + +///国家 +class CountryPage extends StatefulWidget { + bool isDialog = false; + + CountryPage({this.isDialog = false}); + + @override + _CountryPageState createState() => _CountryPageState(isDialog); +} + +class _CountryPageState extends State { + bool isDialog = false; + Country? selectCountryInfo; + + _CountryPageState(this.isDialog); + + @override + void initState() { + super.initState(); + Provider.of(context, listen: false).fetchCountryList(); + } + + @override + void dispose() { + Provider.of( + context, + listen: false, + ).clearCountrySelection(); + super.dispose(); + } + + /// 获取业务逻辑策略 + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + widget.isDialog + ? Container() + : Image.asset( + "sc_images/splash/sc_splash.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + Scaffold( + backgroundColor: + widget.isDialog + ? _strategy.getCountryPageScaffoldBackgroundColor() + : Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: + widget.isDialog + ? null + : SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.countryRegion, + onTag: () { + Provider.of( + context, + listen: false, + ).clearCountrySelection(); + SCNavigatorUtils.goBack(context); + }, + actions: [ + Consumer( + builder: (_, provider, __) { + return Container( + margin: EdgeInsets.only(right: 20.w), + child: SCDebounceWidget( + onTap: () { + if (selectCountryInfo != null) { + Provider.of( + context, + listen: false, + ).setCountry(selectCountryInfo!); + provider.updateCurrentCountry( + selectCountryInfo, + ); + SCNavigatorUtils.goBack(context); + } + }, + child: Image.asset( + selectCountryInfo != null + ? _strategy.getCountrySelectOkIcon() + : _strategy.getCountrySelectUnOkIcon(), + width: 38.w, + height: 22.w, + ), + ), + ); + }, + ), + ], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + widget.isDialog ? SizedBox(height: 10.w) : Container(), + widget.isDialog + ? Row( + children: [ + SCDebounceWidget( + child: Container( + width: 50.w, + height: 30.w, + alignment: AlignmentDirectional.centerStart, + margin: EdgeInsetsDirectional.only(start: 10.w), + child: Icon( + SCGlobalConfig.lang == "ar" + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + size: 28.w, + color: _strategy.getCountryPageIconColor(), + ), + ), + onTap: () { + Provider.of( + context, + listen: false, + ).clearCountrySelection(); + SmartDialog.dismiss(tag: "showCountryPage"); + }, + ), + Spacer(), + Consumer( + builder: (_, provider, __) { + return Container( + margin: EdgeInsets.only(right: 20.w), + child: SCDebounceWidget( + onTap: () { + if (selectCountryInfo != null) { + Provider.of( + context, + listen: false, + ).setCountry(selectCountryInfo!); + provider.updateCurrentCountry( + selectCountryInfo, + ); + SmartDialog.dismiss(tag: "showCountryPage"); + } + }, + child: Image.asset( + selectCountryInfo != null + ? _strategy.getCountrySelectOkIcon() + : _strategy.getCountrySelectUnOkIcon(), + width: 38.w, + height: 22.w, + ), + ), + ); + }, + ), + ], + ) + : Container(), + widget.isDialog + ? Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.selectYourCountry, + textColor: _strategy.getCountryPagePrimaryTextColor(), + fontSize: 16.sp, + ), + ], + ) + : Container(), + widget.isDialog ? SizedBox(height: 12.w) : Container(), + Expanded( + child: Consumer( + builder: (_, provider, __) { + return ListView.separated( + itemCount: provider.countryModeList.length, // 列表项数量 + padding: EdgeInsets.only(bottom: 15.w), + itemBuilder: (context, index) { + return _buildCountryItem( + index, + provider.countryModeList[index], + provider, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: 10.w); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildCountryItem( + int subIndex, + CountryMode countryModeList, + SCAppGeneralManager provider, + ) { + return Column( + children: [ + Container( + margin: EdgeInsets.only(left: 25.w), + alignment: Alignment.centerLeft, + child: text( + countryModeList.prefix, + textColor: + widget.isDialog + ? _strategy.getCountryPageSecondaryTextColor() + : Colors.white, + fontSize: 15.sp, + ), + ), + SizedBox(height: 5.w), + Container( + margin: EdgeInsets.symmetric(horizontal: 15.w), + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: _strategy.getCountryPageContainerBackgroundColor(), + border: Border.all( + color: _strategy.getCountryItemBorderColor(), + width: 1.w, + ), + ), + child: Column( + spacing: 3.w, + children: List.generate(countryModeList.prefixCountrys.length, ( + cIndex, + ) { + Country country = countryModeList.prefixCountrys[cIndex]; + return Row( + children: [ + SizedBox(width: 15.w), + CustomCachedImage( + imageUrl: country.nationalFlag ?? "", + width: 28.w, + height: 19.w, + ), + SizedBox(width: 5.w), + Expanded( + child: text( + country.aliasName ?? "", + textColor: _strategy.getCountryPagePrimaryTextColor(), + fontSize: 15.sp, + ), + ), + GestureDetector( + child: Container( + color: Colors.transparent, + padding: EdgeInsets.all(8.w), + child: Image.asset( + selectCountryInfo?.id == country.id + ? _strategy.getCountryRadioSelectedIcon() + : _strategy.getCountryRadioUnselectedIcon(), + width: 16.w, + color: + selectCountryInfo?.id != country.id + ? Colors.black38 + : null, + height: 16.w, + ), + ), + onTap: () { + selectCountryInfo = provider.chooseCountry( + subIndex, + cIndex, + ); + setState(() {}); + }, + ), + ], + ); + }), + ), + ), + ], + ); + } +} diff --git a/lib/modules/country/country_route.dart b/lib/modules/country/country_route.dart new file mode 100644 index 0000000..b8a9090 --- /dev/null +++ b/lib/modules/country/country_route.dart @@ -0,0 +1,21 @@ +import 'package:fluro/fluro.dart'; +import 'package:yumi/modules/country/country_page.dart'; + +import 'package:yumi/app/routes/sc_router_init.dart'; + +class CountryRoute implements SCIRouterProvider { + static String country = '/country'; + + @override + void initRouter(FluroRouter router) { + router.define( + country, + handler: Handler( + handlerFunc: (_, params) { + String? isDialog = params['isDialog']?.first; + return CountryPage(isDialog: isDialog == 'true'); + }, + ), + ); + } +} diff --git a/lib/modules/gift/bag/gift_bag_page.dart b/lib/modules/gift/bag/gift_bag_page.dart new file mode 100644 index 0000000..0c79457 --- /dev/null +++ b/lib/modules/gift/bag/gift_bag_page.dart @@ -0,0 +1,153 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/custom_cached_image.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +class GiftBagPage extends StatefulWidget { + const GiftBagPage({Key? key}) : super(key: key); + + @override + _GiftBagPageState createState() => _GiftBagPageState(); +} + +class _GiftBagPageState extends State with TickerProviderStateMixin { + PageController _giftPageController = PageController(); + int _index = 0; + + int checkedIndex = 0; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return ref.giftResList.isEmpty + ? mainEmpty(textColor: Colors.white54,msg: SCAppLocalizations.of(context)!.noData) + : Column( + children: [ + Expanded( + child: PageView.builder( + controller: _giftPageController, + onPageChanged: (i) { + setState(() { + _index = i; + }); + }, + itemBuilder: (c, i) { + var current = (ref.giftResList.length - (i * 8)); + int size = + current > 8 + ? 8 + : current % 8 == 0 + ? 8 + : current % 8; + return GridView.builder( + shrinkWrap: true, + padding: EdgeInsets.symmetric(horizontal: 12.w), + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.80, + mainAxisSpacing: 8.w, + crossAxisSpacing: 8.w, + ), + itemCount: size, + itemBuilder: (BuildContext context, int index) { + return _bagItem( + ref.giftResList[(i * 8) + index], + ref, + ); + }, + ); + }, + itemCount: (ref.giftResList.length / 8).ceil(), + ), + ), + _indicator(ref), + SizedBox(height: 10.w), + ], + ); + }, + ); + } + + _indicator(SCAppGeneralManager ref) { + var size = (ref.giftResList.length) / 8; + var list = []; + for (int i = 0; i < size; i++) { + list.add( + Container( + width: 6.5.w, + height: 6.5.w, + margin: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _index == i ? SocialChatTheme.primaryColor : Color(0xffDADADA), + ), + ), + ); + } + return Row(mainAxisAlignment: MainAxisAlignment.center, children: list); + } + + Widget _bagItem(SocialChatGiftRes gift, SCAppGeneralManager ref) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.white10, + border: Border.all( + color: + checkedIndex == ref.giftResList.indexOf(gift) + ? SocialChatTheme.primaryColor + : Colors.transparent, + width: 1.w, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomCachedImage( + imageUrl: gift.giftPhoto ?? "", + fit: BoxFit.cover, + width: 48.w, + height: 48.w, + ), + SizedBox(height: 10.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + _strategy.getGiftPageGoldCoinIcon(), + width: 14.w, + height: 14.w, + ), + SizedBox(width: 3.w), + text("${gift.giftCandy}", fontSize: 10.sp), + ], + ), + ], + ), + ), + onTap: () { + setState(() { + checkedIndex = ref.giftResList.indexOf(gift); + // widget.checkedCall(checkedIndex); + }); + }, + ); + } + + +} \ No newline at end of file diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart new file mode 100644 index 0000000..7a3ec0e --- /dev/null +++ b/lib/modules/gift/gift_page.dart @@ -0,0 +1,965 @@ +import 'dart:convert'; +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/main.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_dialog_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/gift/gift_system_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/modules/gift/gift_tab_page.dart'; +import '../../shared/data_sources/models/enum/sc_gift_type.dart'; +import '../../shared/data_sources/models/message/sc_floating_message.dart'; +import '../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; + +class GiftPage extends StatefulWidget { + SocialChatUserProfile? toUser; + + GiftPage({super.key, this.toUser}); + + @override + _GiftPageState createState() => _GiftPageState(); +} + +class _GiftPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + /// 业务逻辑策略访问器 + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + // int checkedIndex = 0; + SocialChatGiftRes? checkedGift; + final List _pages = []; + final List _tabs = []; + RtcProvider? rtcProvider; + + bool isAll = false; + List listMai = []; + + bool noShowNumber = false; + + ///选中人员 + Set set = {}; + + ///数量的箭头是否朝上 + bool isNumberUp = true; + + ///数量 + int number = 1; + + int giveType = 1; + + int giftType = 0; + Debouncer debouncer = Debouncer(); + + @override + void initState() { + super.initState(); + rtcProvider = Provider.of(context, listen: false); + Provider.of(context, listen: false).giftList(); + Provider.of(context, listen: false).giftActivityList(); + // Provider.of(context, listen: false).giftBackpack(); + Provider.of(context, listen: false).balance(); + _pages.add( + GiftTabPage("ALL", (int checkedI) { + checkedGift = null; + var all = + Provider.of( + context, + listen: false, + ).giftByTab["ALL"]; + if (all != null) { + checkedGift = all[checkedI]; + } + number = 1; + noShowNumber = false; + setState(() { + giftType = 0; + }); + }), + ); + + _tabController = TabController(length: _pages.length, vsync: this); + _tabController.addListener(() {}); // 监听切换 + rtcProvider?.roomWheatMap.forEach((k, v) { + if (v.user != null) { + if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { + listMai.add(HeadSelect(true, v)); + } else { + listMai.add(HeadSelect(false, v)); + } + } + isAll = true; + for (var mai in listMai) { + if (!mai.isSelect) { + isAll = false; + } + } + }); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift)); + return Consumer( + builder: (context, ref, child) { + return SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SCGlobalConfig.isReview + ? Container() + : ((AccountStorage() + .getCurrentUser() + ?.userProfile + ?.firstRecharge ?? + false) + ? GestureDetector( + child: Stack( + alignment: AlignmentDirectional.bottomEnd, + children: [ + Transform.flip( + flipX: + SCGlobalConfig.lang == "ar" + ? true + : false, // 水平翻转 + flipY: false, // 垂直翻转设为 false + child: Image.asset( + _strategy + .getGiftPageFirstRechargeRoomTagIcon(), + height: 75.w, + ), + ), + SCGlobalConfig.lang == "ar" + ? PositionedDirectional( + end: 22.w, + bottom: 38.w, + child: Image.asset( + _strategy + .getGiftPageFirstRechargeTextIcon( + 'ar', + ), + height: 13.w, + ), + ) + : PositionedDirectional( + end: 16.w, + bottom: 38.w, + child: Image.asset( + _strategy + .getGiftPageFirstRechargeTextIcon( + 'en', + ), + height: 13.w, + ), + ), + PositionedDirectional( + end: 34.w, + bottom: 13.w, + child: CountdownTimer( + expiryDate: + DateTime.fromMillisecondsSinceEpoch( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.firstRechargeEndTime ?? + 0, + ), + color: Colors.white, + fontSize: 12.w, + ), + ), + ], + ), + onTap: () { + SCDialogUtils.showFirstRechargeDialog( + navigatorKey.currentState!.context, + ); + }, + ) + : Container()), + ], + ), + _buildGiftHead(), + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + color: Color(0xff09372E).withOpacity(0.5), + constraints: BoxConstraints(maxHeight: 430.w), + child: Column( + children: [ + SizedBox(height: 12.w), + Row( + children: [ + SizedBox(width: 8.w), + widget.toUser == null + ? Builder( + builder: (ct) { + return GestureDetector( + child: Image.asset( + isAll + ? "sc_images/room/sc_icon_gift_all_en.png" + : "sc_images/room/sc_icon_gift_all_no.png", + width: 28.w, + height: 28.w, + ), + onTap: () { + isAll = !isAll; + for (var mai in listMai) { + mai.isSelect = isAll; + } + setState(() {}); + // showGiveTypeDialog(ct); + }, + ); + }, + ) + : Container(), + SizedBox(width: 8.w), + widget.toUser == null + ? Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: + listMai + .map((e) => _maiHead(e)) + .toList(), + ), + ), + ) + : Stack( + alignment: AlignmentDirectional.center, + children: [ + Container( + padding: EdgeInsets.all(2.w), + child: netImage( + url: widget.toUser?.userAvatar ?? "", + width: 26.w, + defaultImg: + _strategy + .getMePageDefaultAvatarImage(), + shape: BoxShape.circle, + ), + ), + PositionedDirectional( + bottom: 0, + end: 0, + child: Image.asset( + "sc_images/login/sc_icon_login_ser_select.png", + width: 10.w, + height: 10.w, + ), + ), + ], + ), + SizedBox(width: 12.w), + ], + ), + Row( + children: [ + SizedBox(width: 5.w), + Expanded( + child: Container( + height: 28.w, + child: TabBar( + tabAlignment: TabAlignment.start, + labelPadding: EdgeInsets.symmetric( + horizontal: 8.w, + ), + labelColor: SocialChatTheme.primaryLight, + + indicatorWeight: 0, + isScrollable: true, + indicator: SCFixedWidthTabIndicator( + width: 15.w, + color: SocialChatTheme.primaryLight, + ), + unselectedLabelColor: Colors.white54, + labelStyle: TextStyle(fontSize: 14.sp), + unselectedLabelStyle: TextStyle( + fontSize: 12.sp, + ), + indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + ), + ), + SizedBox(width: 5.w), + ], + ), + Expanded( + child: TabBarView( + physics: NeverScrollableScrollPhysics(), + controller: _tabController, + children: _pages, + ), + ), + Row( + children: [ + SizedBox(width: 15.w), + GestureDetector( + onTap: () { + SmartDialog.dismiss(tag: "showGiftControl"); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + WalletRoute.recharge, + replace: false, + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 8.w, + horizontal: 8.w, + ), + width: 120.w, + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(5), + ), + child: Row( + children: [ + Image.asset( + _strategy.getGiftPageGoldCoinIcon(), + width: 14.w, + height: 14.w, + ), + SizedBox(width: 5.w), + Consumer( + builder: (context, ref, child) { + return Expanded( + child: text( + "${ref.myBalance}", + fontSize: 12.sp, + ), + ); + }, + ), + SizedBox(width: 5.w), + Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 14.w, + ), + ], + ), + ), + ), + Spacer(), + Builder( + builder: (ct) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (noShowNumber) { + return; + } + isNumberUp = false; + _showNumber(ct); + setState(() {}); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(5), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + text("$number", fontSize: 12.sp), + Icon( + isNumberUp + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 5.w), + GestureDetector( + onTap: () { + giveGifts(); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 8.w, + horizontal: 20.w, + ), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: + BorderRadius.circular(5), + ), + child: text( + SCAppLocalizations.of( + context, + )!.send, + fontSize: 14.sp, + ), + ), + ), + ], + ), + ), + ); + }, + ), + SizedBox(width: 15.w), + ], + ), + SizedBox(height: 15.w), + ], + ), + ), + ), + ), + ], + ), + ); + }, + ); + } + + void showGiveTypeDialog(BuildContext ct) { + SmartDialog.showAttach( + tag: "showGiveType", + targetContext: ct, + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + scalePointBuilder: (selfSize) => Offset(selfSize.width, 10), + builder: (_) { + return Container( + height: 135.w, + width: 200.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(_strategy.getGiftPageGiveTypeBackground()), + fit: BoxFit.fill, + ), + ), + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + child: Column( + children: [ + SizedBox(height: 5.w), + Expanded( + child: GestureDetector( + child: Row( + children: [ + Image.asset( + _strategy.getGiftPageAllOnMicrophoneIcon(), + height: 18.w, + width: 18.w, + ), + SizedBox(width: 8.w), + Expanded( + child: text( + SCAppLocalizations.of(context)!.allOnMicrophone, + fontSize: 14.sp, + textColor: Colors.white54, + ), + ), + SizedBox(width: 8.w), + Image.asset( + giveType == 0 + ? _strategy.getCommonSelectIcon() + : _strategy.getCommonUnselectIcon(), + width: 15.w, + height: 15.w, + ), + ], + ), + onTap: () { + giveType = 0; + SmartDialog.dismiss(tag: "showGiveType"); + selecteAllMicUsers(); + setState(() {}); + }, + ), + ), + Expanded( + child: GestureDetector( + child: Row( + children: [ + Image.asset( + _strategy.getGiftPageUsersOnMicrophoneIcon(), + height: 18.w, + width: 18.w, + ), + SizedBox(width: 8.w), + Expanded( + child: text( + SCAppLocalizations.of(context)!.usersOnMicrophone, + fontSize: 14.sp, + textColor: Colors.white54, + ), + ), + SizedBox(width: 8.w), + Image.asset( + giveType == 1 + ? _strategy.getCommonSelectIcon() + : _strategy.getCommonUnselectIcon(), + width: 15.w, + height: 15.w, + ), + ], + ), + onTap: () { + giveType = 1; + SmartDialog.dismiss(tag: "showGiveType"); + setState(() {}); + }, + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + giveType = 2; + SmartDialog.dismiss(tag: "showGiveType"); + setState(() {}); + }, + behavior: HitTestBehavior.opaque, + child: Row( + children: [ + Image.asset( + _strategy.getGiftPageAllInTheRoomIcon(), + height: 18.w, + width: 18.w, + ), + SizedBox(width: 8.w), + Expanded( + child: text( + SCAppLocalizations.of(context)!.allInTheRoom, + fontSize: 14.sp, + textColor: Colors.white54, + ), + ), + SizedBox(width: 8.w), + Image.asset( + giveType == 2 + ? _strategy.getCommonSelectIcon() + : _strategy.getCommonUnselectIcon(), + width: 15.w, + height: 15.w, + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _maiHead(HeadSelect shead) { + // 是否选中 + return GestureDetector( + onTap: () { + isAll = true; + for (var mai in listMai) { + if (shead.mic?.user?.id == mai.mic?.user?.id) { + mai.isSelect = !mai.isSelect; + } + if (!mai.isSelect) { + isAll = false; + } + } + + setState(() {}); + }, + child: Stack( + alignment: Alignment.bottomCenter, + clipBehavior: Clip.none, + children: [ + Stack( + alignment: AlignmentDirectional.center, + children: [ + Container( + padding: EdgeInsets.all(2.w), + child: netImage( + url: shead.mic?.user?.userAvatar ?? "", + width: 26.w, + defaultImg: _strategy.getMePageDefaultAvatarImage(), + shape: BoxShape.circle, + ), + ), + PositionedDirectional( + bottom: 0, + end: 0, + child: Container( + alignment: AlignmentDirectional.center, + width: 12.w, + height: 12.w, + decoration: BoxDecoration( + color: Colors.black38, + shape: BoxShape.circle, + border: + shead.isSelect + ? Border.all( + color: SocialChatTheme.primaryLight, + width: 1.w, + ) + : null, + ), + child: text( + "${shead.mic?.micIndex}", + textColor: Colors.white, + fontSize: 8.sp, + ), + ), + ), + shead.isSelect + ? PositionedDirectional( + bottom: 0, + end: 0, + child: Image.asset( + "sc_images/login/sc_icon_login_ser_select.png", + width: 12.w, + height: 12.w, + ), + ) + : Container(), + ], + ), + ], + ), + ); + } + + ///数量选项 + void _showNumber(BuildContext ct) { + SmartDialog.showAttach( + tag: "showNumber", + targetContext: ct, + maskColor: Colors.transparent, + alignment: Alignment.topLeft, + animationType: SmartAnimationType.fade, + scalePointBuilder: (selfSize) => Offset(selfSize.width, 10), + onDismiss: () { + isNumberUp = true; + setState(() {}); + }, + builder: (_) { + return Transform.translate( + offset: Offset(SCGlobalConfig.lang == "ar" ? 20 : -20, -5), + child: CheckNumber( + onNumberChanged: (number) { + this.number = number; + isNumberUp = true; + setState(() {}); + }, + ), + ); + }, + ); + } + + ///选中所有在座位上的用户 + void selecteAllMicUsers() { + for (var mai in listMai) { + mai.isSelect = true; + } + } + + ///赠送礼物 + void giveGifts() { + List acceptUserIds = []; + List acceptUsers = []; + + if (widget.toUser != null) { + acceptUserIds.add(widget.toUser?.id ?? ""); + acceptUsers.add(MicRes(user: widget.toUser)); + } else { + ///所有在线 + if (giveType == 2) { + for (var value + in Provider.of(context, listen: false).onlineUsers) { + acceptUsers.add(MicRes(user: value)); + acceptUserIds.add(value.id ?? ""); + } + } else { + for (var mu in listMai) { + if (mu.isSelect) { + acceptUsers.add(mu.mic!); + acceptUserIds.add(mu.mic!.user!.id ?? ""); + } + } + } + } + if (acceptUserIds.isEmpty) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectTheRecipient); + return; + } + if (checkedGift == null) { + return; + } + SCChatRoomRepository() + .giveGift( + acceptUserIds, + checkedGift!.id ?? "", + number, + false, + roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", + ) + .then((result) { + // SCTts.show(SCAppLocalizations.of(context)!.giftGivingSuccessful); + sendGiftMsg(acceptUsers); + Provider.of( + context, + listen: false, + ).updateBalance(result); + }) + .catchError((e) {}); + } + + void sendGiftMsg(List acceptUsers) { + ///发送一条IM消息 + for (var u in acceptUsers) { + Provider.of( + navigatorKey.currentState!.context, + listen: false, + ).dispatchMessage( + Msg( + groupId: + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, + gift: checkedGift, + user: AccountStorage().getCurrentUser()?.userProfile, + toUser: u.user, + number: number, + type: SCRoomMsgType.gift, + role: + Provider.of( + navigatorKey.currentState!.context, + listen: false, + ).currenRoom?.entrants?.roles ?? + "", + msg: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", + ), + addLocal: true, + ); + if (rtcProvider?.currenRoom?.roomProfile?.roomSetting?.showHeartbeat ?? + false) { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + Provider.of( + navigatorKey.currentState!.context, + listen: false, + ).retrieveMicrophoneList(); + }, + ); + } + num coins = checkedGift!.giftCandy! * number; + if (coins > 9999) { + var fMsg = SCFloatingMessage( + type: 1, + userAvatarUrl: + AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "", + userName: + AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? + "", + toUserName: u.user?.userNickname ?? "", + giftUrl: checkedGift?.giftPhoto ?? "", + roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", + coins: coins, + number: number, + ); + OverlayManager().addMessage(fMsg); + } + if (checkedGift!.giftSourceUrl != null && checkedGift!.special != null) { + if (checkedGift!.special!.contains(SCGiftType.ANIMSCION.name) || + checkedGift!.special!.contains(SCGiftType.GLOBAL_GIFT.name)) { + if (SCGlobalConfig.isGiftSpecialEffects) { + SCGiftVapSvgaManager().play(checkedGift!.giftSourceUrl!); + } + } + } + } + } + + /// 将数字giftType转换为字符串类型,用于活动礼物头部背景 + String _giftTypeToString(int giftType) { + switch (giftType) { + case 1: // ACTIVITY + return 'ACTIVITY'; + case 2: // LUCKY_GIFT -> LUCK + return 'LUCK'; + case 3: // CP + return 'CP'; + case 5: // MAGIC + return 'MAGIC'; + default: + return 'ACTIVITY'; + } + } + + _buildGiftHead() { + if (giftType == 1 || giftType == 2 || giftType == 3 || giftType == 5) { + // 获取基础路径 + String basePath = _strategy.getGiftPageActivityGiftHeadBackground( + _giftTypeToString(giftType), + ); + + // 添加语言后缀 + String imagePath; + if (SCGlobalConfig.lang == "ar") { + // 移除扩展名,添加 _ar 后缀,然后重新添加扩展名 + if (basePath.endsWith('.png')) { + imagePath = basePath.substring(0, basePath.length - 4) + '_ar.png'; + } else { + imagePath = basePath + '_ar'; + } + } else { + if (basePath.endsWith('.png')) { + imagePath = basePath.substring(0, basePath.length - 4) + '_en.png'; + } else { + imagePath = basePath + '_en'; + } + } + + // 确定高度 + double height = giftType == 5 ? 80.w : 65.w; + + return Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + child: Image.asset(imagePath, height: height, fit: BoxFit.fill), + ); + } + return Container(); + } + + ///发送一条消息,幸运礼物,房间里所有人都能看到礼物飘向麦位的动画 + void sendLuckGiftAnimOtherMsg(List acceptUsers) { + Provider.of( + navigatorKey.currentState!.context, + listen: false, + ).dispatchMessage( + Msg( + groupId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, + gift: checkedGift, + type: SCRoomMsgType.luckGiftAnimOther, + msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()), + ), + addLocal: false, + ); + } +} + +class CheckNumber extends StatelessWidget { + final Function(int) onNumberChanged; + late BuildContext context; + + CheckNumber({super.key, required this.onNumberChanged}); + + @override + Widget build(BuildContext context) { + this.context = context; + return Container( + alignment: AlignmentDirectional.topEnd, + margin: EdgeInsets.only(right: width(22)), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12.w)), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 25, sigmaY: 25), + child: Container( + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.all(Radius.circular(height(6))), + ), + width: width(75), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: height(10)), + _item(1), + _item(7), + _item(17), + _item(77), + _item(777), + _item(7777), + SizedBox(height: height(10)), + ], + ), + ), + ), + ), + ); + } + + _item(int number) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SmartDialog.dismiss(tag: "showNumber"); + onNumberChanged(number); + }, + child: SizedBox( + height: height(24), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: width(10)), + Text( + "$number", + style: TextStyle( + fontSize: sp(14), + color: Colors.white, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + SizedBox(width: width(10)), + ], + ), + ), + ); + } +} + +class HeadSelect { + bool isSelect = false; + MicRes? mic; + + HeadSelect(this.isSelect, this.mic); +} diff --git a/lib/modules/gift/gift_tab_page.dart b/lib/modules/gift/gift_tab_page.dart new file mode 100644 index 0000000..b179936 --- /dev/null +++ b/lib/modules/gift/gift_tab_page.dart @@ -0,0 +1,343 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +import '../../shared/data_sources/models/enum/sc_gift_type.dart'; + +class GiftTabPage extends StatefulWidget { + String type; + bool isDark = true; + Function(int checkedIndex) checkedCall; + + GiftTabPage(this.type, this.checkedCall, {super.key, this.isDark = true}); + + @override + _GiftTabPageState createState() => _GiftTabPageState(); +} + +class _GiftTabPageState extends State { + PageController _giftPageController = PageController(); + int _index = 0; + int checkedIndex = 0; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.type == "CUSTOMIZED") { + widget.checkedCall(1); + } else { + widget.checkedCall(0); + } + }); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return (ref.giftByTab[widget.type] ?? []).isEmpty + ? mainEmpty( + textColor: Colors.white54, + msg: SCAppLocalizations.of(context)!.noData, + ) + : Column( + children: [ + SizedBox(height: 23.w,), + Expanded( + child: PageView.builder( + controller: _giftPageController, + onPageChanged: (i) { + setState(() { + _index = i; + }); + }, + itemBuilder: (c, i) { + var current = + (ref.giftByTab[widget.type]!.length - (i * 8)); + int size = + current > 8 + ? 8 + : current % 8 == 0 + ? 8 + : current % 8; + return GridView.builder( + shrinkWrap: true, + padding: EdgeInsets.symmetric(horizontal: 12.w), + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 0.75, + mainAxisSpacing: 8.w, + crossAxisSpacing: 8.w, + ), + itemCount: size, + itemBuilder: (BuildContext context, int index) { + return _bagItem( + ref.giftByTab[widget.type]![(i * 8) + index], + ref, + ); + }, + ); + }, + itemCount: (ref.giftByTab[widget.type]!.length / 8).ceil(), + ), + ), + _indicator(ref), + SizedBox(height: 10.w), + ], + ); + }, + ); + } + + Widget _bagItem(SocialChatGiftRes gift, SCAppGeneralManager ref) { + return gift.id == "-1000" + ? GestureDetector( + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.white10, + border: Border.all(color: Colors.transparent, width: 1.w), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + _strategy.getGiftPageCustomizedRuleIcon(), + fit: BoxFit.cover, + width: 48.w, + height: 48.w, + ), + SizedBox(height: 5.w), + Container( + height: 23.w, + margin: EdgeInsets.symmetric(horizontal: 3.w), + alignment: Alignment.center, + child: text( + SCAppLocalizations.of(context)!.rulesUpload, + maxLines: 2, + fontSize: 10.sp, + letterSpacing: 0.1, + lineHeight: 1, + textAlign: TextAlign.center, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + ], + ), + ), + ], + ), + onTap: () { + SmartDialog.show( + tag: "showCustomizedRule", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return SafeArea( + top: false, + child: ClipRect( + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Stack( + alignment: Alignment.topCenter, + children: [ + Container( + height: ScreenUtil().screenHeight * 0.7, + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + text( + SCAppLocalizations.of( + context, + )!.customizedGiftRules, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(height: 7.w), + Expanded( + child: SingleChildScrollView( + child: Container( + margin: EdgeInsets.symmetric( + horizontal: 12.w, + ), + child: Text( + SCAppLocalizations.of( + context, + )!.customizedGiftRulesContent, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + SizedBox(height: 10.w), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ) + : GestureDetector( + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.white10, + border: Border.all( + color: + checkedIndex == + ref.giftByTab[widget.type]!.indexOf(gift) + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + netImage( + url: gift.giftPhoto ?? "", + fit: BoxFit.cover, + width: 48.w, + height: 48.w, + ), + SizedBox(height: 5.w), + Container( + height: 23.w, + margin: EdgeInsets.symmetric(horizontal: 3.w), + alignment: Alignment.center, + child: text( + gift.giftName ?? "", + maxLines: 2, + fontSize: 10.sp, + letterSpacing: 0.1, + lineHeight: 1, + textAlign: TextAlign.center, + fontWeight: FontWeight.w600, + textColor: widget.isDark ? Colors.white : Colors.black, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + _strategy.getGiftPageGoldCoinIcon(), + width: 14.w, + height: 14.w, + ), + SizedBox(width: 3.w), + text( + "${gift.giftCandy}", + fontSize: 10.sp, + textColor: + widget.isDark ? Colors.white : Colors.black, + ), + ], + ), + ], + ), + ), + Positioned( + right: 3.w, + child: Column( + spacing: 3.w, + children: [ + SizedBox(height: 3.w), + // 只添加需要的 widget + if (gift.special!.contains(SCGiftType.ANIMSCION.name)) + Image.asset( + _strategy.getGiftPageGiftEffectIcon(SCGiftType.ANIMSCION.name), + width: 16.w, + height: 16.w, + fit: BoxFit.fill, + ), + if (gift.special!.contains(SCGiftType.MUSIC.name)) + Image.asset( + _strategy.getGiftPageGiftMusicIcon(SCGiftType.MUSIC.name), + width: 16.w, + height: 16.w, + fit: BoxFit.fill, + ), + if (gift.giftTab == (SCGiftType.LUCKY_GIFT.name)) + Image.asset( + _strategy.getGiftPageGiftLuckIcon(SCGiftType.LUCKY_GIFT.name), + width: 16.w, + height: 16.w, + fit: BoxFit.fill, + ), + if (gift.giftTab == (SCGiftType.CP.name)) + Image.asset( + _strategy.getGiftPageGiftCpIcon(SCGiftType.CP.name), + width: 16.w, + height: 16.w, + fit: BoxFit.fill, + ), + ], + ), + ), + ], + ), + onTap: () { + setState(() { + checkedIndex = ref.giftByTab[widget.type]!.indexOf(gift); + widget.checkedCall(checkedIndex); + }); + }, + ); + } + + _indicator(SCAppGeneralManager ref) { + var size = (ref.giftByTab[widget.type]!.length) / 8; + var list = []; + for (int i = 0; i < size; i++) { + list.add( + Container( + width: 6.5.w, + height: 6.5.w, + margin: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _index == i ? SocialChatTheme.primaryColor : Color(0xffDADADA), + ), + ), + ); + } + return Row(mainAxisAlignment: MainAxisAlignment.center, children: list); + } +} diff --git a/lib/modules/home/index_home_page.dart b/lib/modules/home/index_home_page.dart new file mode 100644 index 0000000..b36a46a --- /dev/null +++ b/lib/modules/home/index_home_page.dart @@ -0,0 +1,212 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; + +///Home页面 +class SCIndexHomePage extends StatefulWidget { + @override + _SCIndexHomePageState createState() => _SCIndexHomePageState(); +} + +class _SCIndexHomePageState extends State + with TickerProviderStateMixin { + TabController? _tabController; + final List _pages = []; + final List _tabs = []; + bool refresh = false; // 👈 改为 false,初始显示加载 + + @override + void initState() { + super.initState(); + // 在构建完成后加载数据 + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateTabs(); + }); + } + + void _updateTabs() { + _tabController?.dispose(); + _pages.clear(); + + final strategy = SCGlobalConfig.businessLogicStrategy; + _pages.addAll(strategy.getHomeTabPages(context)); + + _tabController = TabController( + length: _pages.length, + vsync: this, + initialIndex: strategy.getHomeInitialTabIndex(), + ); + // 可选:监听切换事件 + _tabController?.addListener(() {}); + + // 数据更新完成后,刷新界面 + if (mounted) { + setState(() { + refresh = true; + }); + } + } + + @override + void dispose() { + _tabController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // 如果数据未准备好,显示加载指示器 + if (!refresh || _tabController == null || _pages.isEmpty) { + return Center( + child: Container( + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(8), + ), + width: 55.w, + height: 55.w, + child: const CupertinoActivityIndicator(), + ), + ); + } + + // 正常渲染界面 + _tabs.clear(); + final strategy = SCGlobalConfig.businessLogicStrategy; + _tabs.addAll(strategy.getHomeTabLabels(context)); + + return Stack( + children: [ + Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: Column( + children: [ + // 自定义顶部栏(包含状态栏和 TabBar) + SafeArea( + child: Row( + children: [ + // TabBar 直接放在这里 + Expanded( + child: TabBar( + tabAlignment: TabAlignment.start, + isScrollable: true, + splashFactory: NoSplash.splashFactory, + overlayColor: MaterialStateProperty.all( + Colors.transparent, + ), + indicator: const BoxDecoration(), + labelStyle: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + fontSize: 19.sp, + color:SocialChatTheme.primaryLight, + ), + unselectedLabelStyle: TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14.sp, + color: Colors.white + ), + indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: Image.asset( + "sc_images/index/sc_icon_serach.png", + width: 25.w, + height: 25.w, + ), + onPressed: + () => SCNavigatorUtils.push( + context, + SCMainRoute.mainSearch, + ), + ), + ], + ), + ), + ], + ), + ), + // TabBarView 直接作为剩余空间 + Expanded( + child: TabBarView(controller: _tabController, children: _pages), + ), + ], + ), + ), + Consumer( + builder: (context, ref, child) { + if (SCGlobalConfig.isReview) return Container(); + final strategy = SCGlobalConfig.businessLogicStrategy; + if (strategy.shouldShowFirstRechargePrompt()) { + final position = strategy.getFirstRechargePosition(); + return PositionedDirectional( + top: position['top'], + bottom: position['bottom'], + start: position['start'], + end: position['end'], + child: GestureDetector( + child: Column( + children: [ + Image.asset( + "sc_images/index/sc_icon_first_recharge_tag.webp", + height: 70.w, + ), + Transform.translate( + offset: Offset(0, -10), + child: Container( + height: 23.w, + width: 100.w, + alignment: AlignmentDirectional.center, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/index/sc_icon_first_recharge_btn.png", + ), + fit: BoxFit.fill, + ), + ), + child: CountdownTimer( + expiryDate: DateTime.fromMillisecondsSinceEpoch( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.firstRechargeEndTime ?? + 0, + ), + color: Colors.white, + ), + ), + ), + ], + ), + onTap: () { + strategy.onFirstRechargeTap(context); + }, + ), + ); + } + return Container(); + }, + ), + ], + ); + } +} diff --git a/lib/modules/home/popular/event/home_event_page.dart b/lib/modules/home/popular/event/home_event_page.dart new file mode 100644 index 0000000..915a5e3 --- /dev/null +++ b/lib/modules/home/popular/event/home_event_page.dart @@ -0,0 +1,159 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import '../../../../app_localizations.dart'; +import '../../../../shared/tools/sc_banner_utils.dart'; +import '../../../../shared/data_sources/models/enum/sc_banner_type.dart'; +import '../../../../shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import '../../../../shared/business_logic/models/res/sc_index_banner_res.dart'; +import '../../../../ui_kit/components/sc_compontent.dart'; + +class HomeEventPage extends StatefulWidget { + @override + _HomeEventPageState createState() => _HomeEventPageState(); +} + +class _HomeEventPageState extends State + with SingleTickerProviderStateMixin { + bool isLoading = false; + List homeBanners = []; + final RefreshController _refreshController = RefreshController( + initialRefresh: false, + ); + + @override + void initState() { + super.initState(); + _loadData(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: Column( + children: [ + SizedBox(height: ScreenUtil().statusBarHeight + 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.explore, + textColor: SocialChatTheme.primaryLight, + fontSize: 18.sp, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, + ), + ], + ), + SizedBox(height: 8.w,), + Row( + children: [ + SizedBox(width: 20.w), + text( + SCAppLocalizations.of(context)!.popularEvents, + textColor: SocialChatTheme.primaryLight, + fontSize: 14.sp, + ), + ], + ), + Expanded( + child: SmartRefresher( + enablePullDown: true, + enablePullUp: false, + controller: _refreshController, + onRefresh: () { + _loadData(); + }, + onLoading: () {}, + child: + homeBanners.isNotEmpty + ? SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [_banner()], + ), + ) + : GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + _loadData(); + }, + child: + isLoading + ? Center( + child: CupertinoActivityIndicator( + color: Colors.white24, + ), + ) + : mainEmpty( + msg: SCAppLocalizations.of(context)!.noData, + ), + ), + ), + ), + ], + ), + ); + } + + _banner() { + return homeBanners.isNotEmpty + ? Container( + margin: EdgeInsets.symmetric(horizontal: 10.w), + child: ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: homeBanners.length, + // 列表项数量 + padding: EdgeInsets.only(bottom: 15.w), + itemBuilder: (context, index) { + return GestureDetector( + onTap: () { + print('ads:${homeBanners[index].toJson()}'); + SCBannerUtils.openBanner(homeBanners[index], context); + }, + child: netImage( + height: 118.w, + url: homeBanners[index].cover ?? "", + fit: BoxFit.fill, + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: 10.w); + }, + ), + ) + : Container(); + } + + _loadData() async { + setState(() { + isLoading = true; + }); + try { + var banners = await SCConfigRepositoryImp().getBanner(); + homeBanners.clear(); + for (var v in banners) { + if ((v.displayPosition ?? "").contains(SCBannerType.HOME_ALERT.name)) { + homeBanners.add(v); + } + } + _refreshController.refreshCompleted(); + _refreshController.loadComplete(); + isLoading = false; + if (mounted) setState(() {}); + } catch (e) { + _refreshController.refreshCompleted(); + _refreshController.loadComplete(); + isLoading = false; + if (mounted) setState(() {}); + } + } +} diff --git a/lib/modules/home/popular/follow/sc_room_follow_page.dart b/lib/modules/home/popular/follow/sc_room_follow_page.dart new file mode 100644 index 0000000..27a07c8 --- /dev/null +++ b/lib/modules/home/popular/follow/sc_room_follow_page.dart @@ -0,0 +1,281 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; + +///关注房间 +class SCRoomFollowPage extends SCPageList { + @override + _RoomFollowPageState createState() => _RoomFollowPageState(); +} + +class _RoomFollowPageState + extends SCPageListState { + String? lastId; + bool _isLoading = true; // 添加加载状态 + + @override + void initState() { + super.initState(); + enablePullUp = true; + backgroundColor = Colors.transparent; + isGridView = true; + gridViewCount = 2; + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: buildList(context), + ); + } + + @override + Widget buildItem(FollowRoomRes roomRes) { + return GestureDetector( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.transparent, + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/index/sc_icon_room_bord.png"), + fit: BoxFit.fill, + ), + ), + child: netImage( + url: roomRes.roomProfile?.roomCover ?? "", + borderRadius: BorderRadius.circular(12.w), + width: 200.w, + height: 200.w, + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 6.w), + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/index/sc_icon_index_room_brd.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + Consumer( + builder: (_, provider, __) { + return netImage( + url: + "${provider.findCountryByName(roomRes.roomProfile?.countryName ?? "")?.nationalFlag}", + width: 20.w, + height: 13.w, + borderRadius: BorderRadius.circular(2.w), + ); + }, + ), + SizedBox(width: 5.w), + Expanded( + child: SizedBox( + height: 21.w, + child: text( + roomRes.roomProfile?.roomName ?? "", + fontSize: 15.sp, + textColor: Color(0xffffffff), + fontWeight: FontWeight.w400, + ), + // (roomRes.roomProfile?.roomName?.length ?? 0) > 10 + // ? Marquee( + // text: roomRes.roomProfile?.roomName ?? "", + // style: TextStyle( + // fontSize: 15.sp, + // color: Color(0xffffffff), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // ), + // scrollAxis: Axis.horizontal, + // crossAxisAlignment: CrossAxisAlignment.start, + // blankSpace: 20.0, + // velocity: 40.0, + // pauseAfterRound: Duration(seconds: 1), + // accelerationDuration: Duration(seconds: 1), + // accelerationCurve: Curves.easeOut, + // decelerationDuration: Duration( + // milliseconds: 500, + // ), + // decelerationCurve: Curves.easeOut, + // ) + // : Text( + // roomRes.roomProfile?.roomName ?? "", + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // style: TextStyle( + // fontSize: 15.sp, + // color: Color(0xffffffff), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // ), + // ), + ), + ), + SizedBox(width: 5.w), + (roomRes + .roomProfile + ?.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? Image.asset( + "sc_images/general/sc_icon_online_user.png", + width: 14.w, + height: 14.w, + ) + : Image.asset( + "sc_images/index/sc_icon_room_suo.png", + width: 20.w, + height: 20.w, + ), + (roomRes + .roomProfile + ?.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? SizedBox(width: 3.w) + : Container(height: 10.w), + (roomRes + .roomProfile + ?.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? text( + roomRes.roomProfile?.extValues?.memberQuantity ?? "0", + fontSize: 12.sp, + ) + : Container(height: 10.w), + SizedBox(width: 10.w), + ], + ), + ), + getRoomCoverHeaddress(roomRes).isNotEmpty + ? Transform.translate( + offset: Offset(0, -5.w), + child: Transform.scale( + scaleX: 1.1, + scaleY: 1.12, + child: Image.asset(getRoomCoverHeaddress(roomRes)), + ), + ) + : Container(), + + (roomRes.roomProfile?.roomGameIcon?.isNotEmpty ?? false) + ? PositionedDirectional( + top: 8.w, + end: 8.w, + child: netImage( + url: roomRes.roomProfile?.roomGameIcon ?? "", + width: 25.w, + height: 25.w, + borderRadius: BorderRadius.circular(4.w), + ), + ) + : Container(), + ], + ), + ), + onTap: () { + Provider.of( + context, + listen: false, + ).joinVoiceRoomSession(context, roomRes.roomProfile?.id ?? ""); + }, + ); + } + + String getRoomCoverHeaddress(FollowRoomRes roomRes) { + return ""; + } + + @override + empty() { + List list = []; + list.add(SizedBox(height: height(30))); + list.add( + Image.asset( + 'sc_images/general/sc_icon_loading.png', + width: 120.w, + height: 120.w, + ), + ); + list.add(SizedBox(height: height(15))); + list.add( + Text( + SCAppLocalizations.of(context)!.youHaventFollowed, + style: TextStyle( + fontSize: sp(14), + color: Color(0xff999999), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ); + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: list, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + if (page == 1) { + lastId = null; + } + try { + var roomList = await SCAccountRepository().followRoomList(lastId: lastId); + if (roomList.isNotEmpty) { + lastId = roomList.last.id; + } + onSuccess(roomList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } finally { + // 无论成功失败,都隐藏骨架屏 + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/modules/home/popular/history/sc_room_history_page.dart b/lib/modules/home/popular/history/sc_room_history_page.dart new file mode 100644 index 0000000..1afdaf3 --- /dev/null +++ b/lib/modules/home/popular/history/sc_room_history_page.dart @@ -0,0 +1,279 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; + +///历史房间 +class SCRoomHistoryPage extends SCPageList { + @override + _SCRoomHistoryPageState createState() => _SCRoomHistoryPageState(); +} + +class _SCRoomHistoryPageState + extends SCPageListState { + String? lastId; + bool _isLoading = true; // 添加加载状态 + + @override + void initState() { + super.initState(); + enablePullUp = true; + backgroundColor = Colors.transparent; + isGridView = true; + gridViewCount = 2; + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: buildList(context), + ); + } + + @override + Widget buildItem(FollowRoomRes roomRes) { + return GestureDetector( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.transparent, + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/index/sc_icon_room_bord.png"), + fit: BoxFit.fill, + ), + ), + child: netImage( + url: roomRes.roomProfile?.roomCover ?? "", + borderRadius: BorderRadius.circular(12.w), + width: 200.w, + height: 200.w, + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 6.w), + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/index/sc_icon_index_room_brd.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + Consumer( + builder: (_, provider, __) { + return netImage( + url: + "${provider.findCountryByName(roomRes.roomProfile?.countryName ?? "")?.nationalFlag}", + width: 20.w, + height: 13.w, + borderRadius: BorderRadius.circular(2.w), + ); + }, + ), + SizedBox(width: 5.w), + Expanded( + child: SizedBox( + height: 21.w, + child: text( + roomRes.roomProfile?.roomName ?? "", + fontSize: 15.sp, + textColor: Color(0xffffffff), + fontWeight: FontWeight.w400, + ), + + // (roomRes.roomProfile?.roomName?.length ?? 0) > 10 + // ? Marquee( + // text: roomRes.roomProfile?.roomName ?? "", + // style: TextStyle( + // fontSize: 15.sp, + // color: Color(0xffffffff), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // ), + // scrollAxis: Axis.horizontal, + // crossAxisAlignment: CrossAxisAlignment.start, + // blankSpace: 20.0, + // velocity: 40.0, + // pauseAfterRound: Duration(seconds: 1), + // accelerationDuration: Duration(seconds: 1), + // accelerationCurve: Curves.easeOut, + // decelerationDuration: Duration( + // milliseconds: 500, + // ), + // decelerationCurve: Curves.easeOut, + // ) + // : Text( + // roomRes.roomProfile?.roomName ?? "", + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // style: TextStyle( + // fontSize: 15.sp, + // color: Color(0xffffffff), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // ), + // ), + ), + ), + SizedBox(width: 5.w), + (roomRes + .roomProfile + ?.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? Image.asset( + "sc_images/general/sc_icon_online_user.png", + width: 14.w, + height: 14.w, + ) + : Image.asset( + "sc_images/index/sc_icon_room_suo.png", + width: 20.w, + height: 20.w, + ), + (roomRes + .roomProfile + ?.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? SizedBox(width: 3.w) + : Container(height: 10.w), + (roomRes + .roomProfile + ?.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? text( + roomRes.roomProfile?.extValues?.memberQuantity ?? "0", + fontSize: 12.sp, + ) + : Container(height: 10.w), + SizedBox(width: 10.w), + ], + ), + ), + getRoomCoverHeaddress(roomRes).isNotEmpty + ? Transform.translate( + offset: Offset(0, -5.w), + child: Transform.scale( + scaleX: 1.1, + scaleY: 1.12, + child: Image.asset(getRoomCoverHeaddress(roomRes)), + ), + ) + : Container(), + (roomRes.roomProfile?.roomGameIcon?.isNotEmpty ?? false) + ? PositionedDirectional( + top: 8.w, + end: 8.w, + child: netImage( + url: roomRes.roomProfile?.roomGameIcon ?? "", + width: 25.w, + height: 25.w, + borderRadius: BorderRadius.circular(4.w), + ), + ) + : Container(), + ], + ), + ), + onTap: () { + Provider.of( + context, + listen: false, + ).joinVoiceRoomSession(context, roomRes.roomProfile?.id ?? ""); + }, + ); + } + + @override + empty() { + List list = []; + list.add(SizedBox(height: height(30))); + list.add( + Image.asset( + 'sc_images/general/sc_icon_loading.png', + width: 120.w, + height: 120.w, + ), + ); + list.add(SizedBox(height: height(15))); + list.add( + Text( + SCAppLocalizations.of(context)!.noData, + style: TextStyle( + fontSize: sp(14), + color: Color(0xff999999), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ); + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: list, + ); + } + + String getRoomCoverHeaddress(FollowRoomRes roomRes) { + return ""; + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + if (page == 1) { + lastId = null; + } + try { + var roomList = await SCAccountRepository().trace(lastId: lastId); + if (roomList.isNotEmpty) { + lastId = roomList.last.id; + } + onSuccess(roomList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } finally { + // 无论成功失败,都隐藏骨架屏 + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/modules/home/popular/mine/sc_home_mine_page.dart b/lib/modules/home/popular/mine/sc_home_mine_page.dart new file mode 100644 index 0000000..a07b0e1 --- /dev/null +++ b/lib/modules/home/popular/mine/sc_home_mine_page.dart @@ -0,0 +1,599 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:marquee/marquee.dart'; +import 'package:provider/provider.dart'; +import '../../../../app/config/business_logic_strategy.dart'; +import '../../../../app/constants/sc_global_config.dart'; +import '../../../../shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import '../../../../shared/business_logic/models/res/follow_room_res.dart'; +import '../../../../services/general/sc_app_general_manager.dart'; +import '../../../../services/room/rc_room_manager.dart'; +import '../../../../services/audio/rtc_manager.dart'; +import '../../../../ui_kit/components/sc_compontent.dart'; +import '../../../../ui_kit/theme/socialchat_theme.dart'; +import '../follow/sc_room_follow_page.dart'; +import '../history/sc_room_history_page.dart'; + +class SCHomeMinePage extends SCPageList { + @override + _HomeMinePageState createState() => _HomeMinePageState(); +} + +class _HomeMinePageState extends SCPageListState + with SingleTickerProviderStateMixin { + List historyRooms = []; + String? lastId; + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + late TabController _tabController; + final List _pages = [ + SCRoomHistoryPage(), + SCRoomFollowPage(), + + ]; + bool isLoading = false; + final List _tabs = []; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + enablePullUp = true; + backgroundColor = Colors.transparent; + loadData(1); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.recent)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.followed)); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.myRoom, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ), + ], + ), + SizedBox(height: 5.w), + Consumer( + builder: (_, provider, __) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + provider.myRoom == null + ? "sc_images/index/sc_icon_my_room_no_bg.png" + : "sc_images/index/sc_icon_my_room_has_bg.png", + ), + fit: BoxFit.fill, + ), + ), + width: ScreenUtil().screenWidth, + height: 85.w, + child: _buildMyRoom(provider), + ); + }, + ), + SizedBox(height: 5.w), + Row( + children: [ + SizedBox(width: 5.w), + TabBar( + tabAlignment: TabAlignment.start, + labelPadding: EdgeInsets.symmetric(horizontal: 12.w), + labelColor: SocialChatTheme.primaryLight, + isScrollable: true, + indicator: BoxDecoration(), + unselectedLabelColor: Colors.white, + labelStyle: TextStyle( + fontSize: 15.sp, + fontFamily: 'MyCustomFont', + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: TextStyle( + fontSize: 13.sp, + fontFamily: 'MyCustomFont', + fontWeight: FontWeight.w500, + ), + indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + physics: NeverScrollableScrollPhysics(), + children: _pages, + ), + ) + ], + ); + } + + _buildMyRoom(SocialChatRoomManager provider) { + return provider.myRoom != null + ? GestureDetector( + behavior: HitTestBehavior.opaque, + child: Row( + children: [ + SizedBox(width: 10.w), + netImage( + url: provider.myRoom?.roomCover ?? "", + borderRadius: BorderRadius.all(Radius.circular(8.w)), + width: 70.w, + ), + SizedBox(width: 10.w), + Expanded( + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 6.w), + Row( + children: [ + netImage( + url: + Provider.of( + context, + listen: false, + ) + .findCountryByName( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.countryName ?? + "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all( + Radius.circular(3.w), + ), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + Container( + constraints: BoxConstraints( + maxWidth: 200.w, + maxHeight: 24.w, + ), + child: + (provider.myRoom?.roomName?.length ?? 0) > 18 + ? Marquee( + text: provider.myRoom?.roomName ?? "", + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 40.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + provider.myRoom?.roomName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + text( + provider.myRoom?.roomDesc ?? "", + fontSize: 14.sp, + textColor: Colors.white, + ), + text( + "ID:${provider.myRoom?.roomAccount}", + fontSize: 12.sp, + textColor: Colors.white, + ), + ], + ), + ], + ), + ), + SizedBox(width: 12.w), + ], + ), + onTap: () { + String roomId = + Provider.of( + context, + listen: false, + ).myRoom?.id ?? + ""; + Provider.of( + context, + listen: false, + ).joinVoiceRoomSession(context, roomId); + }, + ) + : GestureDetector( + child: Row( + children: [ + SizedBox(width: 10.w), + Image.asset( + "sc_images/index/sc_icon_index_creat_room_tag.png", + height: 70.w, + width: 70.w, + ), + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.crateMyRoom, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ), + text( + SCAppLocalizations.of(context)!.startYourBrandNewJourney, + fontSize: 12.sp, + textColor: Colors.white, + ), + ], + ), + ), + Image.asset( + "sc_images/index/sc_icon_my_room_tag2.png", + height: 25.w, + width: 25.w, + ), + SizedBox(width: 10.w), + Icon( + Icons.chevron_right_outlined, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 15.w), + ], + ), + onTap: () { + provider.createNewRoom(context); + }, + ); + } + + void _loadOtherData() { + SCLoadingManager.show(); + Provider.of( + context, + listen: false, + ).fetchMyRoomData(); + SCAccountRepository() + .trace() + .then((res) { + historyRooms = res; + SCLoadingManager.hide(); + setState(() {}); + }) + .catchError((_) { + SCLoadingManager.hide(); + }); + } + + _buildHistoryRoomItem(FollowRoomRes roomRes) { + return GestureDetector( + child: SizedBox( + width: 70.w, + height: 70.w, + child: Stack( + children: [ + netImage( + url: roomRes.roomProfile?.roomCover ?? "", + width: 70.w, + height: 70.w, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(8.w), + ), + getRoomCoverHeaddress(roomRes).isNotEmpty + ? Transform.translate( + offset: Offset(0, -2.w), + child: Transform.scale( + scaleX: 1.1, + scaleY: 1.12, + child: Image.asset(getRoomCoverHeaddress(roomRes)), + ), + ) + : Container(), + (roomRes.roomProfile?.roomGameIcon?.isNotEmpty ?? false) + ? PositionedDirectional( + top: 8.w, + end: 8.w, + child: netImage( + url: roomRes.roomProfile?.roomGameIcon ?? "", + width: 15.w, + height: 15.w, + borderRadius: BorderRadius.circular(4.w), + ), + ) + : Container(), + PositionedDirectional( + top: 3.w, + end: 5.w, + child: + (roomRes + .roomProfile + ?.extValues + ?.roomSetting + ?.password + ?.isNotEmpty ?? + false) + ? Image.asset( + "sc_images/index/sc_icon_room_suo.png", + width: 16.w, + height: 16.w, + ) + : Container(), + ), + ], + ), + ), + onTap: () { + Provider.of( + context, + listen: false, + ).joinVoiceRoomSession(context, roomRes.roomProfile?.id ?? ""); + }, + ); + } + + @override + Widget buildItem(FollowRoomRes res) { + return SCDebounceWidget( + child: SizedBox( + height: 105.w, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(13.w), + border: Border.all( + color: _strategy.getExploreRoomBorderColor(), + width: _strategy.getExploreRoomBorderWidth().w, + ), + ), + margin: EdgeInsetsDirectional.symmetric( + horizontal: 10.w, + vertical: 3.w, + ), + ), + Row( + children: [ + SizedBox(width: 20.w), + netImage( + url: res.roomProfile?.roomCover ?? "", + fit: BoxFit.cover, + borderRadius: BorderRadius.all(Radius.circular(12.w)), + width: 85.w, + height: 85.w, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 15.w), + Row( + children: [ + SizedBox(width: 8.w), + Consumer( + builder: (_, provider, __) { + return netImage( + url: + "${provider.findCountryByName(res.roomProfile?.countryName ?? "")?.nationalFlag}", + width: 25.w, + height: 15.w, + borderRadius: BorderRadius.all( + Radius.circular(3.w), + ), + ); + }, + ), + SizedBox(width: 5.w), + Container( + constraints: BoxConstraints( + maxHeight: 21.w, + maxWidth: 150.w, + ), + child: + (res.roomProfile?.roomName?.length ?? 0) > + _strategy + .getExploreRoomNameMarqueeThreshold() + ? Marquee( + text: res.roomProfile?.roomName ?? "", + style: TextStyle( + fontSize: 15.sp, + color: Colors.black, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + res.roomProfile?.roomName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15.sp, + color: Colors.black, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + SizedBox(height: 3.w), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 8.w), + text( + "ID:${res.roomProfile?.roomAccount}", + fontWeight: FontWeight.w600, + textColor: Colors.black, + fontSize: 15.sp, + ), + ], + ), + SizedBox(height: 3.w), + Row( + children: [ + SizedBox(width: 8.w), + Image.asset( + "sc_images/msg/sc_icon_message_activity.png", + height: 25.w, + width: 25.w, + ), + SizedBox(width: 4.w), + Container( + constraints: BoxConstraints( + maxHeight: 21.w, + maxWidth: 170.w, + ), + child: + (res.roomProfile?.roomDesc?.length ?? 0) > + _strategy + .getExploreRoomDescMarqueeThreshold() + ? Marquee( + text: res.roomProfile?.roomDesc ?? "", + style: TextStyle( + fontSize: 15.sp, + color: Colors.black, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + res.roomProfile?.roomDesc ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15.sp, + color: Colors.black, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ], + ), + ], + ), + ], + ), + ), + onTap: () { + Provider.of( + context, + listen: false, + ).joinVoiceRoomSession(context, res.roomProfile?.id ?? ""); + }, + ); + } + + @override + builderDivider() { + // return Divider( + // height: 1.w, + // color: Color(0xff3D3277).withOpacity(0.5), + // indent: 15.w, + // ); + return Container(height: 8.w); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + _loadOtherData(); + if (page == 1) { + lastId = null; + } + try { + var roomList = await SCAccountRepository().followRoomList(lastId: lastId); + if (roomList.isNotEmpty) { + lastId = roomList.last.id; + } + onSuccess(roomList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + String getRoomCoverHeaddress(FollowRoomRes roomRes) { + return ""; + } +} diff --git a/lib/modules/home/popular/party/sc_home_party_page.dart b/lib/modules/home/popular/party/sc_home_party_page.dart new file mode 100644 index 0000000..06cb1c4 --- /dev/null +++ b/lib/modules/home/popular/party/sc_home_party_page.dart @@ -0,0 +1,553 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:marquee/marquee.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; +import '../../../../app_localizations.dart'; +import '../../../../app/constants/sc_global_config.dart'; +import '../../../../app/routes/sc_fluro_navigator.dart'; +import '../../../../shared/tools/sc_banner_utils.dart'; +import '../../../../shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import '../../../../shared/business_logic/models/res/follow_room_res.dart'; +import '../../../../shared/business_logic/models/res/room_res.dart'; +import '../../../../services/general/sc_app_general_manager.dart'; +import '../../../../services/audio/rtc_manager.dart'; +import '../../../../ui_kit/components/sc_compontent.dart'; +import '../../../../ui_kit/components/text/sc_text.dart'; +import '../../../index/main_route.dart'; + +class SCHomePartyPage extends StatefulWidget { + @override + _HomePartyPageState createState() => _HomePartyPageState(); +} + +class _HomePartyPageState extends State + with SingleTickerProviderStateMixin { + List historyRooms = []; + String? lastId; + bool isLoading = false; + final RefreshController _refreshController = RefreshController( + initialRefresh: false, + ); + List rooms = []; + int _currentIndex = 0; + + @override + void initState() { + super.initState(); + loadData(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + SmartRefresher( + enablePullDown: true, + enablePullUp: false, + controller: _refreshController, + onRefresh: () { + loadData(); + }, + onLoading: () {}, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: AlignmentDirectional.center, + children: [ + Consumer( + builder: (context, ref, child) { + return _banner(ref); + }, + ), + ], + ), + Consumer( + builder: (context, ref, child) { + return _banner2(ref); + }, + ), + rooms.isEmpty + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + loadData(); + }, + child: + isLoading + ? Center( + child: CupertinoActivityIndicator( + color: Colors.white24, + ), + ) + : mainEmpty( + msg: SCAppLocalizations.of(context)!.noData, + ), + ) + : GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // 每行2个 + childAspectRatio: 1, // 宽高比 + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + padding: EdgeInsets.all(10), + itemCount: rooms!.length, + itemBuilder: (context, index) { + return _buildItem(rooms[index], index); + }, + ), + ], + ), + ), + ), + ], + ), + ); + } + + _banner(SCAppGeneralManager ref) { + if (ref.exploreBanners.isEmpty) { + return Container(); + } + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + CarouselSlider( + options: CarouselOptions( + height: 95.w, + autoPlay: true, + // 启用自动播放 + enlargeCenterPage: false, + // 居中放大当前页面 + enableInfiniteScroll: true, + // 启用无限循环 + autoPlayAnimationDuration: Duration(milliseconds: 800), + // 自动播放动画时长 + viewportFraction: 1, + // 视口分数 + onPageChanged: (index, reason) { + _currentIndex = index; + setState(() {}); + }, + ), + items: + ref.exploreBanners.map((item) { + return GestureDetector( + child: netImage( + url: item.cover ?? "", + height: 95.w, + width: ScreenUtil().screenWidth * 0.9, + fit: BoxFit.fill, + borderRadius: BorderRadius.circular(12.w), + ), + onTap: () { + print('ads:${item.toJson()}'); + SCBannerUtils.openBanner(item, context); + }, + ); + }).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: + ref.exploreBanners.asMap().entries.map((entry) { + return Container( + width: 6.0, + height: 6.0, + margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + _currentIndex == entry.key ? Colors.blue : Colors.white, + ), + ); + }).toList(), + ), + ], + ); + } + + _banner2(SCAppGeneralManager ref) { + String roomGiftOneAvatar = ""; + String roomGiftTwoAvatar = ""; + String roomGiftThreeAvatar = ""; + if (ref.appLeaderResult?.roomGiftsLeaderboard != null && + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly != null) { + if ((ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.isNotEmpty ?? + false) && + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.first != null) { + roomGiftOneAvatar = + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.first.avatar ?? + ""; + } + + if (ref.appLeaderResult!.roomGiftsLeaderboard!.weekly!.length > 1 && + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?[1] != null) { + roomGiftTwoAvatar = + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly![1].avatar ?? ""; + } + if (ref.appLeaderResult!.roomGiftsLeaderboard!.weekly!.length > 2 && + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?[2] != null) { + roomGiftThreeAvatar = + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly![2].avatar ?? ""; + } + } + + String wealthOneAvatar = ""; + String wealthTwoAvatar = ""; + String wealthThreeAvatar = ""; + + if (ref.appLeaderResult?.giftsSendLeaderboard != null && + ref.appLeaderResult?.giftsSendLeaderboard?.weekly != null) { + if ((ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.isNotEmpty ?? + false) && + ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.first != null) { + wealthOneAvatar = + ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.first.avatar ?? + ""; + } + + if (ref.appLeaderResult!.giftsSendLeaderboard!.weekly!.length > 1 && + ref.appLeaderResult?.giftsSendLeaderboard?.weekly?[1] != null) { + wealthTwoAvatar = + ref.appLeaderResult?.giftsSendLeaderboard?.weekly![1].avatar ?? ""; + } + if (ref.appLeaderResult!.giftsSendLeaderboard!.weekly!.length > 2 && + ref.appLeaderResult?.giftsSendLeaderboard?.weekly?[2] != null) { + wealthThreeAvatar = + ref.appLeaderResult?.giftsSendLeaderboard?.weekly![2].avatar ?? ""; + } + } + + String charmOneAvatar = ""; + String charmTwoAvatar = ""; + String charmThreeAvatar = ""; + + if (ref.appLeaderResult?.giftsReceivedLeaderboard != null && + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly != null) { + if ((ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?.isNotEmpty ?? + false) && + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?.first != + null) { + charmOneAvatar = + ref + .appLeaderResult + ?.giftsReceivedLeaderboard + ?.weekly + ?.first + .avatar ?? + ""; + } + + if (ref.appLeaderResult!.giftsReceivedLeaderboard!.weekly!.length > 1 && + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?[1] != null) { + charmTwoAvatar = + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly![1].avatar ?? + ""; + } + if (ref.appLeaderResult!.giftsReceivedLeaderboard!.weekly!.length > 2 && + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?[2] != null) { + charmThreeAvatar = + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly![2].avatar ?? + ""; + } + } + + if (roomGiftOneAvatar.isEmpty && + wealthOneAvatar.isEmpty && + charmOneAvatar.isEmpty) { + return Container(); + } + return SizedBox( + height: 130.w, + child: Row( + children: [ + Expanded( + child: GestureDetector( + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + start: 26.w, + top: 65.w, + child: head(url: wealthTwoAvatar, width: 30.w), + ), + Positioned( + top: 35.w, + child: head(url: wealthOneAvatar, width: 36.w), + ), + PositionedDirectional( + end: 24.w, + top: 65.w, + child: head(url: wealthThreeAvatar, width: 30.w), + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getPopularLeaderboardBackgroundImage('wealth'), + fit: BoxFit.fill, + ), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.wealthRankUrl)}&showTitle=false", + replace: false, + ); + }, + ), + ), + Expanded( + child: GestureDetector( + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + start: 24.w, + top: 68.w, + child: head(url: roomGiftTwoAvatar, width: 30.w), + ), + Positioned( + top: 35.w, + child: head(url: roomGiftOneAvatar, width: 38.w), + ), + PositionedDirectional( + end: 25.w, + top: 69.w, + child: head(url: roomGiftThreeAvatar, width: 30.w), + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getPopularLeaderboardBackgroundImage('room'), + fit: BoxFit.fill, + ), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&showTitle=false", + replace: false, + ); + }, + ), + ), + Expanded( + child: GestureDetector( + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + start: 26.w, + top: 66.w, + child: head(url: charmTwoAvatar, width: 30.w), + ), + Positioned( + top: 38.w, + child: head(url: charmOneAvatar, width: 37.w), + ), + PositionedDirectional( + end: 25.w, + top: 66.w, + child: head(url: charmThreeAvatar, width: 30.w), + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getPopularLeaderboardBackgroundImage('charm'), + fit: BoxFit.fill, + ), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.charmRankUrl)}&showTitle=false", + replace: false, + ); + }, + ), + ), + ], + ), + ); + } + + loadData() { + setState(() { + isLoading = true; + }); + SCChatRoomRepository() + .discovery(allRegion: true) + .then((values) { + rooms = values; + isLoading = false; + _refreshController.refreshCompleted(); + _refreshController.loadComplete(); + if (mounted) setState(() {}); + }) + .catchError((e) { + _refreshController.loadNoData(); + _refreshController.refreshCompleted(); + isLoading = false; + if (mounted) setState(() {}); + }); + Provider.of(context, listen: false).loadMainBanner(); + Provider.of(context, listen: false).appLeaderboard(); + } + + _buildItem(SocialChatRoomRes res, int index) { + return SCDebounceWidget( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.transparent, + ), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/index/sc_icon_room_bord.png"), + fit: BoxFit.fill, + ), + ), + child: netImage( + url: res.roomCover ?? "", + borderRadius: BorderRadius.circular(12.w), + width: 200.w, + height: 200.w, + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 6.w), + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/index/sc_icon_index_room_brd.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + Consumer( + builder: (_, provider, __) { + return netImage( + url: + "${provider.findCountryByName(res.countryName ?? "")?.nationalFlag}", + width: 20.w, + height: 13.w, + borderRadius: BorderRadius.circular(2.w), + ); + }, + ), + SizedBox(width: 5.w), + Expanded( + child: SizedBox( + height: 21.w, + child: text( + res.roomName ?? "", + fontSize: 15.sp, + textColor: Color(0xffffffff), + fontWeight: FontWeight.w400, + ), + // (roomRes.roomProfile?.roomName?.length ?? 0) > 10 + // ? Marquee( + // text: roomRes.roomProfile?.roomName ?? "", + // style: TextStyle( + // fontSize: 15.sp, + // color: Color(0xffffffff), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // ), + // scrollAxis: Axis.horizontal, + // crossAxisAlignment: CrossAxisAlignment.start, + // blankSpace: 20.0, + // velocity: 40.0, + // pauseAfterRound: Duration(seconds: 1), + // accelerationDuration: Duration(seconds: 1), + // accelerationCurve: Curves.easeOut, + // decelerationDuration: Duration( + // milliseconds: 500, + // ), + // decelerationCurve: Curves.easeOut, + // ) + // : Text( + // roomRes.roomProfile?.roomName ?? "", + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // style: TextStyle( + // fontSize: 15.sp, + // color: Color(0xffffffff), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // ), + // ), + ), + ), + SizedBox(width: 5.w), + (res.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? Image.asset( + "sc_images/general/sc_icon_online_user.png", + width: 14.w, + height: 14.w, + ) + : Image.asset( + "sc_images/index/sc_icon_room_suo.png", + width: 20.w, + height: 20.w, + ), + (res.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? SizedBox(width: 3.w) + : Container(height: 10.w), + (res.extValues + ?.roomSetting + ?.password + ?.isEmpty ?? + false) + ? text( + res.extValues?.memberQuantity ?? "0", + fontSize: 12.sp, + ) + : Container(height: 10.w), + SizedBox(width: 10.w), + ], + ), + ), + ], + ), + ), + onTap: () { + Provider.of( + context, + listen: false, + ).joinVoiceRoomSession(context, res.id ?? ""); + }, + ); + } +} diff --git a/lib/modules/index/index_page.dart b/lib/modules/index/index_page.dart new file mode 100644 index 0000000..1956ea9 --- /dev/null +++ b/lib/modules/index/index_page.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; +import 'package:yumi/modules/home/index_home_page.dart'; +import 'package:yumi/modules/chat/message/sc_message_page.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import '../../shared/tools/sc_heartbeat_utils.dart'; +import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; +import '../../ui_kit/components/sc_float_ichart.dart'; +import '../home/popular/event/home_event_page.dart'; +import '../user/me_page2.dart'; + +/** + * 首页 + */ +class SCIndexPage extends StatefulWidget { + @override + _SCIndexPageState createState() => _SCIndexPageState(); +} + +class _SCIndexPageState extends State { + int _currentIndex = 0; + final List _pages = []; + final List _bottomItems = []; + SCAppGeneralManager? generalProvider; + + @override + void initState() { + super.initState(); + generalProvider = Provider.of(context, listen: false); + Provider.of( + context, + listen: false, + ).initializeRealTimeCommunicationManager(context); + Provider.of(context, listen: false).init(context); + Provider.of( + context, + listen: false, + ).getUserIdentity(); + Provider.of(context, listen: false).balance(); + SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); + DataPersistence.setLang(SCGlobalConfig.lang); + OverlayManager().activate(); + WakelockPlus.enable(); + String roomId = DataPersistence.getLastTimeRoomId(); + if (roomId.isNotEmpty) { + SCAccountRepository().quitRoom(roomId).catchError((e) { + return true; // 错误已处理 + }); + } + } + + @override + void dispose() { + WakelockPlus.disable(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + SCFloatIchart().init(context); + _initWidget(); + return WillPopScope( + onWillPop: _doubleExit, + child: Stack( + children: [ + Image.asset( + "sc_images/index/sc_icon_index_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + SafeArea( + top: false, + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: _pages[_currentIndex], + bottomNavigationBar: Container( + height: 85.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/index/sc_index_bottom_navigation_bar_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: BottomNavigationBar( + elevation: 0, + backgroundColor: Colors.transparent, + selectedLabelStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14.sp, + ), + unselectedLabelStyle: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 13.sp, + ), + type: BottomNavigationBarType.fixed, + selectedItemColor: Color(0xffBF854A), + unselectedItemColor: Color(0xffC4C4C4), + showUnselectedLabels: true, + showSelectedLabels: true, + items: _bottomItems, + currentIndex: _currentIndex, + onTap: (index) => setState(() => _currentIndex = index), + ), + ), + ), + ), + ], + ), + ); + } + + ///双击退出 + int _lastClickTime = 0; + + Future _doubleExit() { + int nowTime = DateTime.now().microsecondsSinceEpoch; + if (_lastClickTime != 0 && nowTime - _lastClickTime > 1500) { + return Future.value(true); + } else { + _lastClickTime = DateTime.now().microsecondsSinceEpoch; + Future.delayed(const Duration(milliseconds: 1500), () { + _lastClickTime = 0; + }); + return Future.value(false); + } + } + + void _initWidget() { + _pages.clear(); + _bottomItems.clear(); + _pages.add(SCIndexHomePage()); + _pages.add(HomeEventPage()); + // _pages.add(IndexDynamicPage()); + _pages.add(SCMessagePage()); + _pages.add(MePage2()); + + _bottomItems.add( + BottomNavigationBarItem( + icon: Image.asset( + "sc_images/index/sc_icon_home_no.png", + width: 35.w, + height: 35.w, + ), + activeIcon: Image.asset( + "sc_images/index/sc_icon_home_en.png", + width: 35.w, + height: 35.w, + ), + label: SCAppLocalizations.of(context)!.home, + ), + ); + _bottomItems.add( + BottomNavigationBarItem( + icon: Image.asset( + "sc_images/index/sc_icon_home_no.png", + width: 35.w, + height: 35.w, + ), + activeIcon: Image.asset( + "sc_images/index/sc_icon_home_en.png", + width: 35.w, + height: 35.w, + ), + label: SCAppLocalizations.of(context)!.explore, + ), + ); + + _bottomItems.add( + BottomNavigationBarItem( + icon: Selector( + selector: (c, p) => p.allUnReadCount, + shouldRebuild: (prev, next) => prev != next, + builder: (_, allUnReadCount, __) { + return allUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${allUnReadCount > 99 ? "99+" : allUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Image.asset( + "sc_images/index/sc_icon_message_no.png", + width: 35.w, + height: 35.w, + ), + ) + : Image.asset( + "sc_images/index/sc_icon_message_no.png", + width: 35.w, + height: 35.w, + ); + }, + ), + activeIcon: Selector( + selector: (c, p) => p.allUnReadCount, + shouldRebuild: (prev, next) => prev != next, + builder: (_, allUnReadCount, __) { + return allUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${allUnReadCount > 99 ? "99+" : allUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Image.asset( + "sc_images/index/sc_icon_message_en.png", + width: 35.w, + height: 35.w, + ), + ) + : Image.asset( + "sc_images/index/sc_icon_message_en.png", + width: 35.w, + height: 35.w, + ); + }, + ), + label: SCAppLocalizations.of(context)!.message, + ), + ); + _bottomItems.add( + BottomNavigationBarItem( + icon: Image.asset( + "sc_images/index/sc_icon_me_no.png", + width: 35.w, + height: 35.w, + ), + activeIcon: Image.asset( + "sc_images/index/sc_icon_me_en.png", + width: 35.w, + height: 35.w, + ), + label: SCAppLocalizations.of(context)!.me, + ), + ); + } +} diff --git a/lib/modules/index/main_route.dart b/lib/modules/index/main_route.dart new file mode 100644 index 0000000..20b281a --- /dev/null +++ b/lib/modules/index/main_route.dart @@ -0,0 +1,200 @@ +import 'dart:convert'; + +import 'package:fluro/fluro.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/modules/user/fans/fans_user_list_page.dart'; +import 'package:yumi/modules/settings/language/language_page.dart'; +import 'package:yumi/modules/report/report_page.dart'; +import 'package:yumi/modules/webview/webview_page.dart'; + +import 'package:yumi/app/routes/sc_router_init.dart'; +import 'package:yumi/modules/admin/editing/sc_editing_user_room_page.dart'; +import 'package:yumi/modules/admin/search/sc_edit_room_search_admin_page.dart'; +import 'package:yumi/modules/admin/search/sc_edit_user_search_admin_page.dart'; +import 'package:yumi/modules/user/profile/person_detail_page.dart'; +import 'package:yumi/modules/search/sc_search_page.dart'; +import 'package:yumi/modules/media/image_preview_page.dart'; +import 'package:yumi/modules/media/video_player_page.dart'; +import 'package:yumi/modules/user/follow/follow_user_list_page.dart'; +import 'package:yumi/modules/user/level/level_page.dart'; +import 'package:yumi/modules/user/visitor/visitor_user_list_page.dart'; + +import '../chat/message/sc_message_page.dart'; +import '../user/edit/edit_user_info_page2.dart'; + +class SCMainRoute implements SCIRouterProvider { + static String report = '/main/report'; + static String person = '/main/person'; + static String edit = '/main/person/edit'; + static String follow = '/main/me/follow'; + static String vistors = '/main/me/vistors'; + static String fans = '/main/me/fans'; + static String mainSearch = '/main/mainSearch'; + static String language = '/main/language'; + static String levelList = '/level/levelList'; + static String videoPlayer = '/main/videoPlayer'; + static String imagePreview = '/main/imagePreview'; + static String message = '/main/messagePage'; + static String editRoomSearchAdmin = '/main/editRoomSearchAdmin'; + static String editUserSearchAdmin = '/main/editUserSearchAdmin'; + static String editingUserRoomAdmin = '/main/editingUserRoomAdmin'; + static String webViewPage = '/main/webViewPage'; + + @override + void initRouter(FluroRouter router) { + router.define( + report, + handler: Handler( + handlerFunc: + (_, params) => ReportPage( + type: params['type']!.first, + tageId: params['tageId']!.first, + ), + ), + ); + router.define( + person, + handler: Handler( + handlerFunc: + (_, params) => PersonDetailPage( + isMe: params['isMe']!.first, + tageId: params['tageId']!.first, + ), + ), + ); + router.define( + mainSearch, + handler: Handler(handlerFunc: (_, params) => SearchPage()), + ); + router.define( + message, + handler: Handler( + handlerFunc: (_, Map> params) { + String? isFromRoom = params['isFromRoom']?.first; + return SCMessagePage(isFromRoom: isFromRoom == "true"); + }, + ), + ); + + router.define( + language, + handler: Handler(handlerFunc: (_, params) => LanguagePage()), + ); + router.define( + edit, + handler: Handler(handlerFunc: (_, params) => EditUserInfoPage2()), + ); + router.define( + follow, + handler: Handler(handlerFunc: (_, params) => FollowUserListPage()), + ); + router.define( + vistors, + handler: Handler(handlerFunc: (_, params) => VisitorUserListPage()), + ); + router.define( + fans, + handler: Handler(handlerFunc: (_, params) => FansUserListPage()), + ); + router.define( + levelList, + handler: Handler(handlerFunc: (_, params) => LevelPage()), + ); + router.define( + editRoomSearchAdmin, + handler: Handler(handlerFunc: (_, params) => SCEditRoomSearchAdminPage()), + ); + router.define( + editUserSearchAdmin, + handler: Handler(handlerFunc: (_, params) => SCEditUserSearchAdminPage()), + ); + router.define( + videoPlayer, + handler: Handler( + handlerFunc: (_, Map> params) { + String? rawUrl = params['videoUrl']?.first; + String decodedVideoUrl = + rawUrl != null ? Uri.decodeComponent(rawUrl) : ""; + return VideoPlayerPage(videoUrl: decodedVideoUrl); + }, + ), + ); + router.define( + imagePreview, + handler: Handler( + handlerFunc: (_, Map> params) { + List imageUrls = []; + int initialIndex = 0; + + try { + // 解析 imageUrls 参数(JSON 字符串) + if (params['imageUrls'] != null && + params['imageUrls']!.isNotEmpty) { + String encodedUrls = params['imageUrls']!.first; + String decodedUrls = Uri.decodeComponent(encodedUrls); + List urlList = jsonDecode(decodedUrls); + imageUrls = urlList.map((item) => item.toString()).toList(); + } + + // 解析 initialIndex + if (params['initialIndex'] != null && + params['initialIndex']!.isNotEmpty) { + initialIndex = int.tryParse(params['initialIndex']!.first) ?? 0; + } + } catch (e) { + print('解析图片预览参数失败: $e'); + // 可以返回错误页面或默认页面 + } + + return ImagePreviewPage( + imageUrls: imageUrls, + initialIndex: initialIndex, + ); + }, + ), + ); + + router.define( + editingUserRoomAdmin, + handler: Handler( + handlerFunc: (_, Map> params) { + String? type = params['type']?.first; + String? profileJson = params['profile']?.first; + SocialChatUserProfile? userProfile; + SocialChatRoomRes? roomProfile; + if (type == "User") { + userProfile = SocialChatUserProfile.fromJson( + jsonDecode(profileJson ?? ""), + ); + } else { + roomProfile = SocialChatRoomRes.fromJson( + jsonDecode(profileJson ?? ""), + ); + } + + return SCEditingUserRoomPage( + type: type ?? "", + userProfile: userProfile, + roomProfile: roomProfile, + ); + }, + ), + ); + + router.define( + webViewPage, + handler: Handler( + handlerFunc: (_, params) { + String? url = params['url']?.first; + String decodedUrl = url != null ? Uri.decodeComponent(url) : ""; + return WebViewPage( + title: params['title']?.first ?? "", + url: decodedUrl, + showTitle: params['showTitle']?.first ?? "", + ); + }, + ), + ); + } +} diff --git a/lib/modules/media/image_preview_page.dart b/lib/modules/media/image_preview_page.dart new file mode 100644 index 0000000..ca2909c --- /dev/null +++ b/lib/modules/media/image_preview_page.dart @@ -0,0 +1,148 @@ +import 'dart:io'; + +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +class ImagePreviewPage extends StatefulWidget { + final List imageUrls; + final int initialIndex; + final PageController? pageController; + + const ImagePreviewPage({ + Key? key, + required this.imageUrls, + this.initialIndex = 0, + this.pageController, + }) : super(key: key); + + @override + _ImagePreviewPageState createState() => _ImagePreviewPageState(); +} + +class _ImagePreviewPageState extends State { + late PageController _pageController; + late int _currentIndex; + bool _showAppBar = true; + + @override + void initState() { + super.initState(); + + _currentIndex = widget.initialIndex; + _pageController = + widget.pageController ?? + PageController(initialPage: widget.initialIndex); + } + + void _onPageChanged(int index) { + setState(() { + _currentIndex = index; + }); + } + + void _toggleAppBar() { + setState(() { + _showAppBar = !_showAppBar; + }); + } + + void _onTap() { + _toggleAppBar(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SCGlobalConfig.businessLogicStrategy.getImagePreviewBackgroundColor(), + body: Stack( + children: [ + // 图片画廊 + PhotoViewGallery.builder( + scrollPhysics: const BouncingScrollPhysics(), + builder: _buildImage, + itemCount: widget.imageUrls.length, + loadingBuilder: + (context, event) => Center( + child: Container( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator( + value: + event == null + ? 0 + : event.cumulativeBytesLoaded / + event.expectedTotalBytes!, + color: SCGlobalConfig.businessLogicStrategy.getImagePreviewLoadingIndicatorColor(), + ), + ), + ), + backgroundDecoration: BoxDecoration(color: SCGlobalConfig.businessLogicStrategy.getImagePreviewGalleryBackgroundColor()), + pageController: _pageController, + onPageChanged: _onPageChanged, + enableRotation: true, + customSize: MediaQuery.of(context).size, + ), + + // 顶部应用栏 + if (_showAppBar) _buildAppBar(), + ], + ), + ); + } + + PhotoViewGalleryPageOptions _buildImage(BuildContext context, int index) { + final imageUrl = widget.imageUrls[index]; + return PhotoViewGalleryPageOptions( + imageProvider: _getImageProvider(imageUrl), + initialScale: PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained * 0.8, + maxScale: PhotoViewComputedScale.covered * 2.0, + heroAttributes: PhotoViewHeroAttributes(tag: imageUrl), + onTapUp: (_, __, ___) => _onTap(), + ); + } + + ImageProvider _getImageProvider(String imageUrl) { + if (imageUrl.startsWith('http')) { + return ExtendedNetworkImageProvider( + imageUrl, + cache: true, // 启用缓存(默认即为true) + cacheMaxAge: Duration(days: 7), // 设置缓存有效期 + ); + } else { + return FileImage(File(imageUrl)); + } + } + + Widget _buildAppBar() { + return Positioned( + top: 0, + left: 0, + right: 0, + child: AppBar( + backgroundColor: SCGlobalConfig.businessLogicStrategy.getImagePreviewAppBarBackgroundColor(), + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: SCGlobalConfig.businessLogicStrategy.getImagePreviewBackIconColor()), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + '${_currentIndex + 1} / ${widget.imageUrls.length}', + style: TextStyle(color: SCGlobalConfig.businessLogicStrategy.getImagePreviewTextColor()), + ), + centerTitle: true, + ), + ); + } + + @override + void dispose() { + if (widget.pageController == null) { + _pageController.dispose(); + } + super.dispose(); + } +} diff --git a/lib/modules/media/video_player_page.dart b/lib/modules/media/video_player_page.dart new file mode 100644 index 0000000..20d2170 --- /dev/null +++ b/lib/modules/media/video_player_page.dart @@ -0,0 +1,314 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:video_player/video_player.dart'; + +class VideoPlayerPage extends StatefulWidget { + final String videoUrl; + + const VideoPlayerPage({Key? key, required this.videoUrl}) : super(key: key); + + @override + _VideoPlayerPageState createState() => _VideoPlayerPageState(); +} + +class _VideoPlayerPageState extends State { + late VideoPlayerController _controller; + late Future _initializeVideoPlayerFuture; + + bool _isPlaying = false; + bool _showControls = true; + Duration _currentPosition = Duration.zero; + Duration _totalDuration = Duration.zero; + + // 控制界面显示和隐藏的定时器 + Timer? _hideControlsTimer; + + @override + void initState() { + super.initState(); + _initializeVideoPlayer(); + } + + void _initializeVideoPlayer() { + final pathType = SCPathUtils.getPathType(widget.videoUrl); + if (pathType == PathType.file) { + _controller = VideoPlayerController.file(File(widget.videoUrl)); + _initializeVideoPlayerFuture = _controller.initialize().then((_) { + setState(() { + _totalDuration = _controller.value.duration; + }); + }); + } else if (pathType == PathType.network) { + _controller = VideoPlayerController.networkUrl( + Uri.parse(widget.videoUrl), + ); + _initializeVideoPlayerFuture = _controller.initialize().then((_) { + setState(() { + _totalDuration = _controller.value.duration; + }); + }); + } + + // 添加监听器来更新进度 + _controller.addListener(_updateProgress); + } + + void _updateProgress() { + if (mounted) { + setState(() { + _currentPosition = _controller.value.position; + _totalDuration = _controller.value.duration; + _isPlaying = _controller.value.isPlaying; + }); + } + } + + void _togglePlayPause() { + setState(() { + if (_controller.value.isPlaying) { + _controller.pause(); + _isPlaying = false; + } else { + _controller.play(); + _isPlaying = true; + _resetHideControlsTimer(); + } + }); + } + + void _seekToPosition(Duration position) { + _controller.seekTo(position); + } + + void _resetHideControlsTimer() { + _hideControlsTimer?.cancel(); + _hideControlsTimer = Timer(Duration(seconds: SCGlobalConfig.businessLogicStrategy.getVideoPlayerControlsDisplayDuration()), () { + if (mounted && _controller.value.isPlaying) { + setState(() { + _showControls = false; + }); + } + }); + } + + void _toggleControls() { + setState(() { + _showControls = !_showControls; + }); + if (_showControls && _controller.value.isPlaying) { + _resetHideControlsTimer(); + } + } + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final hours = twoDigits(duration.inHours); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + + if (duration.inHours > 0) { + return '$hours:$minutes:$seconds'; + } else { + return '$minutes:$seconds'; + } + } + + @override + void dispose() { + _hideControlsTimer?.cancel(); + _controller.removeListener(_updateProgress); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SCGlobalConfig.businessLogicStrategy.getVideoPlayerBackgroundColor(), + body: Center( + child: FutureBuilder( + future: _initializeVideoPlayerFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return GestureDetector( + onTap: _toggleControls, + child: Stack( + alignment: Alignment.center, + children: [ + // 视频播放器 + AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ), + + // 控制界面 + if (_showControls) ...[ + // 半透明背景 + Container(color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerControlsBackgroundColor()), + + // 播放/暂停按钮 + IconButton( + icon: Icon( + _isPlaying ? Icons.pause : Icons.play_arrow, + color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerIconColor(), + size: 50, + ), + onPressed: _togglePlayPause, + ), + + // 底部控制栏 + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerBottomControlsBackgroundColor(), + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // 进度条 + Slider( + value: + _currentPosition.inMilliseconds.toDouble(), + min: 0, + max: _totalDuration.inMilliseconds.toDouble(), + onChanged: (value) { + final newPosition = Duration( + milliseconds: value.toInt(), + ); + _seekToPosition(newPosition); + }, + onChangeStart: (_) { + _hideControlsTimer?.cancel(); + }, + onChangeEnd: (_) { + if (_controller.value.isPlaying) { + _resetHideControlsTimer(); + } + }, + activeColor: SCGlobalConfig.businessLogicStrategy.getVideoPlayerProgressBarActiveColor(), + inactiveColor: SCGlobalConfig.businessLogicStrategy.getVideoPlayerProgressBarInactiveColor(), + ), + + // 时间信息和控制按钮 + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + // 当前时间 + Text( + _formatDuration(_currentPosition), + style: TextStyle( + color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerTextColor(), + fontSize: 14, + ), + ), + + // 控制按钮 + Row( + children: [ + // 播放/暂停按钮 + IconButton( + icon: Icon( + _isPlaying + ? Icons.pause + : Icons.play_arrow, + color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerIconColor(), + ), + onPressed: _togglePlayPause, + ), + + // 10秒前进 + IconButton( + icon: Icon( + Icons.forward_5, + color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerIconColor(), + ), + onPressed: () { + final newPosition = + _currentPosition + + const Duration(seconds: 5); + if (newPosition <= _totalDuration) { + _seekToPosition(newPosition); + } else { + _seekToPosition(_totalDuration); + } + }, + ), + + // 10秒后退 + IconButton( + icon: Icon( + Icons.replay_5, + color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerIconColor(), + ), + onPressed: () { + final newPosition = + _currentPosition - + const Duration(seconds: 5); + if (newPosition >= Duration.zero) { + _seekToPosition(newPosition); + } else { + _seekToPosition(Duration.zero); + } + }, + ), + ], + ), + + // 总时长 + Text( + _formatDuration(_totalDuration), + style: TextStyle( + color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerTextColor(), + fontSize: 14, + ), + ), + ], + ), + ], + ), + ), + ), + ] else if (!_controller.value.isPlaying) ...[ + // 当视频暂停但控制界面隐藏时,显示播放按钮 + IconButton( + icon: Icon( + Icons.play_arrow, + color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerIconColor(), + size: 50, + ), + onPressed: _togglePlayPause, + ), + ], + + // 加载指示器 + if (snapshot.connectionState == ConnectionState.waiting) + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(SCGlobalConfig.businessLogicStrategy.getVideoPlayerLoadingIndicatorColor()), + ), + ], + ), + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(SCGlobalConfig.businessLogicStrategy.getVideoPlayerLoadingIndicatorColor()), + ), + SizedBox(height: 16), + Text('loading...', style: TextStyle(color: SCGlobalConfig.businessLogicStrategy.getVideoPlayerTextColor())), + ], + ); + } + }, + ), + ), + ); + } +} diff --git a/lib/modules/report/report_page.dart b/lib/modules/report/report_page.dart new file mode 100644 index 0000000..3d5f175 --- /dev/null +++ b/lib/modules/report/report_page.dart @@ -0,0 +1,405 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/custom_cached_image.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; + +///举报 +class ReportPage extends StatefulWidget { + final String type; + + ///被举报的id(用户id或者房间id) + final String tageId; + + const ReportPage({Key? key, required this.type, required this.tageId}) + : super(key: key); + + @override + _ReportPageState createState() => _ReportPageState(); +} + +class _ReportPageState extends State { + num selectedIndex = 0; + String reportReason = ""; + String pic01 = ""; + String pic02 = ""; + String pic03 = ""; + final TextEditingController _descriptionController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return SCDebounceWidget(child: Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + actions: [], + title: SCAppLocalizations.of(context)!.report, + backgroundColor: Colors.transparent, + ), + body: SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of( + context, + )!.pleaseSelectTheTypeContent, + fontSize: 14.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getReportPageHintTextColor(), + ), + SizedBox(width: 15.w), + ], + ), + Container( + margin: EdgeInsets.only(top: 10.w, left: 15.w, right: 15.w), + padding: EdgeInsets.symmetric(horizontal: 15.w), + child: Column( + children: [ + _item(SCAppLocalizations.of(context)!.spam, 3), + _item(SCAppLocalizations.of(context)!.fraud, 4), + _item( + SCAppLocalizations.of(context)!.maliciousHarassment, + 0, + ), + _item(SCAppLocalizations.of(context)!.other, 1), + Row( + children: [ + text( + SCAppLocalizations.of(context)!.description, + fontSize: 14.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(), + ), + Spacer(), + ], + ), + SizedBox(height: 8.w), + Container( + padding: EdgeInsets.all(5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: SCGlobalConfig.businessLogicStrategy.getReportPageContainerBackgroundColor(), + ), + child: TextField( + controller: _descriptionController, + onChanged: (text) {}, + maxLength: 1000, + maxLines: 5, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.reportInputTips, + hintStyle: TextStyle( + color: SCGlobalConfig.businessLogicStrategy.getReportPageSecondaryHintTextColor(), + fontSize: 14.sp, + ), + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + fillColor: Colors.white54, + counterStyle: TextStyle(color: Colors.white) + ), + style: TextStyle( + fontSize: sp(15), + color: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(), + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + SizedBox(height: 5.w), + Row( + children: [ + text( + SCAppLocalizations.of(context)!.screenshotTips, + fontSize: 14.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor(), + ), + Spacer(), + ], + ), + SizedBox(height: 8.w), + Row( + children: [ + Expanded( + child: GestureDetector( + child: Stack( + children: [ + pic01.isNotEmpty + ? CustomCachedImage( + imageUrl: pic01, + height: 100.w, + ) + : Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'), + height: 100.w, + ), + pic01.isNotEmpty + ? Positioned( + top: 5.w, + right: 5.w, + child: GestureDetector( + child: Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'), + width: 14.w, + height: 14.w, + ), + onTap: () { + setState(() { + pic01 = ""; + }); + }, + ), + ) + : Container(), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + pic01 = url; + }); + } + }); + }, + ), + ), + SizedBox(width: 5.w), + Expanded( + child: GestureDetector( + child: Stack( + children: [ + pic02.isNotEmpty + ? CustomCachedImage( + imageUrl: pic02, + height: 100.w, + ) + : Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'), + height: 100.w, + ), + pic02.isNotEmpty + ? Positioned( + top: 5.w, + right: 5.w, + child: GestureDetector( + child: Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'), + width: 14.w, + height: 14.w, + ), + onTap: () { + setState(() { + pic02 = ""; + }); + }, + ), + ) + : Container(), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + pic02 = url; + }); + } + }); + }, + ), + ), + SizedBox(width: 5.w), + Expanded( + child: GestureDetector( + child: Stack( + children: [ + pic03.isNotEmpty + ? CustomCachedImage( + imageUrl: pic03, + height: 100.w, + ) + : Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageIcon('addPic'), + height: 100.w, + ), + pic03.isNotEmpty + ? Positioned( + top: 5.w, + right: 5.w, + child: GestureDetector( + child: Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageIcon('closePic'), + width: 14.w, + height: 14.w, + ), + onTap: () { + setState(() { + pic03 = ""; + }); + }, + ), + ) + : Container(), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + pic03 = url; + }); + } + }); + }, + ), + ), + ], + ), + SizedBox(height: 15.w), + ], + ), + ), + SizedBox(height: 45.w), + SCDebounceWidget( + onTap: () async { + SCLoadingManager.show(); + String imageUrls = ""; + if (pic01.isNotEmpty) { + imageUrls = "$imageUrls,$pic01"; + } + if (pic02.isNotEmpty) { + imageUrls = "$imageUrls,$pic02"; + } + if (pic03.isNotEmpty) { + imageUrls = "$imageUrls,$pic03"; + } + SCChatRoomRepository() + .reported( + AccountStorage().getCurrentUser()?.userProfile?.id ?? + "", + widget.tageId, + selectedIndex, + reportedContent: _descriptionController.text, + imageUrls: imageUrls, + ) + .then((result) { + SCLoadingManager.hide(); + SCTts.show( + SCAppLocalizations.of(context)!.reportSucc, + ); + Navigator.of(context).pop(); + }) + .catchError((_) { + SCLoadingManager.hide(); + }); + + }, + child: Container( + height: 40.w, + width: double.infinity, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 35.w), + decoration: BoxDecoration( + color: SCGlobalConfig.businessLogicStrategy.getReportPageSubmitButtonBackgroundColor(), + borderRadius: BorderRadius.circular(25.w), + ), + child: Text( + SCAppLocalizations.of(context)!.submit, + style: TextStyle(color: SCGlobalConfig.businessLogicStrategy.getReportPageSubmitButtonTextColor(), fontSize: 16.sp), + ), + ), + ), + SizedBox(height: 35.w), + ], + ), + ), + ), + ), + ], + ), onTap: (){ + FocusScope.of(context).unfocus(); + }); + } + + Widget _item(String str, int index) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + FocusScope.of(context).unfocus(); // 彻底取消当前上下文的所有焦点 + setState(() { + reportReason = str; + selectedIndex = index; + }); + }, + child: Container( + width: double.infinity, + height: 40.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + text(str, fontSize: 14.sp, textColor: SCGlobalConfig.businessLogicStrategy.getReportPagePrimaryTextColor()), + Spacer(), + selectedIndex == index + ? Image.asset( + SCGlobalConfig.businessLogicStrategy.getReportPageIcon('checked'), + width: 20.w, + fit: BoxFit.fitWidth, + ) + : Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getReportPageUnselectedBorderColor(), width: 2.w), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/room/block/blocked_list_page.dart b/lib/modules/room/block/blocked_list_page.dart new file mode 100644 index 0000000..7bdb224 --- /dev/null +++ b/lib/modules/room/block/blocked_list_page.dart @@ -0,0 +1,162 @@ +import 'dart:ui' as ui; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/room_black_list_res.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +///房间用户 黑名单 +class BlockedListPage extends SCPageList { + String? roomId; + + BlockedListPage({super.key, this.roomId}); + + @override + _BlockedListPageState createState() => _BlockedListPageState(roomId); +} + +class _BlockedListPageState + extends SCPageListState { + String? roomId; + + _BlockedListPageState(this.roomId); + + @override + void initState() { + super.initState(); + enablePullUp = false; + backgroundColor = Colors.transparent; + isShowDivider = false; + loadData(1); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: ScreenUtil().screenWidth, + color: Color(0xff09372E).withOpacity(0.5), + child: Column( + children: [ + SizedBox(height: 15.w), + text( + SCAppLocalizations.of(context)!.blockedList, + fontSize: 14.sp, + textColor: Colors.white, + ), + SizedBox(height: 10.w), + SizedBox(height: 350.w, child: buildList(context)), + ], + ), + ), + ), + ), + ); + } + + @override + Widget buildItem(RoomBlackListRes res) { + return Container( + margin: REdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), + padding: REdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: Color(0xffF2F2F2), + borderRadius: BorderRadius.circular(10.w), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: ClipOval( + child: CachedNetworkImage( + imageUrl: res.blacklistUserProfile?.userAvatar ?? "", + width: 42.w, + height: 42.w, + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.blacklistUserProfile?.id}&tageId=${res.blacklistUserProfile?.id}", + ); + }, + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + constraints: BoxConstraints(maxWidth: 190.w), + child: text( + res.blacklistUserProfile?.userNickname ?? "", + fontSize: 14.sp, + textColor: Colors.white, + ), + ), + SizedBox(height: 4.w), + text( + "ID:${res.blacklistUserProfile?.getID()}", + fontSize: 10.sp, + textColor: Colors.white, + ), + ], + ), + ), + SizedBox(width: 3.w), + GestureDetector( + child: Image.asset( + "sc_images/room/sc_icon_remve_block.png", + width: 22.w, + height: 22.w, + ), + onTap: () { + SCChatRoomRepository() + .removeBlacklist( + res.roomId ?? "", + res.blacklistUserProfile?.id ?? "", + ) + .whenComplete(() { + loadData(1); + }); + }, + ), + SizedBox(width: 15.w), + ], + ), + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + var roomList = await SCAccountRepository().roomBlacklist(roomId ?? "", "0"); + onSuccess(roomList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } +} diff --git a/lib/modules/room/chat/all/all_chat_page.dart b/lib/modules/room/chat/all/all_chat_page.dart new file mode 100644 index 0000000..d3e07ba --- /dev/null +++ b/lib/modules/room/chat/all/all_chat_page.dart @@ -0,0 +1,149 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; + +class AllChatPage extends StatefulWidget { + @override + _AllChatPageState createState() => _AllChatPageState(); +} + +class _AllChatPageState extends State { + ScrollController _controller = ScrollController(); + bool showGoBottom = true; + bool _isDisposed = false; + List _msgList = []; + late RtmProvider provider; + + @override + void initState() { + super.initState(); + _controller.addListener(_scrollListener); + provider = Provider.of(context, listen: false); + var msgList = provider.roomAllMsgList; + provider.msgAllListener = _onNewMsg; + _msgList.addAll(msgList ??= []); + // 使用安全的方式初始化滚动位置 + _initScrollPosition(); + } + + void _scrollListener() { + final position = _controller.position; + + // 容差范围(5像素)避免精度问题 + const tolerance = 10.0; + + // 判断是否在第一个项目(颠倒列表的底部) + final isAtBottom = position.pixels < position.minScrollExtent + tolerance; + + // 判断是否在最后一个项目(颠倒列表的顶部) + // final isAtTop = position.pixels >= position.maxScrollExtent - tolerance; + if (isAtBottom != showGoBottom) { + showGoBottom = isAtBottom; + if (!_isDisposed) { + setState(() {}); + } + } + } + + // 安全初始化滚动位置 + void _initScrollPosition() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + if (_isDisposed) return; + _controller.jumpTo(0.0); + }); + } + + _onNewMsg(Msg msg) { + if (_isDisposed) return; // 如果组件已销毁,直接返回 + if (msg.groupId == "-1000") { + ///清屏 + _msgList.clear(); + setState(() {}); + return; + } + if (!_msgList.contains(msg)) { + setState(() { + _msgList.insert(0, msg); + }); + _controller.jumpTo(0.0); + } + } + + @override + void dispose() { + _isDisposed = true; + provider.msgAllListener = null; + _controller.removeListener(_scrollListener); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + body: Stack( + alignment: Alignment.bottomLeft, + children: [ + ListView.builder( + reverse: true, + controller: _controller, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + return MsgItem( + msg: _msgList[index], + onClick: (SocialChatUserProfile? user) { + Provider.of(context, listen: false).clickSite( + Provider.of( + context, + listen: false, + ).userOnMaiInIndex(user?.id ?? ""), + clickUser: user, + ); + }, + ); + }, + itemCount: _msgList.length, + ), + Visibility( + visible: !showGoBottom, + child: GestureDetector( + onTap: () { + if (_controller.hasClients) { + _controller.jumpTo(0.0); + } + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: width(15)), + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(52), + color: Colors.white, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + SCAppLocalizations.of(context)!.scrollToTheBottom, + style: TextStyle(fontSize: 10.sp), + ), + SizedBox(width: 4.w), + Icon(Icons.chevron_right, size: 10.w), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/room/chat/chat/chat_page.dart b/lib/modules/room/chat/chat/chat_page.dart new file mode 100644 index 0000000..cc0fecb --- /dev/null +++ b/lib/modules/room/chat/chat/chat_page.dart @@ -0,0 +1,150 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; + +class ChatPage extends StatefulWidget { + @override + _ChatPageState createState() => _ChatPageState(); +} + +class _ChatPageState extends State { + ScrollController _controller = ScrollController(); + bool showGoBottom = true; + bool _isDisposed = false; + List _msgList = []; + late RtmProvider provider; + + @override + void initState() { + super.initState(); + _controller.addListener(_scrollListener); + provider = Provider.of(context, listen: false); + var msgList = provider.roomChatMsgList; + provider.msgChatListener = _onNewMsg; + _msgList.addAll(msgList ??= []); + // 使用安全的方式初始化滚动位置 + _initScrollPosition(); + } + + void _scrollListener() { + final position = _controller.position; + + // 容差范围(5像素)避免精度问题 + const tolerance = 10.0; + + // 判断是否在第一个项目(颠倒列表的底部) + final isAtBottom = position.pixels < position.minScrollExtent + tolerance; + + // 判断是否在最后一个项目(颠倒列表的顶部) + // final isAtTop = position.pixels >= position.maxScrollExtent - tolerance; + if (isAtBottom != showGoBottom) { + showGoBottom = isAtBottom; + if (!_isDisposed) { + setState(() {}); + } + } + } + + // 安全初始化滚动位置 + void _initScrollPosition() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + if (_isDisposed) return; + _controller.jumpTo(0.0); + }); + } + + _onNewMsg(Msg msg) { + if (_isDisposed) return; // 如果组件已销毁,直接返回 + if (msg.groupId == "-1000") { + ///清屏 + _msgList.clear(); + setState(() {}); + return; + } + if (!_msgList.contains(msg)) { + setState(() { + _msgList.insert(0, msg); + }); + _controller.jumpTo(0.0); + } + } + + @override + void dispose() { + _isDisposed = true; + provider.msgChatListener = null; + _controller.removeListener(_scrollListener); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + body: Stack( + alignment: Alignment.bottomLeft, + children: [ + ListView.builder( + reverse: true, + controller: _controller, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + return MsgItem( + msg: _msgList[index], + onClick: (SocialChatUserProfile? user) { + Provider.of(context, listen: false).clickSite( + Provider.of( + context, + listen: false, + ).userOnMaiInIndex(user?.id ?? ""), + clickUser: user, + ); + }, + ); + }, + itemCount: _msgList.length, + ), + Visibility( + visible: !showGoBottom, + child: GestureDetector( + onTap: () { + if (_controller.hasClients) { + _controller.jumpTo(0.0); + } + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: width(15)), + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(52), + color: Colors.white, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + SCAppLocalizations.of(context)!.scrollToTheBottom, + style: TextStyle(fontSize: 10.sp), + ), + SizedBox(width: 4.w), + Icon(Icons.chevron_right, size: 10.w), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/room/chat/gift/gift_chat_page.dart b/lib/modules/room/chat/gift/gift_chat_page.dart new file mode 100644 index 0000000..fb06253 --- /dev/null +++ b/lib/modules/room/chat/gift/gift_chat_page.dart @@ -0,0 +1,141 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; + +class GiftChatPage extends StatefulWidget { + @override + _GiftChatPageState createState() => _GiftChatPageState(); +} + +class _GiftChatPageState extends State { + ScrollController _controller = ScrollController(); + bool showGoBottom = true; + bool _isDisposed = false; + List _msgList = []; + late RtmProvider provider; + + @override + void initState() { + super.initState(); + _controller.addListener(_scrollListener); + provider = Provider.of(context, listen: false); + var msgList = provider.roomGiftMsgList; + provider.msgGiftListener = _onNewMsg; + _msgList.addAll(msgList ??= []); + // 使用安全的方式初始化滚动位置 + _initScrollPosition(); + } + + void _scrollListener() { + final position = _controller.position; + + // 容差范围(5像素)避免精度问题 + const tolerance = 10.0; + + // 判断是否在第一个项目(颠倒列表的底部) + final isAtBottom = position.pixels < position.minScrollExtent + tolerance; + + // 判断是否在最后一个项目(颠倒列表的顶部) + // final isAtTop = position.pixels >= position.maxScrollExtent - tolerance; + if (isAtBottom != showGoBottom) { + showGoBottom = isAtBottom; + if (!_isDisposed) { + setState(() {}); + } + } + } + + // 安全初始化滚动位置 + void _initScrollPosition() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + if (_isDisposed) return; + _controller.jumpTo(0.0); + }); + } + + _onNewMsg(Msg msg) { + if (_isDisposed) return; // 如果组件已销毁,直接返回 + if (msg.groupId == "-1000") { + ///清屏 + _msgList.clear(); + setState(() {}); + return; + } + if (!_msgList.contains(msg)) { + setState(() { + _msgList.insert(0, msg); + }); + _controller.jumpTo(0.0); + } + } + + @override + void dispose() { + _isDisposed = true; + provider.msgGiftListener = null; + _controller.removeListener(_scrollListener); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + body: Stack( + alignment: Alignment.bottomLeft, + children: [ + ListView.builder( + reverse: true, + controller: _controller, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + return MsgItem( + msg: _msgList[index], + onClick: (SocialChatUserProfile? user) {}, + ); + }, + itemCount: _msgList.length, + ), + Visibility( + visible: !showGoBottom, + child: GestureDetector( + onTap: () { + if (_controller.hasClients) { + _controller.jumpTo(0.0); + } + }, + child: Container( + margin: EdgeInsets.symmetric(horizontal: width(15)), + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(52), + color: Colors.white, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + SCAppLocalizations.of(context)!.scrollToTheBottom, + style: TextStyle(fontSize: 10.sp), + ), + SizedBox(width: 4.w), + Icon(Icons.chevron_right, size: 10.w), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/room/detail/room_detail_page.dart b/lib/modules/room/detail/room_detail_page.dart new file mode 100644 index 0000000..e7b5487 --- /dev/null +++ b/lib/modules/room/detail/room_detail_page.dart @@ -0,0 +1,833 @@ +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/modules/room/manager/room_member_page.dart'; +import 'package:yumi/modules/room/voice_room_route.dart'; +import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +import '../../../shared/data_sources/sources/local/user_manager.dart'; +import '../../../shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import '../../../ui_kit/components/sc_compontent.dart'; +import '../../../ui_kit/components/sc_debounce_widget.dart'; +import '../../../ui_kit/components/sc_tts.dart'; +import '../../../ui_kit/components/dialog/dialog_base.dart'; +import '../../../ui_kit/components/text/sc_text.dart'; +import '../../../main.dart'; +import '../../index/main_route.dart'; + +///房间详情 +class RoomDetailPage extends StatefulWidget { + ///是否是房主 + final bool isHomeowner; + + const RoomDetailPage(this.isHomeowner, {super.key}); + + @override + _RoomDetailPageState createState() => _RoomDetailPageState(); +} + +class _RoomDetailPageState extends State { + @override + void initState() { + super.initState(); + Provider.of(context, listen: false).loadRoomInfo( + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + "", + ); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Consumer( + builder: (context, ref, child) { + return ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + height: widget.isHomeowner ? 300.w : 400.w, + decoration: BoxDecoration( + color: Color(0xff09372E).withOpacity(0.5), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + ), + child: Column( + children: [ + SizedBox(height: 15.w), + text( + widget.isHomeowner + ? SCAppLocalizations.of(context)!.roomSetting + : SCAppLocalizations.of(context)!.roomDetails, + fontSize: 14.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + Expanded( + child: SingleChildScrollView( + child: Column( + spacing: 10.w, + children: [ + SizedBox(height: 15.w), + GestureDetector( + child: Container( + decoration: BoxDecoration( + color: Color(0xffF2F2F2), + borderRadius: BorderRadius.circular(10.w), + ), + margin: EdgeInsets.symmetric(horizontal: 15.w), + padding: EdgeInsets.all(15.w), + child: Row( + children: [ + netImage( + url: + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomCover ?? + "", + width: 48.w, + height: 48.w, + borderRadius: BorderRadius.all( + Radius.circular(8.w), + ), + ), + SizedBox(width: 8.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: + ScreenUtil().screenWidth * 0.6, + ), + child: text( + textColor: Colors.black, + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomName ?? + "", + fontSize: 14.sp, + ), + ), + SizedBox(width: 3.w), + widget.isHomeowner + ? Image.asset( + "sc_images/room/sc_icon_room_edit.png", + width: 13.w, + height: 13.w, + ) + : Container(), + ], + ), + SizedBox(height: 5.w), + Row( + children: [ + text( + "ID:${ref.currenRoom?.roomProfile?.userProfile?.getID()}", + fontSize: 12.sp, + textColor: Colors.black, + ), + SizedBox(width: 3.w), + GestureDetector( + child: Container( + padding: EdgeInsets.all(3.w), + child: Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 11.w, + height: 11.w, + color: Colors.black, + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData( + text: + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomAccount ?? + "", + ), + ); + SCTts.show( + SCAppLocalizations.of( + context, + )!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + ], + ), + ), + onTap: () { + if (!widget.isHomeowner) { + return; + } + SCNavigatorUtils.push( + context, + "${VoiceRoomRoute.roomEdit}?need=false", + replace: false, + ); + }, + ), + if (!widget.isHomeowner) + SCDebounceWidget( + child: Container( + decoration: BoxDecoration( + color: Color(0xffF2F2F2), + borderRadius: BorderRadius.circular(10.w), + ), + margin: EdgeInsets.symmetric(horizontal: 15.w), + padding: EdgeInsets.all(15.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + text( + "${SCAppLocalizations.of(context)!.roomOwner}:", + fontSize: 15.sp, + textColor: Colors.black, + ), + SizedBox(height: 3.w), + Row( + children: [ + netImage( + url: + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.userAvatar ?? + "", + shape: BoxShape.circle, + width: 40.w, + height: 40.w, + ), + SizedBox(width: 10.w), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + socialchatNickNameText( + maxWidth: 135.w, + textColor: Colors.black, + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.userNickname ?? + "", + fontSize: 14.sp, + fontWeight: FontWeight.w600, + type: + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.getVIP() + ?.name ?? + "", + needScroll: + (ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 13, + ), + Row( + children: [ + text( + "ID:${ref.currenRoom?.roomProfile?.userProfile?.account}", + textColor: Colors.black, + fontSize: 14.sp, + ), + SizedBox(width: 3.w), + GestureDetector( + child: Container( + padding: EdgeInsets.all(3.w), + child: Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 11.w, + height: 11.w, + color: Colors.black, + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData( + text: + ref + .currenRoom + ?.roomProfile + ?.userProfile + ?.account ?? + "", + ), + ); + SCTts.show( + SCAppLocalizations.of( + context, + )!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + ], + ), + ], + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + replace: false, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == ref.currenRoom?.roomProfile?.userProfile?.id}&tageId=${ref.currenRoom?.roomProfile?.userProfile?.id}", + ); + }, + ), + SCDebounceWidget( + child: Container( + decoration: BoxDecoration( + color: Color(0xffF2F2F2), + borderRadius: BorderRadius.circular(10.w), + ), + margin: EdgeInsets.symmetric(horizontal: 15.w), + padding: EdgeInsets.all(15.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + text( + textColor: Colors.black, + fontSize: 14.sp, + SCAppLocalizations.of( + context, + )!.roomAnnouncement, + ), + Spacer(), + widget.isHomeowner + ? Icon( + Icons.keyboard_arrow_right_sharp, + color: Colors.black, + size: 15.w, + ) + : Container(), + ], + ), + Row( + children: [ + Consumer( + builder: (context, ref, child) { + return Expanded( + child: text( + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomDesc ?? + "", + fontSize: 13.sp, + maxLines: 2, + textColor: Color(0xffB1B1B1), + ), + ); + }, + ), + ], + ), + ], + ), + ), + onTap: () { + if (!widget.isHomeowner) { + return; + } + SCNavigatorUtils.push( + context, + "${VoiceRoomRoute.roomEdit}?need=false", + replace: false, + ); + }, + ), + widget.isHomeowner + ? Row( + children: [ + SizedBox(width: 15.w), + Expanded( + child: SCDebounceWidget( + child: Container( + height: 45.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 10.w, + ), + color: SocialChatTheme.primaryLight, + ), + child: Row( + children: [ + SizedBox(width: 20.w), + text( + SCAppLocalizations.of( + context, + )!.roomMember, + textColor: Colors.white, + fontSize: 14.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right_sharp, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 15.w), + ], + ), + ), + onTap: () { + Navigator.of(context).pop(); + showBottomInBottomDialog( + context, + RoomMemberPage( + roomId: + Provider.of( + context!, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id ?? + "", + isHomeowner: widget.isHomeowner, + ), + ); + }, + ), + ), + SizedBox(width: 10.w), + Expanded( + child: SCDebounceWidget( + child: Container( + height: 45.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 10.w, + ), + color: SocialChatTheme.primaryLight, + ), + child: Row( + children: [ + SizedBox(width: 20.w), + text( + SCAppLocalizations.of( + context, + )!.roomEdit, + textColor: Colors.white, + fontSize: 14.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right_sharp, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 15.w), + ], + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${VoiceRoomRoute.roomEdit}?need=false", + replace: false, + ); + }, + ), + ), + SizedBox(width: 15.w), + ], + ) + : Container(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 15.w, + children: [ + ref.isTourists() + ? SCDebounceWidget( + child: Container( + width: 160.w, + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular( + 35.w, + ), + ), + height: 42.w, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/room/sc_icon_join_room_member.png", + height: 20.w, + width: 20.w, + ), + SizedBox(width: 8.w), + text( + ref.isTourists() + ? SCAppLocalizations.of( + context, + )!.join + : SCAppLocalizations.of( + context, + )!.giveUpIdentity, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 3.w), + text( + "(${ref.currenRoom?.roomProfile?.roomSetting?.joinGolds ?? 0})", + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + ), + onTap: () { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: + SCAppLocalizations.of( + context, + )!.tips, + msg: + ref.isTourists() + ? SCAppLocalizations.of( + context, + )!.joinMemberTips2 + : SCAppLocalizations.of( + context, + )!.leaveRoomIdentityTips, + btnText: + SCAppLocalizations.of( + context, + )!.confirm, + onEnsure: () { + SCChatRoomRepository() + .changeRoomRole( + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id ?? + "", + ref.isTourists() + ? SCRoomRolesType + .ADMIN + .name + : SCRoomRolesType + .TOURIST + .name, + UserManager() + .getCurrentUser() + ?.userProfile + ?.id ?? + "", + UserManager() + .getCurrentUser() + ?.userProfile + ?.id ?? + "", + "ONESELF_JOIN", + ) + .then((reslt) { + ref.currenRoom = ref + .currenRoom + ?.copyWith( + entrants: ref + .currenRoom + ?.entrants + ?.copyWith( + roles: + SCRoomRolesType + .MEMBER + .name, + ), + ); + setState(() {}); + SCTts.show( + SCAppLocalizations.of( + navigatorKey + .currentState! + .context, + )!.operationSuccessful, + ); + }); + }, + ); + }, + ); + }, + ) + : SizedBox.shrink(), + ref.currenRoom?.roomProfile?.roomProfile?.userId == + UserManager() + .getCurrentUser() + ?.userProfile + ?.id + ? SizedBox.shrink() + : Selector( + selector: + (c, p) => + p.isFollowRoomRes?.followRoom ?? + false, + shouldRebuild: (prev, next) => prev != next, + builder: (_, follow, __) { + return SCDebounceWidget( + child: Container( + width: 160.w, + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular( + 35.w, + ), + ), + height: 42.w, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Row( + children: [ + Image.asset( + !follow + ? "sc_images/room/sc_icon_follow_room_un.png" + : "sc_images/room/sc_icon_follow_room_en.png", + width: 20.w, + height: 20.w, + ), + SizedBox(width: 8.w), + text( + !follow + ? SCAppLocalizations.of( + context, + )!.follow + : SCAppLocalizations.of( + context, + )!.following, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + ], + ), + ), + onTap: () { + if (follow) { + SmartDialog.show( + tag: "unFollowDialog", + alignment: Alignment.center, + animationType: + SmartAnimationType.fade, + builder: (_) { + return Container( + width: + ScreenUtil().screenWidth * + 0.75, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.all( + Radius.circular(12.w), + ), + ), + child: Column( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + SizedBox(height: 15.w), + text( + SCAppLocalizations.of( + context, + )!.unFollow, + fontSize: 16.sp, + textColor: Colors.black, + fontWeight: + FontWeight.bold, + ), + SizedBox(height: 15.w), + text( + SCAppLocalizations.of( + context, + )!.sureUnfollowThisRoom, + fontWeight: + FontWeight.w600, + textColor: Colors.grey, + fontSize: 14.sp, + ), + SizedBox(height: 15.w), + Row( + mainAxisSize: + MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + GestureDetector( + behavior: + HitTestBehavior + .opaque, + onTap: () { + SmartDialog.dismiss( + tag: + "unFollowDialog", + ); + }, + child: Container( + alignment: + AlignmentDirectional + .center, + decoration: + BoxDecoration( + borderRadius: + BorderRadius.circular( + 35.w, + ), + color: Color( + 0xffF2F2F2, + ), + ), + height: 35.w, + width: 95.w, + child: text( + SCAppLocalizations.of( + context, + )!.cancel, + textColor: + Colors.grey, + fontSize: 15.sp, + fontWeight: + FontWeight + .w600, + ), + ), + ), + SizedBox(width: 18.w), + GestureDetector( + onTap: () { + ref.followCurrentVoiceRoom(); + }, + behavior: + HitTestBehavior + .opaque, + child: Container( + alignment: + AlignmentDirectional + .center, + decoration: BoxDecoration( + color: + SocialChatTheme + .primaryLight, + borderRadius: + BorderRadius.circular( + 35.w, + ), + ), + height: 35.w, + width: 95.w, + child: text( + SCAppLocalizations.of( + context, + )!.unFollow, + textColor: + Colors + .white, + fontSize: 15.sp, + fontWeight: + FontWeight + .w600, + ), + ), + ), + ], + ), + SizedBox(height: 25.w), + ], + ), + ); + }, + ); + } else { + ref.followCurrentVoiceRoom(); + } + }, + ); + }, + ), + ], + ), + SizedBox(height: 10.w,) + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/modules/room/edit/room_edit_page.dart b/lib/modules/room/edit/room_edit_page.dart new file mode 100644 index 0000000..5de4505 --- /dev/null +++ b/lib/modules/room/edit/room_edit_page.dart @@ -0,0 +1,527 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/services/room/rc_room_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import '../../../shared/tools/sc_lk_dialog_util.dart'; +import '../../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; +import '../../../shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart'; +import '../../../ui_kit/widgets/room/switch_model/room_mic_switch_page.dart'; +import '../block/blocked_list_page.dart'; +import '../voice_room_route.dart'; + +///编辑房间信息 +class RoomEditPage extends StatefulWidget { + String needRestCurrentRoomInfo = "false"; + + RoomEditPage({super.key, this.needRestCurrentRoomInfo = "false"}); + + @override + _RoomEditPageState createState() => _RoomEditPageState(); +} + +class _RoomEditPageState extends State { + RtcProvider? rtcProvider; + bool isEdit = false; + final TextEditingController _roomNameController = TextEditingController(); + final TextEditingController _roomAnnouncementController = + TextEditingController(); + final int _roomNameMaxLength = 24; + int _roomNameCurrentLength = 0; + String roomCover = ""; + final int _roomAnnouncementMaxLength = 100; + int _roomAnnouncementCurrentLength = 0; + + @override + void initState() { + super.initState(); + rtcProvider = Provider.of(context, listen: false); + _roomNameController.addListener(() { + setState(() { + _roomNameCurrentLength = _getActualCharacterCount( + _roomNameController.text, + ); + }); + }); + _roomAnnouncementController.addListener(() { + setState(() { + _roomAnnouncementCurrentLength = _getActualCharacterCount( + _roomAnnouncementController.text, + ); + }); + }); + _roomNameController.text = + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomName ?? ""; + _roomNameCurrentLength = _getActualCharacterCount(_roomNameController.text); + _roomAnnouncementController.text = + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomDesc ?? ""; + _roomAnnouncementCurrentLength = _getActualCharacterCount( + _roomAnnouncementController.text, + ); + roomCover = + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""; + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () { + if (!isEdit) { + SCNavigatorUtils.goBack(context); + return Future.value(false); + } + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.theModificationsMade, + onEnsure: () { + SCNavigatorUtils.goBack(context); + }, + ); + }, + ); + return Future.value(false); + }, + child: Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.roomSetting, + onTag: () { + if (widget.needRestCurrentRoomInfo == "true") { + Provider.of(context, listen: false).currenRoom = + null; + } + Navigator.pop(context); + }, + actions: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w), + margin: EdgeInsetsDirectional.only(end: 8.w), + child: text( + SCAppLocalizations.of(context)!.save, + textColor: Colors.white, + fontSize: 15.sp, + ), + ), + onTap: () { + submit(context); + }, + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 35.w), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(width: 20.w,), + GestureDetector( + child: Stack( + alignment: Alignment.bottomRight, + children: [ + netImage( + url: roomCover, + borderRadius: BorderRadius.all( + Radius.circular(12), + ), + width: 90.w, + height: 90.w, + ), + Icon(Icons.camera_alt, size: 25.w,color: Colors.white,), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, ( + bool success, + String url, + ) { + if (success) { + setState(() { + roomCover = url; + }); + } + }); + }, + ), + ], + ), + SizedBox(height: 25.w), + Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.roomName, + textColor: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15.sp, + ), + ], + ), + SizedBox(height: 10.w), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 25.w), + padding: EdgeInsets.only( + left: width(12), + right: width(12), + ), + alignment: AlignmentDirectional.centerStart, + height: 45.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + border: Border.all(color: Color(0xff5FFFB7), width: 0.5), + gradient: LinearGradient(colors: [Color(0xff5FFFB7).withOpacity(0.1),Color(0xff5FFFB7).withOpacity(0.6)]), + borderRadius: BorderRadius.all( + Radius.circular(height(8)), + ), + ), + child: TextField( + controller: _roomNameController, + onChanged: (text) { + isEdit = true; + }, + inputFormatters: [ + SCAccurateLengthLimitingTextInputFormatter( + _roomNameMaxLength, + ), + ], + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.enterRoomName, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + contentPadding: EdgeInsets.only(top: 0.w), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + fillColor: Colors.white54, + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 3.w), + text( + '$_roomNameCurrentLength/$_roomNameMaxLength', + textColor: Colors.white54, + ), + ], + ), + ), + style: TextStyle( + fontSize: sp(15), + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + ), + ], + ), + SizedBox(height: 15.w), + Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.roomAnnouncement, + textColor: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15.sp, + ), + ], + ), + SizedBox(height: 10.w), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 25.w), + padding: EdgeInsets.only( + top: 5.w, + left: width(12), + right: width(12), + ), + alignment: AlignmentDirectional.centerStart, + height: 110.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + border: Border.all(color: Color(0xff5FFFB7), width: 0.5), + gradient: LinearGradient(colors: [Color(0xff5FFFB7).withOpacity(0.1),Color(0xff5FFFB7).withOpacity(0.6)]), + borderRadius: BorderRadius.all( + Radius.circular(height(8)), + ), + ), + child: TextField( + controller: _roomAnnouncementController, + onChanged: (text) { + isEdit = true; + }, + inputFormatters: [ + SCAccurateLengthLimitingTextInputFormatter( + _roomAnnouncementMaxLength, + ), + ], + maxLines: 5, + decoration: InputDecoration( + hintText: "", + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + contentPadding: EdgeInsets.only(top: 0.w), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + fillColor: Colors.white54, + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 3.w), + text( + '$_roomAnnouncementCurrentLength/$_roomAnnouncementMaxLength', + textColor: Colors.white54, + ), + ], + ), + ), + style: TextStyle( + fontSize: sp(15), + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + ), + ], + ), + widget.needRestCurrentRoomInfo != "true" + ? Column( + children: [ + SizedBox(height: 12.w), + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.roomTheme2, + textColor: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 15.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 25.w), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + VoiceRoomRoute.roomTheme, + replace: false, + ); + }, + ), + SizedBox(height: 12.w), + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.micManagement, + textColor: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 15.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 25.w), + ], + ), + onTap: () { + SmartDialog.show( + tag: "showRoomMicSwitch", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + debounce: true, + builder: (_) { + return RoomMicSwitchPage(); + }, + ); + }, + ), + SizedBox(height: 12.w), + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 25.w), + text( + SCAppLocalizations.of(context)!.blockedList2, + textColor: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 15.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + color: Colors.white, + size: 20.w, + ), + SizedBox(width: 25.w), + ], + ), + onTap: () { + showBottomInBottomDialog( + context, + BlockedListPage( + roomId: + Provider.of( + context!, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id ?? + "", + ), + ); + }, + ), + SizedBox(height: 15.w), + ], + ) + : Container(), + ], + ), + ), + ), + ], + ), + ); + } + + int _getActualCharacterCount(String text) { + return text.characters.length; + } + + void submit(BuildContext context) async { + if (_roomNameController.text.trim().isEmpty) { + SCTts.show("Room name not empty!"); + return; + } + if (_roomAnnouncementController.text.trim().isEmpty) { + SCTts.show("Room announcement not empty!"); + return; + } + SCLoadingManager.show(context: context); + var roomInfo = await SCAccountRepository().editRoomInfo( + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", + roomCover, + _roomNameController.text, + _roomAnnouncementController.text, + SCRoomInfoEventType.AVAILABLE.name, + ); + Provider.of( + context, + listen: false, + ).updateMyRoomInfo(roomInfo); + if (widget.needRestCurrentRoomInfo == "true") { + ///需要创建群组 + var c = await Provider.of( + context, + listen: false, + ).createRoomGroup( + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomName ?? "", + ); + if (c.code == 0) { + SCLoadingManager.hide(); + SCNavigatorUtils.goBack(context); + Provider.of( + context, + listen: false, + ).joinVoiceRoomSession(context, roomInfo.id ?? "", clearRoomData: true); + } else { + SCLoadingManager.hide(); + SCTts.show("${c.code}"); + } + } else { + Provider.of( + context, + listen: false, + ).loadRoomInfo(roomInfo.id ?? ""); + SCLoadingManager.hide(); + SCNavigatorUtils.goBack(context); + } + } + +} diff --git a/lib/modules/room/manager/room_member_page.dart b/lib/modules/room/manager/room_member_page.dart new file mode 100644 index 0000000..9b8f493 --- /dev/null +++ b/lib/modules/room/manager/room_member_page.dart @@ -0,0 +1,400 @@ +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/room_member_res.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; + +import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +import '../../../ui_kit/components/sc_compontent.dart'; +import '../../../ui_kit/components/sc_debounce_widget.dart'; +import '../../../ui_kit/components/sc_page_list.dart'; +import '../../../ui_kit/components/sc_tts.dart'; +import '../../../ui_kit/components/text/sc_text.dart'; + +///房间成员列表 +class RoomMemberPage extends SCPageList { + String? roomId = ""; + bool isHomeowner = false; + + RoomMemberPage({super.key, this.roomId, this.isHomeowner = false}); + + @override + _RoomMemberPageState createState() => + _RoomMemberPageState(roomId, isHomeowner); +} + +class _RoomMemberPageState + extends SCPageListState, RoomMemberPage> { + String? roomId = ""; + String? lastId; + bool isHomeowner = false; + List> mList = []; + String optTips = ""; + + _RoomMemberPageState(this.roomId, this.isHomeowner); + + @override + void initState() { + super.initState(); + enablePullUp = false; + enablePullDown = false; + backgroundColor = Colors.transparent; + loadData(1); + } + + @override + Widget build(BuildContext context) { + optTips = SCAppLocalizations.of(context)!.operationSuccessful; + return SafeArea( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: ScreenUtil().screenWidth, + color: Color(0xff09372E).withOpacity(0.5), + child: Column( + children: [ + SizedBox(height: 15.w), + text( + SCAppLocalizations.of(context)!.roomMember, + fontSize: 14.sp, + textColor: Colors.white, + ), + SizedBox(height: 10.w), + SizedBox(height: 350.w, child: buildList(context)), + ], + ), + ), + ), + ), + ); + } + + @override + Widget buildItem(List list) { + return Column( + children: [ + list.isNotEmpty ? buildTag(list.first.roles) : Container(), + SizedBox(height: 3.w), + Column( + children: List.generate(list.length, (cIndex) { + SocialChatRoomMemberRes userInfo = list[cIndex]; + return GestureDetector( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Color(0xffF2F2F2), + ), + child: Row( + children: [ + head( + url: userInfo.userProfile?.userAvatar ?? "", + width: 52.w, + headdress: + userInfo.userProfile?.getHeaddress()?.sourceUrl, + ), + SizedBox(width: 3.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 240.w, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + msgRoleTag( + userInfo.roles ?? "", + width: 20.w, + height: 20.w, + ), + SizedBox(width: 3.w), + socialchatNickNameText( + maxWidth: 160.w, + userInfo.userProfile?.userNickname ?? "", + fontSize: 14.sp, + textColor: Colors.black, + type: + userInfo.userProfile?.getVIP()?.name ?? + "", + needScroll: + (userInfo + .userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 14, + ), + SizedBox(width: 3.w), + userInfo.userProfile?.getVIP() != null + ? netImage( + url: + userInfo.userProfile + ?.getVIP() + ?.cover ?? + "", + width: 25.w, + height: 25.w, + ) + : Container(), + SizedBox(width: 5.w), + userInfo.userProfile?.wearBadge?.isNotEmpty ?? + false + ? SizedBox( + height: 25.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: + userInfo + .userProfile + ?.wearBadge + ?.length ?? + 0, + itemBuilder: (context, index) { + return netImage( + width: 25.w, + height: 25.w, + url: + userInfo + .userProfile + ?.wearBadge?[index] + .selectUrl ?? + "", + ); + }, + separatorBuilder: ( + BuildContext context, + int index, + ) { + return SizedBox(width: 5.w); + }, + ), + ) + : Container(), + ], + ), + ), + ), + SizedBox(height: 3.w), + text( + "ID:${userInfo.userProfile?.getID()}", + fontSize: 12.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + ], + ), + Spacer(), + userInfo.roles != SCRoomRolesType.HOMEOWNER.name && + isHomeowner + ? SCDebounceWidget( + child: Image.asset( + "sc_images/room/sc_icon_remve_block.png", + width: 20.w, + height: 20.w, + ), + onTap: () { + SCChatRoomRepository() + .changeRoomRole( + roomId ?? "", + SCRoomRolesType.TOURIST.name, + userInfo.userProfile?.id ?? "", + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id ?? + "", + "OTHER_CHANGE", + ) + .whenComplete(() { + SCTts.show(optTips); + Msg msg = Msg( + groupId: + Provider.of( + context, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomAccount, + msg: 'TOURIST', + toUser: userInfo.userProfile, + type: SCRoomMsgType.roomRoleChange, + ); + Provider.of( + context, + listen: false, + ).dispatchMessage(msg, addLocal: true); + Navigator.of(context).pop(); + }); + }, + ) + : Container(), + ], + ), + ), + onTap: () { + Navigator.of(context).pop(); + num index = Provider.of( + context, + listen: false, + ).userOnMaiInIndex(userInfo.id ?? ""); + Provider.of( + context, + listen: false, + ).clickSite(index, clickUser: userInfo.userProfile); + }, + ); + }), + ), + ], + ); + } + + @override + empty() { + List list = []; + list.add(SizedBox(height: height(30))); + list.add(CupertinoActivityIndicator()); + list.add(SizedBox(height: height(15))); + list.add( + Text( + SCAppLocalizations.of(context)!.noData, + style: TextStyle( + fontSize: sp(14), + color: Color(0xff999999), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ); + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: list, + ); + } + + @override + builderDivider() { + // return Divider( + // height: 1.w, + // color: Color(0xff3D3277).withOpacity(0.5), + // indent: 15.w, + // ); + return Container(height: 8.w); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List>) onSuccess, + Function? onErr, + }) async { + if (page == 1) { + lastId = null; + mList.clear(); + mList.add([]); + mList.add([]); + mList.add([]); + } + try { + var userList = await SCChatRoomRepository().roomMember( + roomId ?? "", + lastId: lastId, + ); + for (var user in userList) { + if (user.roles == SCRoomRolesType.HOMEOWNER.name) { + // mList["Homeowner"]?.add(user); + mList[0].add(user); + } else if (user.roles == SCRoomRolesType.ADMIN.name) { + // mList["Administrator"]?.add(user); + mList[1].add(user); + } else if (user.roles == SCRoomRolesType.MEMBER.name) { + // mList["Member"]?.add(user); + mList[2].add(user); + } + } + onSuccess(mList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + Widget buildTag(String? roles) { + if (roles == SCRoomRolesType.HOMEOWNER.name) { + return Row( + children: [ + SizedBox(width: 5.w), + text( + SCAppLocalizations.of(context)!.owner, + fontSize: 13.sp, + textColor: Colors.white, + ), + Spacer(), + ], + ); + } else if (roles == SCRoomRolesType.ADMIN.name) { + return Row( + children: [ + SizedBox(width: 5.w), + text( + SCAppLocalizations.of(context)!.admin, + fontSize: 13.sp, + textColor: Colors.white, + ), + Spacer(), + // Image.asset( + // "images/room/sc_icon_add_user.png", + // width: 25.w, + // height: 25.w, + // ), + // SizedBox(width: 3.w), + ], + ); + } else if (roles == SCRoomRolesType.MEMBER.name) { + return Row( + children: [ + SizedBox(width: 5.w), + text( + SCAppLocalizations.of(context)!.member, + fontSize: 13.sp, + textColor: Colors.white, + ), + Spacer(), + // Image.asset( + // "images/room/sc_icon_add_user.png", + // width: 25.w, + // height: 25.w, + // ), + // SizedBox(width: 3.w), + ], + ); + } + return Container(); + } +} diff --git a/lib/modules/room/online/room_online_page.dart b/lib/modules/room/online/room_online_page.dart new file mode 100644 index 0000000..2116294 --- /dev/null +++ b/lib/modules/room/online/room_online_page.dart @@ -0,0 +1,226 @@ +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +import '../../../ui_kit/components/sc_compontent.dart'; +import '../../../ui_kit/components/sc_page_list.dart'; +import '../../../ui_kit/components/sc_tts.dart'; +import '../../../ui_kit/components/text/sc_text.dart'; + +///房间用户在线列表 +class RoomOnlinePage extends SCPageList { + String? roomId = ""; + + RoomOnlinePage({super.key, this.roomId}); + + @override + _RoomOnlinePageState createState() => _RoomOnlinePageState(roomId); +} + +class _RoomOnlinePageState + extends SCPageListState { + String? roomId = ""; + + _RoomOnlinePageState(this.roomId); + + @override + void initState() { + super.initState(); + enablePullUp = false; + backgroundColor = Colors.transparent; + loadData(1); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + ), + child: Column( + children: [ + SizedBox(height: 15.w), + text( + "User(${items.length})", + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.black, + ), + SizedBox(height: 10.w), + SizedBox(height: 350.w, child: buildList(context)), + ], + ), + ), + ); + } + + @override + Widget buildItem(SocialChatUserProfile userInfo) { + return GestureDetector( + child: Container( + margin: EdgeInsets.symmetric(vertical: 3.w), + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.transparent, + ), + child: Row( + children: [ + GestureDetector( + child: head( + url: userInfo.userAvatar ?? "", + width: 55.w, + headdress: userInfo.getHeaddress()?.sourceUrl, + ), + onTap: () { + Navigator.of(context).pop(); + num index = Provider.of( + context, + listen: false, + ).userOnMaiInIndex(userInfo.id ?? ""); + Provider.of( + context, + listen: false, + ).clickSite(index, clickUser: userInfo); + }, + ), + SizedBox(width: 3.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + msgRoleTag(userInfo.roles ?? "", width: 20.w, height: 20.w), + SizedBox(width: 3.w), + socialchatNickNameText( + textColor: Colors.black, + maxWidth: 200.w, + userInfo.userNickname ?? "", + fontSize: 14.sp, + type: userInfo.getVIP()?.name ?? "", + needScroll: + (userInfo.userNickname?.characters.length ?? 0) > 16, + ), + getVIPBadge( + userInfo.getVIP()?.name, + width: 45.w, + height: 25.w, + ), + // ListView.separated( + // scrollDirection: Axis.horizontal, + // shrinkWrap: true, + // itemCount: userInfo.wearBadge?.length ?? 0, + // itemBuilder: (context, index) { + // return netImage( + // width: 25.w, + // height: 25.w, + // url: userInfo.wearBadge?[index].selectUrl ?? "", + // ); + // }, + // separatorBuilder: (BuildContext context, int index) { + // return SizedBox(width: 5.w); + // }, + // ), + ], + ), + SizedBox(height: 3.w), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 3.w), + child: Row( + textDirection: TextDirection.ltr, + children: [ + text( + "ID:${userInfo.getID()}", + fontSize: 12.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: userInfo.getID() ?? ""), + ); + SCTts.show(SCAppLocalizations.of(context)!.copiedToClipboard); + }, + ), + ], + ), + ], + ), + ), + onTap: () {}, + ); + } + + @override + empty() { + List list = []; + list.add(SizedBox(height: height(30))); + list.add(Image.asset('sc_images/general/sc_icon_loading.png')); + list.add(SizedBox(height: height(15))); + list.add( + Text( + "No data", + style: TextStyle( + fontSize: sp(14), + color: Color(0xff999999), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ); + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: list, + ); + } + + @override + builderDivider() { + // return Divider( + // height: 1.w, + // color: Color(0xff3D3277).withOpacity(0.5), + // indent: 15.w, + // ); + return Container(height: 8.w); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + // var roomList = await SCChatRoomRepository().roomOnlineUsers(roomId ?? ""); + await Provider.of(context!, listen: false).fetchOnlineUsersList(); + List userList = + Provider.of(context!, listen: false).onlineUsers; + onSuccess(userList); + } +} diff --git a/lib/modules/room/rank/room_gift_rank_page.dart b/lib/modules/room/rank/room_gift_rank_page.dart new file mode 100644 index 0000000..8e65676 --- /dev/null +++ b/lib/modules/room/rank/room_gift_rank_page.dart @@ -0,0 +1,101 @@ +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/modules/room/rank/room_gift_rank_tab_page.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_reward_info_res.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +import '../../../shared/data_sources/models/enum/sc_date_type.dart'; + +class RoomGiftRankPage extends StatefulWidget { + @override + _RoomGiftRankPageState createState() => _RoomGiftRankPageState(); +} + +class _RoomGiftRankPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = []; + final List _tabs = []; + SCRoomRewardInfoRes? res; + + @override + void initState() { + super.initState(); + String roomId = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + ""; + _pages.add(RoomGiftRankTabPage(roomId, SCDateType.DAY.name)); + _pages.add(RoomGiftRankTabPage(roomId, SCDateType.WEEK.name)); + _pages.add(RoomGiftRankTabPage(roomId, SCDateType.MONTH.name)); + _tabController = TabController(length: _pages.length, vsync: this); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.daily)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.weekly)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.monthly)); + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + decoration: BoxDecoration(color: Colors.black54), + height: 350.w, + child: Column( + children: [ + TabBar( + tabAlignment: TabAlignment.start, + labelPadding: EdgeInsets.symmetric(horizontal: 12.w), + labelColor: SocialChatTheme.primaryLight, + isScrollable: true, + indicator: BoxDecoration(), + unselectedLabelColor: Colors.white54, + labelStyle: TextStyle( + fontSize: 17.sp, + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w500, + ), + indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + + Expanded( + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + +} diff --git a/lib/modules/room/rank/room_gift_rank_tab_page.dart b/lib/modules/room/rank/room_gift_rank_tab_page.dart new file mode 100644 index 0000000..54c48dc --- /dev/null +++ b/lib/modules/room/rank/room_gift_rank_tab_page.dart @@ -0,0 +1,215 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/main.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart'; +import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; + +class RoomGiftRankTabPage extends SCPageList { + String roomId; + String dataType; + + RoomGiftRankTabPage(this.roomId, this.dataType); + + @override + _RoomGiftRankTabPageState createState() => + _RoomGiftRankTabPageState(roomId, dataType); +} + +class _RoomGiftRankTabPageState + extends SCPageListState { + String roomId; + String dataType; + + _RoomGiftRankTabPageState(this.roomId, this.dataType); + + @override + void initState() { + super.initState(); + enablePullUp = false; + backgroundColor = Colors.transparent; + loadData(1); + } + + @override + Widget build(BuildContext context) { + return buildList(context); + } + + @override + Widget buildItemOne(RoomGiftRankRes userInfo, int index) { + return GestureDetector( + child: Container( + margin: EdgeInsets.symmetric(vertical: 3.w), + padding: EdgeInsets.symmetric(horizontal: 16.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.transparent, + ), + child: Row( + children: [ + index == 0 + ? Image.asset( + "sc_images/room/sc_icon_room_contribute_rank1.png", + width: 22.w, + height: 22.w, + ) + : (index == 1 + ? Image.asset( + "sc_images/room/sc_icon_room_contribute_rank2.png", + width: 22.w, + height: 22.w, + ) + : (index == 2 + ? Image.asset( + "sc_images/room/sc_icon_room_contribute_rank3.png", + width: 22.w, + height: 22.w, + ) + : Container( + width: 22.w, + alignment: AlignmentDirectional.center, + child: text( + "${index + 1}", + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 14.sp, + ), + ))), + SizedBox(width: 10.w), + GestureDetector( + child: head( + url: userInfo.userProfile?.userAvatar ?? "", + width: 55.w, + // headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl, + ), + onTap: () { + showBottomInCenterDialog( + navigatorKey.currentState!.context, + RoomUserInfoCard(userId: userInfo.userProfile?.id), + ); + SmartDialog.dismiss(tag: "showRoomGiftRankPage"); + }, + ), + SizedBox(width: 3.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + socialchatNickNameText( + maxWidth: 170.w, + userInfo.userProfile?.userNickname ?? "", + fontSize: 14.sp, + type: userInfo.userProfile?.getVIP()?.name ?? "", + needScroll: + (userInfo.userProfile?.userNickname?.characters.length ?? 0) > + 14, + ), + ], + ), + SizedBox(height: 3.w), + GestureDetector( + child: Container( + child: Row( + textDirection: TextDirection.ltr, + children: [ + text( + "ID:${userInfo.userProfile?.getID()}", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: userInfo.userProfile?.getID() ?? ""), + ); + SCTts.show(SCAppLocalizations.of(context)!.copiedToClipboard); + }, + ), + ], + ), + Spacer(), + Image.asset( + "sc_images/general/sc_icon_jb.png", + width: 20.w, + height: 20.w, + ), + SizedBox(width: 3.w), + text( + "${userInfo.totalFormat ?? 0}", + fontSize: 12.sp, + textColor: Color(0xFFFFB627), + ), + SizedBox(width: 5.w), + ], + ), + ), + onTap: () {}, + ); + } + + @override + empty() { + return mainEmpty( + image: Image.asset( + 'sc_images/general/sc_icon_loading.png', + width: 121.w, + fit: BoxFit.fitWidth, + ), + msg: SCAppLocalizations.of(context)!.noData, + textColor: Colors.white54, + ); + } + + @override + builderDivider() { + // return Divider( + // height: 1.w, + // color: Color(0xff3D3277).withOpacity(0.5), + // indent: 15.w, + // ); + return Container(height: 4.w); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + var list = await SCChatRoomRepository().roomContributionRank( + roomId, + dataType, + size: 20, + ); + onSuccess(list); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } +} diff --git a/lib/modules/room/rank/room_reward_rule_page.dart b/lib/modules/room/rank/room_reward_rule_page.dart new file mode 100644 index 0000000..48aebad --- /dev/null +++ b/lib/modules/room/rank/room_reward_rule_page.dart @@ -0,0 +1,80 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +import 'package:yumi/ui_kit/widgets/room_reward_countdown_timer.dart'; + +class RoomRewardRulePage extends StatefulWidget { + @override + _RoomRewardRulePageState createState() => _RoomRewardRulePageState(); +} + +class _RoomRewardRulePageState extends State { + @override + Widget build(BuildContext context) { + return SafeArea( + child: Container( + height: ScreenUtil().screenHeight * 0.85, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_room_reward_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 38.w), + Stack( + alignment: Alignment.center, + children: [ + Image.asset( + SCGlobalConfig.lang == "ar" + ? "sc_images/room/sc_icon_room_reward_rule_title_ar.png" + : "sc_images/room/sc_icon_room_reward_rule_title_en.png", + width: 190.w, + ), + Row( + children: [ + SizedBox(width: 20.w), + Transform.flip( + flipX: SCGlobalConfig.lang == "ar" ? true : false, // 水平翻转 + flipY: false, // 垂直翻转设为 false + child: GestureDetector( + child: Image.asset( + "sc_images/room/sc_icon_roomgift_rule_back_bg.png", + width: 20.w, + ), + onTap: () { + SmartDialog.dismiss(tag: "showRoomRewardRulePage"); + }, + ), + ), + ], + ), + ], + ), + Expanded( + child: SingleChildScrollView( + child: Container( + margin: EdgeInsets.symmetric( + horizontal: 15.w, + ).copyWith(top: 15.w), + child: Image.asset( + SCGlobalConfig.lang == "ar" + ? "sc_images/room/sc_icon_room_reward_rule_content_ar.png" + : "sc_images/room/sc_icon_room_reward_rule_content_en.png", + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/room/seat/sc_seat_item.dart b/lib/modules/room/seat/sc_seat_item.dart new file mode 100644 index 0000000..ac61c19 --- /dev/null +++ b/lib/modules/room/seat/sc_seat_item.dart @@ -0,0 +1,538 @@ +import 'dart:ui'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; + +import '../../../shared/data_sources/models/enum/sc_room_special_mike_type.dart'; + +///麦位 +class SCSeatItem extends StatefulWidget { + final num index; + final bool isGameModel; + + const SCSeatItem({Key? key, required this.index, this.isGameModel = false}) + : super(key: key); + + @override + _SCSeatItemState createState() => _SCSeatItemState(); +} + +class _SCSeatItemState extends State with TickerProviderStateMixin { + RtcProvider? provider; + JoinRoomRes? room; + MicRes? roomSeat; + final GlobalKey _targetKey = GlobalKey(); + + @override + void initState() { + super.initState(); + provider = Provider.of(context, listen: false); + provider?.bindTargetKey(widget.index, _targetKey); + } + + @override + Widget build(BuildContext context) { + roomSeat = provider?.roomWheatMap[widget.index]; + room = provider?.currenRoom; + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: Column( + children: [ + SizedBox( + key: _targetKey, + width: widget.isGameModel ? 40.w : 55.w, + height: widget.isGameModel ? 40.w : 55.w, + child: Stack( + alignment: Alignment.center, + children: [ + Sonic( + index: widget.index, + room: room, + isGameModel: widget.isGameModel, + ), + roomSeat?.user != null + ? head( + url: roomSeat?.user?.userAvatar ?? "", + width: widget.isGameModel ? 40.w : 55.w, + headdress: + SCPathUtils.getFileExtension( + roomSeat?.user + ?.getHeaddress() + ?.sourceUrl ?? + "", + ).toLowerCase() == + ".mp4" && + window.locale.languageCode == "ar" + ? "" + : roomSeat?.user?.getHeaddress()?.sourceUrl, + ) + : (roomSeat!.micLock! + ? Image.asset( + "sc_images/room/sc_icon_seat_lock.png", + width: widget.isGameModel ? 38.w : 52.w, + height: widget.isGameModel ? 38.w : 52.w, + ) + : Image.asset( + "sc_images/room/sc_icon_seat_open.png", + width: widget.isGameModel ? 38.w : 52.w, + height: widget.isGameModel ? 38.w : 52.w, + )), + Positioned( + bottom: widget.isGameModel ? 2.w : 5.w, + right: widget.isGameModel ? 2.w : 5.w, + child: + roomSeat!.micMute! + ? Image.asset( + "sc_images/room/sc_icon_room_seat_mic_mute.png", + width: 14.w, + height: 14.w, + ) + : Container(), + ), + IgnorePointer( + child: Emoticons( + index: widget.index, + isGameModel: widget.isGameModel, + ), + ), + ], + ), + ), + widget.isGameModel + ? Container() + : (roomSeat?.user != null + ? Container( + width: 64.w, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 角色标签 + msgRoleTag( + roomSeat?.user?.roles ?? "", + width: 15.w, + height: 15.w, + ), + // 用户名文本 + Flexible( + child: socialchatNickNameText( + fontWeight: FontWeight.w600, + roomSeat?.user?.userNickname ?? "", + fontSize: 10.sp, + type: roomSeat?.user?.getVIP()?.name ?? "", + needScroll: + (roomSeat + ?.user + ?.userNickname + ?.characters + .length ?? + 0) > + 8, + ), + ), + ], + ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 16.w), + text( + "NO.${widget.index}", + fontSize: 10.sp, + fontWeight: FontWeight.w600, + ), + ], + )), + widget.isGameModel + ? Container() + : (room?.roomProfile?.roomSetting?.showHeartbeat == true + ? Container( + padding: EdgeInsets.symmetric( + vertical: 1.w, + horizontal: 5.w, + ), + margin: EdgeInsets.symmetric(horizontal: 5.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/room/sc_icon_gift_heartbeat.png", + width: 13.w, + ), + SizedBox(width: 3.w), + text( + _heartbeatVaFormat(), + fontWeight: FontWeight.w600, + fontSize: 10.sp, + ), + ], + ), + ) + : Container()), + ], + ), + onTap: () { + Provider.of( + context, + listen: false, + ).clickSite(widget.index); + }, + ); + } + + String _heartbeatVaFormat() { + int value = (roomSeat?.user?.heartbeatVal ?? 0).toInt(); + if (value > 99999) { + return "${(value / 1000).toStringAsFixed(0)}k"; + } + return "$value"; + } +} + +class Emoticons extends StatefulWidget { + final num index; + final bool isGameModel; + + const Emoticons({Key? key, required this.index, this.isGameModel = false}) + : super(key: key); + + @override + _EmoticonsState createState() => _EmoticonsState(); +} + +class _EmoticonsState extends State with TickerProviderStateMixin { + late AnimationController emoticonsAniCtr; + late CurvedAnimation curvedAnimation; + + bool showIn = false; + MicRes? playingRes; + + List pathList = []; + String? giftPath; + List giftList = []; + bool showResult = false; + + @override + void initState() { + // TODO: implement initState + emoticonsAniCtr = AnimationController( + vsync: this, + duration: Duration(milliseconds: 350), + ); + curvedAnimation = CurvedAnimation( + curve: Curves.ease, + parent: emoticonsAniCtr, + ); + emoticonsAniCtr.addStatusListener((status) { + if (status == AnimationStatus.dismissed) { + _checkStart(); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (BuildContext context, RtcProvider provider, Widget? child) { + MicRes? micRes = provider.roomWheatMap[widget.index]; + if (micRes?.user == null) { + return Container(); + } + String? emojiPath = provider.roomWheatMap[widget.index]?.emojiPath; + if (emojiPath != null) { + pathList.add(micRes); + micRes?.setEmojiPath = null; + _checkStart(); + } + if (playingRes != null) { + return FadeTransition( + opacity: curvedAnimation, + child: + playingRes?.type == SCRoomMsgType.roomDice + ? FutureBuilder( + future: Future.delayed(Duration(milliseconds: 2000)), + // 传入Future + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + // 根据snapshot的状态来构建UI + if (snapshot.connectionState == ConnectionState.done) { + return Image.asset( + "sc_images/room/sc_icon_dice_${micRes?.number}.png", + height: widget.isGameModel ? 38.w : 45.w, + ); + } else { + return Image.asset( + "sc_images/room/sc_icon_dice_animl.webp", + height: widget.isGameModel ? 38.w : 45.w, + ); + } + }, + ) + : (playingRes?.type == SCRoomMsgType.roomRPS + ? FutureBuilder( + future: Future.delayed(Duration(milliseconds: 2000)), + // 传入Future + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + // 根据snapshot的状态来构建UI + if (snapshot.connectionState == + ConnectionState.done) { + return Image.asset( + "sc_images/room/sc_icon_rps_${micRes?.number}.png", + height: widget.isGameModel ? 38.w : 45.w, + ); + } else { + return Image.asset( + "sc_images/room/sc_icon_rps_animal.webp", + height: widget.isGameModel ? 38.w : 45.w, + ); + } + }, + ) + : netImage( + url: playingRes?.number ?? "", + width: widget.isGameModel ? 65.w : 75.w, + )), + ); + } else if (giftPath != null) { + return FadeTransition( + opacity: curvedAnimation, + child: netImage( + url: "$giftPath", + width: widget.isGameModel ? 65.w : 75.w, + borderRadius: BorderRadius.circular(4.w), + ), + ); + } + return Container(); + }, + ); + } + + void _checkStart() { + print('emoticonsAniCtr.status:${emoticonsAniCtr.status}'); + print('emoticonsAniCtr.path:${playingRes}'); + if (!showIn) { + if (pathList.isNotEmpty) { + playingRes = pathList.first; + pathList.removeAt(0); + print('播放表情:$playingRes'); + emoticonsAniCtr.forward(); + Future.delayed(Duration(milliseconds: 5)).then((value) { + setState(() { + showIn = true; + }); + }); + + /// 延迟3秒移除表情包 + Future.delayed(Duration(seconds: 3)).then((value) { + emoticonsAniCtr.reverse(); + showIn = false; + playingRes = null; + _checkStart(); + }); + } else if (giftList.isNotEmpty) { + giftPath = giftList.first; + giftList.removeAt(0); + print('播放礼物:$playingRes'); + emoticonsAniCtr.forward(); + Future.delayed(Duration(milliseconds: 10)).then((value) { + setState(() { + showIn = true; + }); + }); + + /// 延迟3秒移除表情包 + Future.delayed(Duration(seconds: 1)).then((value) { + emoticonsAniCtr.reverse(); + showIn = false; + giftPath = null; + _checkStart(); + }); + } + } + } +} + +class Sonic extends StatefulWidget { + final num index; + final bool isGameModel; + final JoinRoomRes? room; + + const Sonic({ + Key? key, + required this.index, + required this.room, + this.isGameModel = false, + }) : super(key: key); + + @override + _SonicState createState() => _SonicState(); +} + +class _SonicState extends State with TickerProviderStateMixin { + List ctrList = []; + int lastIndex = 0; + late RtcProvider provider; + + @override + void initState() { + // TODO: implement initState + super.initState(); + for (int i = 0; i < 4; i++) { + ctrList.add( + AnimationController(duration: Duration(milliseconds: 900), vsync: this), + ); + } + for (var element in ctrList) { + element.addStatusListener((status) { + if (status == AnimationStatus.completed) { + element.reset(); + } + }); + } + provider = Provider.of(context, listen: false); + + provider.addSoundVoiceChangeListener(_onVoiceChange); + } + + @override + void dispose() { + // TODO: implement dispose + provider.removeSoundVoiceChangeListener(_onVoiceChange); + for (var element in ctrList) { + element.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + SonicItem(ctrList[0], widget.room, isGameModel: widget.isGameModel), + SonicItem(ctrList[1], widget.room, isGameModel: widget.isGameModel), + SonicItem(ctrList[2], widget.room, isGameModel: widget.isGameModel), + SonicItem(ctrList[3], widget.room, isGameModel: widget.isGameModel), + ], + ); + } + + void _checkSoundAni(int volume) async { + if (volume <= 20) return; + // await Future.delayed(Duration(milliseconds: 10)); + // var dateTime = DateTime.now(); + // for(int i =0 ; i < 5000000;i++){ + // lastIndex = 1; + // } + // print('执行耗时:${DateTime.now().millisecondsSinceEpoch - dateTime.millisecondsSinceEpoch}'); + ctrList.forEach((element) { + if (element.value == 0) { + if (lastIndex == 0) { + element.forward(); + lastIndex = ctrList.indexOf(element); + return; + } else { + if (ctrList[lastIndex].value >= 0.35 || + ctrList[lastIndex].status == AnimationStatus.dismissed) { + element.forward(); + lastIndex = ctrList.indexOf(element); + return; + } + } + } + }); + } + + _onVoiceChange(num index, int volum) { + print('说话声音:$volum'); + if (widget.index == index) { + _checkSoundAni(volum); + } + } +} + +class SonicItem extends AnimatedWidget { + Animation animation; + JoinRoomRes? room; + bool isGameModel = false; + + SonicItem(this.animation, this.room, {Key? key, this.isGameModel = false}) + : super(listenable: animation); + + @override + Widget build(BuildContext context) { + return Visibility( + visible: animation!.value > 0, + child: ScaleTransition( + scale: Tween(begin: 1.0, end: 1.4).animate(animation!), + child: Container( + height: width(isGameModel ? 34 : 52), + width: width(isGameModel ? 34 : 52), + decoration: + (room?.roomProfile?.roomSetting?.roomSpecialMikeType ?? "") + .isEmpty || + (room?.roomProfile?.roomSetting?.roomSpecialMikeType != + SCRoomSpecialMikeType.TYPE_VIP3.name && + room?.roomProfile?.roomSetting?.roomSpecialMikeType != + SCRoomSpecialMikeType.TYPE_VIP4.name && + room?.roomProfile?.roomSetting?.roomSpecialMikeType != + SCRoomSpecialMikeType.TYPE_VIP5.name && + room?.roomProfile?.roomSetting?.roomSpecialMikeType != + SCRoomSpecialMikeType.TYPE_VIP6.name) + ? BoxDecoration( + shape: BoxShape.circle, + color: SocialChatTheme.primaryColor.withOpacity( + 1 - animation!.value, + ), + ) + : BoxDecoration(), + child: + room?.roomProfile?.roomSetting?.roomSpecialMikeType == + SCRoomSpecialMikeType.TYPE_VIP3.name + ? Image.asset( + "sc_images/room/sc_icon_room_vip3_sonic_anim.webp", + ) + : (room?.roomProfile?.roomSetting?.roomSpecialMikeType == + SCRoomSpecialMikeType.TYPE_VIP4.name + ? Image.asset( + "sc_images/room/sc_icon_room_vip4_sonic_anim.webp", + ) + : (room?.roomProfile?.roomSetting?.roomSpecialMikeType == + SCRoomSpecialMikeType.TYPE_VIP5.name + ? Image.asset( + "sc_images/room/sc_icon_room_vip5_sonic_anim.webp", + ) + : (room + ?.roomProfile + ?.roomSetting + ?.roomSpecialMikeType == + SCRoomSpecialMikeType.TYPE_VIP6.name + ? Image.asset( + "sc_images/room/sc_icon_room_vip6_sonic_anim.webp", + ) + : Container()))), + ), + ), + ); + } +} diff --git a/lib/modules/room/them/custom/room_theme_custom_page.dart b/lib/modules/room/them/custom/room_theme_custom_page.dart new file mode 100644 index 0000000..965b7c8 --- /dev/null +++ b/lib/modules/room/them/custom/room_theme_custom_page.dart @@ -0,0 +1,115 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; + +class RoomThemeCustomPage extends StatefulWidget { + @override + _RoomThemeCustomPageState createState() => _RoomThemeCustomPageState(); +} + +class _RoomThemeCustomPageState extends State { + String picPath = ""; + final Debouncer debouncer = Debouncer(); + + bool uploadSucc = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + SizedBox(height: 35.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + child: + picPath.isNotEmpty + ? netImage( + url: picPath, + height: 300.w, + width: 200.w, + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ) + : Image.asset( + "sc_images/general/sc_icon_add_pic.png", + height: 120.w, + ), + onTap: () { + SCPickUtils.pickImage( + context, + aspectRatio: 0.56, + backOriginalFile: false, + (bool success, String url) { + if (url.isNotEmpty) { + setState(() { + picPath = url; + uploadSucc = true; + }); + } + }, + ); + }, + ), + ], + ), + SizedBox(height: 10.w), + text( + SCAppLocalizations.of(context)!.themeGoToUploadTips, + maxLines: 6, + fontSize: 14.sp, + textColor: Colors.white, + ), + SizedBox(height: 30.w), + GestureDetector( + child: Container( + height: 46.w, + width: 250.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.all(Radius.circular(30.w)), + ), + child: text( + uploadSucc + ? "10000${SCAppLocalizations.of(context)!.coins}/30${SCAppLocalizations.of(context)!.day}" + : SCAppLocalizations.of(context)!.goToUpload, + textColor: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + if (picPath.isNotEmpty) { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + SCStoreRepositoryImp().roomThemeCustomize(picPath).then(( + value, + ) { + picPath = ""; + setState(() {}); + SCTts.show( + SCAppLocalizations.of(context)!.operationSuccessful, + ); + }); + }, + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/modules/room/them/room_theme_page.dart b/lib/modules/room/them/room_theme_page.dart new file mode 100644 index 0000000..c985d59 --- /dev/null +++ b/lib/modules/room/them/room_theme_page.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/modules/room/them/custom/room_theme_custom_page.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/modules/user/my_items/theme/bags_theme_page.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +import '../../../app/constants/sc_global_config.dart'; + +///房间主题 +class RoomThemePage extends StatefulWidget { + @override + _RoomThemePageState createState() => _RoomThemePageState(); +} + +class _RoomThemePageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = []; + final List _tabs = []; + + @override + void initState() { + super.initState(); + _pages.add(BagsThemePage()); + _pages.add(RoomThemeCustomPage()); + _tabController = TabController(length: _pages.length, vsync: this); + _tabController.addListener(() {}); // 监听切换 + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.all)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.custom)); + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getLanguagePageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.theme, + actions: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.all(5.w), + margin: EdgeInsetsDirectional.only(end: 10.w), + child: Image.asset( + "sc_images/store/sc_icon_bag_shop.png", + width: 20.w, + height: 20.w, + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: true, + ); + }, + ), + ], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + TabBar( + tabAlignment: TabAlignment.center, + labelPadding: EdgeInsets.symmetric(horizontal: 18.w), + labelColor: SocialChatTheme.primaryLight, + isScrollable: true, + indicator: BoxDecoration(), + unselectedLabelColor: Colors.white38, + labelStyle: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + ), + indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart new file mode 100644 index 0000000..9563260 --- /dev/null +++ b/lib/modules/room/voice_room_page.dart @@ -0,0 +1,269 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/services/gift/gift_animation_manager.dart'; +import 'package:yumi/services/gift/gift_system_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; +import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart'; +import 'package:yumi/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/room_head_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/ui_kit/widgets/room/room_online_user_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/room_play_widget.dart'; + +import '../../ui_kit/components/sc_float_ichart.dart'; +import '../../ui_kit/widgets/room/seat/room_seat_widget.dart'; +import 'chat/all/all_chat_page.dart'; +import 'chat/chat/chat_page.dart'; +import 'chat/gift/gift_chat_page.dart'; + +///语聊房 +class VoiceRoomPage extends StatefulWidget { + const VoiceRoomPage({super.key}); + + @override + _VoiceRoomPageState createState() => _VoiceRoomPageState(); +} + +class _VoiceRoomPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = [AllChatPage(), ChatPage(), GiftChatPage()]; + final List _tabs = []; + late StreamSubscription _subscription; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: _pages.length, vsync: this); + Provider.of(context, listen: false).msgFloatingGiftListener = + _floatingGiftListener; + _tabController.addListener(() {}); // 监听切换 + + _subscription = eventBus.on().listen((event) { + if (mounted) { + Provider.of(context, listen: false).clearAllGiftData(); + Provider.of( + context, + listen: false, + ).toggleGiftAnimationVisibility(false); + } + }); + } + + @override + void dispose() { + _tabController.dispose(); // 释放资源 + _subscription.cancel(); + super.dispose(); + } + + bool roomThemeBackActi(JoinRoomRes? room) { + if (room?.roomProps?.roomTheme != null) { + if (room?.roomProps?.roomTheme?.themeBack != null && + room!.roomProps!.roomTheme!.themeBack!.isNotEmpty) { + if ((room.roomProps?.roomTheme?.expireTime ?? 0) > + DateTime.now().millisecondsSinceEpoch) { + return true; + } + } + } + return false; + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.all)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chat)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift)); + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + if (!didPop) { + SCFloatIchart().show(); + SCNavigatorUtils.goBack(context); + } + }, + child: Scaffold( + backgroundColor: + SCGlobalConfig.businessLogicStrategy.getVoiceRoomBackgroundColor(), + resizeToAvoidBottomInset: false, + body: SafeArea( + top: false, + child: Stack( + children: [ + Consumer( + builder: (context, ref, child) { + return roomThemeBackActi(ref.currenRoom) + ? netImage( + url: + ref.currenRoom?.roomProps?.roomTheme?.themeBack ?? + "", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + noDefaultImg: true, + fit: BoxFit.cover, + ) + : Image.asset( + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomDefaultBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ); + }, + ), + Column( + children: [ + SizedBox(height: ScreenUtil().setWidth(42)), + RoomHeadWidget(), + SizedBox(height: ScreenUtil().setWidth(5)), + RoomOnlineUserWidget(), + RoomSeatWidget(), + SizedBox(height: 2.w,), + Expanded( + child: Stack( + children: [ + Column( + children: [_buildChatView(), RoomBottomWidget()], + ), + LGiftAnimalPage(), + Transform.translate( + offset: Offset(0, -20), + child: RoomAnimationQueueScreen(), + ), + ], + ), + ), + ], + ), + // _buildPlayViews(), + VapPlusSvgaPlayer(tag: "room_gift"), + + ///幸运礼物中奖动画 + LuckGiftNomorAnimWidget(), + ], + ), + ), + ), + ); + } + + ///消息 + Widget _buildChatView() { + return Expanded( + child: Stack( + alignment: AlignmentDirectional.bottomEnd, + children: [ + Column( + children: [ + TabBar( + tabAlignment: TabAlignment.start, + labelPadding: SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabLabelPadding() + .copyWith( + left: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabLabelPadding() + .left * + ScreenUtil().setWidth(1), + right: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabLabelPadding() + .right * + ScreenUtil().setWidth(1), + ), + labelColor: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabLabelColor(), + isScrollable: true, + indicator: BoxDecoration(), + unselectedLabelColor: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabUnselectedLabelColor(), + labelStyle: SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabLabelStyle() + .copyWith( + fontSize: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabLabelStyle() + .fontSize! * + ScreenUtil().setSp(1), + ), + unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabUnselectedLabelStyle() + .copyWith( + fontSize: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabUnselectedLabelStyle() + .fontSize! * + ScreenUtil().setSp(1), + ), + indicatorColor: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabIndicatorColor(), + dividerColor: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomTabDividerColor(), + controller: _tabController, + tabs: _tabs, + ), + Expanded( + child: Container( + margin: SCGlobalConfig.businessLogicStrategy + .getVoiceRoomChatContainerMargin() + .copyWith( + end: + SCGlobalConfig.businessLogicStrategy + .getVoiceRoomChatContainerMargin() + .end * + ScreenUtil().setWidth(1), + ), + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + ), + ), + ], + ), + RoomPlayWidget(), + ], + ), + ); + } + + ///礼物上飘动画 + _floatingGiftListener(Msg msg) { + if (Provider.of(context, listen: false).hideLGiftAnimal) { + return; + } + var giftModel = LGiftModel(); + giftModel.labelId = "${msg.gift?.id}${msg.user?.id}${msg.toUser?.id}"; + giftModel.sendUserName = msg.user?.userNickname ?? ""; + giftModel.sendToUserName = msg.toUser?.userNickname ?? ""; + giftModel.sendUserPic = msg.user?.userAvatar ?? ""; + giftModel.giftPic = msg.gift?.giftPhoto ?? ""; + giftModel.giftCount = msg.number ?? 0; + Provider.of( + context, + listen: false, + ).enqueueGiftAnimation(giftModel); + } +} diff --git a/lib/modules/room/voice_room_route.dart b/lib/modules/room/voice_room_route.dart new file mode 100644 index 0000000..12f2f5e --- /dev/null +++ b/lib/modules/room/voice_room_route.dart @@ -0,0 +1,20 @@ +import 'package:fluro/fluro.dart'; +import 'package:yumi/modules/room/edit/room_edit_page.dart'; +import 'package:yumi/modules/room/them/room_theme_page.dart'; +import 'package:yumi/modules/room/voice_room_page.dart'; +import 'package:yumi/app/routes/sc_router_init.dart'; +class VoiceRoomRoute implements SCIRouterProvider { + static String voiceRoom = '/room'; + static String roomEdit = '/room/roomEdit'; + static String roomTheme = '/room/roomTheme'; + + @override + void initRouter(FluroRouter router) { + router.define(voiceRoom, + handler: Handler(handlerFunc: (_, params) => VoiceRoomPage())); + router.define(roomTheme, + handler: Handler(handlerFunc: (_, params) => RoomThemePage())); + router.define(roomEdit, + handler: Handler(handlerFunc: (_, params) => RoomEditPage(needRestCurrentRoomInfo: params['need']!.first))); + } +} diff --git a/lib/modules/search/sc_search_page.dart b/lib/modules/search/sc_search_page.dart new file mode 100644 index 0000000..419fc4b --- /dev/null +++ b/lib/modules/search/sc_search_page.dart @@ -0,0 +1,749 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/socialchat_gradient_button.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; + +import '../../services/general/sc_app_general_manager.dart'; +import '../../ui_kit/components/sc_compontent.dart'; +import '../../ui_kit/components/sc_debounce_widget.dart'; +import '../../ui_kit/components/sc_page_list.dart'; +import '../../ui_kit/components/text/sc_text.dart'; + +///搜索房间 +class SearchPage extends SCPageList { + @override + _SearchPageState createState() => _SearchPageState(); +} + +class _SearchPageState extends SCPageListState + with SingleTickerProviderStateMixin { + String search = ''; + List historys = []; + + late TabController _tabController; + + bool enablePullUp = false; + bool enablePullDown = false; + + late TextEditingController _textEditingController; + + @override + void dispose() { + super.dispose(); + } + + @override + void initState() { + super.initState(); + _loadHistory(); + _tabController = TabController(vsync: this, length: 2); + _textEditingController = TextEditingController(); + _textEditingController.addListener(() { + if (_textEditingController.text.isEmpty) { + search = ""; + _loadHistory(); + } + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageScaffoldBackgroundColor(), + body: Column( + children: [ + Container( + height: kToolbarHeight + ScreenUtil().statusBarHeight, + padding: EdgeInsets.only(top: ScreenUtil().statusBarHeight), + alignment: AlignmentDirectional.centerStart, + child: Row( + children: [ + SizedBox(width: width(10)), + SCDebounceWidget( + child: Icon( + Icons.chevron_left, + color: + SCGlobalConfig.businessLogicStrategy + .getSearchPageBackIconColor(), + size: 25.w, + ), + onTap: () { + SCNavigatorUtils.goBack(context); + }, + ), + Expanded( + child: searchWidget( + padding: EdgeInsets.symmetric(horizontal: 2.w), + hint: + SCAppLocalizations.of(context)!.pleaseEnterContent, + controller: _textEditingController, + borderColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageInputBorderColor(), + textColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageInputTextColor(), + ), + ), + socialchatGradientButton( + text: SCAppLocalizations.of(context)!.search, + radius: 25, + textSize: 14.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageButtonTextColor(), + gradient: LinearGradient( + colors: + SCGlobalConfig.businessLogicStrategy + .getSearchPageButtonGradient(), + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + onPress: () { + _reqSearch(); + }, + ), + SizedBox(width: width(10)), + ], + ), + ), + search.isEmpty || search == "" ? _history() : _result(), + ], + ), + ), + ], + ); + } + + _history() { + if (historys.isEmpty) { + return mainEmpty(msg: SCAppLocalizations.of(context)!.noData); + } else { + return Expanded( + child: Column( + children: [ + SizedBox(height: 20.w), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: width(15)), + text( + SCAppLocalizations.of(context)!.history, + fontSize: 16.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageHistoryTitleTextColor(), + fontWeight: FontWeight.bold, + ), + Expanded(child: SizedBox(width: width(1))), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + historys.clear(); + _saveHistory(""); + }); + }, + child: Row( + children: [ + Image.asset( + 'sc_images/general/sc_icon_delete.png', + width: 17.w, + ), + //SizedBox(width: width(5),), + //Text("清空", style: TextStyle(fontSize: 13.sp, color: Color(0xffaaaaaa)),), + SizedBox(width: width(15)), + ], + ), + ), + ], + ), + SizedBox(height: height(10)), + Expanded( + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 12.w), + Expanded( + child: Wrap( + runSpacing: 10, + spacing: 10, + direction: Axis.horizontal, + children: + historys + .asMap() + .keys + .map((e) => _historyItem(historys[e])) + .toList(), + ), + ), + SizedBox(width: 12.w), + ], + ), + ], + ), + ), + ], + ), + ); + } + } + + _saveHistory(String msg) async { + historys.removeWhere((history) => history == msg || history.isEmpty); + + if (!historys.any((history) => history.startsWith(msg))) { + if (msg.isNotEmpty) { + historys.insert(0, msg); + } + } + + setState(() {}); + DataPersistence.setSearchHistroy( + AccountStorage().getCurrentUser()?.userProfile?.account ?? "", + jsonEncode(historys), + ); + } + + _loadHistory() async { + String json = DataPersistence.getSearchHistroy( + AccountStorage().getCurrentUser()?.userProfile?.account ?? "", + ); + historys = List.from(jsonDecode(json)); + setState(() {}); + } + + Widget _historyItem(String history) { + return GestureDetector( + onTap: () { + _textEditingController.text = history; + _reqSearch(); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 6.w), + decoration: BoxDecoration( + color: + SCGlobalConfig.businessLogicStrategy + .getSearchPageHistoryItemBackgroundColor(), + borderRadius: BorderRadius.circular(52.w), + ), + child: Text( + history, + style: TextStyle( + fontSize: sp(13), + color: + SCGlobalConfig.businessLogicStrategy + .getSearchPageHistoryItemTextColor(), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + ); + } + + _result() { + return Expanded( + child: Column( + children: [ + Theme( + data: ThemeData( + brightness: Brightness.light, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 1.w), + decoration: BoxDecoration( + // color: Colors.white, + //border: Border(bottom: BorderSide(color: Color(0xffEEEEEE),width: 0.5.w)) + ), + child: TabBar( + tabs: + [ + SCAppLocalizations.of(context)!.rooms, + SCAppLocalizations.of(context)!.users, + ].map((e) => Tab(text: e)).toList(), + indicator: SCFixedWidthTabIndicator( + width: 15.w, + gradient: LinearGradient( + colors: [ + SCGlobalConfig.businessLogicStrategy + .getSearchPageTabIndicatorGradientStartColor(), + SCGlobalConfig.businessLogicStrategy + .getSearchPageTabIndicatorGradientEndColor(), + ], + ), + ), + indicatorSize: TabBarIndicatorSize.label, + // 控制指示器宽度 + labelPadding: EdgeInsetsDirectional.only(start: 15, end: 15), + isScrollable: true, + tabAlignment: TabAlignment.start, + controller: _tabController, + labelColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageTabSelectedLabelColor(), + dividerColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageTabDividerColor(), + unselectedLabelColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageTabUnselectedLabelColor(), + unselectedLabelStyle: + SCGlobalConfig.businessLogicStrategy + .getSearchPageTabUnselectedLabelStyle(), + labelStyle: + SCGlobalConfig.businessLogicStrategy + .getSearchPageTabSelectedLabelStyle(), + ), + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + SearchRoomList(search, (String text) { + _saveHistory(text); + }), + SearchUserList(search, (String text) { + _saveHistory(text); + }), + ], + ), + ), + ], + ), + ); + } + + void _reqSearch() { + search = _textEditingController.text; + setState(() {}); + if (search.trim().isNotEmpty) { + _saveHistory(search); + } else { + items.clear(); + } + } +} + +class SearchRoomList extends StatefulWidget { + final String text; + Function(String text) saveHistroy; + + SearchRoomList(this.text, this.saveHistroy); + + @override + _SearchRoomListState createState() => _SearchRoomListState(saveHistroy); +} + +class _SearchRoomListState extends State { + List? rooms; + Function(String text) saveHistroy; + Timer? _searchTimer; + + _SearchRoomListState(this.saveHistroy); + + @override + void initState() { + loadPage(); + super.initState(); + } + + @override + void dispose() { + _searchTimer?.cancel(); + super.dispose(); + } + + @override + void didUpdateWidget(SearchRoomList oldWidget) { + var oldText = oldWidget.text; + var text = widget.text; + if (oldText != text) { + _searchTimer?.cancel(); + _searchTimer = Timer(Duration(milliseconds: 550), () { + loadPage(); + }); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + if (rooms == null) { + return Center(child: CupertinoActivityIndicator()); + } else if (rooms!.length == 0) { + return empty(); + } + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, // 每行2个 + childAspectRatio: 1, // 宽高比 + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + padding: EdgeInsets.all(10), + itemCount: rooms!.length, + itemBuilder: (context, index) { + return buildItem(rooms![index]); + }, + ); + } + + ///构建列表项 + Widget buildItem(SocialChatRoomRes e) { + //return roomItem(room: e,context: context); + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + if (context != null) { + Provider.of( + context, + listen: false, + ).joinVoiceRoomSession(context, e.id ?? ""); + } + }, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/index/sc_icon_room_bord.png"), + fit: BoxFit.fill, + ), + ), + child: netImage( + url: e.roomCover ?? "", + borderRadius: BorderRadius.circular(12.w), + width: 200.w, + height: 200.w, + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 6.w), + margin: EdgeInsets.symmetric(horizontal: 1.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/index/sc_icon_index_room_brd.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + Consumer( + builder: (_, provider, __) { + return netImage( + url: + "${provider.findCountryByName(e.countryName ?? "")?.nationalFlag}", + width: 20.w, + height: 13.w, + borderRadius: BorderRadius.circular(2.w), + ); + }, + ), + SizedBox(width: 5.w), + Expanded( + child: SizedBox( + height: 21.w, + child: text( + e.roomName ?? "", + fontSize: 15.sp, + textColor: Color(0xffffffff), + fontWeight: FontWeight.w400, + ), + + // (roomRes.roomProfile?.roomName?.length ?? 0) > 10 + // ? Marquee( + // text: roomRes.roomProfile?.roomName ?? "", + // style: TextStyle( + // fontSize: 15.sp, + // color: Color(0xffffffff), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // ), + // scrollAxis: Axis.horizontal, + // crossAxisAlignment: CrossAxisAlignment.start, + // blankSpace: 20.0, + // velocity: 40.0, + // pauseAfterRound: Duration(seconds: 1), + // accelerationDuration: Duration(seconds: 1), + // accelerationCurve: Curves.easeOut, + // decelerationDuration: Duration( + // milliseconds: 500, + // ), + // decelerationCurve: Curves.easeOut, + // ) + // : Text( + // roomRes.roomProfile?.roomName ?? "", + // maxLines: 1, + // overflow: TextOverflow.ellipsis, + // style: TextStyle( + // fontSize: 15.sp, + // color: Color(0xffffffff), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // ), + // ), + ), + ), + SizedBox(width: 5.w), + (e.extValues?.roomSetting?.password?.isEmpty ?? false) + ? Image.asset( + "sc_images/general/sc_icon_online_user.png", + width: 14.w, + height: 14.w, + ) + : Image.asset( + "sc_images/index/sc_icon_room_suo.png", + width: 20.w, + height: 20.w, + ), + (e.extValues?.roomSetting?.password?.isEmpty ?? false) + ? SizedBox(width: 3.w) + : Container(height: 10.w), + (e.extValues?.roomSetting?.password?.isEmpty ?? false) + ? text(e.extValues?.memberQuantity ?? "0", fontSize: 12.sp) + : Container(height: 10.w), + SizedBox(width: 10.w), + ], + ), + ), + (e.roomGameIcon?.isNotEmpty ?? false) + ? PositionedDirectional( + top: 8.w, + end: 8.w, + child: netImage( + url: e.roomGameIcon ?? "", + width: 25.w, + height: 25.w, + borderRadius: BorderRadius.circular(4.w), + ), + ) + : Container(), + ], + ), + ); + } + + loadPage() async { + widget.saveHistroy(widget.text); + try { + rooms = await SCChatRoomRepository().searchRoom(widget.text); + } catch (e) { + } finally { + setState(() {}); + } + } + + empty() { + return mainEmpty(msg: SCAppLocalizations.of(context)!.noData); + } +} + +class SearchUserList extends SCPageList { + final String text; + Function(String text) saveHistroy; + + SearchUserList(this.text, this.saveHistroy); + + @override + _SearchUserListState createState() => _SearchUserListState(saveHistroy); +} + +class _SearchUserListState + extends SCPageListState { + Function(String text) saveHistroy; + + _SearchUserListState(this.saveHistroy); + + Timer? _searchTimer; + + @override + void initState() { + // TODO: implement initState + backgroundColor = Colors.transparent; + loadData(1); + super.initState(); + } + + @override + void dispose() { + _searchTimer?.cancel(); + super.dispose(); + } + + @override + void didUpdateWidget(SCPageList oldWidget) { + // TODO: implement didUpdateWidget + var oldText = (oldWidget as SearchUserList).text; + var text = (widget as SearchUserList).text; + if (oldText != text) { + _searchTimer?.cancel(); + _searchTimer = Timer(Duration(milliseconds: 550), () { + loadData(1); + }); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return buildList(context); + } + + ///构建列表项 + Widget buildItem(SocialChatUserProfile data) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == data.id}&tageId=${data.id}", + ); + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 5.w), + margin: EdgeInsets.symmetric(horizontal: 25.w), + child: Row( + children: [ + head(url: data.userAvatar ?? "", width: 55.w), + SizedBox(width: 2.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + xb( + data.userSex, + color: + data.userSex == 1 + ? Colors.blue + : Colors.purpleAccent, + ), + SizedBox(width: 5.w), + // richText( + // txt: data.userNickname ?? "", + // key: (widget as SearchUserList).text, + // highlight: SocialChatTheme.primaryColor, + // defaultColor: Colors.black, + // ), + Expanded( + child: socialchatNickNameText( + textColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageHistoryTitleTextColor(), + data.userNickname ?? "", + fontSize: 15.sp, + type: data.getVIP()?.name ?? "", + needScroll: + (data.userNickname?.characters.length ?? 0) > 18, + ), + ), + SizedBox(width: 5.w), + // dj(data.level.toString()), + ], + ), + GestureDetector( + child: Container( + child: text( + "ID:${data.getID()}", + fontSize: 12.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getSearchPageHistoryTitleTextColor(), + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + Clipboard.setData(ClipboardData(text: data.getID())); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } + + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + saveHistroy((widget as SearchUserList).text); + SCAccountRepository() + .searchUser((widget as SearchUserList).text) + .then((data) { + List users = []; + users.add(data); + onSuccess(users); + }) + .catchError((e) { + if (onErr != null) { + onErr(); + } + }); + } + + @override + empty() { + return mainEmpty(msg: SCAppLocalizations.of(context)!.noData); + } + + @override + builderDivider() { + return Divider( + height: height(1), + color: SocialChatTheme.dividerColor, + indent: width(85), + endIndent: width(15), + ); + } +} diff --git a/lib/modules/settings/language/language_page.dart b/lib/modules/settings/language/language_page.dart new file mode 100644 index 0000000..4523191 --- /dev/null +++ b/lib/modules/settings/language/language_page.dart @@ -0,0 +1,245 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/services/localization/localization_manager.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +///语言选择 +class LanguagePage extends StatefulWidget { + @override + _LanguagePageState createState() => _LanguagePageState(); +} + +class _LanguagePageState extends State { + int selectType = 1; + + @override + void initState() { + super.initState(); + String langCode = DataPersistence.getLang(); + if (langCode == "ar") { + selectType = 1; + }else if(langCode == "tr"){ + selectType = 2; + }else if(langCode == "bn"){ + selectType = 3; + } else { + selectType = 0; + } + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getLanguagePageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageScaffoldBackgroundColor(), + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.language, + actions: [], + ), + body: Column( + children: [ + Row( + children: [ + SizedBox(width: 15.w), + Expanded( + child: text( + "English", + textColor: SCGlobalConfig.businessLogicStrategy.getLanguagePagePrimaryTextColor(), + fontSize: 15.sp, + ), + ), + Checkbox( + value: selectType == 0, + checkColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckColor(), + activeColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxActiveColor(), + onChanged: (b) { + if (selectType == 0) { + selectType = 1; + } else { + selectType = 0; + } + setState(() {}); + if (b!) { + Provider.of( + context, + listen: false, + ).changeAppLanguage(const Locale('en', '')); + } + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderRadius()), + ), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return BorderSide( + color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(), + width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(), + ); // 选中时保留蓝色边框 + } + return BorderSide( + color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(), + width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(), + ); // 未选中时灰色边框 + }), + ), + ], + ), + Row( + children: [ + SizedBox(width: 15.w), + Expanded( + child: text( + "العربية", + textColor: SCGlobalConfig.businessLogicStrategy.getLanguagePagePrimaryTextColor(), + fontSize: 15.sp, + ), + ), + Checkbox( + value: selectType == 1, + checkColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckColor(), + activeColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxActiveColor(), + onChanged: (b) { + if (selectType == 1) { + selectType = 0; + } else { + selectType = 1; + } + setState(() {}); + if (b!) { + Provider.of( + context, + listen: false, + ).changeAppLanguage(const Locale('ar', '')); + } + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderRadius()), + ), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return BorderSide( + color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(), + width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(), + ); // 选中时保留蓝色边框 + } + return BorderSide( + color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(), + width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(), + ); // 未选中时灰色边框 + }), + ), + ], + ), + Row( + children: [ + SizedBox(width: 15.w), + Expanded( + child: text( + "Türkçe", + textColor: SCGlobalConfig.businessLogicStrategy.getLanguagePagePrimaryTextColor(), + fontSize: 15.sp, + ), + ), + Checkbox( + value: selectType == 2, + checkColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckColor(), + activeColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxActiveColor(), + onChanged: (b) { + if (selectType == 2) { + selectType = 0; + } else { + selectType = 2; + } + setState(() {}); + if (b!) { + Provider.of( + context, + listen: false, + ).changeAppLanguage(const Locale('tr', '')); + } + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderRadius()), + ), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return BorderSide( + color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(), + width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(), + ); // 选中时保留蓝色边框 + } + return BorderSide( + color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(), + width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(), + ); // 未选中时灰色边框 + }), + ), + ], + ), Row( + children: [ + SizedBox(width: 15.w), + Expanded( + child: text( + "বাংলা", + textColor: SCGlobalConfig.businessLogicStrategy.getLanguagePagePrimaryTextColor(), + fontSize: 15.sp, + ), + ), + Checkbox( + value: selectType == 3, + checkColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckColor(), + activeColor: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxActiveColor(), + onChanged: (b) { + if (selectType == 3) { + selectType = 0; + } else { + selectType = 3; + } + setState(() {}); + if (b!) { + Provider.of( + context, + listen: false, + ).changeAppLanguage(const Locale('bn', '')); + } + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderRadius()), + ), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return BorderSide( + color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(), + width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(), + ); // 选中时保留蓝色边框 + } + return BorderSide( + color: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderColor(), + width: SCGlobalConfig.businessLogicStrategy.getLanguagePageCheckboxBorderWidth(), + ); // 未选中时灰色边框 + }), + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/modules/splash/splash_page.dart b/lib/modules/splash/splash_page.dart new file mode 100644 index 0000000..31bf213 --- /dev/null +++ b/lib/modules/splash/splash_page.dart @@ -0,0 +1,117 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_version_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart'; +import 'package:yumi/modules/auth/login_route.dart'; + +class SplashPage extends StatefulWidget { + @override + _SplashPageState createState() => _SplashPageState(); +} + +class _SplashPageState extends State { + Timer? _timer; + int countdown = 6; + int status = 0; + SCStartPageRes? currentStartPage; + + List startPages = []; + + @override + void initState() { + super.initState(); + FileCacheManager.getInstance().getFilePath(); + getStartPage(); + _startTimer(context); + } + + ///加载启动页配置 + void getStartPage() { + SCConfigRepositoryImp().getStartPage().then((result) { + startPages = result; + if (startPages.length > 1) { + int index = Random().nextInt(startPages.length); + currentStartPage = result[index]; + } else { + currentStartPage = result.first; + } + + status = 1; + setState(() {}); + }); + } + + @override + void dispose() { + _cancelTimer(); + super.dispose(); + } + + /// 启动倒计时 + void _startTimer(BuildContext context) { + // 计时器(`Timer`)组件的定期(`periodic`)构造函数,创建一个新的重复计时器。 + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + if (countdown == 0) { + _cancelTimer(); + _goMainPage(context); + return; + } + countdown--; + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Material( + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getSplashPageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy.getSplashPageIcon(), + width: 107.w, + height: 159.w, + ), + ], + ), + ); + } + + void _goMainPage(BuildContext context) async { + try { + await SCVersionUtils.checkReview(); + var user = AccountStorage().getCurrentUser(); + var token = AccountStorage().getToken(); + if (user != null && token.isNotEmpty) { + SCNavigatorUtils.push(context, SCRoutes.home, replace: true); + } else { + SCNavigatorUtils.push(context, LoginRouter.login, replace: true); + } + } catch (e) { + SCNavigatorUtils.push(context, LoginRouter.login, replace: true); + } + } + + /// 取消倒计时的计时器。 + void _cancelTimer() { + // 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。 + _timer?.cancel(); + } +} + + diff --git a/lib/modules/store/chatbox/store_chatbox_page.dart b/lib/modules/store/chatbox/store_chatbox_page.dart new file mode 100644 index 0000000..5890dc1 --- /dev/null +++ b/lib/modules/store/chatbox/store_chatbox_page.dart @@ -0,0 +1,243 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/widgets/store/props_store_chatbox_detail_dialog.dart'; + +import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; + +///聊天气泡框 +class StoreChatboxPage extends SCPageList { + @override + _StoreChatboxPageState createState() => _StoreChatboxPageState(); +} + +class _StoreChatboxPageState + extends SCPageListState { + //折扣 + double disCount = 1; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.78, + ); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: buildList(context), + ); + } + + @override + Widget buildItem(StoreListResBean res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: + res.isSelecte + ? Color(0xff18F2B1).withOpacity(0.1) + : SCGlobalConfig.businessLogicStrategy + .getStoreItemBackgroundColor(), + border: Border.all( + color: + res.isSelecte + ? SCGlobalConfig.businessLogicStrategy + .getStoreItemSelectedBorderColor() + : SCGlobalConfig.businessLogicStrategy + .getStoreItemUnselectedBorderColor(), + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 10.w), + netImage( + url: res.res.propsResources?.cover ?? "", + height: 45.w, + fit: BoxFit.contain, + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + text( + res.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 11.sp, + ), + ], + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 10.w), + Expanded( + child: Text.rich( + textAlign: TextAlign.center, + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStoreItemGoldIcon(), + width: 22.w, + ), + ), + TextSpan(text: " "), + disCount < 1 + ? TextSpan( + text: "${res.res.propsPrices![0].amount}", + style: TextStyle( + fontSize: 12.sp, + color: + SCGlobalConfig.businessLogicStrategy + .getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ), + ) + : TextSpan(), + disCount < 1 ? TextSpan(text: " ") : TextSpan(), + TextSpan( + text: + "${((res.res.propsPrices![0].amount ?? 0) * disCount).toInt()}/${res.res.propsPrices![0].days} ${SCAppLocalizations.of(context)!.day}", + style: TextStyle( + fontSize: 12.sp, + color: + SCGlobalConfig.businessLogicStrategy + .getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + strutStyle: StrutStyle( + height: 1.3, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ), + ), + SizedBox(width: 10.w), + ], + ), + ], + ), + ], + ), + ), + onTap: () { + _selectItem(res); + _showDetail(res.res); + }, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + List beans = []; + var storeList = await SCStoreRepositoryImp().storeList( + SCCurrencyType.GOLD.name, + SCPropsType.CHAT_BUBBLE.name, + ); + for (var value in storeList) { + beans.add(StoreListResBean(value, false)); + } + onSuccess(beans); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + void _showDetail(StoreListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsStoreChatboxDetailDialog(res,disCount,); + }, + ); + } + + void _selectItem(StoreListResBean res) { + for (var value in items) { + value.isSelecte = false; + } + res.isSelecte = true; + setState(() {}); + } + + void _buy(StoreListResBean res) { + if (res.res.propsPrices!.isEmpty) { + return; + } + SCLoadingManager.show(); + num days = res.res.propsPrices![0].days ?? 0; + SCStoreRepositoryImp() + .storePurchasing( + res.res.id ?? "", + SCPropsType.CHAT_BUBBLE.name, + SCCurrencyType.GOLD.name, + "$days", + ) + .then((value) { + SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful); + Provider.of( + context, + listen: false, + ).updateBalance(value); + SCLoadingManager.hide(); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} + +class StoreListResBean { + StoreListRes res; + bool isSelecte = false; + + StoreListResBean(this.res, this.isSelecte); +} diff --git a/lib/modules/store/headdress/store_headdress_page.dart b/lib/modules/store/headdress/store_headdress_page.dart new file mode 100644 index 0000000..ec3af35 --- /dev/null +++ b/lib/modules/store/headdress/store_headdress_page.dart @@ -0,0 +1,244 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/widgets/store/props_store_headdress_detail_dialog.dart'; + +import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; + +///头饰 +class StoreHeaddressPage extends SCPageList { + @override + _StoreHeaddressPageState createState() => _StoreHeaddressPageState(); +} + +class _StoreHeaddressPageState + extends SCPageListState { + //折扣 + double disCount = 1; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.78, + ); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: buildList(context), + ); + } + + @override + Widget buildItem(StoreListResBean res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: + res.isSelecte + ? Color(0xff18F2B1).withOpacity(0.1) + : SCGlobalConfig.businessLogicStrategy + .getStoreItemBackgroundColor(), + border: Border.all( + color: + res.isSelecte + ? SCGlobalConfig.businessLogicStrategy + .getStoreItemSelectedBorderColor() + : SCGlobalConfig.businessLogicStrategy + .getStoreItemUnselectedBorderColor(), + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 10.w), + netImage( + url: res.res.propsResources?.cover ?? "", + width: 55.w, + height: 55.w, + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + text( + res.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 11.sp, + ), + ], + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 10.w), + Expanded( + child: Text.rich( + textAlign: TextAlign.center, + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStoreItemGoldIcon(), + width: 22.w, + ), + ), + TextSpan(text: " "), + disCount < 1 + ? TextSpan( + text: "${res.res.propsPrices![0].amount}", + style: TextStyle( + fontSize: 12.sp, + color: + SCGlobalConfig.businessLogicStrategy + .getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ), + ) + : TextSpan(), + disCount < 1 ? TextSpan(text: " ") : TextSpan(), + TextSpan( + text: + "${((res.res.propsPrices![0].amount ?? 0) * disCount).toInt()}/${res.res.propsPrices![0].days} ${SCAppLocalizations.of(context)!.day}", + style: TextStyle( + fontSize: 12.sp, + color: + SCGlobalConfig.businessLogicStrategy + .getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + strutStyle: StrutStyle( + height: 1.3, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ), + ), + SizedBox(width: 10.w), + ], + ), + ], + ), + ], + ), + ), + onTap: () { + _selectItem(res); + _showDetail(res.res); + + }, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + List beans = []; + var storeList = await SCStoreRepositoryImp().storeList( + SCCurrencyType.GOLD.name, + SCPropsType.AVATAR_FRAME.name, + ); + for (var value in storeList) { + beans.add(StoreListResBean(value, false)); + } + onSuccess(beans); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + void _showDetail(StoreListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsStoreHeaddressDetailDialog(res, disCount); + }, + ); + } + + void _selectItem(StoreListResBean res) { + for (var value in items) { + value.isSelecte = false; + } + res.isSelecte = true; + setState(() {}); + } + + void _buy(StoreListResBean res) { + if (res.res.propsPrices!.isEmpty) { + return; + } + SCLoadingManager.show(); + num days = res.res.propsPrices![0].days ?? 0; + SCStoreRepositoryImp() + .storePurchasing( + res.res.id ?? "", + SCPropsType.AVATAR_FRAME.name, + SCCurrencyType.GOLD.name, + "$days", + ) + .then((value) { + SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful); + Provider.of( + context, + listen: false, + ).updateBalance(value); + SCLoadingManager.hide(); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} + +class StoreListResBean { + StoreListRes res; + bool isSelecte = false; + + StoreListResBean(this.res, this.isSelecte); +} diff --git a/lib/modules/store/mountains/store_mountains_page.dart b/lib/modules/store/mountains/store_mountains_page.dart new file mode 100644 index 0000000..62d43d2 --- /dev/null +++ b/lib/modules/store/mountains/store_mountains_page.dart @@ -0,0 +1,210 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:yumi/ui_kit/widgets/store/props_store_mountains_detail_dialog.dart'; +import 'package:yumi/modules/store/headdress/store_headdress_page.dart'; + +import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; + +///坐骑 +class StoreMountainsPage extends SCPageList { + StoreMountainsPage(); + + @override + _StoreMountainsPageState createState() => _StoreMountainsPageState(); +} + +class _StoreMountainsPageState + extends SCPageListState { + //折扣 + double disCount = 1; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.78, + ); + loadData(1); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: buildList(context), + ); + } + + @override + Widget buildItem(StoreListResBean res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: + res.isSelecte + ? Color(0xff18F2B1).withOpacity(0.1) + : SCGlobalConfig.businessLogicStrategy + .getStoreItemBackgroundColor(), + border: Border.all( + color: + res.isSelecte + ? SCGlobalConfig.businessLogicStrategy + .getStoreItemSelectedBorderColor() + : SCGlobalConfig.businessLogicStrategy + .getStoreItemUnselectedBorderColor(), + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 10.w), + netImage( + url: res.res.propsResources?.cover ?? "", + width: 55.w, + height: 55.w, + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + text( + res.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 11.sp, + ), + ], + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 10.w), + Expanded( + child: Text.rich( + textAlign: TextAlign.center, + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset( + SCGlobalConfig.businessLogicStrategy.getStoreItemGoldIcon(), + width: 22.w, + ), + ), + TextSpan(text: " "), + disCount < 1 + ? TextSpan( + text: "${res.res.propsPrices![0].amount}", + style: TextStyle( + fontSize: 12.sp, + color: SCGlobalConfig.businessLogicStrategy.getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ), + ) + : TextSpan(), + disCount < 1 ? TextSpan(text: " ") : TextSpan(), + TextSpan( + text: + "${((res.res.propsPrices![0].amount ?? 0) * disCount).toInt()}/${res.res.propsPrices![0].days} ${SCAppLocalizations.of(context)!.day}", + style: TextStyle( + fontSize: 12.sp, + color: SCGlobalConfig.businessLogicStrategy.getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + strutStyle: StrutStyle( + height: 1.3, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ), + ), + SizedBox(width: 10.w), + ], + ), + ], + ), + ], + ), + ), + onTap: () { + _selectItem(res); + _showDetail(res.res); + }, + ); + } + + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + List beans = []; + var storeList = await SCStoreRepositoryImp().storeList( + SCCurrencyType.GOLD.name, + SCPropsType.RIDE.name, + ); + for (var value in storeList) { + beans.add(StoreListResBean(value, false)); + } + onSuccess(beans); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + void _showDetail(StoreListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsStoreMountainsDetailDialog(res, disCount); + }, + ); + } + + void _selectItem(StoreListResBean res) { + for (var value in items) { + value.isSelecte = false; + } + res.isSelecte = true; + setState(() {}); + } +} diff --git a/lib/modules/store/store_page.dart b/lib/modules/store/store_page.dart new file mode 100644 index 0000000..42c09d3 --- /dev/null +++ b/lib/modules/store/store_page.dart @@ -0,0 +1,213 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/modules/store/theme/store_theme_page.dart'; +import 'package:provider/provider.dart'; +import 'package:tancent_vap/utils/constant.dart'; +import 'package:tancent_vap/widgets/vap_view.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/widgets/headdress/headdress_widget.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/modules/store/chatbox/store_chatbox_page.dart'; +import 'package:yumi/modules/store/headdress/store_headdress_page.dart'; +import 'package:yumi/modules/store/mountains/store_mountains_page.dart'; + +import '../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; + +class StorePage extends StatefulWidget { + @override + _StorePageState createState() => _StorePageState(); +} + +class _StorePageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = []; + final List _tabs = []; + Timer? _timer; + + @override + void initState() { + super.initState(); + _pages.add(StoreHeaddressPage()); + _pages.add( + StoreMountainsPage(), + ); + _pages.add( + StoreThemePage(), + ); + _pages.add(StoreChatboxPage()); + _tabController = TabController(length: _pages.length, vsync: this); + _tabController.addListener(() { + }); + Provider.of(context, listen: false).balance(); + } + + @override + void dispose() { + super.dispose(); + _timer?.cancel(); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.headdress)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.mountains)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.theme)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chatBox)); + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getLanguagePageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.store, + actions: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.all(5.w), + margin: EdgeInsetsDirectional.only( + end: + SCGlobalConfig.businessLogicStrategy + .getStorePageShoppingBagMargin() + .end + .w, + ), + child: Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStorePageShoppingBagIcon(), + width: 20.w, + height: 20.w, + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + StoreRoute.bags, + replace: true, + ); + }, + ), + ], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TabBar( + tabAlignment: TabAlignment.center, + isScrollable: true, + labelColor: Colors.white, + unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .copyWith( + fontSize: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .fontSize + ?.sp, + ), + // indicatorPadding: EdgeInsets.symmetric( + // vertical: 5.w, + // horizontal: 15.w, + // ), + indicator: SCFixedWidthTabIndicator( + width: 15.w, + color: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabIndicatorColor(), + ), + dividerColor: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabDividerColor(), + controller: _tabController, + tabs: _tabs, + ), + ], + ), + SizedBox(height: 5.w), + Expanded( + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + Consumer( + builder: (context, ref, child) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + padding: EdgeInsets.only(bottom: 12.w,top: 12.w), + child: Row( + children: [ + SizedBox(width: 10.w), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIcon(), + width: 25.w, + height: 25.w, + ), + SizedBox(width: 5.w), + text( + "${ref.myBalance}", + fontSize: 14.sp, + textColor: + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldTextColor(), + ), + SizedBox(width: 4.w), + Icon( + Icons.chevron_right, + size: 20.w, + color: + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIconColor(), + ), + ], + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + WalletRoute.recharge, + replace: false, + ); + }, + ); + }, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/store/store_route.dart b/lib/modules/store/store_route.dart new file mode 100644 index 0000000..0bb408b --- /dev/null +++ b/lib/modules/store/store_route.dart @@ -0,0 +1,22 @@ +import 'package:fluro/fluro.dart'; +import 'package:yumi/modules/store/store_page.dart'; +import 'package:yumi/modules/user/my_items/my_items_page.dart'; + +import 'package:yumi/app/routes/sc_router_init.dart'; + +class StoreRoute implements SCIRouterProvider { + static String list = '/main/store/list'; + static String bags = '/main/store/bag'; + + @override + void initRouter(FluroRouter router) { + router.define( + list, + handler: Handler(handlerFunc: (_, params) => StorePage()), + ); + router.define( + bags, + handler: Handler(handlerFunc: (_, params) => MyItemsPage()), + ); + } +} diff --git a/lib/modules/store/theme/store_theme_page.dart b/lib/modules/store/theme/store_theme_page.dart new file mode 100644 index 0000000..14352f4 --- /dev/null +++ b/lib/modules/store/theme/store_theme_page.dart @@ -0,0 +1,239 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/widgets/store/props_store_theme_detail_dialog.dart'; +import 'package:yumi/modules/store/headdress/store_headdress_page.dart'; + +import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; + +///房间主题 +class StoreThemePage extends SCPageList { + + StoreThemePage(); + + @override + _StoreThemePagePageState createState() => _StoreThemePagePageState(); +} + +class _StoreThemePagePageState + extends SCPageListState { + + + //折扣 + double disCount = 1; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.78, + ); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: buildList(context), + ); + } + + @override + Widget buildItem(StoreListResBean res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: + res.isSelecte + ? Color(0xff18F2B1).withOpacity(0.1) + : SCGlobalConfig.businessLogicStrategy + .getStoreItemBackgroundColor(), + border: Border.all( + color: + res.isSelecte + ? SCGlobalConfig.businessLogicStrategy + .getStoreItemSelectedBorderColor() + : SCGlobalConfig.businessLogicStrategy + .getStoreItemUnselectedBorderColor(), + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 10.w), + netImage( + url: res.res.propsResources?.cover ?? "", + width: 55.w, + height: 55.w, + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + text( + res.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 11.sp, + ), + ], + ), + SizedBox(height: 3.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 10.w), + Expanded( + child: Text.rich( + textAlign: TextAlign.center, + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset( + SCGlobalConfig.businessLogicStrategy.getStoreItemGoldIcon(), + width: 22.w, + ), + ), + TextSpan(text: " "), + disCount < 1 + ? TextSpan( + text: "${res.res.propsPrices![0].amount}", + style: TextStyle( + fontSize: 12.sp, + color: SCGlobalConfig.businessLogicStrategy.getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ), + ) + : TextSpan(), + disCount < 1 ? TextSpan(text: " ") : TextSpan(), + TextSpan( + text: + "${((res.res.propsPrices![0].amount ?? 0) * disCount).toInt()}/${res.res.propsPrices![0].days} ${SCAppLocalizations.of(context)!.day}", + style: TextStyle( + fontSize: 12.sp, + color: SCGlobalConfig.businessLogicStrategy.getStoreItemPriceTextColor(), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + strutStyle: StrutStyle( + height: 1.3, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ), + ), + SizedBox(width: 10.w), + ], + ), + ], + ), + ], + ), + ), + onTap: () { + _selectItem(res); + _showDetail(res.res); + }, + ); + } + + void _showDetail(StoreListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsStoreThemeDetailDialog(res, disCount); + }, + ); + } + + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + List beans = []; + var storeList = await SCStoreRepositoryImp().storeList( + SCCurrencyType.GOLD.name, + SCPropsType.THEME.name, + ); + for (var value in storeList) { + beans.add(StoreListResBean(value, false)); + } + onSuccess(beans); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + void _selectItem(StoreListResBean res) { + for (var value in items) { + value.isSelecte = false; + } + res.isSelecte = true; + setState(() {}); + } + + void _buy(StoreListResBean res) { + num days = 0; + if (res.res.propsPrices!.length > 1) { + days = res.res.propsPrices![1].days ?? 0; + } else { + days = res.res.propsPrices![0].days ?? 0; + } + SCStoreRepositoryImp() + .storePurchasing( + res.res.id ?? "", + SCPropsType.THEME.name, + SCCurrencyType.GOLD.name, + "$days", + ) + .then((value) { + SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful); + Provider.of( + context, + listen: false, + ).updateBalance(value); + }); + } +} diff --git a/lib/modules/user/crop/crop_image_page.dart b/lib/modules/user/crop/crop_image_page.dart new file mode 100644 index 0000000..6175931 --- /dev/null +++ b/lib/modules/user/crop/crop_image_page.dart @@ -0,0 +1,206 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:image_cropper/image_cropper.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; + +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_general_repository_imp.dart'; + +typedef OnUpLoadCallBack = void Function(bool success, String url); + +class CropImagePage extends StatefulWidget { + final double? aspectRatio; // 需要锁定的宽高比,例如 1 表示 1:1 + final bool? backOriginalFile; + final OnUpLoadCallBack? onUpLoadCallBack; + final File image; // 原始图片文件 + final bool needUpload; + + const CropImagePage(this.image, { + Key? key, + this.onUpLoadCallBack, + this.aspectRatio, + this.backOriginalFile = true, + this.needUpload = true, + }) : super(key: key); + + @override + _CropImageRouteState createState() => _CropImageRouteState(); +} + +class _CropImageRouteState extends State { + late Image imageView; + + @override + void initState() { + super.initState(); + imageView = Image.file(widget.image, fit: BoxFit.contain); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xff333333), + body: SafeArea( + top: false, + child: Stack( + alignment: Alignment.center, + children: [ + // 预览原图 + Align( + alignment: Alignment.center, + child: Container( + width: double.infinity, + height: + SCScreen.designHeight - + (kToolbarHeight * 2) - + ScreenUtil().bottomBarHeight - + ScreenUtil().statusBarHeight, + child: imageView, + ), + ), + + // 顶部栏 + Align( + alignment: Alignment.topCenter, + child: Container( + height: kToolbarHeight + ScreenUtil().statusBarHeight, + width: double.infinity, + padding: EdgeInsets.only( + left: width(15), + right: width(15), + top: ScreenUtil().statusBarHeight, + ), + color: Colors.transparent, + child: Stack( + alignment: Alignment.centerLeft, + children: [ + GestureDetector( + onTap: () => SCNavigatorUtils.goBackWithParams(context, ''), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + SCGlobalConfig.lang == "ar" + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + color: Colors.white, + ), + ), + ), + Align( + alignment: Alignment.center, + child: Text( + SCAppLocalizations.of(context)!.crop, + style: TextStyle(fontSize: 18.sp, color: Colors.white), + ), + ), + ], + ), + ), + ), + + // 底部完成按钮 + Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + height: kToolbarHeight + ScreenUtil().bottomBarHeight, + color: const Color(0xff666666), + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: () => + _cropAndUpload(context, widget.image, needUpload:widget.needUpload), + child: Padding( + padding: const EdgeInsets.all(8.0).copyWith(right: 18), + child: Text( + SCAppLocalizations.of(context)!.finish, + style: TextStyle(color: Colors.white, fontSize: 16.sp), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + /// 使用 image_cropper 进行裁剪并上传 + Future _cropAndUpload(BuildContext context, + File originalFile, { + bool needUpload = true, + }) async { + // 调用系统裁剪界面 + CroppedFile? cropped = await ImageCropper().cropImage( + sourcePath: originalFile.path, + aspectRatio: + widget.aspectRatio != null + ? CropAspectRatio(ratioX: widget.aspectRatio!, ratioY: 1) + : null, + uiSettings: [ + AndroidUiSettings( + toolbarTitle: SCAppLocalizations.of(context)!.crop, + toolbarColor: Colors.black, + toolbarWidgetColor: Colors.white, + hideBottomControls: true, + lockAspectRatio: widget.aspectRatio != null, + ), + IOSUiSettings( + title: SCAppLocalizations.of(context)!.crop, + resetButtonHidden: true, + rotateButtonsHidden: true, + aspectRatioPickerButtonHidden: true, + rotateClockwiseButtonHidden: true, + aspectRatioLockEnabled: widget.aspectRatio != null, + ), + ], + ); + + // 用户可能直接点“取消”,此时 cropped 为 null + File fileToUpload; + if (cropped == null) { + if (widget.backOriginalFile ?? true) { + fileToUpload = originalFile; + } else { + widget.onUpLoadCallBack?.call(false, ""); + return; + } + } else { + fileToUpload = File(cropped.path); + } + if (needUpload) { + // 弹出上传中的 loading + SCLoadingManager.show(context: context); + await upload(fileToUpload); + } else { + widget.onUpLoadCallBack?.call(true, fileToUpload.path); + SCNavigatorUtils.goBack(context); + } + } + + /// 上传文件至oss + Future upload(File file) async { + try { + String fileUrl = await SCGeneralRepositoryImp().upload(file); + SCLoadingManager.hide(); + SCNavigatorUtils.goBack(context); + await Future.delayed(const Duration(milliseconds: 100)); + widget.onUpLoadCallBack?.call(true, fileUrl); + } catch (e) { + SCTts.show("upload fail $e"); + SCLoadingManager.hide(); + } + } + + /* + * 获取 OssToken(如果依然需要,可保留) + */ + Future _getOssToken() async {} +} diff --git a/lib/modules/user/edit/edit_user_info_page2.dart b/lib/modules/user/edit/edit_user_info_page2.dart new file mode 100644 index 0000000..977721f --- /dev/null +++ b/lib/modules/user/edit/edit_user_info_page2.dart @@ -0,0 +1,670 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/main.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import '../../../shared/tools/sc_pick_utils.dart'; +import '../../../shared/business_logic/models/res/country_res.dart'; +import '../../../shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart'; +import '../../country/country_route.dart'; + +class EditUserInfoPage2 extends StatefulWidget { + const EditUserInfoPage2({Key? key}) : super(key: key); + + @override + _EditUserInfoPage2State createState() => _EditUserInfoPage2State(); +} + +class _EditUserInfoPage2State extends State + with SingleTickerProviderStateMixin { + String? userCover; + + String? bornMonth; + String? autograph; + String? hobby; + String? bornDay; + String? bornYear; + String nickName = ""; + DateTime? birthdayDate; + num? sex; + num? age; + Country? country; + + @override + void initState() { + super.initState(); + userCover = + AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? ""; + nickName = + AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? ""; + autograph = AccountStorage().getCurrentUser()?.userProfile?.autograph ?? ""; + hobby = AccountStorage().getCurrentUser()?.userProfile?.hobby ?? ""; + country = Provider.of( + context, + listen: false, + ).findCountryByName( + AccountStorage().getCurrentUser()?.userProfile?.countryName ?? "", + ); + num m = AccountStorage().getCurrentUser()?.userProfile?.bornMonth ?? 0; + if (m < 10) { + bornMonth = "0$m"; + } else { + bornMonth = "$m"; + } + num d = AccountStorage().getCurrentUser()?.userProfile?.bornDay ?? 0; + if (d < 10) { + bornDay = "0$d"; + } else { + bornDay = "$d"; + } + bornYear = "${AccountStorage().getCurrentUser()?.userProfile?.bornYear}"; + age = + DateTime.now().year - + (AccountStorage().getCurrentUser()?.userProfile?.bornYear ?? 0); + sex = AccountStorage().getCurrentUser()?.userProfile?.userSex; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: true, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.editProfile, + actions: [], + ), + body: Consumer( + builder: (context, ref, child) { + return Column( + spacing: 10.w, + children: [ + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.profilePhoto, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + Spacer(), + Stack( + alignment: AlignmentDirectional.bottomEnd, + children: [ + netImage( + url: + UserManager() + .getCurrentUser() + ?.userProfile + ?.userAvatar ?? + "", + width: 45.w, + height: 45.w, + shape: BoxShape.circle, + ), + ], + ), + Icon( + Icons.keyboard_arrow_right, + size: 20.w, + color: Colors.white70, + ), + SizedBox(width: 8.w), + ], + ), + onTap: () { + SCPickUtils.pickImage(context, (bool success, String url) { + if (success) { + submit(context); + } + }); + }, + ), + SizedBox(height: 3.w), + _buildItem( + "${SCAppLocalizations.of(context)!.userName}:", + nickName ?? + "", + () { + _showInputBioHobby(nickName, 3); + }, + ), + _buildItem( + "${SCAppLocalizations.of(context)!.gender}:", + sex == 1 + ? SCAppLocalizations.of(context)!.man + : SCAppLocalizations.of(context)!.woman, + () { + _showSexDialog(); + }, + ), + + _buildItem( + "${SCAppLocalizations.of(context)!.birthday}:", + "$bornYear-$bornMonth-$bornDay", + () { + _selectDate(); + }, + ), + _buildControlItem(), + + _buildItem( + "${SCAppLocalizations.of(context)!.bio}:", + autograph ?? "", + () { + _showInputBioHobby(autograph ?? "", 1); + }, + ), + + _buildItem( + "${SCAppLocalizations.of(context)!.hobby}:", + hobby ?? "", + () { + _showInputBioHobby(hobby ?? "", 2); + }, + ), + ], + ); + }, + ), + ), + ], + ); + } + + Future _selectDate() async { + SmartDialog.show( + tag: "showSelectDate", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + onDismiss: () { + submit(context); + }, + builder: (_) { + return SafeArea( + top: false, + child: Container( + height: 240.w, + padding: EdgeInsets.only(top: 6), + color: CupertinoColors.systemBackground, + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Container( + alignment: Alignment.topCenter, + child: text( + SCAppLocalizations.of(context)!.cancel, + textColor: Colors.black87, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showSelectDate"); + }, + ), + Expanded( + child: Container( + alignment: Alignment.topCenter, + child: text( + SCAppLocalizations.of(context)!.birthday, + textColor: Colors.black87, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + GestureDetector( + child: Container( + alignment: Alignment.topCenter, + child: text( + SCAppLocalizations.of(context)!.confirm, + textColor: Color(0xff7726FF), + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + num m = birthdayDate!.month; + if (m < 10) { + bornMonth = "0$m"; + } else { + bornMonth = "$m"; + } + num d = birthdayDate!.day; + if (d < 10) { + bornDay = "0$d"; + } else { + bornDay = "$d"; + } + bornYear = "${birthdayDate?.year}"; + setState(() {}); + SmartDialog.dismiss(tag: "showSelectDate"); + }, + ), + SizedBox(width: 10.w), + ], + ), + Expanded( + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: _getBefor18(), + maximumDate: _getBefor18(), + onDateTimeChanged: (date) { + birthdayDate = date; + age = DateTime.now().year - date.year; + }, + ), + ), + ], + ), + ), + ); + }, + ); + } + + DateTime _getBefor18() { + DateTime currentDate = DateTime.now(); + DateTime eighteenYearsAgo = DateTime( + currentDate.year - 18, + currentDate.month, + currentDate.day, + currentDate.hour, + currentDate.minute, + currentDate.second, + currentDate.millisecond, + currentDate.microsecond, + ); + return eighteenYearsAgo; + } + + int sTime = 0; + + void submit(BuildContext context) async { + if (nickName.isEmpty) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseEnterNickname); + return; + } + int bTime = DateTime.now().millisecondsSinceEpoch; + if (bTime - sTime > 5000) { + sTime = bTime; + SCLoadingManager.show(); + try { + await SCAccountRepository().updateUserInfo( + userAvatar: userCover, + userSex: sex, + userNickname: nickName, + age: age, + bornDay: birthdayDate?.day, + bornMonth: birthdayDate?.month, + bornYear: birthdayDate?.year, + hobby: hobby, + autograph: autograph, + countryId: country?.id, + ); + Provider.of(context, listen: false).needUpDataUserInfo = + true; + SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); + } catch (e) { + debugPrint(e.toString()); + SCLoadingManager.hide(); + } + Provider.of( + context, + listen: false, + ).fetchUserProfileData(); + SCLoadingManager.hide(); + } + } + + void _showSexDialog() { + showCenterDialog( + context, + Container( + height: 150.w, + width: ScreenUtil().screenWidth * 0.7, + decoration: BoxDecoration( + color: Color(0xff09372E), + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 15.w), + text( + SCAppLocalizations.of(context)!.pleaseSelectYourGender, + fontSize: 15.sp, + textColor: Colors.white, + ), + SizedBox(height: 5.w), + Divider(color: Colors.black12, thickness: 0.5, height: 14.w), + SizedBox(height: 8.w), + GestureDetector( + onTap: () { + sex = 1; + setState(() {}); + submit(context); + SCNavigatorUtils.goBack(context); + }, + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + xb(1, color: Colors.white, height: 18.w), + SizedBox(width: 5.w), + text( + SCAppLocalizations.of(context)!.man, + textColor: Colors.white, + fontSize: 15.sp, + ), + ], + ), + ), + SizedBox(height: 8.w), + Divider(color: Colors.black12, thickness: 0.5, height: 14.w), + SizedBox(height: 8.w), + GestureDetector( + onTap: () { + sex = 0; + setState(() {}); + submit(context); + SCNavigatorUtils.goBack(context); + }, + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + xb(0, color: Colors.white, height: 18.w), + SizedBox(width: 5.w), + text( + SCAppLocalizations.of(context)!.woman, + textColor: Colors.white, + fontSize: 15.sp, + ), + ], + ), + ), + ], + ), + ), + ); + } + + _buildItem(String title, String value, Function() func) { + return SCDebounceWidget( + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 15.w), + text( + title, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + Expanded( + child: Container( + alignment: AlignmentDirectional.centerEnd, + child: text( + value, + textColor: Colors.white, + fontSize: 15.sp, + ), + ), + ), + Icon( + Icons.keyboard_arrow_right, + size: 20.w, + color: Colors.white70, + ), + SizedBox(width: 8.w), + ], + ), + SizedBox(height: 8.w), + Container( + margin: EdgeInsets.only(left: 15.w, right: 15.w), + color: Colors.white12, + height: 0.5.w, + width: ScreenUtil().screenWidth, + ), + ], + ), + onTap: () { + func.call(); + }, + ); + } + + _buildControlItem() { + return SCDebounceWidget( + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.country, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + Spacer(), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + netImage( + url: country?.nationalFlag ?? "", + width: 26.w, + height: 16.w, + borderRadius: BorderRadius.all(Radius.circular(2.w)), + ), + SizedBox(width: 5.w), + text( + country?.countryName ?? "", + textColor: Colors.white, + fontSize: 15.sp, + ), + ], + ), + Icon( + Icons.keyboard_arrow_right, + size: 20.w, + color: Colors.white70, + ), + SizedBox(width: 8.w), + ], + ), + SizedBox(height: 8.w), + Container( + margin: EdgeInsets.only(left: 15.w, right: 15.w), + color: Colors.white12, + height: 0.5.w, + width: ScreenUtil().screenWidth, + ), + ], + ), + onTap: () { + SCNavigatorUtils.push(context, CountryRoute.country, replace: false).then( + (res) { + var c = + Provider.of( + navigatorKey.currentState!.context, + listen: false, + ).selectCountryInfo; + if (c != null) { + country = c; + submit(navigatorKey.currentState!.context); + setState(() {}); + } + }, + ); + }, + ); + } + + void _showInputBioHobby(String content, int type) { + SmartDialog.dismiss(tag: "showInputBioHobby"); + TextEditingController _inputController = TextEditingController(); + _inputController.text = content; + SmartDialog.show( + tag: "showInputBioHobby", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return Container( + height: 250.w, + margin: EdgeInsets.symmetric(horizontal: 20.w), + decoration: BoxDecoration( + color: Color(0xff09372E), + borderRadius: BorderRadius.circular(10.w), + ), + child: Column( + children: [ + SizedBox(height: 8.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Icon(Icons.close, color: Colors.white, size: 20.w), + onTap: () { + SmartDialog.dismiss(tag: "showInputBioHobby"); + }, + ), + Spacer(), + text( + SCAppLocalizations.of(context)!.pleaseEnterContent, + textColor: Colors.white, + fontSize: 15.sp, + ), + Spacer(), + SizedBox(width: 20.w), + ], + ), + SizedBox(height: 15.w), + Container( + margin: EdgeInsets.symmetric(horizontal: 25.w), + padding: EdgeInsets.only(top: 5.w, left: 12.w, right: 12.w), + alignment: AlignmentDirectional.centerStart, + height: 110.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(8.w)), + border: Border.all(color: Color(0xffE6E6E6), width: 0.5.w), + ), + child: TextField( + controller: _inputController, + maxLines: 5, + inputFormatters: [ + SCAccurateLengthLimitingTextInputFormatter(50), + ], + decoration: InputDecoration( + isDense: true, + hintText: "", + hintStyle: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + contentPadding: EdgeInsets.only(top: 0.w), + counterText: '', + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), + // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), + fillColor: Colors.white54, + ), + style: TextStyle( + fontSize: 15.w, + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + SizedBox(height: 15.w), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 60.w), + height: 42.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.w), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.confirm, + textColor: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showInputBioHobby"); + if (type == 1) { + autograph = _inputController.text; + setState(() {}); + } else if (type == 2) { + hobby = _inputController.text; + setState(() {}); + }else if (type == 3) { + nickName = _inputController.text; + setState(() {}); + } + submit(navigatorKey.currentState!.context); + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/modules/user/fans/fans_user_list_page.dart b/lib/modules/user/fans/fans_user_list_page.dart new file mode 100644 index 0000000..bfc9b98 --- /dev/null +++ b/lib/modules/user/fans/fans_user_list_page.dart @@ -0,0 +1,205 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +class FansUserListPage extends SCPageList { + @override + _FansUserListPageState createState() => _FansUserListPageState(); +} + +class _FansUserListPageState + extends SCPageListState { + String? lastId; + + @override + void initState() { + super.initState(); + enablePullUp = true; + backgroundColor = Colors.transparent; + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar(title: SCAppLocalizations.of(context)!.fansList, actions: []), + body: buildList(context), + ), + ], + ); + } + + @override + Widget buildItem(FollowUserRes res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration(), + child: Row( + children: [ + SizedBox(width: 10.w), + head( + url: res.userProfile?.userAvatar ?? "", + width: 55.w, + headdress: res.userProfile?.getHeaddress()?.sourceUrl, + ), + SizedBox(width: 5.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + socialchatNickNameText( + maxWidth: 160.w, + res.userProfile?.userNickname ?? "", + fontSize: 14.sp, + textColor: Colors.white, + type: res.userProfile?.getVIP()?.name ?? "", + needScroll: + (res.userProfile?.userNickname?.characters.length ?? + 0) > + 14, + ), + SizedBox(width: 3.w), + res.userProfile?.getVIP() != null + ? netImage( + url: res.userProfile?.getVIP()?.cover ?? "", + width: 25.w, + height: 25.w, + ) + : Container(), + SizedBox(width: 3.w), + Container( + width: (res.userProfile?.age ?? 0) > 999 ? 58.w : 48.w, + height: 24.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + res.userProfile?.userSex == 0 + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png", + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + xb(res.userProfile?.userSex), + text( + "${res.userProfile?.age}", + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 3.w), + ], + ), + ), + ], + ), + Row( + children: [ + GestureDetector( + child: SizedBox( + child: text( + "ID:${res.userProfile?.getID()}", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: res.userProfile?.getID() ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + SizedBox(width: 8.w), + getWealthLevel( + res.userProfile?.wealthLevel ?? 0, + width: 58.w, + height: 30.w, + ), + SizedBox(width: 8.w), + getUserLevel( + res.userProfile?.charmLevel ?? 0, + width: 58.w, + height: 30.w, + ), + ], + ), + ], + ), + ), + SizedBox(width: 15.w), + ], + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}", + ); + }, + ); + } + + @override + builderDivider() { + // return Divider( + // height: 1.w, + // color: Color(0xff3D3277).withOpacity(0.5), + // indent: 15.w, + // ); + return Container(height: 8.w); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + if (page == 1) { + lastId = null; + } + try { + var fansList = await SCAccountRepository().fansMyList(lastId: lastId); + if (fansList.isNotEmpty) { + lastId = fansList.last.id; + } + onSuccess(fansList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } +} diff --git a/lib/modules/user/follow/follow_user_list_page.dart b/lib/modules/user/follow/follow_user_list_page.dart new file mode 100644 index 0000000..9763917 --- /dev/null +++ b/lib/modules/user/follow/follow_user_list_page.dart @@ -0,0 +1,208 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +class FollowUserListPage extends SCPageList { + @override + _FollowUserListPageState createState() => _FollowUserListPageState(); +} + +class _FollowUserListPageState + extends SCPageListState { + String? lastId; + + @override + void initState() { + super.initState(); + enablePullUp = true; + backgroundColor = Colors.transparent; + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.followList, + actions: [], + ), + body: buildList(context), + ), + ], + ); + } + + @override + Widget buildItem(FollowUserRes res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration(), + child: Row( + children: [ + SizedBox(width: 10.w), + head( + url: res.userProfile?.userAvatar ?? "", + width: 55.w, + headdress: res.userProfile?.getHeaddress()?.sourceUrl, + ), + SizedBox(width: 5.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + socialchatNickNameText( + maxWidth: 160.w, + res.userProfile?.userNickname ?? "", + fontSize: 14.sp, + textColor: Colors.white, + type: res.userProfile?.getVIP()?.name ?? "", + needScroll: + (res.userProfile?.userNickname?.characters.length ?? + 0) > + 14, + ), + SizedBox(width: 3.w), + res.userProfile?.getVIP() != null + ? netImage( + url: res.userProfile?.getVIP()?.cover ?? "", + width: 25.w, + height: 25.w, + ) + : Container(), + SizedBox(width: 3.w), + Container( + width: (res.userProfile?.age ?? 0) > 999 ? 58.w : 48.w, + height: 24.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + res.userProfile?.userSex == 0 + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png", + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + xb(res.userProfile?.userSex), + text( + "${res.userProfile?.age}", + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 3.w), + ], + ), + ), + ], + ), + Row( + children: [ + GestureDetector( + child: SizedBox( + child: text( + "ID:${res.userProfile?.getID()}", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: res.userProfile?.getID() ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + SizedBox(width: 8.w), + getWealthLevel( + res.userProfile?.wealthLevel ?? 0, + width: 58.w, + height: 30.w, + ), + SizedBox(width: 8.w), + getUserLevel( + res.userProfile?.charmLevel ?? 0, + width: 58.w, + height: 30.w, + ), + ], + ), + ], + ), + ), + SizedBox(width: 15.w), + ], + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}", + ); + }, + ); + } + + @override + builderDivider() { + // return Divider( + // height: 1.w, + // color: Color(0xff3D3277).withOpacity(0.5), + // indent: 15.w, + // ); + return Container(height: 8.w); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + if (page == 1) { + lastId = null; + } + try { + var followList = await SCAccountRepository().followMyList(lastId: lastId); + if (followList.isNotEmpty) { + lastId = followList.last.id; + } + onSuccess(followList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } +} diff --git a/lib/modules/user/level/level_page.dart b/lib/modules/user/level/level_page.dart new file mode 100644 index 0000000..f4fd9e3 --- /dev/null +++ b/lib/modules/user/level/level_page.dart @@ -0,0 +1,99 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/modules/user/level/user/user_level_page.dart'; +import 'package:yumi/modules/user/level/wealth/wealth_level_page.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +class LevelPage extends StatefulWidget { + @override + _LevelPageState createState() => _LevelPageState(); +} + +class _LevelPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = [WealthLevelPage(), UserLevelPage()]; + int type = 0; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: _pages.length, vsync: this); + _tabController.addListener(() { + type = _tabController.index; + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + appBar: SocialChatStandardAppBar( + backButtonColor: Colors.white, + title: SCAppLocalizations.of(context)!.level, + actions: [], + ), + backgroundColor: Colors.transparent, + body: SafeArea( + top: false, + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: TabBar( + tabs: + [ + SCAppLocalizations.of(context)!.wealthLevel, + SCAppLocalizations.of(context)!.userLevel, + ].map((e) => Tab(text: e)).toList(), + indicator: BoxDecoration(), + //indicatorColor: Color(0xFF9F44F9), + indicatorSize: TabBarIndicatorSize.label, + labelPadding: EdgeInsets.only(left: 20, right: 20), + //Tab之间的间距,默认是kTabLabelPadding + isScrollable: false, + controller: _tabController, + labelColor: SocialChatTheme.primaryLight, + dividerColor: Colors.transparent, + unselectedLabelColor: Colors.white54, + labelStyle: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + unselectedLabelStyle: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(width: 10.w), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/user/level/user/user_level_page.dart b/lib/modules/user/level/user/user_level_page.dart new file mode 100644 index 0000000..7bb4e41 --- /dev/null +++ b/lib/modules/user/level/user/user_level_page.dart @@ -0,0 +1,219 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_string_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +import '../../../../shared/data_sources/models/enum/sc_level_type.dart'; + +class UserLevelPage extends StatefulWidget { + @override + _UserLevelPageState createState() => _UserLevelPageState(); +} + +class _UserLevelPageState extends State { + SCUserLevelExpRes? res; + + @override + void initState() { + super.initState(); + SCAccountRepository() + .userLevelConsumptionExp( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + SCLevelType.CHARM.name, + ) + .then((value) { + res = value; + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return res != null + ? SingleChildScrollView( + child: Column( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: 25.w, vertical: 15.w), + height: 108.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/level/sc_icon_user_level_user_info_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + SizedBox(width: 11.w), + head( + url: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userAvatar ?? + "", + width: 80.w, + ), + SizedBox(width: 18.w), + SizedBox( + width: 200.w, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + socialchatNickNameText( + maxWidth: 115.w, + fontWeight: FontWeight.bold, + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname ?? + "", + fontSize: 15.sp, + type: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.getVIP() + ?.name ?? + "", + needScroll: + (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 10, + ), + SizedBox(width: 3.w), + getUserLevel( + res?.level ?? 0, + width: 58.w, + height: 30.w, + ), + ], + ), + SizedBox(height: 3.w), + Stack( + children: [ + Container( + width: 185.w, + height: 6.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white24, + ), + ), + ], + ), + SizedBox(height: 5.w), + Row( + children: [ + SizedBox(width: 5.w), + text( + res?.thatExperienceFormat ?? "", + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 12.sp, + ), + Spacer(), + text( + SCStringUtils.formatNumericValue( + getLevelTotalExperience(), + ), + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 12.sp, + ), + SizedBox(width: 20.w), + ], + ), + ], + ), + ), + ], + ), + ), + text( + SCAppLocalizations.of(context)!.medalAndAvatarFrameRewards, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + textColor: SocialChatTheme.primaryLight, + ), + text( + SCAppLocalizations.of(context)!.higherLevelFancierAvatarFrame, + fontSize: 12.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + Image.asset("sc_images/level/sc_icon_user_level_center_bg_1.png"), + Image.asset("sc_images/level/sc_icon_user_level_center_bg_2.png"), + text( + SCAppLocalizations.of(context)!.howToUpgrade, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + textColor: SocialChatTheme.primaryLight, + ), + SizedBox(height: 3), + text( + SCAppLocalizations.of(context)!.spendCoinsToGainExperiencePoints, + fontSize: 12.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + SizedBox(height: 25.w), + GestureDetector( + child: Container( + height: 45.w, + width: 180.w, + alignment: AlignmentDirectional.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99.w), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.toConsume, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + SCNavigatorUtils.push(context, StoreRoute.list); + }, + ), + SizedBox(height: 20.w,) + ], + ), + ) + : Container(); + } + + double getLevelExperience(double width) { + int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0"); + int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0"); + int levelExp = thatExp + nexExp; + return (thatExp / levelExp) * width; + } + + num getLevelTotalExperience() { + int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0"); + int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0"); + int levelExp = thatExp + nexExp; + return levelExp; + } +} diff --git a/lib/modules/user/level/wealth/wealth_level_page.dart b/lib/modules/user/level/wealth/wealth_level_page.dart new file mode 100644 index 0000000..9575839 --- /dev/null +++ b/lib/modules/user/level/wealth/wealth_level_page.dart @@ -0,0 +1,220 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:marquee/marquee.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_string_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +import '../../../../shared/data_sources/models/enum/sc_level_type.dart'; + +class WealthLevelPage extends StatefulWidget { + @override + _WealthLevelPageState createState() => _WealthLevelPageState(); +} + +class _WealthLevelPageState extends State { + SCUserLevelExpRes? res; + + @override + void initState() { + super.initState(); + SCAccountRepository() + .userLevelConsumptionExp( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + SCLevelType.WEALTH.name, + ) + .then((value) { + res = value; + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return res != null + ? SingleChildScrollView( + child: Column( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: 25.w, vertical: 15.w), + height: 108.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/level/sc_icon_user_level_wealth_info_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + SizedBox(width: 11.w), + head( + url: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userAvatar ?? + "", + width: 80.w, + ), + SizedBox(width: 18.w), + SizedBox( + width: 200.w, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + socialchatNickNameText( + maxWidth: 125.w, + fontWeight: FontWeight.bold, + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname ?? + "", + fontSize: 15.sp, + type: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.getVIP() + ?.name ?? + "", + needScroll: + (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 10, + ), + SizedBox(width: 3.w), + getWealthLevel( + res?.level ?? 0, + width: 58.w, + height: 30.w, + ), + ], + ), + SizedBox(height: 3.w), + Stack( + children: [ + Container( + width: 185.w, + height: 6.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white24, + ), + ), + ], + ), + SizedBox(height: 5.w), + Row( + children: [ + SizedBox(width: 5.w), + text( + res?.thatExperienceFormat ?? "", + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 12.sp, + ), + Spacer(), + text( + SCStringUtils.formatNumericValue( + getLevelTotalExperience(), + ), + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 12.sp, + ), + SizedBox(width: 20.w), + ], + ), + ], + ), + ), + ], + ), + ), + text( + SCAppLocalizations.of(context)!.medalAndAvatarFrameRewards, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + textColor: SocialChatTheme.primaryLight, + ), + text( + SCAppLocalizations.of(context)!.higherLevelFancierAvatarFrame, + fontSize: 12.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + Image.asset("sc_images/level/sc_icon_user_wealth_center_bg_1.png"), + Image.asset("sc_images/level/sc_icon_user_wealth_center_bg_2.png"), + text( + SCAppLocalizations.of(context)!.howToUpgrade, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + textColor:SocialChatTheme.primaryLight, + ), + SizedBox(height: 3), + text( + SCAppLocalizations.of(context)!.spendCoinsToGainExperiencePoints, + fontSize: 12.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + SizedBox(height: 25.w), + GestureDetector( + child: Container( + height: 45.w, + width: 180.w, + alignment: AlignmentDirectional.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99.w), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.toConsume, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + SCNavigatorUtils.push(context, StoreRoute.list); + }, + ), + SizedBox(height: 20.w,) + ], + ), + ) + : Container(); + } + + double getLevelExperience(double width) { + int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0"); + int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0"); + int levelExp = thatExp + nexExp; + return (thatExp / levelExp) * width; + } + + num getLevelTotalExperience() { + int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0"); + int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0"); + int levelExp = thatExp + nexExp; + return levelExp; + } +} diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart new file mode 100644 index 0000000..52afcca --- /dev/null +++ b/lib/modules/user/me_page2.dart @@ -0,0 +1,571 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/modules/user/settings/settings_route.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; + +class MePage2 extends StatefulWidget { + const MePage2({super.key}); + + @override + State createState() => _MePage2State(); +} + +class _MePage2State extends State { + Map _counterMap = {}; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadPageData(); + }); + } + + void _loadPageData() { + final profileManager = Provider.of( + context, + listen: false, + ); + profileManager.fetchUserProfileData(); + profileManager.balance(); + _loadCounter(); + } + + Future _loadCounter() async { + final userId = AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; + if (userId.isEmpty) return; + + try { + final userCounterList = await SCAccountRepository().userCounter(userId); + if (!mounted) return; + setState(() { + _counterMap = Map.fromEntries( + userCounterList.map( + (counter) => MapEntry(counter.counterType ?? "", counter), + ), + ); + }); + } catch (_) {} + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: true, + bottom: false, + child: Consumer( + builder: (context, profileManager, child) { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.fromLTRB(10.w, 12.w, 10.w, 24.w), + child: Column( + children: [ + _buildProfileSection(), + SizedBox(height: 12.w), + _buildStatsCard(), + SizedBox(height: 12.w), + _buildEntryRow(), + SizedBox(height: 12.w), + _buildWalletCard(), + SizedBox(height: 12.w), + _buildMenuCard2(), + SizedBox(height: 12.w), + _buildMenuCard([ + _MenuRowData( + title: SCAppLocalizations.of(context)!.settings, + assetIcon: 'sc_images/index/sc_icon_settings.png', + onTap: () { + SCNavigatorUtils.push( + context, + SettingsRoute.settings, + replace: false, + ); + }, + ), + _MenuRowData( + title: SCAppLocalizations.of(context)!.language, + assetIcon: 'sc_images/general/sc_icon_setting_language.png', + onTap: () { + SCNavigatorUtils.push(context, SCMainRoute.language); + }, + ), + ]), + ], + ), + ); + }, + ), + ); + } + + Widget _buildProfileSection() { + final profile = AccountStorage().getCurrentUser()?.userProfile; + + return SCDebounceWidget( + onTap: () { + SCNavigatorUtils.push( + context, + '${SCMainRoute.person}?isMe=true&tageId=${profile?.id ?? ""}', + replace: false, + ); + }, + child: Column( + children: [ + SizedBox( + width: 240.w, + height: 130.w, + child: Stack( + alignment: Alignment.topCenter, + children: [ + head( + url: profile?.userAvatar ?? '', + width: 96.w, + height: 96.w, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1), + headdress: profile?.getHeaddress()?.sourceUrl, + showDefault: true, + ), + ], + ), + ), + SizedBox(height: 8.w), + Text( + profile?.userNickname ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 2.w), + Text( + 'ID:${profile?.account ?? ''}', + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Widget _buildStatsCard() { + return _buildGlassCard( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + radius: 8.w, + child: Row( + children: [ + _buildStatItem( + value: _counterText('INTERVIEW'), + label: SCAppLocalizations.of(context)!.vistors, + onTap: () => SCNavigatorUtils.push(context, SCMainRoute.vistors), + ), + _buildStatDivider(), + _buildStatItem( + value: _counterText('SUBSCRIPTION'), + label: SCAppLocalizations.of(context)!.following, + onTap: () => SCNavigatorUtils.push(context, SCMainRoute.follow), + ), + _buildStatDivider(), + _buildStatItem( + value: _counterText('FANS'), + label: SCAppLocalizations.of(context)!.friends, + onTap: () => SCNavigatorUtils.push(context, SCMainRoute.fans), + ), + ], + ), + ); + } + + Widget _buildEntryRow() { + return Row( + children: [ + _buildEntryItem( + title: SCAppLocalizations.of(context)!.store, + iconPath: 'sc_images/index/sc_icon_shop.png', + onTap: () => SCNavigatorUtils.push(context, StoreRoute.list), + ), + SizedBox(width: 10.w), + _buildEntryItem( + title: SCAppLocalizations.of(context)!.bag, + iconPath: 'sc_images/index/sc_icon_bag.png', + onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags), + ), + SizedBox(width: 10.w), + _buildEntryItem( + title: SCAppLocalizations.of(context)!.level, + iconPath: 'sc_images/index/sc_icon_level.png', + onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList), + ), + ], + ); + } + + Widget _buildWalletCard() { + return SCDebounceWidget( + onTap: () => SCNavigatorUtils.push(context, WalletRoute.recharge), + child: Container( + width: double.infinity, + height: 90.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + image: const DecorationImage( + image: AssetImage('sc_images/index/sc_icon_wallet_bg.png'), + fit: BoxFit.fill, + ), + ), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + SCAppLocalizations.of(context)!.wallet, + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + color: Colors.white, + ), + ), + SizedBox(height: 4.w), + Row( + children: [ + Image.asset( + 'sc_images/general/sc_icon_jb.png', + width: 28.w, + height: 28.w, + ), + SizedBox(width: 6.w), + Consumer( + builder: (context, ref, child) { + return Text( + ':${_balanceText(ref.myBalance)}', + style: TextStyle( + color: Colors.white, + fontSize: 24.sp, + fontWeight: FontWeight.w700, + fontStyle: FontStyle.italic, + ), + ); + }, + ), + ], + ), + ], + ), + ), + Image.asset( + 'sc_images/index/sc_icon_wallet_icon.png', + width: 68.w, + height: 68.w, + ), + ], + ), + ), + ); + } + + Widget _buildMenuCard2() { + final userProfile = context.watch(); + final items = <_MenuRowData>[]; + + // 1. 主播相关 + if (userProfile.userIdentity?.anchor ?? false) { + if (userProfile.userIdentity?.agent ?? false) { + items.add( + _MenuRowData( + title: SCAppLocalizations.of(context)!.agentCenter, + assetIcon: 'sc_images/index/at_icon_agent_center.png', + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.agencyCenterUrl)}&showTitle=false", + ); + }, + ), + ); + } else { + items.add( + _MenuRowData( + title: SCAppLocalizations.of(context)!.hostCenter, + assetIcon: 'sc_images/index/at_icon_host_center.png', + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.hostCenterUrl)}&showTitle=false", + ); + }, + ), + ); + } + } + + // 2. BD 相关(非管理员时) + if (!(userProfile.userIdentity?.admin ?? false)) { + if (userProfile.userIdentity?.bdLeader ?? false) { + items.add( + _MenuRowData( + title: SCAppLocalizations.of(context)!.bdLeader, + assetIcon: 'sc_images/index/at_icon_bd_leader.png', + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdLeaderUrl)}&showTitle=false", + ); + }, + ), + ); + } else if (userProfile.userIdentity?.bd ?? false) { + items.add( + _MenuRowData( + title: SCAppLocalizations.of(context)!.bdCenter, + assetIcon: 'sc_images/index/at_icon_bd_center.png', + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.bdCenterUrl)}&showTitle=false", + ); + }, + ), + ); + } + } + + // 3. 货运代理 + if (userProfile.userIdentity?.freightAgent ?? false) { + items.add( + _MenuRowData( + title: SCAppLocalizations.of(context)!.rechargeAgency, + assetIcon: 'sc_images/index/at_icon_recharge_agency.png', + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.coinSellerUrl)}&showTitle=false", + ); + }, + ), + ); + } + + // 4. 管理员(注意:你的原逻辑中管理员和上面BD逻辑有重叠,这里按原样单独处理) + if (userProfile.userIdentity?.admin ?? false) { + items.add( + _MenuRowData( + title: SCAppLocalizations.of(context)!.adminCenter, + assetIcon: 'sc_images/index/at_icon_admin_center.png', + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.adminUrl)}&showTitle=false", + ); + }, + ), + ); + } + return _buildMenuCard(items); + } + + Widget _buildMenuCard(List<_MenuRowData> items) { + // 没有任何项时,不显示整个卡片 + if (items.isEmpty) return const SizedBox.shrink(); + return _buildGlassCard( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.w), + radius: 4.w, + child: Column( + children: [ + for (int i = 0; i < items.length; i++) ...[_buildMenuRow(items[i])], + ], + ), + ); + } + + Widget _buildMenuRow(_MenuRowData item) { + return SCDebounceWidget( + onTap: item.onTap, + child: SizedBox( + height: 46.w, + child: Row( + children: [ + Image.asset( + item.assetIcon!, + width: 24.w, + height: 24.w, + color: Colors.white, + ), + + SizedBox(width: 12.w), + Expanded( + child: Text( + item.title, + style: TextStyle( + color: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + Icon( + SCGlobalConfig.lang == 'ar' + ? Icons.keyboard_arrow_left + : Icons.keyboard_arrow_right, + color: Colors.white, + size: 18.w, + ), + ], + ), + ), + ); + } + + Widget _buildEntryItem({ + required String title, + required String iconPath, + required VoidCallback onTap, + }) { + return Expanded( + child: SCDebounceWidget( + onTap: onTap, + child: Container( + height: 110.w, + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('sc_images/person/sc_icon_me_menu_1_bg.png'), + fit: BoxFit.fill, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(iconPath, width: 54.w, height: 54.w), + SizedBox(height: 4.w), + Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatItem({ + required String value, + required String label, + required VoidCallback onTap, + }) { + return Expanded( + child: SCDebounceWidget( + onTap: onTap, + child: Column( + children: [ + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 1.w), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + Widget _buildStatDivider() { + return Container( + width: 0.5.w, + height: 16.w, + color: const Color(0xFFE6E6E6).withOpacity(0.8), + margin: EdgeInsets.symmetric(horizontal: 6.w), + ); + } + + Widget _buildGlassCard({ + required Widget child, + required EdgeInsets padding, + required double radius, + }) { + return Container( + width: double.infinity, + padding: padding, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radius), + border: Border.all(color: const Color(0xFFB4FCCE), width: 1.w), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF0A7D49).withOpacity(0.20), + const Color(0xFF031513).withOpacity(0.35), + ], + ), + ), + child: child, + ); + } + + String _counterText(String type) { + return _counterMap[type]?.quantity ?? '0'; + } + + String _balanceText(double balance) { + if (balance >= 1000) { + return '${(balance / 1000).toStringAsFixed(2)}k'; + } + return balance.toStringAsFixed(balance % 1 == 0 ? 0 : 2); + } +} + +class _MenuRowData { + const _MenuRowData({ + required this.title, + required this.onTap, + this.assetIcon, + this.iconData, + }); + + final String title; + final String? assetIcon; + final IconData? iconData; + final VoidCallback onTap; +} diff --git a/lib/modules/user/my_items/chatbox/bags_chatbox_page.dart b/lib/modules/user/my_items/chatbox/bags_chatbox_page.dart new file mode 100644 index 0000000..c64a18a --- /dev/null +++ b/lib/modules/user/my_items/chatbox/bags_chatbox_page.dart @@ -0,0 +1,383 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/widgets/bag/props_bag_chatbox_detail_dialog.dart'; +import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; +import 'package:yumi/modules/store/store_route.dart'; + +import '../../../../shared/data_sources/models/enum/sc_props_type.dart'; +import '../../../../ui_kit/components/sc_debounce_widget.dart'; +import '../../../../ui_kit/theme/socialchat_theme.dart'; + +///背包-气泡框 +class BagsChatboxPage extends SCPageList { + @override + _BagsChatboxPageState createState() => _BagsChatboxPageState(); +} + +class _BagsChatboxPageState + extends SCPageListState { + ///自己当前佩戴的气泡框 + PropsResources? myChatbox; + BagsListRes? selecteChatbox; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.88, + ); + myChatbox = AccountStorage().getChatbox(); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + Expanded(child: buildList(context)), + selecteChatbox != null ? SizedBox(height: 10.w) : Container(), + selecteChatbox != null + ? Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: Column( + children: [ + SizedBox(height: 5.w), + Row( + spacing: 5.w, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${SCAppLocalizations.of(context)!.expirationTime}:", + textColor: Colors.white, + fontSize: 12.sp, + ), + CountdownTimer( + fontSize: 11.sp, + color: SocialChatTheme.primaryLight, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + selecteChatbox?.expireTime ?? 0, + ), + ), + ], + ), + SizedBox(height: 10.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.renewal, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: Colors.white, + ), + ), + onTap: () { + ///过期 续费操作 + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: + myChatbox?.id == + selecteChatbox?.propsResources?.id + ? Colors.white + : SocialChatTheme.primaryLight, + ), + child: text( + myChatbox?.id == + selecteChatbox?.propsResources?.id + ? SCAppLocalizations.of(context)!.inUse + : SCAppLocalizations.of(context)!.use, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: + myChatbox?.id != + selecteChatbox?.propsResources?.id + ? Colors.white + : Color(0xffB1B1B1), + ), + ), + onTap: () { + if (myChatbox?.id != + selecteChatbox?.propsResources?.id) { + ///佩戴 + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selecteChatbox!, false); + }, + ); + }, + ); + } else { + ///卸下 + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUnUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selecteChatbox!, true); + }, + ); + }, + ); + } + }, + ), + ], + ), + SizedBox(height: 10.w), + ], + ), + ) + : Container(), + ], + ), + ); + } + + @override + Widget buildItem(BagsListRes res) { + return GestureDetector( + child: Container( + decoration:BoxDecoration( + color:Color(0xff18F2B1).withOpacity(0.1), + border: Border.all( + color: + selecteChatbox?.propsResources?.id == res.propsResources?.id + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ) , + child: Stack( + alignment: Alignment.center, + children: [ + Column( + children: [ + SizedBox(height: 25.w), + netImage( + url: res.propsResources?.cover ?? "", + height: 35.w, + fit: BoxFit.contain, + ), + SizedBox(height: 8.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + text( + res.propsResources?.name ?? "", + textColor: Colors.black, + fontSize: 12.sp, + ), + ], + ), + ], + ), + PositionedDirectional( + top: 0, + start: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(6.w), + topEnd: Radius.circular(12.w), + bottomEnd: Radius.circular(12.w), + ), + ), + child: Row( + children: [ + CountdownTimer( + fontSize: 9.sp, + color: Colors.white, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + res.expireTime ?? 0, + ), + ), + SizedBox(width: 3.w), + Image.asset( + "sc_images/store/sc_icon_bag_clock.png", + width: 22.w, + height: 22.w, + ), + ], + ), + ), + ), + PositionedDirectional( + bottom: 5.w, + end: 5.w, + child: GestureDetector( + onTap: () { + selecteChatbox = res; + setState(() {}); + _showDetail(res); + }, + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.all(5.w), + child: Image.asset( + "sc_images/store/sc_icon_shop_item_search.png", + width: 18.w, + height: 18.w, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + onTap: () { + selecteChatbox = res; + setState(() {}); + }, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + var storeList = await SCStoreRepositoryImp().storeBackpack( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + SCPropsType.CHAT_BUBBLE.name, + ); + for (var v in storeList) { + if (v.propsResources?.id == myChatbox?.id) { + selecteChatbox = v; + continue; + } + } + onSuccess(storeList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + void _showDetail(BagsListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsBagChatboxDetailDialog(res); + }, + onDismiss: () { + myChatbox = AccountStorage().getChatbox(); + setState(() {}); + }, + ); + } + + void _use(BagsListRes res, bool unload) { + SCLoadingManager.show(context: context); + SCStoreRepositoryImp() + .switchPropsUse( + SCPropsType.CHAT_BUBBLE.name, + res.propsResources?.id ?? "", + unload, + ) + .then((value) async { + setState(() {}); + if (!unload) { + myChatbox = res.propsResources; + SCTts.show(SCAppLocalizations.of(context)!.successfulWear); + } else { + myChatbox = null; + SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded); + } + Provider.of( + context, + listen: false, + ).fetchUserProfileData(loadGuardCount: false); + SCLoadingManager.hide(); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} diff --git a/lib/modules/user/my_items/headdress/bags_headdress_page.dart b/lib/modules/user/my_items/headdress/bags_headdress_page.dart new file mode 100644 index 0000000..25009e4 --- /dev/null +++ b/lib/modules/user/my_items/headdress/bags_headdress_page.dart @@ -0,0 +1,380 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/ui_kit/widgets/bag/props_bag_headdress_detail_dialog.dart'; +import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; +import 'package:provider/provider.dart'; + +import '../../../../shared/data_sources/models/enum/sc_props_type.dart'; + +///背包-头饰 +class BagsHeaddressPage extends SCPageList { + @override + _BagsHeaddressPageState createState() => _BagsHeaddressPageState(); +} + +class _BagsHeaddressPageState + extends SCPageListState { + ///自己当前佩戴的头饰 + PropsResources? myHeaddress; + BagsListRes? selecteHeaddress; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.88, + ); + myHeaddress = AccountStorage().getHeaddress(); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + Expanded(child: buildList(context)), + selecteHeaddress != null ? SizedBox(height: 10.w) : Container(), + selecteHeaddress != null + ? Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: Column( + children: [ + SizedBox(height: 5.w,), + Row( + spacing: 5.w, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${SCAppLocalizations.of(context)!.expirationTime}:", + textColor: Colors.white, + fontSize: 12.sp, + ), + CountdownTimer( + fontSize: 11.sp, + color: SocialChatTheme.primaryLight, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + selecteHeaddress?.expireTime ?? 0, + ), + ), + ], + ), + SizedBox(height: 10.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.renewal, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: Colors.white, + ), + ), + onTap: () { + ///过期 续费操作 + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(55.w), + color: myHeaddress?.id == + selecteHeaddress?.propsResources?.id + ?Colors.white:SocialChatTheme.primaryLight, + ), + child: text( + myHeaddress?.id == + selecteHeaddress?.propsResources?.id + ? SCAppLocalizations.of(context)!.inUse + : SCAppLocalizations.of(context)!.use, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: + myHeaddress?.id != + selecteHeaddress?.propsResources?.id + ? Colors.white + : Color(0xffB1B1B1), + ), + ), + onTap: () { + if (myHeaddress?.id != + selecteHeaddress?.propsResources?.id) { + ///佩戴 + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selecteHeaddress!, false); + }, + ); + }, + ); + } else { + ///卸下 + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUnUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selecteHeaddress!, true); + }, + ); + }, + ); + } + }, + ), + ], + ), + SizedBox(height: 10.w), + ], + ), + ) + : Container(), + ], + ), + ); + } + + @override + Widget buildItem(BagsListRes res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color:Color(0xff18F2B1).withOpacity(0.1), + border: Border.all( + color: + selecteHeaddress?.propsResources?.id == res.propsResources?.id + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Column( + children: [ + SizedBox(height: 25.w), + netImage( + url: res.propsResources?.cover ?? "", + width: 55.w, + height: 55.w, + ), + SizedBox(height: 8.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + text( + res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 12.sp, + ), + ], + ), + ], + ), + PositionedDirectional( + top: 0, + start: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(6.w), + topEnd: Radius.circular(12.w), + bottomEnd: Radius.circular(12.w), + ), + ), + child: Row( + children: [ + CountdownTimer( + fontSize: 9.sp, + color: Colors.white, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + res.expireTime ?? 0, + ), + ), + SizedBox(width: 3.w), + Image.asset( + "sc_images/store/sc_icon_bag_clock.png", + width: 22.w, + height: 22.w, + ), + ], + ), + ), + ), + PositionedDirectional( + bottom: 5.w, + end: 5.w, + child: GestureDetector( + onTap: () { + selecteHeaddress = res; + setState(() {}); + _showDetail(res); + }, + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.all(5.w), + child: Image.asset( + "sc_images/store/sc_icon_shop_item_search.png", + width: 13.w, + height: 13.w, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ), + onTap: () { + selecteHeaddress = res; + setState(() {}); + }, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + var storeList = await SCStoreRepositoryImp().storeBackpack( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + SCPropsType.AVATAR_FRAME.name, + ); + for (var v in storeList) { + if (v.propsResources?.id == myHeaddress?.id) { + selecteHeaddress = v; + continue; + } + } + onSuccess(storeList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + void _showDetail(BagsListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsBagHeaddressDetailDialog(res); + }, + onDismiss: () { + myHeaddress = AccountStorage().getHeaddress(); + setState(() {}); + }, + ); + } + + void _use(BagsListRes res, bool unload) { + SCLoadingManager.show(context: context); + SCStoreRepositoryImp() + .switchPropsUse( + SCPropsType.AVATAR_FRAME.name, + res.propsResources?.id ?? "", + unload, + ) + .then((value) async { + setState(() {}); + if (!unload) { + myHeaddress = res.propsResources; + SCTts.show(SCAppLocalizations.of(context)!.successfulWear); + } else { + myHeaddress = null; + SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded); + } + Provider.of( + context, + listen: false, + ).fetchUserProfileData(loadGuardCount: false); + SCLoadingManager.hide(); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} diff --git a/lib/modules/user/my_items/mountains/bags_mountains_page.dart b/lib/modules/user/my_items/mountains/bags_mountains_page.dart new file mode 100644 index 0000000..11e3dbf --- /dev/null +++ b/lib/modules/user/my_items/mountains/bags_mountains_page.dart @@ -0,0 +1,368 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/widgets/bag/props_bag_mountains_detail_dialog.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; +import 'package:yumi/modules/store/store_route.dart'; + +import '../../../../shared/data_sources/models/enum/sc_props_type.dart'; +import '../../../../ui_kit/components/sc_debounce_widget.dart'; +import '../../../../ui_kit/components/dialog/dialog_base.dart'; +import '../../../../ui_kit/theme/socialchat_theme.dart'; + +///坐骑 +class BagsMountainsPage extends SCPageList { + @override + _BagsMountainsPageState createState() => _BagsMountainsPageState(); +} + +class _BagsMountainsPageState + extends SCPageListState { + BagsListRes? selecteMountains; + + ///自己当前佩戴的坐骑 + PropsResources? myMountains; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.88, + ); + myMountains = AccountStorage().getMountains(); + loadData(1); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + Expanded(child: buildList(context)), + selecteMountains != null ? SizedBox(height: 10.w) : Container(), + selecteMountains != null + ? Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: Column( + children: [ + SizedBox(height: 5.w), + Row( + spacing: 5.w, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${SCAppLocalizations.of(context)!.expirationTime}:", + textColor: Colors.white, + fontSize: 12.sp, + ), + CountdownTimer( + fontSize: 11.sp, + color: SocialChatTheme.primaryLight, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + selecteMountains?.expireTime ?? 0, + ), + ), + ], + ), + SizedBox(height: 10.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.renewal, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: Colors.white, + ), + ), + onTap: () { + ///过期 续费操作 + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: + myMountains?.id == + selecteMountains?.propsResources?.id + ? Colors.white + : SocialChatTheme.primaryLight, + ), + child: text( + myMountains?.id == + selecteMountains?.propsResources?.id + ? SCAppLocalizations.of(context)!.inUse + : SCAppLocalizations.of(context)!.use, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: + myMountains?.id != + selecteMountains?.propsResources?.id + ? Colors.white + : Color(0xffB1B1B1), + ), + ), + onTap: () { + if (myMountains?.id != + selecteMountains?.propsResources?.id) { + ///佩戴 + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selecteMountains!, false); + }, + ); + }, + ); + } else { + ///卸下 + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUnUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selecteMountains!, true); + }, + ); + }, + ); + } + }, + ), + ], + ), + SizedBox(height: 10.w), + ], + ), + ) + : Container(), + ], + ), + ); + } + + @override + Widget buildItem(BagsListRes res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color:Color(0xff18F2B1).withOpacity(0.1), + border: Border.all( + color: + selecteMountains?.propsResources?.id == res.propsResources?.id + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Column( + children: [ + SizedBox(height: 25.w), + netImage( + url: res.propsResources?.cover ?? "", + width: 55.w, + height: 55.w, + ), + SizedBox(height: 8.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + text( + res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 12.sp, + ), + ], + ), + ], + ), + PositionedDirectional( + top: 0, + start: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(6.w), + topEnd: Radius.circular(12.w), + bottomEnd: Radius.circular(12.w), + ), + ), + child: Row( + children: [ + CountdownTimer( + fontSize: 9.sp, + color: Colors.white, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + res.expireTime ?? 0, + ), + ), + SizedBox(width: 3.w), + Image.asset( + "sc_images/store/sc_icon_bag_clock.png", + width: 22.w, + height: 22.w, + ), + ], + ), + ), + ), + ], + ), + ), + onTap: () { + selecteMountains = res; + setState(() {}); + }, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + var storeList = await SCStoreRepositoryImp().storeBackpack( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + SCPropsType.RIDE.name, + ); + for (var v in storeList) { + if (v.propsResources?.id == myMountains?.id) { + selecteMountains = v; + continue; + } + } + onSuccess(storeList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + void _showDetail(BagsListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsBagMountainsDetailDialog(res); + }, + onDismiss: () { + myMountains = AccountStorage().getMountains(); + setState(() {}); + }, + ); + } + + void _use(BagsListRes res, bool unload) { + SCLoadingManager.show(context: context); + SCStoreRepositoryImp() + .switchPropsUse( + SCPropsType.RIDE.name, + res.propsResources?.id ?? "", + unload, + ) + .then((value) async { + setState(() {}); + if (!unload) { + myMountains = res.propsResources; + SCTts.show(SCAppLocalizations.of(context)!.successfulWear); + } else { + myMountains = null; + SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded); + } + Provider.of( + context, + listen: false, + ).fetchUserProfileData(loadGuardCount: false); + SCLoadingManager.hide(); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} diff --git a/lib/modules/user/my_items/my_items_page.dart b/lib/modules/user/my_items/my_items_page.dart new file mode 100644 index 0000000..3a6a5d0 --- /dev/null +++ b/lib/modules/user/my_items/my_items_page.dart @@ -0,0 +1,142 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/modules/user/my_items/theme/bags_tab_theme_page.dart'; +import 'package:tancent_vap/utils/constant.dart'; +import 'package:tancent_vap/widgets/vap_view.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/ui_kit/widgets/headdress/headdress_widget.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/modules/user/my_items/chatbox/bags_chatbox_page.dart'; +import 'package:yumi/modules/user/my_items/headdress/bags_headdress_page.dart'; +import 'package:yumi/modules/user/my_items/mountains/bags_mountains_page.dart'; + +import '../../../app/constants/sc_global_config.dart'; +import '../../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; + +class MyItemsPage extends StatefulWidget { + @override + _MyItemsPageState createState() => _MyItemsPageState(); +} + +class _MyItemsPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = []; + final List _tabs = []; + + @override + void initState() { + super.initState(); + _pages.add(BagsHeaddressPage()); + _pages.add( + BagsMountainsPage(), + ); + _pages.add( + BagsTabThemePage(), + ); + _pages.add(BagsChatboxPage()); + _tabController = TabController(length: _pages.length, vsync: this); + _tabController.addListener(() { + }); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.headdress)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.mountains)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.theme)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chatBox)); + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getLanguagePageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.myItems, + actions: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.all(5.w), + margin: EdgeInsetsDirectional.only(end: 10.w), + child: Image.asset( + "sc_images/store/sc_icon_bag_shop.png", + width: 20.w, + height: 20.w, + ), + ), + onTap: () { + SCNavigatorUtils.push(context, StoreRoute.list, replace: true); + }, + ), + ], + ), + body: SafeArea(top:false,child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TabBar( + tabAlignment: TabAlignment.center, + isScrollable: true, + labelColor: Colors.white, + unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .copyWith( + fontSize: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabUnselectedLabelStyle() + .fontSize + ?.sp, + ), + // indicatorPadding: EdgeInsets.symmetric( + // vertical: 5.w, + // horizontal: 15.w, + // ), + indicator: SCFixedWidthTabIndicator( + width: 15.w, + color: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabIndicatorColor(), + ), + dividerColor: + SCGlobalConfig.businessLogicStrategy + .getStorePageTabDividerColor(), + controller: _tabController, + tabs: _tabs, + ), + ], + ), + SizedBox(height: 5.w), + Expanded( + child: TabBarView(controller: _tabController, children: _pages), + ), + ], + ),), + ), + ], + ); + } +} diff --git a/lib/modules/user/my_items/theme/bags_tab_theme_page.dart b/lib/modules/user/my_items/theme/bags_tab_theme_page.dart new file mode 100644 index 0000000..d9628cd --- /dev/null +++ b/lib/modules/user/my_items/theme/bags_tab_theme_page.dart @@ -0,0 +1,72 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/modules/room/them/custom/room_theme_custom_page.dart'; +import 'package:yumi/modules/user/my_items/theme/bags_theme_page.dart'; + +///背包-房间主题 +class BagsTabThemePage extends StatefulWidget { + + + @override + _BagsTabThemePageState createState() => _BagsTabThemePageState(); +} + +class _BagsTabThemePageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = []; + final List _tabs = []; + + @override + void initState() { + super.initState(); + _pages.add( + BagsThemePage(), + ); + _pages.add(RoomThemeCustomPage()); + _tabController = TabController(length: _pages.length, vsync: this); + _tabController.addListener(() {}); // 监听切换 + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.all)); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.custom)); + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: Column( + children: [ + TabBar( + tabAlignment: TabAlignment.center, + labelPadding: EdgeInsets.symmetric(horizontal: 18.w), + labelColor: Colors.white, + isScrollable: true, + indicator: BoxDecoration(), + unselectedLabelColor: Colors.white54, + labelStyle: TextStyle( + fontSize: 15.sp, + fontFamily: 'MyCustomFont', + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: TextStyle( + fontSize: 13.sp, + fontFamily: 'MyCustomFont', + fontWeight: FontWeight.w500, + ), + indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + Expanded( + child: TabBarView(controller: _tabController, children: _pages), + ), + ], + ), + ); + } +} diff --git a/lib/modules/user/my_items/theme/bags_theme_page.dart b/lib/modules/user/my_items/theme/bags_theme_page.dart new file mode 100644 index 0000000..1d3168b --- /dev/null +++ b/lib/modules/user/my_items/theme/bags_theme_page.dart @@ -0,0 +1,359 @@ +import 'dart:convert'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/ui_kit/widgets/bag/props_bag_theme_detail_dialog.dart'; +import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:provider/provider.dart'; +import '../../../../app/constants/sc_room_msg_type.dart'; +import '../../../../shared/tools/sc_loading_manager.dart'; +import '../../../../shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import '../../../../services/audio/rtc_manager.dart'; +import '../../../../services/audio/rtm_manager.dart'; +import '../../../../ui_kit/components/sc_debounce_widget.dart'; +import '../../../../ui_kit/components/dialog/dialog_base.dart'; +import '../../../../ui_kit/components/sc_tts.dart'; +import '../../../../ui_kit/widgets/room/room_msg_item.dart'; + +///背包-房间主题 +class BagsThemePage extends SCPageList { + @override + _BagsThemePageState createState() => _BagsThemePageState(); +} + +class _BagsThemePageState + extends SCPageListState { + SCRoomThemeListRes? selecteTheme; + + @override + void initState() { + super.initState(); + enablePullUp = false; + isGridView = true; + gridViewCount = 3; + padding = EdgeInsets.symmetric(horizontal: 6.w); + backgroundColor = Colors.transparent; + gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + mainAxisSpacing: 12.w, + crossAxisSpacing: 12.w, + childAspectRatio: 0.88, + ); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + Expanded(child: buildList(context)), + selecteTheme != null ? SizedBox(height: 10.w) : Container(), + selecteTheme != null + ? Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: Column( + children: [ + SizedBox(height: 5.w), + Row( + spacing: 5.w, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${SCAppLocalizations.of(context)!.expirationTime}:", + textColor: Colors.white, + fontSize: 12.sp, + ), + CountdownTimer( + fontSize: 11.sp, + color: SocialChatTheme.primaryLight, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + selecteTheme?.expireTime ?? 0, + ), + ), + ], + ), + SizedBox(height: 10.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.renewal, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: Colors.white, + ), + ), + onTap: () { + ///过期 续费操作 + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 120.w, + height: 35.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: + (selecteTheme?.useTheme ?? false) + ? Colors.white + : SocialChatTheme.primaryLight, + ), + child: text( + (selecteTheme?.useTheme ?? false) + ? SCAppLocalizations.of(context)!.inUse + : SCAppLocalizations.of(context)!.use, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + textColor: + !(selecteTheme?.useTheme ?? false) + ? Colors.white + : Color(0xffB1B1B1), + ), + ), + onTap: () { + if (!(selecteTheme?.useTheme ?? false)) { + ///佩戴 + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selecteTheme!, false); + }, + ); + }, + ); + } else { + ///卸下 + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.confirmUnUseTips, + btnText: + SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _use(selecteTheme!, true); + }, + ); + }, + ); + } + }, + ), + ], + ), + SizedBox(height: 10.w), + ], + ), + ) + : Container(), + ], + ), + ); + } + + @override + Widget buildItem(SCRoomThemeListRes res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + border: Border.all( + color: + selecteTheme?.id == res.id + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: Stack( + alignment: Alignment.center, + children: [ + Column( + children: [ + SizedBox(height: 25.w), + netImage( + url: res.themeBack ?? "", + width: 55.w, + height: 55.w, + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + SizedBox(height: 8.w), + ], + ), + PositionedDirectional( + top: 0, + start: 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 5.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(6.w), + topEnd: Radius.circular(12.w), + bottomEnd: Radius.circular(12.w), + ), + ), + child: Row( + children: [ + CountdownTimer( + fontSize: 9.sp, + color: Colors.white, + expiryDate: DateTime.fromMillisecondsSinceEpoch( + res.expireTime ?? 0, + ), + ), + SizedBox(width: 3.w), + Image.asset( + "sc_images/store/sc_icon_bag_clock.png", + width: 22.w, + height: 22.w, + ), + ], + ), + ), + ), + ], + ), + ), + onTap: () { + selecteTheme = res; + setState(() {}); + }, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + var storeList = await SCStoreRepositoryImp().roomThemeBackpack(); + for (var v in storeList) { + if (v.useTheme ?? false) { + selecteTheme = v; + continue; + } + } + onSuccess(storeList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } + + void _use(SCRoomThemeListRes res, bool unload) { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .themeSwitchUse(res.id ?? "", unload) + .then((value) async { + SCLoadingManager.hide(); + SmartDialog.dismiss(tag: "showPropsDetail"); + if (!unload) { + SCTts.show(SCAppLocalizations.of(context)!.successfulWear); + Provider.of( + context!, + listen: false, + ).updateRoomBG(res); + } else { + Provider.of( + context!, + listen: false, + ).updateRoomBG(SCRoomThemeListRes()); + selecteTheme = null; + SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded); + } + loadData(1); + + ///发送一条消息更新远端房间信息 + String? groupId = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.roomAccount; + if (groupId != null) { + Msg msg = Msg( + groupId: groupId, + msg: unload ? "" : jsonEncode(res.toJson()), + type: SCRoomMsgType.roomBGUpdate, + ); + Provider.of( + context!, + listen: false, + ).dispatchMessage(msg, addLocal: false); + } + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } + + void _showDetail(SCRoomThemeListRes res) { + SmartDialog.show( + tag: "showPropsDetail", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return PropsBagThemeDetailDialog(res, () { + loadData(1); + }); + }, + ); + } +} diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart new file mode 100644 index 0000000..45a0aea --- /dev/null +++ b/lib/modules/user/profile/person_detail_page.dart @@ -0,0 +1,1581 @@ +import 'dart:convert'; +import 'dart:ui'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/modules/user/profile/profile/sc_profile_page.dart'; +import 'package:yumi/modules/user/profile/props/sc_giftwall_page.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/tools/sc_user_utils.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +import '../../../app/constants/sc_screen.dart'; +import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; +import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; +import '../../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; +import '../../chat/chat_route.dart'; + +class PersonDetailPage extends StatefulWidget { + final String isMe; + final String tageId; + + const PersonDetailPage({Key? key, required this.isMe, required this.tageId}) + : super(key: key); + + @override + _PersonDetailPageState createState() => _PersonDetailPageState(); +} + +class _PersonDetailPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = []; + final List _tabs = []; + SCUserIdentityRes? userIdentity; + + bool isFollow = false; + bool isLoading = true; + bool isBlacklistLoading = true; + bool isBlacklist = false; + + Map counterMap = {}; + + // 添加滚动控制器 + final ScrollController _scrollController = ScrollController(); + double _opacity = 0.0; + + @override + void initState() { + super.initState(); + + // _pages.add(PersonDynamicListPage(false, widget.tageId)); + _pages.add(SCProfilePage(false, widget.tageId)); + _pages.add(SCGiftwallPage(widget.tageId)); + // _pages.add(RelationShipPage(false, widget.tageId)); + _tabController = TabController( + initialIndex: 0, + length: _pages.length, + vsync: this, + ); + _tabController.addListener(() { + setState(() {}); // 刷新UI + }); // 监听切换 + + // 监听滚动 + _scrollController.addListener(() { + if (_scrollController.hasClients) { + final offset = _scrollController.offset; + // 当滚动到一定位置时,头像和昵称应该完全显示在AppBar上 + // 这里计算透明度,可以根据需要调整 + final newOpacity = (offset / 320).clamp(0.0, 1.0); + if (newOpacity != _opacity) { + setState(() { + _opacity = newOpacity; + }); + } + } + }); + + Provider.of(context, listen: false) + .userProfile = null; + Provider.of( + context, + listen: false, + ).getUserInfoById(widget.tageId); + SCAccountRepository().userIdentity(userId: widget.tageId).then((v) { + userIdentity = v; + setState(() {}); + }); + if (widget.isMe != "true") { + SCAccountRepository() + .followCheck(widget.tageId) + .then((v) { + isFollow = v; + isLoading = false; + setState(() {}); + }) + .catchError((e) { + setState(() { + isLoading = false; + }); + }); + SCAccountRepository() + .blacklistCheck(widget.tageId) + .then((v) { + isBlacklist = v; + isBlacklistLoading = false; + setState(() {}); + if (isBlacklist) { + SCTts.show( + SCAppLocalizations.of(context)!.thisUserHasBeenBlacklisted, + ); + } + }) + .catchError((e) { + setState(() { + isBlacklistLoading = false; + }); + }); + } else { + isBlacklist = false; + isBlacklistLoading = false; + } + userCounter(widget.tageId); + } + + void userCounter(String userId) async { + counterMap.clear(); + var userCounterList = await SCAccountRepository().userCounter(userId); + counterMap = Map.fromEntries( + userCounterList.map( + (counter) => MapEntry(counter.counterType ?? "", counter), + ), + ); + setState(() {}); + } + + Widget _buildTab(int index, String text) { + final isSelected = _tabController.index == index; + return Tab(text: text); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + // _tabs.add(_buildTab(0, SCAppLocalizations.of(context)!.dynamicT)); + _tabs.add(_buildTab(0, SCAppLocalizations.of(context)!.aboutMe)); + _tabs.add(_buildTab(1, SCAppLocalizations.of(context)!.giftwall)); + // _tabs.add(_buildTab(3, SCAppLocalizations.of(context)!.relationShip)); + return Consumer( + builder: (context, ref, child) { + List backgroundPhotos = []; + backgroundPhotos = + (ref.userProfile?.backgroundPhotos ?? []) + .where((t) => t.status == 1) + .toList(); + return Directionality( + textDirection: + window.locale.languageCode == "ar" + ? TextDirection.rtl + : TextDirection.ltr, + child: SafeArea( + top: false, + child: Stack( + children: [ + backgroundPhotos.isNotEmpty + ? (isBlacklistLoading || isBlacklist + ? Container() + : Column( + children: [ + SizedBox( + height: 300.w, + child: CarouselSlider( + options: CarouselOptions( + height: 300.w, + autoPlay: backgroundPhotos.length > 1, + // 启用自动播放 + enlargeCenterPage: false, + // 居中放大当前页面 + aspectRatio: 1 / 1, + // 宽高比 + enableInfiniteScroll: true, + // 启用无限循环 + autoPlayAnimationDuration: Duration( + milliseconds: 800, + ), + // 自动播放动画时长 + viewportFraction: 1, + // 视口分数 + onPageChanged: (index, reason) { + setState(() {}); + }, + ), + items: + backgroundPhotos.map((item) { + return GestureDetector( + child: netImage( + url: item.url ?? "", + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), + onTap: () {}, + ); + }).toList(), + ), + ), + Transform.translate( + offset: Offset(0, -6.w), + child: Container( + decoration: BoxDecoration( + color: Color(0xff083b2f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.w), + topRight: Radius.circular(8.w), + ), + ), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + ), + ), + ], + )) + : Column( + children: [ + SizedBox( + height: 300.w, + child: Image.asset( + 'sc_images/person/sc_icon_my_head_bg_defalt.png', + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), + ), + Transform.translate( + offset: Offset(0, -6.w), + child: Container( + decoration: BoxDecoration( + color: Color(0xff083b2f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.w), + topRight: Radius.circular(8.w), + ), + ), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + ), + ), + ], + ), + Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + appBar: SocialChatStandardAppBar( + backButtonColor: Colors.white, + title: "", + leading: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: width(50), + height: height(30), + alignment: AlignmentDirectional.centerStart, + padding: EdgeInsetsDirectional.only(start: width(15)), + child: Icon( + window.locale.languageCode == "ar" + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + size: 28.w, + color: Colors.white, + ), + ), + ), + actions: [ + // 在AppBar中显示头像和昵称 + if (_opacity > 0.5) ...[ + SizedBox(width: 45.w), + head( + url: ref.userProfile?.userAvatar ?? "", + width: 50.w, + height: 50.w, + headdress: ref.userProfile?.getHeaddress()?.sourceUrl, + ), + SizedBox(width: 8.w), + socialchatNickNameText( + maxWidth: 135.w, + textColor: Colors.white, + ref.userProfile?.userNickname ?? "", + fontSize: 16.sp, + fontWeight: FontWeight.w600, + type: ref.userProfile?.getVIP()?.name ?? "", + needScroll: + (ref + .userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 13, + ), + SizedBox(width: 8.w), + ], + Spacer(), + Builder( + builder: (ct) { + return IconButton( + icon: + widget.isMe == "true" + ? Image.asset( + "sc_images/person/sc_icon_edit_user_info2.png", + width: 22.w, + color: Colors.white, + ) + : Icon( + Icons.more_horiz, + size: 22.w, + color: Colors.white, + ), + onPressed: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.edit, + replace: false, + ); + return; + } + SmartDialog.showAttach( + tag: "showUserInfoOptMenu", + targetContext: ct, + alignment: Alignment.bottomCenter, + maskColor: Colors.transparent, + animationType: SmartAnimationType.fade, + scalePointBuilder: + (selfSize) => Offset(selfSize.width, 10), + builder: (_) { + return GestureDetector( + child: Transform.translate( + offset: Offset(0, -10), + child: Container( + width: 163.w, + margin: EdgeInsetsDirectional.only( + end: 8.w, + ), + decoration: BoxDecoration( + color: Colors.white30, + borderRadius: BorderRadius.circular( + 4, + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 5.w, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + child: text( + SCAppLocalizations.of( + context, + )!.report, + textColor: Colors.white, + fontSize: 12.sp, + ), + onTap: () { + SmartDialog.dismiss( + tag: "showUserInfoOptMenu", + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.report}?type=user&tageId=${ref.userProfile?.id}", + replace: false, + ); + }, + ), + ((!(userIdentity?.admin ?? false) && + !(userIdentity + ?.superAdmin ?? + false)) && + ((Provider.of< + SocialChatUserProfileManager + >( + context, + listen: false, + ).userIdentity?.admin ?? + false))) + ? GestureDetector( + child: Container( + margin: EdgeInsets.only( + top: 5.w, + ), + child: text( + SCAppLocalizations.of( + context, + )!.userEditing, + letterSpacing: 0.1, + textColor: Colors.white, + fontSize: 12.sp, + ), + ), + onTap: () { + SmartDialog.dismiss( + tag: + "showUserInfoOptMenu", + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.editingUserRoomAdmin}?type=User&profile=${Uri.encodeComponent(jsonEncode(ref.userProfile?.toJson()))}", + ); + }, + ) + : Container(), + ((ref.userIdentity?.admin ?? + false) || + (ref + .userIdentity + ?.superFreightAgent ?? + false)) && + !((userIdentity?.admin ?? + false) || + (userIdentity + ?.superFreightAgent ?? + false)) + ? SizedBox(height: 5.w) + : Container(), + ((ref.userIdentity?.admin ?? + false) || + (ref + .userIdentity + ?.superFreightAgent ?? + false)) && + !((userIdentity?.admin ?? + false) || + (userIdentity + ?.superFreightAgent ?? + false)) + ? GestureDetector( + child: text( + letterSpacing: 0.1, + SCAppLocalizations.of( + context, + )!.becomeAgent, + textColor: Colors.white, + fontSize: 12.sp, + ), + onTap: () { + SmartDialog.dismiss( + tag: + "showUserInfoOptMenu", + ); + SCAccountRepository() + .teamCreate( + ref.userProfile + ?.getID() ?? + "", + ); + }, + ) + : Container(), + (ref.userProfile?.isCpRelation ?? + false) + ? GestureDetector( + behavior: + HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.only( + top: 3.w, + ), + child: text( + letterSpacing: 0.1, + SCAppLocalizations.of( + context, + )!.partWays, + textColor: Colors.white, + fontSize: 12.sp, + ), + ), + onTap: () { + SmartDialog.dismiss( + tag: + "showUserInfoOptMenu", + ); + _showPartWaysDialog(ref); + }, + ) + : Container(), + isBlacklistLoading + ? Container() + : GestureDetector( + behavior: + HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.only( + top: 3.w, + ), + child: text( + letterSpacing: 0.1, + isBlacklist + ? SCAppLocalizations.of( + context, + )!.removeFromBlacklist + : SCAppLocalizations.of( + context, + )!.moveToBlacklist, + textColor: Colors.white, + fontSize: 12.sp, + ), + ), + onTap: () { + if ((ref + .userProfile + ?.isCpRelation ?? + false)) { + SmartDialog.show( + tag: + "showConfirmDialog", + alignment: + Alignment.center, + debounce: true, + animationType: + SmartAnimationType + .fade, + builder: (_) { + return MsgDialog( + title: + SCAppLocalizations.of( + context, + )!.tips, + msg: + SCAppLocalizations.of( + context, + )!.youAreCurrentlyCPRelationshipPleaseDissolve, + btnText: + SCAppLocalizations.of( + context, + )!.confirm, + onEnsure: () {}, + ); + }, + ); + return; + } + SCAccountHelper.optBlacklist( + widget.tageId, + isBlacklist, + context, + (isOptBlacklist) { + isBlacklist = + isOptBlacklist; + setState(() {}); + }, + ); + }, + ), + ], + ), + ), + ), + onTap: () { + SmartDialog.dismiss( + tag: "showUserInfoOptMenu", + ); + }, + ); + }, + ); + }, + ); + }, + ), + ], + ), + body: + isBlacklistLoading || isBlacklist + ? Container() + : ExtendedNestedScrollView( + controller: _scrollController, + onlyOneScrollInBody: true, + headerSliverBuilder: ( + BuildContext context, + bool innerBoxIsScrolled, + ) { + return [ + SliverToBoxAdapter( + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox(height: 210.w), // 给AppBar留出空间 + Column( + children: [ + Row( + children: [ + SizedBox(width: 15.w), + (ref + .userProfile + ?.cpList + ?.isNotEmpty ?? + false) + ? SizedBox( + height: 100.w, + child: Stack( + alignment: + Alignment.center, + children: [ + Transform.translate( + offset: Offset( + 0, + -5, + ), + child: Row( + mainAxisSize: + MainAxisSize + .min, + crossAxisAlignment: + CrossAxisAlignment + .center, + children: [ + GestureDetector( + child: netImage( + url: + ref + .userProfile + ?.cpList + ?.first + .meUserAvatar ?? + "", + defaultImg: + "sc_images/general/sc_icon_avar_defalt.png", + width: 56.w, + shape: + BoxShape + .circle, + ), + onTap: () { + String + encodedUrls = Uri.encodeComponent( + jsonEncode([ + ref + .userProfile + ?.cpList + ?.first + .meUserAvatar, + ]), + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", + ); + }, + ), + SizedBox( + width: 32.w, + ), + GestureDetector( + onTap: () { + SCNavigatorUtils.push( + context, + replace: + true, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == ref.userProfile?.cpList?.first.cpUserId}&tageId=${ref.userProfile?.cpList?.first.cpUserId}", + ); + }, + child: netImage( + url: + ref + .userProfile + ?.cpList + ?.first + .cpUserAvatar ?? + "", + defaultImg: + "sc_images/general/sc_icon_avar_defalt.png", + width: 56.w, + shape: + BoxShape + .circle, + ), + ), + ], + ), + ), + IgnorePointer( + child: Transform.translate( + offset: Offset( + 0, + -15, + ), + child: Image.asset( + "sc_images/person/sc_icon_send_cp_requst_dialog_head.png", + ), + ), + ), + ], + ), + ) + : GestureDetector( + child: head( + url: + ref + .userProfile + ?.userAvatar ?? + "", + width: 88.w, + headdress: + ref.userProfile + ?.getHeaddress() + ?.sourceUrl, + ), + onTap: () { + String encodedUrls = + Uri.encodeComponent( + jsonEncode([ + ref + .userProfile + ?.userAvatar, + ]), + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", + ); + }, + ), + ], + ), + SizedBox(height: 6.w), + Row( + children: [ + SizedBox(width: 25.w), + socialchatNickNameText( + maxWidth: 135.w, + ref + .userProfile + ?.userNickname ?? + "", + fontSize: 18.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + type: + ref.userProfile + ?.getVIP() + ?.name ?? + "", + needScroll: + (ref + .userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 13, + ), + SizedBox(width: 3.w), + getVIPBadge( + ref.userProfile + ?.getVIP() + ?.name, + width: 45.w, + height: 25.w, + ), + Row( + children: [ + SizedBox(width: 5.w), + Container( + width: + (ref.userProfile?.age ?? + 0) > + 999 + ? 58.w + : 48.w, + height: 24.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + ref.userProfile?.userSex == + 0 + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png", + ), + ), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment + .center, + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + xb( + ref + .userProfile + ?.userSex, + ), + text( + "${ref.userProfile?.age}", + textColor: + Colors.white, + fontSize: 14.sp, + fontWeight: + FontWeight.w600, + ), + SizedBox(width: 3.w), + ], + ), + ), + ], + ), + ], + ), + SizedBox(height: 5.w), + Row( + children: [ + SizedBox(width: 25.w), + text( + "ID:${ref.userProfile?.account ?? ""}", + textColor: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 16.sp, + ), + ], + ), + SizedBox(height: 5.w), + Consumer< + SocialChatUserProfileManager + >( + builder: (context, ref, child) { + return Container( + height: 72.w, + alignment: + AlignmentDirectional + .center, + width: + ScreenUtil().screenWidth, + margin: EdgeInsets.symmetric( + horizontal: 18.w, + ), + padding: EdgeInsets.symmetric( + horizontal: 35.w, + ), + child: Row( + mainAxisSize: + MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + GestureDetector( + child: Column( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + text( + "${counterMap["INTERVIEW"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: + Colors.white, + fontWeight: + FontWeight + .bold, + ), + text( + SCAppLocalizations.of( + context, + )!.vistors, + fontSize: 14.sp, + textColor: Color( + 0xffB1B1B1, + ), + ), + ], + ), + onTap: () { + if (widget.isMe == + "true") { + SCNavigatorUtils.push( + context, + SCMainRoute + .vistors, + ); + } + }, + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color( + 0xff333333, + ).withOpacity( + 0.0, + ), + Color(0xff333333), + Color( + 0xff333333, + ).withOpacity( + 0.0, + ), + ], + begin: + Alignment + .topCenter, + end: + Alignment + .bottomCenter, + ), + ), + height: 25.w, + width: 1.w, + ), + GestureDetector( + onTap: () { + if (widget.isMe == + "true") { + SCNavigatorUtils.push( + context, + SCMainRoute + .follow, + ); + } + }, + child: Column( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + text( + "${counterMap["SUBSCRIPTION"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: + Colors.white, + fontWeight: + FontWeight + .bold, + ), + text( + SCAppLocalizations.of( + context, + )!.follow, + fontSize: 14.sp, + textColor: Color( + 0xffB1B1B1, + ), + ), + ], + ), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color( + 0xff333333, + ).withOpacity( + 0.0, + ), + Color(0xff333333), + Color( + 0xff333333, + ).withOpacity( + 0.0, + ), + ], + begin: + Alignment + .topCenter, + end: + Alignment + .bottomCenter, + ), + ), + height: 25.w, + width: 1.w, + ), + GestureDetector( + child: Column( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + text( + "${counterMap["FANS"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: + Colors.white, + fontWeight: + FontWeight + .bold, + ), + text( + SCAppLocalizations.of( + context, + )!.fans, + fontSize: 14.sp, + textColor: Color( + 0xffB1B1B1, + ), + ), + ], + ), + onTap: () { + if (widget.isMe == + "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.fans, + ); + } + }, + ), + ], + ), + ); + }, + ), + SizedBox(height: 6.w), + ], + ), + ], + ), + ), + ), + ]; + }, + body: Container( + padding: EdgeInsets.symmetric(horizontal: 3.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.w), + topRight: Radius.circular(8.w), + ), + gradient: + ref.userProfile?.userSex == 0 + ? LinearGradient( + colors: [ + Color(0xff083b2f), + Color(0xff083b2f), + ], + begin: AlignmentDirectional.topStart, + end: AlignmentDirectional.bottomEnd, + ) + : LinearGradient( + colors: [ + Color(0xff083b2f), + Color(0xff083b2f), + ], + begin: AlignmentDirectional.topStart, + end: AlignmentDirectional.bottomEnd, + ), + ), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 6.w), + SizedBox( + height: 35.w, + child: TabBar( + tabAlignment: TabAlignment.start, + indicator: SCFixedWidthTabIndicator( + width: 20.w, + height: 4.w, + gradient: + ref.userProfile?.userSex == 0 + ? LinearGradient( + colors: [ + Color(0xffFFD800), + Color(0xffFFD800), + ], + ) + : LinearGradient( + colors: [ + Color(0xffFFD800), + Color(0xffFFD800), + ], + ), + ), + labelPadding: EdgeInsets.symmetric( + horizontal: 12.w, + ), + labelColor: Colors.white, + isScrollable: true, + unselectedLabelColor: Color( + 0xffB1B1B1, + ), + labelStyle: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: TextStyle( + fontSize: 13.sp, + ), + indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + ), + SizedBox(width: 6.w), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + SizedBox(height: 25.w), + ], + ), + ), + ), + ), + // 底部按钮区域 + isBlacklistLoading || isBlacklist + ? Container() + : Positioned( + bottom: 0, + left: 0, + right: 0, + child: + widget.isMe == "true" + ? Container() + : (isLoading + ? Container() + : Container( + alignment: AlignmentDirectional.center, + width: ScreenUtil().screenWidth, + height: 75.w, + decoration: BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.w), + topRight: Radius.circular(10.w), + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SCDebounceWidget( + debounceTime: Duration( + milliseconds: 800, + ), + child: SizedBox( + width: 110.w, + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/person/sc_icon_person_in_room.png", + width: 23.w, + ), + SizedBox(height: 5.w), + Container( + margin: EdgeInsets.only( + bottom: 0.w, + ), + child: text( + SCAppLocalizations.of( + context, + )!.inRoom, + textColor: SocialChatTheme.primaryLight, + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + ), + onTap: () { + if ((ref + .userProfile + ?.inRoomId ?? + "") + .isNotEmpty) { + SCRoomUtils.goRoom( + ref.userProfile?.inRoomId ?? + "", + context, + ); + } + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + debounceTime: Duration( + milliseconds: 800, + ), + child: Container( + width: 110.w, + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/person/sc_icon_person_tochat.png", + width: 23.w, + ), + SizedBox(height: 5.w), + Container( + margin: EdgeInsets.only( + bottom: 0.w, + ), + child: text( + SCAppLocalizations.of( + context, + )!.message, + textColor: SocialChatTheme.primaryLight, + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + ), + onTap: () async { + var conversation = + V2TimConversation( + type: + ConversationType + .V2TIM_C2C, + userID: widget.tageId, + conversationID: '', + ); + var bool = await Provider.of< + RtmProvider + >( + context, + listen: false, + ).startConversation( + conversation, + ); + if (!bool) return; + var json = jsonEncode( + conversation.toJson(), + ); + SCNavigatorUtils.push( + context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + debounceTime: Duration( + milliseconds: 800, + ), + child: Container( + width: 110.w, + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/person/sc_icon_person_follow.png", + width: 23.w, + ), + SizedBox(height: 5.w), + Container( + margin: EdgeInsets.only( + bottom: 0.w, + ), + child: text( + isFollow + ? SCAppLocalizations.of( + context, + )!.following + : SCAppLocalizations.of( + context, + )!.follow, + textColor: SocialChatTheme.primaryLight, + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + ), + onTap: () { + SCAccountRepository() + .followUser(widget.tageId) + .then((v) { + isFollow = !isFollow; + setState(() {}); + }); + }, + ), + ], + ), + ], + ), + )), + ), + ], + ), + ), + ); + }, + ); + } + + void _showPartWaysDialog(SocialChatUserProfileManager ref) { + SmartDialog.show( + tag: "showPartWaysDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return Container( + height: 530.w, + width: ScreenUtil().screenWidth * 0.9, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_dialog_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 58.w), + Container( + child: text( + SCAppLocalizations.of(context)!.partWays, + fontSize: 22.sp, + textColor: Color(0xffDB5872), + fontWeight: FontWeight.bold, + ), + ), + Transform.translate( + offset: Offset(0, -25), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + top: 28.w, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + head( + url: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userAvatar ?? + "", + width: 90.w, + ), + SizedBox(width: 15.w), + head( + url: ref.userProfile?.userAvatar ?? "", + width: 90.w, + ), + ], + ), + ), + Image.asset( + "sc_images/person/sc_icon_send_cp_requst_dialog_head2.png", + height: 120.w, + fit: BoxFit.fill, + ), + ], + ), + ), + Transform.translate( + offset: Offset(0, -23), + child: Container( + height: 25.w, + width: ScreenUtil().screenWidth * 0.6, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_username_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + socialchatNickNameText( + maxWidth: 100.w, + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname ?? + "", + fontSize: 10.sp, + fontWeight: FontWeight.w600, + type: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.getVIP() + ?.name ?? + "", + needScroll: + (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 8, + ), + SizedBox(width: 20.w), + socialchatNickNameText( + maxWidth: 100.w, + ref.userProfile?.userNickname ?? "", + fontSize: 10.sp, + fontWeight: FontWeight.w600, + type: ref.userProfile?.getVIP()?.name ?? "", + needScroll: + (ref.userProfile?.userNickname?.characters.length ?? + 0) > + 8, + ), + ], + ), + ), + ), + + Container( + padding: EdgeInsets.only(bottom: 12.w), + margin: EdgeInsets.symmetric(horizontal: 18.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_dialog_content.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + Container( + margin: EdgeInsets.symmetric( + horizontal: 35.w, + ).copyWith(top: 45.w), + child: text( + SCAppLocalizations.of( + context, + )!.areYouSureYouWantToPartWaysWithYourCP, + fontWeight: FontWeight.w500, + textColor: Color(0xffFF79A1), + maxLines: 3, + fontSize: 14.sp, + ), + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 20.w), + alignment: AlignmentDirectional.center, + child: text( + SCAppLocalizations.of(context)!.partWaysTips, + fontSize: 10.w, + textColor: Color(0xffFE91B0), + maxLines: 6, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SizedBox(height: 10.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.only(top: 7.w), + alignment: AlignmentDirectional.center, + width: 130.w, + height: 48.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_ok_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: text( + SCAppLocalizations.of(context)!.cancel, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + textColor: Color(0xffDB5872), + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showPartWaysDialog"); + }, + ), + SizedBox(width: 25.w), + GestureDetector( + child: Container( + padding: EdgeInsets.only(top: 7.w), + alignment: AlignmentDirectional.center, + width: 130.w, + height: 48.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_cancel_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: text( + SCAppLocalizations.of(context)!.confirm, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ), + ), + onTap: () { + SCLoadingManager.show(); + SCAccountRepository() + .cpRelationshipDismissApply(ref.userProfile?.id ?? "") + .then((result) { + SmartDialog.dismiss(tag: "showPartWaysDialog"); + SCTts.show( + SCAppLocalizations.of( + context, + )!.operationSuccessful, + ); + SCLoadingManager.hide(); + SCNavigatorUtils.popUntil( + context, + ModalRoute.withName(SCRoutes.home), + ); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + }, + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/modules/user/profile/profile/sc_profile_page.dart b/lib/modules/user/profile/profile/sc_profile_page.dart new file mode 100644 index 0000000..b854812 --- /dev/null +++ b/lib/modules/user/profile/profile/sc_profile_page.dart @@ -0,0 +1,200 @@ +import 'dart:convert'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +class SCProfilePage extends StatefulWidget { + bool isFromMe = false; + String userId = ""; + + SCProfilePage(this.isFromMe, this.userId); + + @override + _ProfilePageState createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + List items = []; + if (widget.isFromMe) { + items = + (AccountStorage().getCurrentUser()?.userProfile?.personalPhotos ?? + []) + .where((t) => t.status == 1) + .toList(); + } else { + items = ref.userProfile?.personalPhotos ?? []; + items = + (ref.userProfile?.personalPhotos ?? []) + .where((t) => t.status == 1) + .toList(); + } + return SingleChildScrollView( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 15.w), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 5.w), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, // 每行2个 + childAspectRatio: 1, // 宽高比 + mainAxisSpacing: 8, + crossAxisSpacing: 8, + ), + itemCount: items.length, + itemBuilder: (context, index) { + return GestureDetector( + child: netImage( + url: items[index].url ?? "", + width: double.infinity, + height: double.infinity, + defaultImg: "sc_images/general/sc_icon_loading.webp", + borderRadius: BorderRadius.circular(12.w), + ), + onTap: () { + List ims = []; + for (var iv in items) { + if ((iv.url?.isNotEmpty ?? false)) { + ims.add(iv.url ?? ""); + } + } + String encodedUrls = Uri.encodeComponent( + jsonEncode(ims), + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=$index", + ); + }, + ); + }, + ), + SizedBox(height: 5.w), + text( + SCAppLocalizations.of(context)!.personal2, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ), + SizedBox(height: 15.w), + _item2( + SCAppLocalizations.of(context)!.country, + widget.isFromMe + ? AccountStorage() + .getCurrentUser() + ?.userProfile + ?.countryName ?? + "" + : ref.userProfile?.countryName ?? "", + ), + SizedBox(height: 10.w), + _item(SCAppLocalizations.of(context)!.language, "English"), + SizedBox(height: 10.w), + _item( + SCAppLocalizations.of(context)!.dateOfBirth, + "${widget.isFromMe ? AccountStorage().getCurrentUser()?.userProfile?.bornYear ?? "" : ref.userProfile?.bornYear}-${widget.isFromMe ? AccountStorage().getCurrentUser()?.userProfile?.bornMonth ?? "" : ref.userProfile?.bornMonth}-${widget.isFromMe ? AccountStorage().getCurrentUser()?.userProfile?.bornDay ?? "" : ref.userProfile?.bornDay}", + ), + SizedBox(height: 10.w), + _item( + SCAppLocalizations.of(context)!.bio, + widget.isFromMe + ? AccountStorage() + .getCurrentUser() + ?.userProfile + ?.autograph ?? + "" + : ref.userProfile?.autograph ?? "", + ), + SizedBox(height: 10.w), + _item( + SCAppLocalizations.of(context)!.hobby, + widget.isFromMe + ? AccountStorage().getCurrentUser()?.userProfile?.hobby ?? + "" + : ref.userProfile?.hobby ?? "", + ), + SizedBox(height: 62.w), + ], + ), + ), + ); + }, + ); + } + + _item(String name, String value) { + return Row( + children: [ + SizedBox( + width: 100.w, + child: text(name, textColor: Color(0xffB1B1B1), fontSize: 13.sp), + ), + Expanded( + child: text( + value, + fontSize: 13.sp, + textColor: Colors.white, + maxLines: 2, + ), + ), + ], + ); + } + + _item2(String name, String value) { + return Row( + children: [ + SizedBox( + width: 100.w, + child: text(name, textColor: Color(0xffB1B1B1), fontSize: 13.sp), + ), + netImage( + url: + Provider.of( + context, + listen: false, + ).findCountryByName(value)?.nationalFlag ?? + "", + borderRadius: BorderRadius.all(Radius.circular(3.w)), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + Expanded( + child: text( + value, + fontSize: 13.sp, + textColor: Colors.white, + maxLines: 2, + ), + ), + ], + ); + } + + +} diff --git a/lib/modules/user/profile/props/sc_giftwall_page.dart b/lib/modules/user/profile/props/sc_giftwall_page.dart new file mode 100644 index 0000000..d4b2160 --- /dev/null +++ b/lib/modules/user/profile/props/sc_giftwall_page.dart @@ -0,0 +1,84 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart'; + +///礼物墙 +class SCGiftwallPage extends StatefulWidget { + String tageId = ""; + + SCGiftwallPage(this.tageId); + + @override + _GiftwallPageState createState() => _GiftwallPageState(); +} + +class _GiftwallPageState extends State { + List giftWallList = []; + + @override + void initState() { + super.initState(); + SCGiftRepositoryImp() + .giftWall(widget.tageId) + .then((result) { + giftWallList = result; + setState(() {}); + }) + .catchError((e) {}); + } + + @override + Widget build(BuildContext context) { + return giftWallList.isEmpty + ? mainEmpty( + msg: SCAppLocalizations.of(context)!.noData, + textColor: Colors.white, + ) + : SingleChildScrollView( + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, // 每行2个 + childAspectRatio: 0.82, // 宽高比 + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + padding: EdgeInsets.all(10), + itemCount: giftWallList.length, + itemBuilder: (context, index) { + return _buildItem(giftWallList[index]); + }, + ), + ); + ; + } + + Widget _buildItem(SocialChatGiftRes gift) { + return Column( + children: [ + Container( + width: 68.w, + height: 68.w, + decoration: BoxDecoration( + color: Color(0xffD9D9D9), + borderRadius: BorderRadius.circular(10.w), + ), + alignment: Alignment.center, + child: netImage(url: gift.giftPhoto ?? "", width: 36.w), + ), + SizedBox(height: 9.w), + text( + "x ${gift.quantity}", + fontSize: 13.sp, + textColor: Colors.white, + ) + ], + ); + } +} diff --git a/lib/modules/user/settings/about/about_page.dart b/lib/modules/user/settings/about/about_page.dart new file mode 100644 index 0000000..3243017 --- /dev/null +++ b/lib/modules/user/settings/about/about_page.dart @@ -0,0 +1,134 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/ui_kit/widgets/pop/pop_route.dart'; +import 'package:yumi/modules/webview/webview_page.dart'; + +class AboutPage extends StatefulWidget { + @override + _AboutPageState createState() => _AboutPageState(); +} + +class _AboutPageState extends State { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar(title: SCAppLocalizations.of(context)!.about, actions: []), + body: SafeArea(top: false,child: Column( + children: [ + SizedBox(height: 35.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/splash/sc_icon_splash_icon.png", + width: 80.w, + height: 120.w, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text("v${SCGlobalConfig.version}(${SCGlobalConfig.build})", fontSize: 14.sp, textColor: Colors.white54), + ], + ), + SizedBox(height: 15.w,), + Container( + padding: EdgeInsets.symmetric(vertical: 12.w), + margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 35.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.termsofService, + textColor: Colors.white, + fontSize: 13.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + size: 15.w, + color: Colors.white, + ), + SizedBox(width: 18.w), + ], + ), + onTap: () { + Navigator.push( + context, + PopRoute( + child: WebViewPage( + title: SCAppLocalizations.of(context)!.termsofService, + url: '${SCGlobalConfig.userAgreementUrl}', + ), + ), + ); + }, + ), + Divider(color: Colors.white12, thickness: 0.5), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.privaceyPolicy, + textColor: Colors.white, + fontSize: 13.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + size: 15.w, + color: Colors.white, + ), + SizedBox(width: 18.w), + ], + ), + onTap: () { + Navigator.push( + context, + PopRoute( + child: WebViewPage( + title: SCAppLocalizations.of(context)!.privaceyPolicy, + url: + '${SCGlobalConfig.privacyAgreementUrl}', + ), + ), + ); + }, + ), + ], + ), + ) + ], + ),), + ), + ], + ); + } +} diff --git a/lib/modules/user/settings/account/account_page.dart b/lib/modules/user/settings/account/account_page.dart new file mode 100644 index 0000000..174e486 --- /dev/null +++ b/lib/modules/user/settings/account/account_page.dart @@ -0,0 +1,132 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/modules/user/settings/settings_route.dart'; + +class AccountPage extends StatefulWidget { + @override + _AccountPageState createState() => _AccountPageState(); +} + +class _AccountPageState extends State { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getAccountPageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: SCGlobalConfig.businessLogicStrategy.getAccountPageScaffoldBackgroundColor(), + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.account, + actions: [], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + Container( + alignment: Alignment.bottomLeft, + margin: EdgeInsets.only(left: 15.w), + child: text( + SCAppLocalizations.of(context)!.account, + textColor: SCGlobalConfig.businessLogicStrategy.getAccountPageSecondaryTextColor(), + fontSize: 12.sp, + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 12.w), + margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: SCGlobalConfig.businessLogicStrategy.getAccountPageMainContainerBackgroundColor(), + ), + child: Column( + children: [ + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.setAccount, + textColor: SCGlobalConfig.businessLogicStrategy.getAccountPagePrimaryTextColor(), + fontSize: 13.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + size: 15.w, + color: SCGlobalConfig.businessLogicStrategy.getAccountPageIconColor(), + ), + SizedBox(width: 18.w), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + SettingsRoute.setAccount, + replace: false, + ); + }, + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 12.w), + margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: SCGlobalConfig.businessLogicStrategy.getAccountPageMainContainerBackgroundColor(), + ), + child: Column( + children: [ + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.deleteAccount, + textColor: SCGlobalConfig.businessLogicStrategy.getAccountPagePrimaryTextColor(), + fontSize: 13.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + size: 15.w, + color: SCGlobalConfig.businessLogicStrategy.getAccountPageIconColor(), + ), + SizedBox(width: 18.w), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + SettingsRoute.deleteAccount, + replace: false, + ); + }, + ), + ], + ), + ) + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/user/settings/account/delete/delete_account_page.dart b/lib/modules/user/settings/account/delete/delete_account_page.dart new file mode 100644 index 0000000..0721aa9 --- /dev/null +++ b/lib/modules/user/settings/account/delete/delete_account_page.dart @@ -0,0 +1,168 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; + +import '../../../../../app/constants/sc_global_config.dart'; + +///删除账户 +class DeleteAccountPage extends StatefulWidget { + @override + _DeleteAccountPageState createState() => _DeleteAccountPageState(); +} + +class _DeleteAccountPageState extends State { + int delay = 30; + bool canClick = false; + String btnText = ""; + Timer? _timer; + BuildContext? ct; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + if (delay == 0) { + canClick = true; + _timer?.cancel(); + btnText = SCAppLocalizations.of(ct!)!.deleteAccount; + } else { + delay--; + btnText = SCAppLocalizations.of(ct!)!.deleteAccount2("$delay"); + } + setState(() {}); + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + ct = context; + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getRechargePageBackgroundImage(), + width: ScreenUtil().screenWidth, + fit: BoxFit.fill, + ), + + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.deleteAccount, + actions: [], + ), + body: SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SizedBox(width: 10.w), + text( + SCAppLocalizations.of(context)!.accountDeletionNotice, + fontSize: 14.sp, + textColor: Colors.white38, + ), + ], + ), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: text( + SCAppLocalizations.of(context)!.deleteAccountTips, + fontSize: 12.sp, + maxLines: 30, + textColor: Colors.white, + ), + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 10.w), + + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: text( + SCAppLocalizations.of(context)!.deleteAccountTips2, + fontSize: 12.sp, + maxLines: 30, + textColor: Colors.white38, + ), + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 35.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + color: !canClick ? Colors.black26 : Colors.red, + ), + width: 275.w, + height: 46.w, + child: text( + btnText.isEmpty + ? SCAppLocalizations.of( + context, + )!.deleteAccount2("30") + : btnText, + fontSize: 15.sp, + textColor: Colors.white, + ), + ), + onTap: () { + if (canClick) { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of( + context, + )!.areYouSureYouWantToDeleteYourAccount, + btnText: SCAppLocalizations.of(context)!.delete, + onEnsure: () { + AccountStorage().logout(context); + }, + ); + }, + ); + } + }, + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/user/settings/account/set/pwd/reset_pwd_page.dart b/lib/modules/user/settings/account/set/pwd/reset_pwd_page.dart new file mode 100644 index 0000000..8b9583d --- /dev/null +++ b/lib/modules/user/settings/account/set/pwd/reset_pwd_page.dart @@ -0,0 +1,362 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; + +class ResetPwdPage extends StatefulWidget { + @override + _ResetPwdPageState createState() => _ResetPwdPageState(); +} + +class _ResetPwdPageState extends State { + ///显示密码 + bool showPass = false; + bool showPass3 = false; + bool showPass2 = false; + + ///账户控制器 + TextEditingController confirmController = TextEditingController(); + + ///密码控制器 + TextEditingController passController = TextEditingController(); + + ///旧密码 + TextEditingController oldPassController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.resetLoginPassword, + actions: [], + ), + body: SafeArea(top: false,child: Column( + children: [ + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric(vertical: 8.w, horizontal: 8.w), + child: text( + SCAppLocalizations.of(context)!.resetLoginPasswordtTips1, + maxLines: 2, + textColor: Colors.white, + fontSize: 10.sp, + ), + ), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.account, + fontSize: 15.sp, + textColor: Colors.white, + ), + ], + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color(0xff18F2B1).withOpacity(0.1) + ), + child: text( + AccountStorage().getCurrentUser()?.userProfile?.account ?? "", + textColor: Colors.white54, + fontSize: 14.sp, + ), + ), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.inputYourOldPassword, + fontSize: 15.sp, + textColor: Colors.white, + ), + ], + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color(0xff18F2B1).withOpacity(0.1) + ), + child: TextField( + controller: oldPassController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')) + ], + + obscureText: !showPass3, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.enterYourOldPassword, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass3 = !showPass3; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass3 + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + color: Colors.white54, + width: 15.w, + ), + ), + ), + ), + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.setYourPassword, + fontSize: 15.sp, + textColor: Colors.white, + ), + ], + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color(0xff18F2B1).withOpacity(0.1) + ), + child: TextField( + controller: passController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')) + ], + + obscureText: !showPass, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.enterYourNewPassword, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass = !showPass; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + color: Colors.white54, + width: 15.w, + ), + ), + ), + ), + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.black12, + ), + child: TextField( + controller: confirmController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')) + ], + + obscureText: !showPass2, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + hintText: SCAppLocalizations.of(context)!.confirmYourPassword, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass2 = !showPass2; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass2 + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + color: Colors.black54, + width: 15.w, + ), + ), + ), + ), + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: text( + SCAppLocalizations.of(context)!.resetLoginPasswordtTips2, + fontSize: 10.sp, + textColor: Colors.white, + maxLines: 3, + ), + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 20.w), + GestureDetector( + child: Container( + height: 44.w, + width: 260.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.submit, + fontSize: 16.sp, + textColor: Colors.white, + ), + ), + onTap: () { + resetPwd(); + }, + ), + ], + ),), + ), + ], + ); + } + + void resetPwd() { + if (confirmController.text.isNotEmpty && + passController.text.isNotEmpty && + oldPassController.text.isNotEmpty) { + if (confirmController.text == passController.text) { + SCLoadingManager.show(context: context); + SCAccountRepository() + .updatePwd(passController.text, oldPassController.text) + .then((d) { + SCLoadingManager.hide(); + SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); + SCNavigatorUtils.goBack(context); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } else { + SCTts.show(SCAppLocalizations.of(context)!.theTwoPasswordsDoNotMatch); + } + } + } +} diff --git a/lib/modules/user/settings/account/set/pwd/set_pwd_page.dart b/lib/modules/user/settings/account/set/pwd/set_pwd_page.dart new file mode 100644 index 0000000..1245eee --- /dev/null +++ b/lib/modules/user/settings/account/set/pwd/set_pwd_page.dart @@ -0,0 +1,298 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; + +class SetPwdPage extends StatefulWidget { + @override + _SetPwdPageState createState() => _SetPwdPageState(); +} + +class _SetPwdPageState extends State { + ///显示密码 + bool showPass = false; + bool showPass2 = false; + + ///账户控制器 + TextEditingController confirmController = TextEditingController(); + + ///密码控制器 + TextEditingController passController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.setLoginPassword, + actions: [], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + Container( + width: ScreenUtil().screenWidth, + color: Colors.black12, + padding: EdgeInsets.symmetric(vertical: 8.w, horizontal: 8.w), + child: text( + SCAppLocalizations.of(context)!.resetLoginPasswordtTips1, + maxLines: 2, + textColor: Colors.white, + fontSize: 10.sp, + ), + ), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.account, + fontSize: 15.sp, + textColor: Colors.white, + ), + ], + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, + ), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: text( + AccountStorage().getCurrentUser()?.userProfile?.account ?? + "", + textColor: Colors.white54, + fontSize: 14.sp, + ), + ), + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.setYourPassword, + fontSize: 15.sp, + textColor: Colors.white, + ), + ], + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, + ), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: TextField( + controller: passController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')), + ], + + obscureText: !showPass, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.enterYourNewPassword, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass = !showPass; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + width: 15.w, + ), + ), + ), + ), + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, + ), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: TextField( + controller: confirmController, + maxLength: 30, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp(r'\s')), + ], + + obscureText: !showPass, + textInputAction: TextInputAction.done, + onChanged: (s) { + setState(() {}); + }, + decoration: InputDecoration( + hintText: + SCAppLocalizations.of(context)!.confirmYourPassword, + hintStyle: TextStyle( + color: Colors.white54, + fontSize: 14.sp, + ), + counterText: '', + contentPadding: EdgeInsets.only(top: 0.w), + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + suffix: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + showPass2 = !showPass2; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Image.asset( + !showPass2 + ? "sc_images/login/sc_icon_pass.png" + : "sc_images/login/sc_icon_pass1.png", + gaplessPlayback: true, + color: Colors.white54, + width: 15.w, + ), + ), + ), + ), + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ), + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: text( + SCAppLocalizations.of( + context, + )!.resetLoginPasswordtTips2, + fontSize: 10.sp, + textColor: Colors.white, + maxLines: 3, + ), + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 20.w), + GestureDetector( + child: Container( + height: 44.w, + width: 260.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: SocialChatTheme.primaryLight, + ), + child: text( + SCAppLocalizations.of(context)!.submit, + fontSize: 16.sp, + textColor: Colors.white, + ), + ), + onTap: () { + setPwd(); + }, + ), + ], + ), + ), + ), + ], + ); + } + + void setPwd() { + if (confirmController.text.isNotEmpty && passController.text.isNotEmpty) { + if (confirmController.text == passController.text) { + SCLoadingManager.show(context: context); + SCAccountRepository() + .bind(passController.text) + .then((f) { + SCLoadingManager.hide(); + SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); + SCNavigatorUtils.goBack(context); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } else { + SCTts.show(SCAppLocalizations.of(context)!.theTwoPasswordsDoNotMatch); + } + } + } +} diff --git a/lib/modules/user/settings/account/set/set_account_page.dart b/lib/modules/user/settings/account/set/set_account_page.dart new file mode 100644 index 0000000..77b7516 --- /dev/null +++ b/lib/modules/user/settings/account/set/set_account_page.dart @@ -0,0 +1,160 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/modules/user/settings/settings_route.dart'; + +class SetAccountPage extends StatefulWidget { + @override + _SetAccountPageState createState() => _SetAccountPageState(); +} + +class _SetAccountPageState extends State { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar(title: SCAppLocalizations.of(context)!.setAccount, actions: []), + body: SafeArea(top: false,child:Column( + children: [ + Container( + alignment: Alignment.bottomLeft, + margin: EdgeInsets.only(left: 15.w), + child: text( + SCAppLocalizations.of(context)!.account, + textColor: Colors.black38, + fontSize: 12.sp, + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 12.w), + margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: Column( + children: [ + SCDebounceWidget( + onTap: () { + SCLoadingManager.show(); + SCAccountRepository().accountIsBind().then((value) { + SCLoadingManager.hide(); + if (value) { + ///修改密码 + SCNavigatorUtils.push( + context, + SettingsRoute.resetPwd, + replace: false, + ); + } else { + ///设置密码 + SCNavigatorUtils.push( + context, + SettingsRoute.setPwd, + replace: false, + ); + } + }).catchError((_){ + SCLoadingManager.hide(); + }); + }, + child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.account, + textColor: Colors.white, + fontSize: 13.sp, + ), + Spacer(), + text( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.account ?? + "", + fontSize: 13.sp, + textColor: Colors.white54, + ), + SizedBox(width: 5.w), + Icon( + Icons.keyboard_arrow_right, + size: 15.w, + color: Colors.white54, + ), + SizedBox(width: 18.w), + ], + ), + ), + Divider(color: Colors.white54, thickness: 0.5), + Platform.isIOS + ? Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.apple, + textColor: Colors.white, + fontSize: 13.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + size: 15.w, + color: Colors.black26, + ), + SizedBox(width: 18.w), + ], + ) + : Container(), + Platform.isIOS + ? Divider(color: Colors.white54, thickness: 0.5) + : Container(), + SCDebounceWidget(child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.google, + textColor: Colors.white, + fontSize: 13.sp, + ), + Spacer(), + Icon( + Icons.keyboard_arrow_right, + size: 15.w, + color: Colors.white54, + ), + SizedBox(width: 18.w), + ], + ),onTap: (){ + + },) , + ], + ), + ), + ], + ) ,), + ), + ], + ); + } +} diff --git a/lib/modules/user/settings/blacklist/user_blacklist_page.dart b/lib/modules/user/settings/blacklist/user_blacklist_page.dart new file mode 100644 index 0000000..d419b74 --- /dev/null +++ b/lib/modules/user/settings/blacklist/user_blacklist_page.dart @@ -0,0 +1,355 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/socialchat_gradient_button.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/message_friend_user_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +class UserBlockedListPage extends SCPageList { + @override + _UserBlockedListPageState createState() => _UserBlockedListPageState(); +} + +class _UserBlockedListPageState + extends SCPageListState { + late TextEditingController _textEditingController; + String? lastId; + + @override + void initState() { + super.initState(); + _textEditingController = TextEditingController(); + enablePullUp = true; + backgroundColor = Colors.transparent; + isShowDivider = true; + isShowFooter = false; + padding = EdgeInsets.symmetric(horizontal: 10.w); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.userBlacklist, + actions: [], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 10.w), + Expanded( + child: searchWidget( + hint: SCAppLocalizations.of(context)!.searchUserId, + controller: _textEditingController, + borderColor: Colors.black12, + textColor: Colors.grey, + ), + ), + socialchatGradientButton( + text: SCAppLocalizations.of(context)!.search, + radius: 25, + textSize: 14.sp, + textColor: Colors.grey, + gradient: LinearGradient( + colors: [Colors.transparent, Colors.transparent], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + onPress: () { + if (_textEditingController.text.isNotEmpty) { + _search(); + } + }, + ), + SizedBox(width: 10.w), + ], + ), + SizedBox(height: 8.w), + Expanded(child: buildList(context)), + SizedBox(height: 8.w), + ], + ), + ), + ), + ], + ); + } + + @override + Widget buildItem(MessageFriendUserRes res) { + return Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Color(0xff18F2B1).withOpacity(0.1), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + SCDebounceWidget(child: head(url: res.userProfile?.userAvatar ?? "", width: 62.w), onTap: (){ + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}", + ); + }) + , + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + netImage( + url: + Provider.of(context, listen: false) + .findCountryByName( + res.userProfile?.countryName ?? "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all(Radius.circular(3.w)), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + socialchatNickNameText( + maxWidth: 148.w, + textColor: Colors.white, + res.userProfile?.userNickname ?? "", + fontSize: 14.sp, + type:res.userProfile?.getVIP()?.name ?? "", + needScroll: + (res.userProfile?.userNickname?.characters.length ?? 0) > + 10, + ), + SizedBox(width: 3.w), + Container( + width: (res.userProfile?.age??0)>999?58.w:48.w, + height: 24.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + res.userProfile?.userSex == 0 + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png", + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + xb(res.userProfile?.userSex), + text( + "${res.userProfile?.age}", + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ], + ), + ), + ], + ), + SizedBox(height: 4.w), + Row( + children: [ + GestureDetector( + child: Container( + child: Row( + textDirection: TextDirection.ltr, + children: [ + text( + "ID:${res.userProfile?.getID()}", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: res.userProfile?.getID() ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + SizedBox(width: 8.w), + getWealthLevel( + res.userProfile?.wealthLevel ?? 0, + width: 58.w, + height: 30.w, + ), + SizedBox(width: 8.w), + getUserLevel( + res.userProfile?.charmLevel ?? 0, + width: 58.w, + height: 30.w, + ), + ], + ), + ], + ), + ), + SCDebounceWidget(child: Image.asset( + "sc_images/room/sc_icon_block_list_delete.png", + height: 20.w, + ), onTap: (){ + _delete(res.userProfile); + }) + , + SizedBox(width: 15.w), + ], + ), + ); + } + + void _search() { + SCAccountRepository() + .userBlacklistList(account: _textEditingController.text) + .then((result) { + items = result; + loadComplete(); + SCLoadingManager.hide(); + setState(() {}); + }) + .catchError((e) { + SCLoadingManager.hide(); + loadComplete(); + setState(() {}); + }); + } + + @override + builderDivider() { + return Divider( + height: 8.w, + color: Color(0xffF2F2F2), + indent: 15.w, + endIndent: 15.w, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + if (_textEditingController.text.isNotEmpty) { + _search(); + } else { + if (page == 1) { + lastId = null; + } + try { + var userList = await SCAccountRepository().userBlacklistList( + lastId: lastId, + ); + if (userList.isNotEmpty) { + lastId = userList.last.id; + } + SCLoadingManager.hide(); + onSuccess(userList); + } catch (e) { + SCLoadingManager.hide(); + if (onErr != null) { + onErr(); + } + } + } + } + + void _delete(SocialChatUserProfile? userProfile) { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: + Alignment.center, + debounce: true, + animationType: + SmartAnimationType + .fade, + builder: (_) { + return MsgDialog( + title: + SCAppLocalizations.of( + context!, + )!.tips, + msg:SCAppLocalizations.of( + context, + )!.areYouSureToCancelBlacklist, + btnText: + SCAppLocalizations.of( + context, + )!.confirm, + onEnsure: () { + SCLoadingManager.show(); + SCAccountRepository() + .deleteUserBlacklist( + userProfile?.id??"", + ) + .then(( + result, + ) { + SCTts.show( + SCAppLocalizations.of( + context, + )!.successfullyRemovedFromTheBlacklist, + ); + loadData(1); + }) + .catchError(( + e, + ) { + SCLoadingManager.hide(); + }); + }, + ); + }, + ); + } +} diff --git a/lib/modules/user/settings/settings_page.dart b/lib/modules/user/settings/settings_page.dart new file mode 100644 index 0000000..55903a1 --- /dev/null +++ b/lib/modules/user/settings/settings_page.dart @@ -0,0 +1,232 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/modules/user/settings/settings_route.dart'; + +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_app_utils.dart'; + +///设置页面 +class SettingsPage extends StatefulWidget { + @override + _SettingsPageState createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + String totalSize = "0"; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getSettingsPageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.settings, + actions: [], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + Container( + alignment: AlignmentDirectional.bottomStart, + margin: EdgeInsetsDirectional.only(start: 15.w), + child: text( + SCAppLocalizations.of(context)!.account, + textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPageSecondaryTextColor(), + fontSize: 12.sp, + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 12.w), + margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: SCGlobalConfig.businessLogicStrategy.getSettingsPageMainContainerBackgroundColor(), + border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getSettingsPageMainContainerBorderColor(), width: 1.w), + ), + child: Column( + children: [ + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.account, + textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPagePrimaryTextColor(), + fontSize: 13.sp, + ), + Spacer(), + Icon( + SCGlobalConfig.lang == "ar" + ? Icons.keyboard_arrow_left + : Icons.keyboard_arrow_right, + size: 15.w, + color: SCGlobalConfig.businessLogicStrategy.getSettingsPageIconColor(), + ), + SizedBox(width: 18.w), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + SettingsRoute.account, + replace: false, + ); + }, + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 12.w), + margin: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: SCGlobalConfig.businessLogicStrategy.getSettingsPageContainerBackgroundColor(), + border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getSettingsPageContainerBorderColor(), width: 1.w), + ), + child: Column( + children: [ + SCDebounceWidget( + child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.blockedList, + textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPagePrimaryTextColor(), + fontSize: 13.sp, + ), + Spacer(), + Icon( + SCGlobalConfig.lang == "ar" + ? Icons.keyboard_arrow_left + : Icons.keyboard_arrow_right, + size: 15.w, + color: SCGlobalConfig.businessLogicStrategy.getSettingsPageIconColor(), + ), + SizedBox(width: 18.w), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + SettingsRoute.blockedList, + replace: false, + ); + }, + ), + ], + ), + ), + SizedBox(height: 8.w), + Container( + alignment: AlignmentDirectional.bottomStart, + margin: EdgeInsetsDirectional.only(start: 15.w), + child: text( + SCAppLocalizations.of(context)!.about, + textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPageAboutTextColor(), + fontSize: 12.sp, + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 12.w), + margin: EdgeInsets.symmetric(vertical: 8.w, horizontal: 15.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: SCGlobalConfig.businessLogicStrategy.getSettingsPageContainerBackgroundColor(), + border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getSettingsPageContainerBorderColor(), width: 1.w), + ), + child: Column( + children: [ + SCDebounceWidget( + onTap: () { + SCNavigatorUtils.push( + context, + SettingsRoute.about, + replace: false, + ); + }, + child: Row( + children: [ + SizedBox(width: 18.w), + text( + SCAppLocalizations.of(context)!.aboutUs, + textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPagePrimaryTextColor(), + fontSize: 13.sp, + ), + Spacer(), + Icon( + SCGlobalConfig.lang == "ar" + ? Icons.keyboard_arrow_left + : Icons.keyboard_arrow_right, + size: 15.w, + color: SCGlobalConfig.businessLogicStrategy.getSettingsPageIconColor(), + ), + SizedBox(width: 18.w), + ], + ), + ), + ], + ), + ), + SizedBox(height: 45.w,), + Row( + children: [ + Expanded( + child: SCDebounceWidget( + child: Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 8.w), + margin: EdgeInsets.symmetric( + horizontal: 80.w, + vertical: 35.w, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.w), + color: SCGlobalConfig.businessLogicStrategy.getSettingsPageButtonBackgroundColor(), + ), + child: text( + SCAppLocalizations.of(context)!.logout, + fontSize: 14.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getSettingsPageButtonTextColor(), + ), + ), + onTap: () { + AccountStorage().logout(context); + }, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/user/settings/settings_route.dart b/lib/modules/user/settings/settings_route.dart new file mode 100644 index 0000000..ac707f4 --- /dev/null +++ b/lib/modules/user/settings/settings_route.dart @@ -0,0 +1,62 @@ +import 'package:fluro/fluro.dart'; +import 'package:fluro/src/fluro_router.dart'; +import 'package:yumi/modules/user/settings/account/set/pwd/reset_pwd_page.dart'; +import 'package:yumi/modules/user/settings/account/set/pwd/set_pwd_page.dart'; +import 'package:yumi/modules/user/settings/settings_page.dart'; + +import 'package:yumi/app/routes/sc_router_init.dart'; +import 'package:yumi/modules/user/settings/about/about_page.dart'; +import 'package:yumi/modules/user/settings/account/account_page.dart'; +import 'package:yumi/modules/user/settings/account/delete/delete_account_page.dart'; +import 'package:yumi/modules/user/settings/account/set/set_account_page.dart'; +import 'package:yumi/modules/user/settings/blacklist/user_blacklist_page.dart'; + +class SettingsRoute implements SCIRouterProvider { + static String settings = '/me/settings'; + static String account = '/me/settings/account'; + static String blockedList = '/me/settings/blockedList'; + static String setAccount = '/me/settings/account/setAccount'; + static String deleteAccount = '/me/settings/account/deleteAccount'; + static String resetPwd = '/me/settings/account/setAccount/resetPwd'; + static String setPwd = '/me/settings/account/setAccount/setPwd'; + static String about = '/me/settings/account/about'; + + @override + void initRouter(FluroRouter router) { + router.define( + settings, + handler: Handler(handlerFunc: (_, params) => SettingsPage()), + ); + router.define( + account, + handler: Handler(handlerFunc: (_, params) => AccountPage()), + ); + router.define( + setAccount, + handler: Handler(handlerFunc: (_, params) => SetAccountPage()), + ); + + router.define( + deleteAccount, + handler: Handler(handlerFunc: (_, params) => DeleteAccountPage()), + ); + router.define( + setPwd, + handler: Handler(handlerFunc: (_, params) => SetPwdPage()), + ); + router.define( + resetPwd, + handler: Handler(handlerFunc: (_, params) => ResetPwdPage()), + ); + router.define( + about, + handler: Handler(handlerFunc: (_, params) => AboutPage()), + ); + + router.define( + blockedList, + handler: Handler(handlerFunc: (_, params) => UserBlockedListPage()), + ); + + } +} diff --git a/lib/modules/user/visitor/visitor_user_list_page.dart b/lib/modules/user/visitor/visitor_user_list_page.dart new file mode 100644 index 0000000..1a69214 --- /dev/null +++ b/lib/modules/user/visitor/visitor_user_list_page.dart @@ -0,0 +1,200 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_page_list.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +class VisitorUserListPage extends SCPageList { + @override + _VisitorUserListPageState createState() => _VisitorUserListPageState(); +} + +class _VisitorUserListPageState + extends SCPageListState { + @override + void initState() { + super.initState(); + enablePullUp = true; + backgroundColor = Colors.transparent; + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + "sc_images/person/sc_icon_edit_userinfo_bg.png", + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.visitorList, + actions: [], + ), + body: buildList(context), + ), + ], + ); + } + + @override + Widget buildItem(FollowUserRes res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration(), + child: Row( + children: [ + SizedBox(width: 10.w), + head( + url: res.userProfile?.userAvatar ?? "", + width: 55.w, + headdress: res.userProfile?.getHeaddress()?.sourceUrl, + ), + SizedBox(width: 5.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + socialchatNickNameText( + maxWidth: 160.w, + res.userProfile?.userNickname ?? "", + fontSize: 14.sp, + textColor: Colors.white, + type: res.userProfile?.getVIP()?.name ?? "", + needScroll: + (res.userProfile?.userNickname?.characters.length ?? + 0) > + 14, + ), + SizedBox(width: 3.w), + res.userProfile?.getVIP() != null + ? netImage( + url: res.userProfile?.getVIP()?.cover ?? "", + width: 25.w, + height: 25.w, + ) + : Container(), + SizedBox(width: 3.w), + Container( + width: (res.userProfile?.age ?? 0) > 999 ? 58.w : 48.w, + height: 24.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + res.userProfile?.userSex == 0 + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png", + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + xb(res.userProfile?.userSex), + text( + "${res.userProfile?.age}", + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 3.w), + ], + ), + ), + ], + ), + Row( + children: [ + GestureDetector( + child: SizedBox( + child: text( + "ID:${res.userProfile?.getID()}", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: res.userProfile?.getID() ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + SizedBox(width: 8.w), + getWealthLevel( + res.userProfile?.wealthLevel ?? 0, + width: 58.w, + height: 30.w, + ), + SizedBox(width: 8.w), + getUserLevel( + res.userProfile?.charmLevel ?? 0, + width: 58.w, + height: 30.w, + ), + ], + ), + ], + ), + ), + SizedBox(width: 15.w), + ], + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == res.userProfile?.id}&tageId=${res.userProfile?.id}", + ); + }, + ); + } + + @override + builderDivider() { + // return Divider( + // height: 1.w, + // color: Color(0xff3D3277).withOpacity(0.5), + // indent: 15.w, + // ); + return Container(height: 8.w); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + try { + var fansList = await SCAccountRepository().visitorList(page); + onSuccess(fansList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } +} diff --git a/lib/modules/wallet/gold/gold_record_page.dart b/lib/modules/wallet/gold/gold_record_page.dart new file mode 100644 index 0000000..471c322 --- /dev/null +++ b/lib/modules/wallet/gold/gold_record_page.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; + +import '../../../shared/business_logic/models/res/sc_gold_record_res.dart'; +import '../../../ui_kit/components/sc_page_list.dart'; +import '../../../ui_kit/components/text/sc_text.dart'; + +class GoldRecordPage extends SCPageList { + @override + _GoldRecordPageState createState() => _GoldRecordPageState(); +} + +class _GoldRecordPageState + extends SCPageListState { + String? lastId; + + @override + void initState() { + super.initState(); + enablePullUp = true; + isShowDivider = false; + backgroundColor = SCGlobalConfig.businessLogicStrategy.getGoldRecordPageListBackgroundColor(); + loadData(1); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getGoldRecordPageBackgroundImage(), + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: SCGlobalConfig.businessLogicStrategy.getGoldRecordPageScaffoldBackgroundColor(), + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.goldList, + actions: [], + ), + body: SafeArea(top: false,child: buildList(context),), + ), + ], + ); + } + + @override + Widget buildItem(SCGoldRecordRes res) { + return GestureDetector( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: SCGlobalConfig.businessLogicStrategy.getGoldRecordPageContainerBackgroundColor(), + border: Border.all(color: SCGlobalConfig.businessLogicStrategy.getGoldRecordPageBorderColor(), width: 1.w), + ), + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 3.w), + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Row( + children: [ + SizedBox(width: 10.w), + Container( + constraints: BoxConstraints(maxWidth: 220.w), + child: text( + res.title ?? "", + fontSize: 13.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getGoldRecordPagePrimaryTextColor(), + fontWeight: FontWeight.w600, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + text( + "${res.type == 0 ? "+" : "-"}${res.quantity} ${SCAppLocalizations.of(context)!.coins}", + fontSize: 13.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getGoldRecordPagePrimaryTextColor(), + fontWeight: FontWeight.w600, + ), + SizedBox(height: 4.w), + text( + SCMDateUtils.formatDateTime2( + DateTime.fromMillisecondsSinceEpoch(res.createTime ?? 0), + ), + fontSize: 11.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getGoldRecordPageSecondaryTextColor(), + ), + ], + ), + ), + SizedBox(width: 12.w), + ], + ), + ), + onTap: () {}, + ); + } + + ///加载数据 + @override + loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) async { + if (page == 1) { + lastId = null; + } + try { + var goldList = await SCAccountRepository().goldRecord(lastId: lastId); + if (goldList.isNotEmpty) { + lastId = goldList.last.id; + } + onSuccess(goldList); + } catch (e) { + if (onErr != null) { + onErr(); + } + } + } +} diff --git a/lib/modules/wallet/recharge/recharge_page.dart b/lib/modules/wallet/recharge/recharge_page.dart new file mode 100644 index 0000000..5dc4581 --- /dev/null +++ b/lib/modules/wallet/recharge/recharge_page.dart @@ -0,0 +1,436 @@ +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/services/payment/apple_payment_manager.dart'; +import 'package:yumi/services/payment/google_payment_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +class RechargePage extends StatefulWidget { + @override + _RechargePageState createState() => _RechargePageState(); +} + +class _RechargePageState extends State { + @override + void initState() { + super.initState(); + Provider.of(context, listen: false).balance(); + if (Platform.isAndroid) { + Provider.of(context, listen: false).initializePaymentProcessor(context); + } else if (Platform.isIOS) { + Provider.of(context, listen: false).initializePaymentProcessor(context); + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getRechargePageBackgroundImage(), + width: ScreenUtil().screenWidth, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: SCGlobalConfig.businessLogicStrategy.getRechargePageScaffoldBackgroundColor(), + resizeToAvoidBottomInset: false, + appBar: SocialChatStandardAppBar( + title: SCAppLocalizations.of(context)!.recharge, + actions: [ + GestureDetector( + child: Container( + margin: EdgeInsetsDirectional.only(end: 15.w), + child: Image.asset( + SCGlobalConfig.businessLogicStrategy.getRechargePageRecordIcon(), + width: 24.w, + height: 24.w, + ), + ), + onTap: () { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: true, + transitionDuration: Duration(milliseconds: 350), + barrierColor: SCGlobalConfig.businessLogicStrategy.getRechargePageDialogBarrierColor(), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.pop(context); + setState(() {}); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox(height: height(68)), + Row( + children: [ + Spacer(), + GestureDetector( + child: Container( + decoration: BoxDecoration( + color: SCGlobalConfig.businessLogicStrategy.getRechargePageDialogContainerBackgroundColor(), + borderRadius: BorderRadius.circular(4), + ), + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 3.w, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox(height: 5.w), + GestureDetector( + child: text( + SCAppLocalizations.of( + context, + )!.goldListort, + textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageDialogTextColor(), + fontSize: 14.sp, + ), + onTap: () { + Navigator.of(context).pop(); + SCNavigatorUtils.push( + context, + WalletRoute.goldRecord, + replace: false, + ); + }, + ), + // SizedBox(height: 5.w), + // Container( + // color: Colors.black26, + // width: 110.w, + // height: 0.5.w, + // ), + // SizedBox(height: 5.w,), + // text( + // SCAppLocalizations.of( + // context, + // )!.rechargeList, + // textColor: Colors.black38, + // fontSize: 14.sp, + // ), + SizedBox(height: 5.w), + ], + ), + ), + onTap: () {}, + ), + SizedBox(width: 10.w), + ], + ), + Expanded(child: SizedBox(width: width(1))), + ], + ), + ); + }, + ); + }, + ), + ], + ), + body: SafeArea( + top: false, + child: Column( + children: [ + _buildWalletSection(), + SizedBox(height: 60.w), + Expanded( + child: Container( + padding: EdgeInsets.symmetric( + vertical: 20.w, + horizontal: 12.w, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + color: SCGlobalConfig.businessLogicStrategy.getRechargePageMainContainerBackgroundColor(), + ), + child: Column( + children: [ + Expanded( + child: + Platform.isAndroid + ? Consumer( + builder: (context, ref, child) { + return ref.products.isNotEmpty + ? ListView.separated( + itemCount: + ref.products.length, // 列表项数量 + padding: EdgeInsets.only( + bottom: 15.w, + ), + itemBuilder: (context, index) { + return _buildGoogleProductItem( + ref.products[index], + ref, + index, + ); + }, + separatorBuilder: ( + BuildContext context, + int index, + ) { + return SizedBox(height: 10.w); + }, + ) + : mainEmpty(); + }, + ) + : Consumer( + builder: (context, ref, child) { + return ref.products.isNotEmpty + ? ListView.separated( + itemCount: + ref.products.length, // 列表项数量 + padding: EdgeInsets.only( + bottom: 15.w, + ), + itemBuilder: (context, index) { + return _buildAppleProductItem( + ref.products[index], + ref, + index, + ); + }, + separatorBuilder: ( + BuildContext context, + int index, + ) { + return SizedBox(height: 10.w); + }, + ) + : mainEmpty(); + }, + ), + ), + ///针对ios的恢复购买 + Platform.isIOS?SCDebounceWidget( + child: Container( + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 60.w), + height: 42.w, + decoration: BoxDecoration( + color: SCGlobalConfig.businessLogicStrategy.getRechargePageButtonBackgroundColor(), + borderRadius: BorderRadius.all( + Radius.circular(height(8)), + ), + ), + child: text( + SCAppLocalizations.of(context)!.restorePurchases, + textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageButtonTextColor(), + fontSize: 16.sp, + ), + ), + onTap: () { + Provider.of( + context, + listen: false, + ).recoverTransactions(); + }, + ):Container(), + SizedBox(height: 15.w), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 60.w), + height: 42.w, + decoration: BoxDecoration( + color: SCGlobalConfig.businessLogicStrategy.getRechargePageButtonBackgroundColor(), + borderRadius: BorderRadius.all( + Radius.circular(height(8)), + ), + ), + child: text( + SCAppLocalizations.of(context)!.recharge, + textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageButtonTextColor(), + fontSize: 16.sp, + ), + ), + onTap: () { + if (Platform.isAndroid) { + Provider.of( + context, + listen: false, + ).processPurchase(); + } else if (Platform.isIOS) { + Provider.of( + context, + listen: false, + ).processPurchase(); + } + }, + ), + SizedBox(height: 45.w), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildWalletSection() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 15.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 45.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getRechargePageGoldIcon(), + width: 36.w, + height: 36.w, + ), + SizedBox(width: 3.w), + Consumer( + builder: (context, ref, child) { + return text( + ref.myBalance > 1000 + ? "${(ref.myBalance / 1000).toStringAsFixed(2)}k" + : "${ref.myBalance}", + fontSize: 16.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageWalletTextColor(), + fontWeight: FontWeight.bold, + ); + }, + ), + ], + ), + ], + ), + ); + } + + + Widget _buildGoogleProductItem( + SelecteProductConfig productConfig, + AndroidPaymentProcessor ref, + int index, + ) { + return GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(3), + color: productConfig.isSelecte ? SCGlobalConfig.businessLogicStrategy.getRechargePageSelectedItemBackgroundColor() : SCGlobalConfig.businessLogicStrategy.getRechargePageUnselectedItemBackgroundColor(), + border: Border.all( + color: productConfig.isSelecte ? SCGlobalConfig.businessLogicStrategy.getRechargePageSelectedItemBorderColor() : SCGlobalConfig.businessLogicStrategy.getRechargePageUnselectedItemBorderColor(), + width: 1.w, + ), + ), + child: Row( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getRechargePageGoldIconByIndex(index), + width: 34.w, + height: 34.w, + ), + SizedBox(width: 5.w), + text( + "${ref.productMap[productConfig.produc.id]?.obtainCandy ?? 0}", + fontSize: 12.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageItemTextColor(), + ), + Spacer(), + text( + productConfig.produc.price, + textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageItemPriceTextColor(), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + ), + ], + ), + ), + onTap: () { + ref.chooseProductConfig(index); + }, + ); + } + + Widget _buildAppleProductItem( + SelecteProductConfig productConfig, + IOSPaymentProcessor ref, + int index, + ) { + return GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: SCGlobalConfig.businessLogicStrategy.getRechargePageAppleItemBackgroundColor(), + border: Border.all( + color: productConfig.isSelecte ? SCGlobalConfig.businessLogicStrategy.getRechargePageSelectedItemBorderColor() : SCGlobalConfig.businessLogicStrategy.getRechargePageUnselectedItemBorderColor(), + width: 1.w, + ), + ), + child: Row( + children: [ + Image.asset( + SCGlobalConfig.businessLogicStrategy.getRechargePageGoldIconByIndex(index), + width: 34.w, + height: 34.w, + ), + SizedBox(width: 5.w), + text( + "${ref.productMap[productConfig.produc.id]?.obtainCandy ?? 0}", + fontSize: 12.sp, + textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageItemTextColor(), + ), + Spacer(), + text( + productConfig.produc.price, + textColor: SCGlobalConfig.businessLogicStrategy.getRechargePageItemPriceTextColor(), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + ), + ], + ), + ), + onTap: () { + ref.chooseProductConfig(index); + }, + ); + } +} + +class SelecteProductConfig { + ProductDetails produc; + bool isSelecte = false; + + SelecteProductConfig(this.produc, this.isSelecte); +} diff --git a/lib/modules/wallet/wallet_route.dart b/lib/modules/wallet/wallet_route.dart new file mode 100644 index 0000000..fc0b7e1 --- /dev/null +++ b/lib/modules/wallet/wallet_route.dart @@ -0,0 +1,18 @@ +import 'package:fluro/fluro.dart'; +import 'package:yumi/modules/wallet/gold/gold_record_page.dart'; +import 'package:yumi/modules/wallet/recharge/recharge_page.dart'; + +import 'package:yumi/app/routes/sc_router_init.dart'; + +class WalletRoute implements SCIRouterProvider { + static String recharge = '/main/me/wallet/recharge'; + static String goldRecord = '/main/me/gold/goldRecord'; + + @override + void initRouter(FluroRouter router) { + router.define(recharge, + handler: Handler(handlerFunc: (_, params) => RechargePage())); + router.define(goldRecord, + handler: Handler(handlerFunc: (_, params) => GoldRecordPage())); + } +} diff --git a/lib/modules/webview/webview_page.dart b/lib/modules/webview/webview_page.dart new file mode 100644 index 0000000..4d4c9df --- /dev/null +++ b/lib/modules/webview/webview_page.dart @@ -0,0 +1,291 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_deviceId_utils.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/main.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +import '../chat/chat_route.dart'; + +class WebViewPage extends StatefulWidget { + final String title; + final String url; + final String showTitle; + + const WebViewPage({ + Key? key, + this.title = "", + this.showTitle = "true", + required this.url, + }) : super(key: key); + + @override + _WebViewPageState createState() => _WebViewPageState(); +} + +class _WebViewPageState extends State { + late WebViewController _controller; + String _title = ""; + double _progress = 0.0; + bool _isLoading = true; + String urlLink = ""; + + @override + void initState() { + super.initState(); + if (widget.url.split("?").length > 1) { + urlLink = "${widget.url}&"; + } else { + urlLink = "${widget.url}?"; + } + urlLink = "${urlLink}lang=${SCGlobalConfig.lang}"; + // 初始化 WebViewController + _controller = + WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..addJavaScriptChannel( + "FlutterPageControl", + onMessageReceived: (sjMessage) async { + String msg = sjMessage.message; + if (sjMessage.message == "close_page") { + SCNavigatorUtils.goBack(context); + } else if (msg.startsWith("view_user_info")) { + //跳转个人信息 + if (msg.contains(":")) { + var sli = msg.split(":"); + if (sli.length > 1) { + String userId = sli[1]; + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == userId}&tageId=$userId", + ); + } + } + } else if (msg.startsWith("go_to_room")) { + //跳转房间 + if (msg.contains(":")) { + var sli = msg.split(":"); + if (sli.length > 1) { + String roomId = sli[1]; + SCRoomUtils.goRoom( + roomId, + navigatorKey.currentState!.context, + ); + } + } + } else if (msg.startsWith("private_chat")) { + //打开私聊 + if (msg.contains(":")) { + var sli = msg.split(":"); + if (sli.length > 1) { + String userId = sli[1]; + var conversation = V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: userId, + conversationID: '', + ); + var bool = await Provider.of( + context, + listen: false, + ).startConversation(conversation); + if (!bool) return; + var json = jsonEncode(conversation.toJson()); + SCNavigatorUtils.push( + context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + } + } + } else if (msg.startsWith("uploadImgFile")) { + picImage(); + } else if (msg.startsWith("editingRoom")) { + SCNavigatorUtils.push(context, SCMainRoute.editRoomSearchAdmin); + } else if (msg.startsWith("editingUser")) { + SCNavigatorUtils.push(context, SCMainRoute.editUserSearchAdmin); + } + }, + ) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + setState(() { + _progress = progress / 100.0; + }); + }, + onPageStarted: (String url) { + setState(() { + _isLoading = true; + }); + }, + onPageFinished: (String url) { + // 延迟500毫秒注入,等待Vue3初始化 + Future.delayed(Duration(milliseconds: 550), () { + _injectJavaScriptInterface(); + }); + setState(() { + _isLoading = false; + _getPageTitle(); + }); + }, + onWebResourceError: (WebResourceError error) { + _onWebResourceError(error); + }, + ), + ) + ..loadRequest(Uri.parse(urlLink)); + } + + @override + void dispose() { + _controller.loadRequest(Uri.parse('about:blank')); + super.dispose(); + } + + void _injectJavaScriptInterface() async { + String imei = await SCDeviceIdUtils.getDeviceId(); + // 注入JavaScript对象和方法 + _controller.runJavaScript(''' + window.app = window.app || {}; + // 注入获取访问信息的方法 + window.app.getAccessOrigin = function() { + // 获取认证信息(这里可以从Flutter传递) + return JSON.stringify({ + 'Authorization': 'Bearer ${AccountStorage().token}', + 'Req-Lang': '${SCGlobalConfig.lang}', + 'Req-App-Intel': 'build=${SCGlobalConfig.build};version=${SCGlobalConfig.version};model=${SCGlobalConfig.model};channel=${SCGlobalConfig.channel};Req-Imei=$imei', + 'Req-Sys-Origin': 'origin=${SCGlobalConfig.origin};originChild=${SCGlobalConfig.originChild}' + }); + }; + + // 注入其他可能需要的方法 + window.app.getAuth = function() { + return window.app.getAccessOrigin(); + }; + + // 可以通过这个方法向Flutter发送消息 + window.app.sendToFlutter = function(data) { + FlutterApp.postMessage(data); + }; + '''); + } + + void picImage() async { + SCPickUtils.pickImage(context, (b, path) { + _controller.runJavaScript(''' + window.app = window.app || {}; + // 注入获取访问信息的方法 + window.app.getImagePath = function() { + // 获取认证信息(这里可以从Flutter传递) + return JSON.stringify({ + 'path': '$path', + }); + }; + + // 注入其他可能需要的方法 + window.app.getAuth = function() { + return window.app.getImagePath(); + }; + + // 可以通过这个方法向Flutter发送消息 + window.app.sendToFlutter = function(data) { + FlutterApp.postMessage(data); + }; + '''); + }, neeCrop: false); + } + + // 获取页面标题 + Future _getPageTitle() async { + try { + final title = + await _controller.runJavaScriptReturningResult("document.title") + as String?; + if (title != null) { + setState(() { + _title = title.replaceAll("\"", ""); + }); + } + } catch (e) { + print("获取标题失败: $e"); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: + widget.showTitle == "true" + ? AppBar( + elevation: 0, + backgroundColor: SCGlobalConfig.businessLogicStrategy.getWebViewPageAppBarBackgroundColor(), + centerTitle: true, + title: Text( + _title.isNotEmpty ? _title : widget.title, + style: TextStyle( + fontSize: sp(18), + color: SCGlobalConfig.businessLogicStrategy.getWebViewPageTitleTextColor(), + ), + ), + leading: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SCNavigatorUtils.goBack(context); + }, + child: Container( + padding: EdgeInsets.only(left: width(15), right: width(15)), + child: Center( + child: Icon(Icons.arrow_back_ios, size: 20.w, color: SCGlobalConfig.businessLogicStrategy.getWebViewPageBackArrowColor()), + ), + ), + ), + systemOverlayStyle: SystemUiOverlayStyle.dark, + ) + : null, + body: SafeArea( + top: false, + child: Stack( + children: [ + WebViewWidget(controller: _controller), + + // 加载指示器 + // if (_isLoading) + // Center( + // child: CircularProgressIndicator( + // valueColor: AlwaysStoppedAnimation(Color(0xffFF6000)), + // ), + // ), + ], + ), + ), + ); + } + + Widget _progressBar(double progress, BuildContext context) { + return progress < 1.0 + ? LinearProgressIndicator( + value: progress, + backgroundColor: SCGlobalConfig.businessLogicStrategy.getWebViewPageProgressBarBackgroundColor(), + minHeight: 1.0, + valueColor: AlwaysStoppedAnimation(SCGlobalConfig.businessLogicStrategy.getWebViewPageProgressBarActiveColor()), + ) + : const SizedBox.shrink(); + } + + void _onWebResourceError(WebResourceError error) { + print('_onWebResourceError:${error.description}'); + // 可以在这里添加错误处理逻辑,比如显示错误页面 + } + +} diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart new file mode 100644 index 0000000..a6c5b09 --- /dev/null +++ b/lib/services/audio/rtc_manager.dart @@ -0,0 +1,1046 @@ +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:fluro/fluro.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/shared/tools/sc_permission_utils.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/services/room/rc_room_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/modules/room/voice_room_route.dart'; +import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart'; +import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; +import '../../shared/tools/sc_heartbeat_utils.dart'; +import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; +import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; +import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart'; +import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; +import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart'; +import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import '../../ui_kit/components/sc_float_ichart.dart'; + +typedef OnSoundVoiceChange = Function(num index, int volum); +typedef RtcProvider = RealTimeCommunicationManager; + +class RealTimeCommunicationManager extends ChangeNotifier { + bool needUpDataUserInfo = false; + + ///当前所在房间 + JoinRoomRes? currenRoom; + + /// 声音音量变化监听 + List _onSoundVoiceChangeList = []; + + ///麦位 + Map roomWheatMap = {}; + + RtcEngine? engine; + BuildContext? context; + RtmProvider? rtmProvider; + + SCIsFollowRoomRes? isFollowRoomRes; + SCRoomRocketStatusRes? roomRocketStatus; + + ///房间是否静音 + bool roomIsMute = false; + + bool closeFullGame = false; + + ///禁音开关 默认关闭 + bool isMic = true; + + ///在线用户列表 + List onlineUsers = []; + + ///房间管理员 + List managerUsers = []; + + ///音乐是否正在播放 + bool isMusicPlaying = false; + + ///房间红包列表 + List redPacketList = []; + + num roomTaskClaimableCount = 0; + + initializeRealTimeCommunicationManager(BuildContext context) { + this.context = context; + } + + Future joinAgoraVoiceChannel() async { + try { + engine = await _initAgoraRtcEngine(); + engine?.setAudioProfile( + profile: AudioProfileType.audioProfileSpeechStandard, + scenario: AudioScenarioType.audioScenarioGameStreaming, + ); + engine?.enableAudioVolumeIndication( + interval: 500, + smooth: 3, + reportVad: true, + ); + await engine?.disableVideo(); + await engine?.setLocalPublishFallbackOption( + StreamFallbackOptions.streamFallbackOptionAudioOnly, + ); + engine?.registerEventHandler( + RtcEngineEventHandler( + onError: (ErrorCodeType err, String msg) { + print('rtc错误${err}'); + }, + onLocalAudioStateChanged: + ( + RtcConnection connection, + LocalAudioStreamState state, + LocalAudioStreamReason reason, + ) {}, + onAudioRoutingChanged: (routing) {}, + onAudioMixingStateChanged: ( + AudioMixingStateType state, + AudioMixingReasonType reason, + ) { + switch (state) { + case AudioMixingStateType.audioMixingStatePlaying: + isMusicPlaying = true; + break; + case AudioMixingStateType.audioMixingStateStopped: + case AudioMixingStateType.audioMixingStateFailed: + isMusicPlaying = false; + break; + default: + break; + } + }, + onRemoteAudioStateChanged: ( + RtcConnection connection, + int remoteUid, + RemoteAudioState state, + RemoteAudioStateReason reason, + int elapsed, + ) { + // print('用户 $remoteUid 音频状态: $state, 原因: $reason'); + }, + onJoinChannelSuccess: (RtcConnection connection, int elapsed) { + print('rtc 自己加入 ${connection.channelId} ${connection.localUid}'); + }, + onUserJoined: (connection, remoteUid, elapsed) { + print('rtc用户 $remoteUid 加入了频道'); + }, + // 监听远端用户离开 + onUserOffline: (connection, remoteUid, reason) { + print('rtc用户 $remoteUid 离开了频道 (原因: ${reason})'); + }, + onTokenPrivilegeWillExpire: ( + RtcConnection connection, + String token, + ) async { + var rtcToken = await SCAccountRepository().getRtcToken( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); + engine?.renewToken(rtcToken.rtcToken ?? ""); + }, + onAudioVolumeIndication: initializeAudioVolumeIndicationCallback, + ), + ); + var rtcToken = await SCAccountRepository().getRtcToken( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); + await engine?.joinChannel( + token: rtcToken.rtcToken ?? "", + channelId: currenRoom?.roomProfile?.roomProfile?.id ?? "", + uid: int.parse( + AccountStorage().getCurrentUser()?.userProfile?.account ?? "0", + ), + options: ChannelMediaOptions( + // 自动订阅所有视频流 + autoSubscribeVideo: false, + // 自动订阅所有音频流 + autoSubscribeAudio: true, + // 发布摄像头采集的视频 + publishCameraTrack: false, + // 发布麦克风采集的音频 + publishMicrophoneTrack: true, + // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众) + clientRoleType: ClientRoleType.clientRoleAudience, + channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, + ), + ); + engine?.muteAllRemoteAudioStreams(roomIsMute); + } catch (e) { + SCTts.show("Join room fail"); + exitCurrentVoiceRoomSession(false); + print('加入失败:${e.runtimeType},${e.toString()}'); + } + } + + Future _initAgoraRtcEngine() async { + RtcEngine? engine; + while (engine == null) { + engine = createAgoraRtcEngine(); + await engine.initialize( + RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid), + ); + } + return engine; + } + + void initializeAudioVolumeIndicationCallback( + RtcConnection connection, + List speakers, + int speakerNumber, + int totalVolume, + ) { + // if (user == null) { + // throw "本地User尚未初始化"; + // } + // print('初始化声音监听器:${user.toJson()}'); + + ///声音全部清零 + roomWheatMap.forEach((k, v) { + roomWheatMap[k]!.setVolume = 0; + }); + for (var info in speakers) { + num? uid = info.uid; + int voloum = info.volume!; + print('${info.uid}在说话---${voloum}'); + + if (voloum > 0) { + ///是本地声音 + if (uid == 0) { + uid = num.parse( + AccountStorage().getCurrentUser()!.userProfile!.account!, + ); + } + + ///在哪个麦 + roomWheatMap.forEach((k, v) { + if (v.user != null) { + if (num.parse(v.user!.account!) == uid) { + roomWheatMap[k]!.setVolume = voloum; + for (var element in _onSoundVoiceChangeList) { + element?.call(k, voloum); + } + } + } + }); + } + } + } + + ///关注/取消房间 + void followCurrentVoiceRoom() async { + var result = await SCAccountRepository().followRoom( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + if (isFollowRoomRes != null) { + isFollowRoomRes = isFollowRoomRes!.copyWith( + followRoom: !isFollowRoomRes!.followRoom!, + ); + } else { + isFollowRoomRes = SCIsFollowRoomRes(followRoom: result); + } + SmartDialog.dismiss(tag: "unFollowDialog"); + notifyListeners(); + } + + int startTime = 0; + + void joinVoiceRoomSession( + BuildContext context, + String roomId, { + String? pwd, + bool clearRoomData = false, + bool needOpenRedenvelope = false, + String redPackId = "", + }) async { + int nextTime = DateTime.now().millisecondsSinceEpoch; + if (nextTime - startTime < 1000) { + //频繁点击 + return; + } + startTime = nextTime; + if (clearRoomData) { + _clearData(); + } + bool hasPermission = await SCPermissionUtils.checkMicrophonePermission(); + if (!hasPermission) { + SCTts.show('Microphone permission is denied.'); + throw ArgumentError('Microphone permission is denied.'); + } + if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { + ///最小化进入房间,或者进入的是同一个房间 + loadRoomInfo(currenRoom?.roomProfile?.roomProfile?.id ?? ""); + Provider.of( + context, + listen: false, + ).fetchUserProfileData(); + retrieveMicrophoneList(); + SCFloatIchart().remove(); + SCNavigatorUtils.push( + context, + '${VoiceRoomRoute.voiceRoom}?id=$roomId', + transition: TransitionType.custom, + transitionDuration: kOpenRoomTransitionDuration, + transitionBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return kOpenRoomTransitionBuilder( + context, + animation, + secondaryAnimation, + child, + ); + }, + ); + } else { + SCFloatIchart().remove(); + if (currenRoom != null) { + await exitCurrentVoiceRoomSession(false); + } + rtmProvider = Provider.of(context, listen: false); + SCLoadingManager.show(context: context); + currenRoom = await SCAccountRepository().entryRoom( + roomId, + pwd: pwd, + redPackId: redPackId, + needOpenRedenvelope: needOpenRedenvelope, + ); + await initializeRoomSession( + needOpenRedenvelope: needOpenRedenvelope, + redPackId: redPackId, + ); + SCLoadingManager.hide(); + notifyListeners(); + } + } + + ///初始化房间相关 + Future initializeRoomSession({ + bool needOpenRedenvelope = false, + String? redPackId, + }) async { + if ((currenRoom?.roomProfile?.roomProfile?.event == + SCRoomInfoEventType.WAITING_CONFIRMED.name || + currenRoom?.roomProfile?.roomProfile?.event == + SCRoomInfoEventType.ID_CHANGE.name) && + currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name) { + ///需要去修改房间信息,创建群聊 + SCNavigatorUtils.push( + context!, + "${VoiceRoomRoute.roomEdit}?need=true", + replace: false, + ); + return Future; + } + SCRoomUtils.roomSCGlobalConfig( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + if (currenRoom?.roomProfile?.roomProfile?.id != + AccountStorage().getCurrentUser()?.userProfile?.id) { + SCChatRoomRepository() + .isFollowRoom(currenRoom?.roomProfile?.roomProfile?.id ?? "") + .then((value) { + isFollowRoomRes = value; + notifyListeners(); + }); + } + SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false); + var joinResult = await rtmProvider?.joinRoomGroup( + currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + "", + ); + SCChatRoomRepository() + .rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "") + .catchError((e) { + return true; // 错误已处理 + }); + rtmProvider?.addMsg( + Msg( + groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + msg: SCAppLocalizations.of(context!)!.systemRoomTips, + type: SCRoomMsgType.systemTips, + ), + ); + rtmProvider?.addMsg( + Msg( + groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + msg: currenRoom?.roomProfile?.roomProfile?.roomDesc, + type: SCRoomMsgType.systemTips, + ), + ); + + ///获取麦位 + retrieveMicrophoneList(); + Provider.of( + context!, + listen: false, + ).fetchContributionLevelData( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + await joinAgoraVoiceChannel(); + if (joinResult?.code == 0) { + Msg joinMsg = Msg( + groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + user: AccountStorage().getCurrentUser()?.userProfile, + msg: "", + role: currenRoom?.entrants?.roles ?? "", + type: SCRoomMsgType.joinRoom, + ); + rtmProvider?.dispatchMessage(joinMsg, addLocal: true); + if (SCGlobalConfig.isEntryVehicleAnimation) { + if (AccountStorage().getCurrentUser()?.userProfile?.getMountains() != + null) { + Future.delayed(Duration(milliseconds: 550), () { + SCGiftVapSvgaManager().play( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.getMountains() + ?.sourceUrl ?? + "", + priority: 100, + type: 1, + ); + }); + } + } + if (rtmProvider?.msgUserJoinListener != null) { + rtmProvider?.msgUserJoinListener!(joinMsg); + } + } + SCChatRoomRepository() + .rocketStatus(currenRoom?.roomProfile?.roomProfile?.id ?? "") + .then((res) { + roomRocketStatus = res; + notifyListeners(); + }) + .catchError((e) {}); + loadRoomRedPacketList(1); + + fetchRoomTaskClaimableCount(); + } + + ///更新房间火箭信息 + void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) { + roomRocketStatus = res; + notifyListeners(); + } + + ///获取在线用户 + Future fetchOnlineUsersList() async { + onlineUsers = await SCChatRoomRepository().roomOnlineUsers( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + managerUsers.clear(); + for (var user in onlineUsers) { + if (user.roles == SCRoomRolesType.ADMIN.name) { + managerUsers.add(user); + } + } + notifyListeners(); + } + + Future retrieveMicrophoneList() async { + bool isOnMic = false; + var roomWheatList = await SCChatRoomRepository().micList( + currenRoom!.roomProfile?.roomProfile?.id ?? "", + ); + for (var roomWheat in roomWheatList) { + roomWheatMap[roomWheat.micIndex!] = roomWheat; + if (roomWheat.user != null && + roomWheat.user!.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + isOnMic = true; + + ///自己在麦上 + SCHeartbeatUtils.scheduleAnchorHeartbeat( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + } + } + notifyListeners(); + SCHeartbeatUtils.scheduleHeartbeat( + SCHeartbeatStatus.VOICE_LIVE.name, + isOnMic, + roomId: currenRoom?.roomProfile?.roomProfile?.id, + ); + Future.delayed(Duration(milliseconds: 1500), () { + fetchOnlineUsersList(); + }); + } + + void fetchRoomTaskClaimableCount() { + SCChatRoomRepository() + .roomTaskClaimableCount() + .then((res) { + roomTaskClaimableCount = res.claimableCount ?? 0; + notifyListeners(); + }) + .catchError((e) {}); + } + + Future> loadRoomRedPacketList( + int current, + ) async { + var result = await SCChatRoomRepository().roomRedPacketList( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + current, + ); + if (current == 1) { + redPacketList = result; + notifyListeners(); + } + return result; + } + + Future exitCurrentVoiceRoomSession(bool isLogout) async { + try { + rtmProvider?.msgAllListener = null; + rtmProvider?.msgChatListener = null; + rtmProvider?.msgGiftListener = null; + SCLoadingManager.show(context: context); + SCGiftVapSvgaManager().dispose(); + SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); + SCHeartbeatUtils.cancelAnchorTimer(); + rtmProvider?.cleanRoomData(); + await engine?.leaveChannel(); + await Future.delayed(Duration(milliseconds: 100)); + await engine?.release(); + await rtmProvider?.quitGroup( + currenRoom!.roomProfile?.roomProfile?.roomAccount ?? "", + ); + await SCAccountRepository().quitRoom( + currenRoom!.roomProfile?.roomProfile?.id ?? "", + ); + _clearData(); + SCLoadingManager.hide(); + if (!isLogout) { + SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home)); + } + } catch (e) { + print('rtc退出房间出错: $e'); + _clearData(); + SCLoadingManager.hide(); + if (!isLogout) { + SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home)); + } + } + } + + ///清空列表数据 + void _clearData() { + roomRocketStatus = null; + rtmProvider + ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = + null; + roomWheatMap.clear(); + onlineUsers.clear(); + needUpDataUserInfo = false; + SCRoomUtils.roomUsersMap.clear(); + roomIsMute = false; + rtmProvider?.roomAllMsgList.clear(); + rtmProvider?.roomChatMsgList.clear(); + redPacketList.clear(); + currenRoom = null; + isMic = true; + isMusicPlaying = false; + SCGiftVapSvgaManager().clearTasks(); + SCRoomUtils.closeAllDialogs(); + SCGlobalConfig.isEntryVehicleAnimation = true; + SCGlobalConfig.isGiftSpecialEffects = true; + SCGlobalConfig.isFloatingAnimationInGlobal = true; + SCGlobalConfig.isLuckGiftSpecialEffects = true; + } + + void toggleRemoteAudioMuteForAllUsers() { + roomIsMute = !roomIsMute; + engine?.muteAllRemoteAudioStreams(roomIsMute); + notifyListeners(); + } + + ///点击的位置 + void clickSite(num index, {SocialChatUserProfile? clickUser}) { + if (index == -1) { + if (clickUser != null) { + if (clickUser.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + ///是自己,直接打开资料卡 + showBottomInCenterDialog( + context!, + RoomUserInfoCard(userId: clickUser.id), + ); + } else { + showBottomInBottomDialog( + context!, + EmptyMaiSelect(index: index, clickUser: clickUser), + ); + } + } + } else { + SocialChatUserProfile? roomWheatUser = roomWheatMap[index]?.user; + showBottomInBottomDialog( + context!, + EmptyMaiSelect(index: index, clickUser: roomWheatUser), + ); + // if (roomWheatUser != null) { + // ///麦上有人 + // if (roomWheatUser.id == + // AccountStorage().getCurrentUser()?.userProfile?.id) { + // ///是自己 + // showBottomInBottomDialog( + // context!, + // EmptyMaiSelect(index: index, clickUser: roomWheatUser), + // ); + // } else { + // showBottomInBottomDialog( + // context!, + // RoomUserInfoCard(userId: roomWheatUser.id), + // ); + // } + // } else { + // showBottomInBottomDialog( + // context!, + // EmptyMaiSelect(index: index, clickUser: roomWheatUser), + // ); + // } + } + } + + addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { + _onSoundVoiceChangeList.add(onSoundVoiceChange); + print('添加监听:${_onSoundVoiceChangeList.length}'); + } + + removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { + _onSoundVoiceChangeList.remove(onSoundVoiceChange); + print('删除监听:${_onSoundVoiceChangeList.length}'); + } + + void shangMai(num index, {String? eventType, String? inviterId}) async { + var myUser = AccountStorage().getCurrentUser()?.userProfile; + if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) { + } else { + if (isTourists()) { + SCTts.show( + SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic, + ); + return; + } + } + + try { + var micGoUpRes = await SCChatRoomRepository().micGoUp( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + eventType: eventType, + inviterId: inviterId, + ); + + /// 设置成主播角色 + engine?.renewToken(micGoUpRes.roomToken ?? ""); + if (!micGoUpRes.micMute!) { + if (!isMic) { + engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + adjustRecordingSignalVolume(100); + await engine?.muteLocalAudioStream(false); + } + } else { + engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + await engine?.muteLocalAudioStream(true); + } + SCHeartbeatUtils.scheduleAnchorHeartbeat( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + var us = roomWheatMap[index]; + us?.copyWith( + user: SocialChatUserProfile( + id: myUser?.id, + account: myUser?.account, + userAvatar: myUser?.userAvatar, + userNickname: myUser?.userNickname, + userSex: myUser?.userSex, + ), + ); + roomWheatMap[index] = us!; + if (us.micMute!) { + ///房主上麦自动解禁麦位 + if (isFz()) { + jieJinMai(index); + } + } + + notifyListeners(); + } catch (ex) { + SCTts.show('Failed to put on the microphone, $ex'); + } + } + + xiaMai(num index) async { + SCChatRoomRepository() + .micGoDown(currenRoom?.roomProfile?.roomProfile?.id ?? "", index) + .whenComplete(() { + //自己 + if (roomWheatMap[index]?.user?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + isMic = true; + engine?.muteLocalAudioStream(true); + } + SCHeartbeatUtils.cancelAnchorTimer(); + + /// 设置成主持人角色 + engine?.renewToken(""); + engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + roomWheatMap[index]?.setUser = null; + notifyListeners(); + }); + } + + ///踢人下麦 + killXiaMai(String userId) { + try { + roomWheatMap.forEach((k, v) async { + if (v.user?.id == userId) { + var canKill = await SCChatRoomRepository().kickOffMicrophone( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + userId, + ); + if (canKill) { + await SCChatRoomRepository().micKill( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + k, + ); + Provider.of(context!, listen: false).dispatchMessage( + Msg( + groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount, + msg: userId, + type: SCRoomMsgType.killXiaMai, + ), + addLocal: false, + ); + roomWheatMap[k]?.setUser = null; + notifyListeners(); + } else { + SCTts.show("cant kill the user"); + } + } + }); + } catch (e) { + SCTts.show("kill the user fail"); + print('踢人下麦失败'); + } + } + + ///自己是否在麦上 + bool isOnMai() { + return roomWheatMap.values + .map((userWheat) => userWheat.user?.id) + .toList() + .contains(AccountStorage().getCurrentUser()?.userProfile?.id); + } + + ///自己是否在指定的麦上 + bool isOnMaiInIndex(num index) { + return roomWheatMap[index]?.user?.id == + AccountStorage().getCurrentUser()?.userProfile?.id; + } + + ///点击的用户在哪个麦上 + num userOnMaiInIndex(String userId) { + num index = -1; + roomWheatMap.forEach((k, value) { + if (value.user?.id == userId) { + index = k; + } + }); + return index; + } + + ///座位上的用户是否开麦 + bool userOnMaiInIndexOfMute(String userId) { + bool micMute = false; + roomWheatMap.forEach((k, value) { + if (value.user?.id == userId) { + value.micMute; + } + }); + return micMute; + } + + ///是否是房主 + bool isFz() { + return currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name; + } + + ///是否是管理 + bool isGL() { + return currenRoom?.entrants?.roles == SCRoomRolesType.ADMIN.name; + } + + ///是否是会员 + bool isHY() { + return currenRoom?.entrants?.roles == SCRoomRolesType.MEMBER.name; + } + + ///是否是游客 + bool isTourists() { + return currenRoom?.entrants?.roles == SCRoomRolesType.TOURIST.name; + } + + ///麦位变动 + void micChange(List? mics) { + if (mics == null || mics.isEmpty) { + return; + } + roomWheatMap.clear(); + for (var mic in mics) { + roomWheatMap[mic.micIndex!] = mic; + if (mic.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { + if (mic.micMute!) { + ///麦克风静音 + engine?.muteLocalAudioStream(true); + } else { + // if (!isMic) { + // engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + // } + // engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + ///这个用户需要禁麦/解禁麦克风 + engine?.muteLocalAudioStream(false); + } + } + } + notifyListeners(); + + ///判断自己是不是在麦上 + num index = userOnMaiInIndex( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); + if (index == -1) { + engine?.muteLocalAudioStream(true); + } + } + + ///找一个空麦位 -1表示没有空位 + num findWheat() { + for (var entry in roomWheatMap.entries) { + if (entry.value.user == null && !entry.value.micLock!) { + return entry.key; + } + } + return -1; + } + + void loadRoomInfo(String roomId) { + SCChatRoomRepository().specific(roomId).then((value) { + currenRoom = currenRoom?.copyWith( + roomProfile: currenRoom?.roomProfile?.copyWith( + roomCounter: value.counter, + roomSetting: value.setting, + roomProfile: currenRoom?.roomProfile?.roomProfile?.copyWith( + roomCover: value.roomCover, + roomName: value.roomName, + roomDesc: value.roomDesc, + ), + ), + ); + + ///如果是游客禁止上麦,已经在麦上的游客需要下麦 + bool touristMike = + currenRoom?.roomProfile?.roomSetting?.touristMike ?? false; + if (!touristMike && isTourists()) { + num index = userOnMaiInIndex( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); + if (index > -1) { + xiaMai(index); + } + } + notifyListeners(); + }); + } + + ///解锁麦位 + void jieFeng(num index) async { + await SCChatRoomRepository().micLock( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + false, + ); + } + + ///锁麦 + void fengMai(num index) async { + await SCChatRoomRepository().micLock( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + true, + ); + } + + ///静音麦克风 + void jinMai(num index) async { + await SCChatRoomRepository().micMute( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + true, + ); + if (isOnMaiInIndex(index)) { + Provider.of( + context!, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + } + var mic = roomWheatMap[index]; + if (mic != null) { + roomWheatMap[index] = mic.copyWith(micMute: true); + notifyListeners(); + } + } + + ///解除静音麦克风 + void jieJinMai(num index) async { + await SCChatRoomRepository().micMute( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + false, + ); + if (isOnMaiInIndex(index)) { + if (!Provider.of(context!, listen: false).isMic) { + if (roomWheatMap[index]?.user != null) { + adjustRecordingSignalVolume(100); + Provider.of( + context!, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + } + } + } + + var mic = roomWheatMap[index]; + if (mic != null) { + roomWheatMap[index] = mic.copyWith(micMute: false); + notifyListeners(); + } + } + + void addOnlineUser(String groupId, SocialChatUserProfile user) { + if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { + return; + } + bool isExtOnlineList = false; + for (var us in onlineUsers) { + if (us.id == user.id) { + isExtOnlineList = true; + break; + } + } + if (!isExtOnlineList) { + Provider.of(context!, listen: false).onlineUsers.add(user); + notifyListeners(); + } + } + + void removOnlineUser(String groupId, String userId) { + if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { + return; + } + SocialChatUserProfile? isExtOnlineUser; + for (var us in onlineUsers) { + if (us.id == userId) { + isExtOnlineUser = us; + break; + } + } + if (isExtOnlineUser != null) { + Provider.of( + context!, + listen: false, + ).onlineUsers.remove(isExtOnlineUser); + notifyListeners(); + } + } + + void starPlayEmoji(Msg msg) { + if (msg.number! > -1) { + var mic = roomWheatMap[msg.number]; + if (mic != null && mic.user != null) { + roomWheatMap[msg.number!] = mic.copyWith( + emojiPath: msg.msg, + type: msg.type, + number: msg.msg, + ); + notifyListeners(); + } + } + } + + ///更新房间背景 + void updateRoomBG(SCRoomThemeListRes res) { + var roomTheme = RoomTheme( + expireTime: res.expireTime, + id: res.id, + themeStatus: res.themeStatus, + themeBack: res.themeBack, + ); + currenRoom?.roomProps?.setRoomTheme(roomTheme); + notifyListeners(); + } + + void updateRoomSetting(RoomSetting roomSetting) { + currenRoom?.roomProfile?.setRoomSetting(roomSetting); + notifyListeners(); + } + + Map>> seatGlobalKeyMap = {}; + + void bindTargetKey(num index, GlobalKey> targetKey) { + seatGlobalKeyMap[index] = targetKey; + } + + GlobalKey>? getSeatGlobalKeyByIndex(String userId) { + ///需要有人的座位 + num index = userOnMaiInIndex(userId); + if (index > -1) { + return seatGlobalKeyMap[index]; + } + } + + ///播放音乐 + void startAudioMixing(String localPath, int cycle) { + if (localPath.isEmpty) { + return; + } + engine?.startAudioMixing( + filePath: localPath, + loopback: false, + cycle: cycle, + ); + } + + void adjustRecordingSignalVolume(int volume) { + engine?.adjustRecordingSignalVolume(volume); + } +} diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart new file mode 100644 index 0000000..02b3b32 --- /dev/null +++ b/lib/services/audio/rtm_manager.dart @@ -0,0 +1,1398 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_message_utils.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; +import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; +import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/gift/gift_system_manager.dart'; + +import '../../shared/data_sources/models/enum/sc_gift_type.dart'; +import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; + +typedef RoomNewMsgListener = Function(Msg msg); +typedef OnNewMessageListener = Function(V2TimMessage? message, {String? msgId}); +typedef OnNewANMessageListener = Function(Records? message); +typedef OnRevokeMessageListener = Function(String msgId); +typedef OnNewGroupMessageListener = + Function(String groupID, V2TimMessage message); +typedef OnMessageRecvC2CReadListener = Function(List messageIDList); +typedef RtmProvider = RealTimeMessagingManager; + +class RealTimeMessagingManager extends ChangeNotifier { + BuildContext? context; + + ///消息列表 + List roomAllMsgList = []; + List roomChatMsgList = []; + List roomGiftMsgList = []; + RoomNewMsgListener? msgAllListener; + RoomNewMsgListener? msgChatListener; + RoomNewMsgListener? msgGiftListener; + + RoomNewMsgListener? msgFloatingGiftListener; + RoomNewMsgListener? msgUserJoinListener; + + /// 当前会话 + V2TimConversation? currentConversation; + + /// 会话列表缓存 + Map conversationMap = {}; + + ///消息已读监听 + OnMessageRecvC2CReadListener? onMessageRecvC2CReadListener; + + ///消息被撤回监听 + OnRevokeMessageListener? onRevokeMessageListener; + + ///新消息监听 单聊 + OnNewMessageListener? onNewMessageCurrentConversationListener; + + OnNewANMessageListener? onNewActivityMessageCurrentConversationListener; + OnNewANMessageListener? onNewNotifcationMessageCurrentConversationListener; + + ///新消息监听 群聊 + Map onNewMessageListenerGroupMap = {}; + int allUnReadCount = 0; + int messageUnReadCount = 0; + int systemUnReadCount = 0; + int customerUnReadCount = 0; + int activityUnReadCount = 0; + int notifcationUnReadCount = 0; + SCBroadCastLuckGiftPush? currentPlayingLuckGift; + final Queue _luckGiftPushQueue = Queue(); + Debouncer debouncer = Debouncer(); + List conversationList = []; + + ///客服 + SocialChatUserProfile? customerInfo; + + void getConversationList() { + List list = conversationMap?.values?.toList() ?? []; + list.removeWhere((element) { + if (element.conversationID == SCGlobalConfig.imAdmin) { + return true; + } + if (element.conversationID == "c2c_${customerInfo?.id}") { + return true; + } + if (element.conversationID == "c2c_atyou-newsletter") { + ///删除这个会话,后台乱发的联系人。。防止无效未读数 + clearC2CHistoryMessage(element.conversationID, false); + return true; + } + if (element.lastMessage == null) { + return true; + } + return false; + }); + list?.sort((e1, e2) { + int time1 = e1.lastMessage?.timestamp ?? 0; + int time2 = e2.lastMessage?.timestamp ?? 0; + return time2.compareTo(time1); + }); + conversationList = list; + systemUnReadCount = + conversationMap[SCGlobalConfig.imAdmin]?.unreadCount ?? 0; + customerUnReadCount = + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; + + notifyListeners(); + } + + init(BuildContext context) async { + this.context = context; + V2TimSDKListener sdkListener = V2TimSDKListener( + onConnectFailed: (int code, String error) { + // 连接失败的回调函数 + // code 错误码 + // error 错误信息 + }, + onConnectSuccess: () { + // SDK 已经成功连接到腾讯云服务器 + }, + onConnecting: () { + // SDK 正在连接到腾讯云服务器 + }, + onKickedOffline: () { + // 当前用户被踢下线,此时可以 UI 提示用户,并再次调用 V2TIMManager 的 login() 函数重新登录。 + }, + onSelfInfoUpdated: (V2TimUserFullInfo info) { + // 登录用户的资料发生了更新 + // info登录用户的资料 + }, + onUserSigExpired: () { + // 在线时票据过期:此时您需要生成新的 userSig 并再次调用 V2TIMManager 的 login() 函数重新登录。 + }, + onUserStatusChanged: (List userStatusList) { + //用户状态变更通知 + //userStatusList 用户状态变化的用户列表 + //收到通知的情况:订阅过的用户发生了状态变更(包括在线状态和自定义状态),会触发该回调 + //在 IM 控制台打开了好友状态通知开关,即使未主动订阅,当好友状态发生变更时,也会触发该回调 + //同一个账号多设备登录,当其中一台设备修改了自定义状态,所有设备都会收到该回调 + }, + ); + V2TimValueCallback initSDKRes = await TencentImSDKPlugin.v2TIMManager + .initSDK( + sdkAppID: int.parse(SCGlobalConfig.tencentImAppid), // SDKAppID + loglevel: LogLevelEnum.V2TIM_LOG_ALL, // 日志登记等级 + listener: sdkListener, // 事件监听器 + ); + if (initSDKRes.code == 0) {} + try { + customerInfo = await SCConfigRepositoryImp().customerService(); + } catch (e) {} + + /// 登录 + await loginTencetRtm(context); + + /// 初始化会话列表 + // _onRefreshConversationSub = FTIM.getContactManager().addRefreshConversationListener(_onRefreshConversation); + TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .addConversationListener( + listener: V2TimConversationListener( + onNewConversation: (conversationList) { + // _onRefreshConversation(conversationList); + initConversation(); + }, + onTotalUnreadMessageCountChanged: (int totalUnreadCount) { + messageUnReadCount = totalUnreadCount; + systemUnReadCount = + conversationMap[SCGlobalConfig.imAdmin]?.unreadCount ?? 0; + customerUnReadCount = + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + }, + ), + ); + TencentImSDKPlugin.v2TIMManager.addGroupListener( + listener: V2TimGroupListener( + onMemberEnter: + (String groupID, List memberList) {}, + onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { + Provider.of( + context, + listen: false, + ).removOnlineUser(groupID, member.userID!); + }, + onMemberKicked: ( + String groupID, + V2TimGroupMemberInfo opUser, + List memberList, + ) { + ///踢出房间 + if (memberList.isNotEmpty) { + if (memberList.first.userID == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + + ///退出房间 + Provider.of( + context!, + listen: false, + ).exitCurrentVoiceRoomSession(false).whenComplete(() { + SCRoomUtils.closeAllDialogs(); + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.kickRoomTips, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () {}, + ); + }, + ); + }); + } + } + }, + ), + ); + + /// 新消息监听 + // FTIM.getMessageManager().addNewMessagesListener(_onNewMessage); + TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( + listener: V2TimAdvancedMsgListener( + onRecvC2CReadReceipt: (List receiptList) { + //会话已读回调 + }, + onRecvMessageModified: (V2TimMessage message) { + // msg 为被修改之后的消息对象 + }, + onRecvMessageReadReceipts: (List receiptList) { + //群聊/单聊已读回调 + List messageIDList = []; + for (var element in receiptList) { + messageIDList.add(element.msgID!); + } + onMessageRecvC2CReadListener?.call(messageIDList); + }, + onRecvMessageRevoked: (String messageId) { + // 在本地维护的消息中处理被对方撤回的消息 + TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .deleteMessages(msgIDs: [messageId]) + .then((result) { + onRevokeMessageListener?.call(messageId); + for (var ms in conversationList) { + if (ms.lastMessage?.msgID == messageId) { + ms.lastMessage?.status = + MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED; + break; + } + } + notifyListeners(); + }); + }, + onRecvNewMessage: (V2TimMessage message) async { + _onNewMessage(message); + }, + onSendMessageProgress: (V2TimMessage message, int progress) { + //文件上传进度回调 + }, + ), + ); + getAllUnReadCount(); + joinBigBroadcastGroup(); + } + + /// 初始化会话 + /// 打开聊天界面的时候调用 + int sTime = 0; + + Future startConversation(V2TimConversation conversation) async { + assert(conversation != null); + int eTime = DateTime.now().millisecondsSinceEpoch; + if (eTime - sTime > 5000) { + sTime = eTime; + currentConversation = conversation; + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .markC2CMessageAsRead(userID: conversation.userID!); + notifyListeners(); + return true; + } else { + return false; + } + } + + ///初始话会话 + Future initConversation() async { + V2TimValueCallback convList = + await TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .getConversationList(nextSeq: '0', count: 100); + List? conversationList = + convList.data?.conversationList; + print('conversationList:${conversationList?.length}'); + conversationMap.clear(); + if (conversationList != null) { + for (V2TimConversation? conversation in conversationList) { + conversationMap[conversation!.conversationID] = conversation; + } + } + getConversationList(); + } + + // /// 所有消息未读数 + // int getAllUnReadCount() { + // allUnReadCount = 0; + // for (var value in conversationList) { + // allUnReadCount += value.unreadCount ?? 0; + // } + // // for (var value in systemConversationList) { + // // i += value.unreadMessageNum; + // // } + // notifyListeners(); + // return allUnReadCount; + // } + + /// 所有消息未读数 + void getAllUnReadCount() async { + V2TimValueCallback res = + await TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .getTotalUnreadMessageCount(); + if (res.code == 0) { + print('初始未读总数: ${res.data}'); + messageUnReadCount = res.data ?? 0; + allUnReadCount = + messageUnReadCount + activityUnReadCount + notifcationUnReadCount; + notifyListeners(); + // 这里可以先用初始值更新UI + } + } + + ///登录IM + Future loginTencetRtm(BuildContext context) async { + SocialChatLoginRes? userModel = AccountStorage().getCurrentUser(); + bool logined = false; + + while (!logined && userModel != null) { + await Future.delayed(Duration(milliseconds: 550)); + try { + if (userModel.userSig != null) { + V2TimCallback res = await TencentImSDKPlugin.v2TIMManager.login( + userID: userModel.userProfile?.id ?? "", + userSig: userModel.userSig ?? "", + ); + print( + 'tim voLogin:${res.code},${userModel.userProfile?.id},${userModel.userSig}', + ); + if (res.code == 0) { + isLogout = false; + // 登录成功逻辑 + logined = true; + print('tim 登录成功'); + await initConversation(); + } else { + // 登录失败逻辑 + //print('timm 需要重新登录2'); + print('tim 登录失败${res.code}'); + SCTts.show('tim login fail'); + } + } else { + //print('timm 需要重新登录sign'); + SCTts.show('tim login fail'); + } + } catch (e) { + //print('timm 登录异常:${e.toString()}'); + SCTts.show('timm login fail:${e.toString()}'); + } + userModel = AccountStorage().getCurrentUser(); + } + } + + _onNewMessage(V2TimMessage message) { + if (message.groupID != null) { + ///全服通知 + if (message.groupID == SCGlobalConfig.bigBroadcastGroup) { + _newBroadCastMsgRecv(message.groupID!, message); + } + + ///群消息 + for (var element in onNewMessageListenerGroupMap.values) { + element?.call(message.groupID!, message); + } + } else { + ///单聊消息 + _onNew1v1Message(message); + } + } + + _onNew1v1Message(V2TimMessage? message, {String? msgId}) async { + for (var element in conversationList) { + if (message?.userID == element?.userID) { + element.lastMessage = message; + if (onNewMessageCurrentConversationListener == null) { + element.unreadCount = element.unreadCount! + 1; + } + } + } + if (message?.userID == customerInfo?.id) { + if (onNewMessageCurrentConversationListener == null) { + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = + (conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0) + 1; + } + } + if (message?.userID == "atyou-admin") { + if (onNewMessageCurrentConversationListener == null) { + conversationMap[SCGlobalConfig.imAdmin]?.unreadCount = + (conversationMap[SCGlobalConfig.imAdmin]?.unreadCount ?? 0) + 1; + } + } + notifyListeners(); + onNewMessageCurrentConversationListener?.call(message, msgId: msgId); + } + + void _onRefreshConversation(List conversations) { + for (V2TimConversation conversation in conversations) { + conversationMap[conversation.conversationID] = conversation; + } + getConversationList(); + } + + ///创建房间im群聊 + Future> createRoomGroup( + String groupID, + String groupName, + ) { + return V2TIMGroupManager().createGroup( + groupID: groupID, + groupType: GroupType.AVChatRoom, + groupName: groupName, + ); + } + + ///加入房间的im群聊 + Future joinRoomGroup(String groupID, String message) async { + _luckGiftPushQueue.clear(); + currentPlayingLuckGift = null; + var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( + groupID: groupID, + message: message, + ); + if (joinResult.code == 0) { + onNewMessageListenerGroupMap[groupID] = _newGroupMsg; + } + return joinResult; + } + + ///发送文本消息 + Future dispatchMessage( + Msg msg, { + bool showEmoticons = true, + bool addLocal = true, + }) async { + if (addLocal) { + addMsg(msg); + } + // 发送消息到腾讯云IM + if (msg.groupId != null) { + await _sendTencentMessage(msg); + } + notifyListeners(); + } + + // 发送腾讯云消息 + Future _sendTencentMessage(Msg msg) async { + try { + switch (msg.type) { + case SCRoomMsgType.text: + case SCRoomMsgType.shangMai: + case SCRoomMsgType.emoticons: + case SCRoomMsgType.xiaMai: + case SCRoomMsgType.killXiaMai: + case SCRoomMsgType.roomRoleChange: + case SCRoomMsgType.roomSettingUpdate: + case SCRoomMsgType.roomBGUpdate: + case SCRoomMsgType.qcfj: + case SCRoomMsgType.fengMai: + case SCRoomMsgType.jieFeng: + case SCRoomMsgType.joinRoom: + case SCRoomMsgType.gift: + case SCRoomMsgType.bsm: + case SCRoomMsgType.roomDice: + case SCRoomMsgType.roomRPS: + case SCRoomMsgType.roomLuckNumber: + case SCRoomMsgType.image: + case SCRoomMsgType.roomGameClose: + case SCRoomMsgType.roomGameCreate: + case SCRoomMsgType.luckGiftAnimOther: + _sendRoomMessage(msg); + break; + default: + break; + } + } catch (e) { + print('发送消息失败: $e'); + // 处理发送失败的情况 + } + } + + ///发送单聊文本消息 + Future sendC2CTextMsg( + String msg, + V2TimConversation toConversation, + ) async { + // 创建文本消息 + V2TimValueCallback createTextMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createTextMessage( + text: msg, // 文本信息 + ); + if (createTextMessageRes.code == 0) { + // 文本信息创建成功 + String id = createTextMessageRes.data!.id!; + // 发送文本消息 + // 在sendMessage时,若只填写receiver则发个人用户单聊消息 + // 若只填写groupID则发群组消息 + // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 + String receiveId = toConversation.userID!; + V2TimValueCallback sendMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getMessageManager() + .sendMessage( + id: id, // 创建的messageid + receiver: toConversation.userID!, // 接收人id + needReadReceipt: true, + groupID: '', // 是否需要已读回执 + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + + ///发送单聊自定义消息 + Future sendC2CCustomMsg( + String msg, + V2TimConversation toConversation, + String extension, + ) async { + // 创建文本消息 + V2TimValueCallback createCustomMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage( + data: msg, // 文本信息 + extension: extension, + ); + if (createCustomMessageRes.code == 0) { + // 文本信息创建成功 + String id = createCustomMessageRes.data!.id!; + // 发送文本消息 + // 在sendMessage时,若只填写receiver则发个人用户单聊消息 + // 若只填写groupID则发群组消息 + // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 + String receiveId = toConversation.userID!; + V2TimValueCallback sendMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + // 创建的messageid + receiver: toConversation.userID!, + // 接收人id + needReadReceipt: true, + isSupportMessageExtension: true, + groupID: '', // 是否需要已读回执 + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + + ///发送单聊图片消息 + Future sendImageMsg({ + List? selectedList, + File? file, + required V2TimConversation conversation, + }) async { + if (file != null) { + File newFile = await SCMessageUtils.createImageElem(file); + V2TimValueCallback createImageMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createImageMessage(imagePath: newFile.path); + if (createImageMessageRes.code == 0) { + String id = createImageMessageRes.data!.id!; + V2TimValueCallback sendMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + receiver: conversation.userID!, + needReadReceipt: true, + groupID: '', + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } else { + if (selectedList != null) { + for (File entity in selectedList) { + String id = ""; + V2TimMessage? message; + //判断是视频或者图片消息 + + if (SCPathUtils.getFileType(entity.path) == "image") { + V2TimValueCallback createImageMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createImageMessage(imagePath: entity.path); + if (createImageMessageRes.code == 0) { + message = createImageMessageRes.data?.messageInfo; + id = createImageMessageRes.data!.id!; + // 创建图片 + File newFile = await SCMessageUtils.createImageElem(entity); + //发送 + V2TimImageElem elem = V2TimImageElem(path: newFile.path); + message?.imageElem = elem; + } + } else if (SCPathUtils.getFileType(entity.path) == "video_pic") { + if (entity.lengthSync() > 50000000) { + SCTts.show(SCAppLocalizations.of(context!)!.theVideoSizeCannotExceed); + return; + } + // 复制一份视频 + String md5Str1 = keyToMd5(entity.path); + File newFile = File( + "${FileCacheManager.videoCachePath}/$md5Str1.mp4", + ); + if (!newFile.existsSync()) { + await entity.copy(newFile.path); + } + // 创建缩略图 + String? thumbImagePath = await SCMessageUtils.generateFileThumbnail( + newFile.path, + 128, + ); + V2TimValueCallback createVideoMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createVideoMessage( + videoFilePath: entity.path, + type: "mp4", + duration: 0, + snapshotPath: thumbImagePath ?? "", + ); + + if (createVideoMessageRes.code == 0) { + message = createVideoMessageRes.data?.messageInfo; + id = createVideoMessageRes.data!.id!; + } + } + // 消息设置 + message?.isSelf = true; + // message.conversation = conversation; + message?.status = MessageStatus.V2TIM_MSG_STATUS_SENDING; + print('组装完成,准备发送:${message?.toJson()}'); + // String id = await FTIM.getMessageManager().sendMessage(message); + // message.msgId = id; + V2TimValueCallback sendMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + receiver: conversation.userID!, + needReadReceipt: true, + groupID: '', + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + } + notifyListeners(); + } + + // 发送消息(房间) + Future _sendRoomMessage(Msg msg) async { + try { + var user = msg.user?.copyWith(); + var toUser = msg.toUser?.copyWith(); + user?.cleanWearHonor(); + user?.cleanWearBadge(); + user?.cleanPhotos(); + toUser?.cleanWearHonor(); + toUser?.cleanWearBadge(); + toUser?.cleanUseProps(); + toUser?.cleanPhotos(); + msg.needUpDataUserInfo = + Provider.of(context!, listen: false).needUpDataUserInfo; + msg.user = user; + msg.toUser = toUser; + final textMsg = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage(data: jsonEncode(msg.toJson())); + + if (textMsg.code != 0) return; + + final sendResult = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: textMsg.data!.id!, + groupID: msg.groupId!, + receiver: '', + ); + if (sendResult.code == 0) {} + } catch (e) { + throw Exception("create fail: $e"); + } + } + + /// 添加消息 + addMsg(Msg msg) { + roomAllMsgList.insert(0, msg); + if (roomAllMsgList.length > 250) { + print('大于200条消息'); + roomAllMsgList.removeAt(roomAllMsgList.length - 1); + } + msgAllListener?.call(msg); + + if (msg.type == SCRoomMsgType.text) { + roomChatMsgList.insert(0, msg); + if (roomChatMsgList.length > 250) { + print('大于200条消息'); + roomChatMsgList.removeAt(roomChatMsgList.length - 1); + } + msgChatListener?.call(msg); + } else if (msg.type == SCRoomMsgType.image) { + roomChatMsgList.insert(0, msg); + if (roomChatMsgList.length > 250) { + print('大于200条消息'); + roomChatMsgList.removeAt(roomChatMsgList.length - 1); + } + msgChatListener?.call(msg); + } else if (msg.type == SCRoomMsgType.gift) { + roomGiftMsgList.insert(0, msg); + if (roomGiftMsgList.length > 250) { + print('大于200条消息'); + roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); + } + msgGiftListener?.call(msg); + msgFloatingGiftListener?.call(msg); + } + } + + bool isLogout = false; + + logout() async { + V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout(); + TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .removeAdvancedMsgListener(); + if (logoutRes.code == 0) { + isLogout = true; + } + } + + ///全服广播消息 + _newBroadCastMsgRecv(String groupID, V2TimMessage message) { + try { + String? customData = message.customElem?.data; + if (customData != null && customData.isNotEmpty) { + final data = json.decode(customData); + var type = data["type"]; + if (type == "SYS_ACTIVITY") { + if (onNewActivityMessageCurrentConversationListener != null) { + var recode = Records.fromJson(data["data"]); + onNewActivityMessageCurrentConversationListener?.call(recode); + } else { + activityUnReadCount = activityUnReadCount + 1; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + } + } else if (type == "SYS_ANNOUNCEMENT") { + if (onNewNotifcationMessageCurrentConversationListener != null) { + var recode = Records.fromJson(data["data"]); + onNewNotifcationMessageCurrentConversationListener?.call(recode); + } else { + notifcationUnReadCount = notifcationUnReadCount + 1; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + } + } else if (type == "GAME_BAISHUN_WIN") { + if (SCGlobalConfig.isReview) { + ///审核状态不播放动画 + return; + } + var fdata = data["data"]; + var winCoins = fdata["currencyDiff"]; + if (winCoins > 14999) { + ///达到5000才飘屏 + SCFloatingMessage msg = SCFloatingMessage( + type: 2, + userId: fdata["account"], + userAvatarUrl: fdata["userAvatar"], + userName: fdata["userNickname"], + giftUrl: fdata["gameUrl"], + roomId: fdata["roomId"], + coins: fdata["currencyDiff"], + ); + OverlayManager().addMessage(msg); + } + } else if (type == "GAME_LUCKY_GIFT") { + } else if (type == "ROCKET_ENERGY_LAUNCH") { + ///火箭触发飘屏 + var fdata = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 3, + roomId: fdata["roomId"], + rocketLevel: fdata["fromLevel"], + userAvatarUrl: fdata["userAvatar"], + userName: fdata["nickname"], + userId: fdata["actualAccount"], + priority: 1000, + ); + OverlayManager().addMessage(msg); + } else if (type == SCRoomMsgType.roomRedPacket) { + ///红包触发飘屏 + var fData = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 4, + roomId: fData["roomId"], + userAvatarUrl: fData["userAvatar"], + userName: fData["userNickname"], + userId: fData["actualAccount"], + toUserId: fData["packetId"], + priority: 1000, + ); + if (msg.roomId == + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id) { + Provider.of( + context!, + listen: false, + ).loadRoomRedPacketList(1); + } + OverlayManager().addMessage(msg); + } else if (type == SCRoomMsgType.inviteRoom) { + ///邀请进入房间 + var fdata = data["data"]; + SCFloatingMessage msg = SCFloatingMessage.fromJson(fdata); + if (msg.toUserId == AccountStorage().getCurrentUser()?.userProfile?.id && + msg.roomId != + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id) { + SmartDialog.dismiss(tag: "showInviteRoom"); + SmartDialog.show( + tag: "showInviteRoom", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return InviteRoomDialog(msg); + }, + ); + } + } + } + } catch (e) {} + } + + _newGroupMsg(String groupID, V2TimMessage message) { + if (groupID != + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.roomAccount) { + return; + } + try { + String? customData = message.customElem?.data; + if (customData != null && customData.isNotEmpty) { + // 直接处理字符串格式的自定义数据 + final data = json.decode(customData); + debugPrint(">>>>>>>>>>>>>>>>>>>消息类型${data["type"]}"); + + if (data["type"] == SCRoomMsgType.roomRedPacket) { + ///房间红包 + var fData = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 4, + roomId: fData["roomId"], + userAvatarUrl: fData["userAvatar"], + userName: fData["userNickname"], + userId: fData["actualAccount"], + toUserId: fData["packetId"], + priority: 1000, + ); + Provider.of( + context!, + listen: false, + ).loadRoomRedPacketList(1); + OverlayManager().addMessage(msg); + return; + } + Msg msg = Msg.fromJson(data); + + if (msg.type == SCRoomMsgType.sendGift || + msg.type == SCRoomMsgType.gameBurstCrystalSprint || + msg.type == SCRoomMsgType.gameBurstCrystalBox) { + ///这个消息暂时不监听 + return; + } + if (msg.type == SCRoomMsgType.bsm) { + if (msg.toUser?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context!)!.tips, + msg: SCAppLocalizations.of( + context!, + )!.invitesYouToTheMicrophone(msg.msg ?? ""), + btnText: SCAppLocalizations.of(context!)!.confirm, + onEnsure: () { + ///上麦 + num index = + Provider.of( + context!, + listen: false, + ).findWheat(); + if (index > -1) { + Provider.of( + context!, + listen: false, + ).shangMai( + index, + eventType: "INVITE", + inviterId: msg.role, + ); + } + }, + ); + }, + ); + } + return; + } + if (msg.type == SCRoomMsgType.killXiaMai) { + ///踢下麦 + if (msg.msg == AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + } + // Provider.of(context!, listen: false).getMicList(); + return; + } + + if (msg.type == SCRoomMsgType.roomSettingUpdate) { + Provider.of( + context!, + listen: false, + ).loadRoomInfo(msg.msg ?? ""); + return; + } + if (msg.type == SCRoomMsgType.roomBGUpdate) { + SCRoomThemeListRes res; + if ((msg.msg ?? "").isNotEmpty) { + res = SCRoomThemeListRes.fromJson(jsonDecode(msg.msg!)); + } else { + res = SCRoomThemeListRes(); + } + Provider.of(context!, listen: false).updateRoomBG(res); + return; + } + if (msg.type == SCRoomMsgType.emoticons) { + Provider.of(context!, listen: false).starPlayEmoji(msg); + return; + } + if (msg.type == SCRoomMsgType.micChange) { + Provider.of( + context!, + listen: false, + ).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); + } else if (msg.type == SCRoomMsgType.refreshOnlineUser) { + Provider.of(context!, listen: false).fetchOnlineUsersList(); + } else if (msg.type == SCRoomMsgType.gameLuckyGift) { + var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); + msg.gift = SocialChatGiftRes(giftPhoto: broadCastRes.data?.giftCover); + msg.awardAmount = broadCastRes.data?.awardAmount; + msg.user = SocialChatUserProfile( + id: broadCastRes.data?.sendUserId, + userNickname: broadCastRes.data?.nickname, + ); + msg.toUser = SocialChatUserProfile( + id: broadCastRes.data?.acceptUserId, + userNickname: broadCastRes.data?.acceptNickname, + ); + addMsg(msg); + if ((broadCastRes.data?.multiple ?? 0) > 0) { + Msg msg2 = Msg( + groupId: '', + msg: '${broadCastRes.data?.multiple}', + type: SCRoomMsgType.gameLuckyGift_5, + ); + + ///5倍率以上聊天页面需要发个消息 + msg2.awardAmount = broadCastRes.data?.awardAmount; + msg2.user = SocialChatUserProfile( + id: broadCastRes.data?.sendUserId, + userNickname: broadCastRes.data?.nickname, + ); + addMsg(msg2); + + if ((broadCastRes.data?.multiple ?? 0) > 2) { + ///3倍率 + SCFloatingMessage msg = SCFloatingMessage( + type: 0, + userId: broadCastRes.data?.sendUserId, + roomId: broadCastRes.data?.roomId, + toUserId: broadCastRes.data?.acceptUserId, + userAvatarUrl: broadCastRes.data?.userAvatar, + userName: broadCastRes.data?.nickname, + toUserName: broadCastRes.data?.acceptNickname, + giftUrl: broadCastRes.data?.giftCover, + number: broadCastRes.data?.giftQuantity, + coins: broadCastRes.data?.awardAmount, + multiple: broadCastRes.data?.multiple, + ); + OverlayManager().addMessage(msg); + addluckGiftPushQueue(broadCastRes); + } + } + + if (broadCastRes.data?.sendUserId == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).updateLuckyRewardAmount(msg.awardAmount ?? 0); + } + } else { + if (msg.type == SCRoomMsgType.joinRoom) { + if (msg.user != null) { + Provider.of( + context!, + listen: false, + ).addOnlineUser(msg.groupId ?? "", msg.user!); + } + if (msgUserJoinListener != null) { + msgUserJoinListener!(msg); + } + + ///坐骑 + if (msg.user?.getMountains() != null) { + if (SCGlobalConfig.isEntryVehicleAnimation) { + SCGiftVapSvgaManager().play( + msg.user?.getMountains()?.sourceUrl ?? "", + priority: 100, + type: 1, + ); + } + } + } else if (msg.type == SCRoomMsgType.gift) { + if (msg.gift!.giftSourceUrl != null && msg.gift!.special != null) { + if (msg.gift!.special!.contains(SCGiftType.ANIMSCION.name) || + msg.gift!.special!.contains(SCGiftType.GLOBAL_GIFT.name)) { + if (SCGlobalConfig.isGiftSpecialEffects) { + SCGiftVapSvgaManager().play(msg.gift!.giftSourceUrl!); + } + } + } + if (Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomSetting?.showHeartbeat ?? + false) { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(); + }, + ); + } + num coins = msg.number! * msg.gift!.giftCandy!; + if (coins > 9999) { + OverlayManager().addMessage( + SCFloatingMessage( + type: 1, + userAvatarUrl: msg.user?.userAvatar ?? "", + userName: msg.user?.userNickname ?? "", + toUserName: msg.toUser?.userNickname ?? "", + toUserAvatarUrl: msg.toUser?.userAvatar ?? "", + giftUrl: msg.gift!.giftPhoto, + number: msg.number, + coins: coins, + roomId: msg.msg, + ), + ); + } + } else if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + if (Provider.of( + context!, + listen: false, + ).hideLGiftAnimal) { + return; + } + eventBus.fire( + GiveRoomLuckWithOtherEvent( + msg.gift?.giftPhoto ?? "", + (jsonDecode(msg.msg ?? "") as List) + .map((e) => e as String) + .toList(), + ), + ); + return; + } else if (msg.type == SCRoomMsgType.roomRoleChange) { + ///房间身份变动 + Provider.of(context!, listen: false).retrieveMicrophoneList(); + if (msg.toUser?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).currenRoom?.entrants?.setRoles(msg.msg); + if (msg.msg == SCRoomRolesType.TOURIST.name && + !(Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomSetting?.touristMike ?? + false)) { + ///如果变成了游客,房间又是禁止游客上麦,需要下麦 + num index = Provider.of( + context!, + listen: false, + ).userOnMaiInIndex( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); + if (index > -1) { + Provider.of( + context!, + listen: false, + ).xiaMai(index); + } + } + } + } else if (msg.type == SCRoomMsgType.roomDice) { + if ((msg.number ?? -1) > -1) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + } + } else if (msg.type == SCRoomMsgType.roomRPS) { + if ((msg.number ?? -1) > -1) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + } + } + addMsg(msg); + } + } + } catch (e) { + throw Exception("message parser fail: $e"); + } + } + + ///加入全服广播群 + joinBigBroadcastGroup() async { + bool joined = false; + while (!isLogout && !joined) { + await Future.delayed(Duration(milliseconds: 550)); + try { + var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( + groupID: SCGlobalConfig.bigBroadcastGroup, + message: "", + ); + if (joinResult.code == 0) { + joined = true; + } + } catch (e) { + //print('timm 登录异常:${e.toString()}'); + SCTts.show('broadcastGroup join fail:${e.toString()}'); + } + } + } + + ///发送全服消息 + sendBigBroadcastGroup(BigBroadcastGroupMessage msg) async { + try { + final textMsg = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage(data: jsonEncode(msg.toJson())); + + if (textMsg.code != 0) return; + + final sendResult = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: textMsg.data!.id!, + groupID: SCGlobalConfig.bigBroadcastGroup, + receiver: '', + ); + if (sendResult.code == 0) {} + } catch (e) { + throw Exception("create fail: $e"); + } + } + + Future quitGroup(String groupID) async { + await TencentImSDKPlugin.v2TIMManager.quitGroup(groupID: groupID); + } + + ///清屏 + void clearMessage() { + roomAllMsgList.clear(); + roomChatMsgList.clear(); + roomGiftMsgList.clear(); + msgChatListener?.call(Msg(groupId: "-1000", msg: "", type: "")); + msgAllListener?.call(Msg(groupId: "-1000", msg: "", type: "")); + msgGiftListener?.call(Msg(groupId: "-1000", msg: "", type: "")); + notifyListeners(); + } + + cleanRoomData() { + roomAllMsgList.clear(); + roomGiftMsgList.clear(); + roomChatMsgList.clear(); + _luckGiftPushQueue.clear(); + currentPlayingLuckGift = null; + onNewMessageListenerGroupMap.forEach((k, v) { + v = null; + }); + onNewMessageListenerGroupMap.clear(); + } + + void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { + if (SCGlobalConfig.isLuckGiftSpecialEffects) { + _luckGiftPushQueue.add(broadCastRes); + playLuckGiftBackCoins(); + } + } + + void cleanLuckGiftBackCoins() { + _luckGiftPushQueue.clear(); + } + + bool showLuckGiftBigHead = true; + + void playLuckGiftBackCoins() { + if (currentPlayingLuckGift != null || _luckGiftPushQueue.isEmpty) { + return; + } + currentPlayingLuckGift = _luckGiftPushQueue.removeFirst(); + notifyListeners(); + Future.delayed(Duration(milliseconds: 2300), () { + showLuckGiftBigHead = false; + }); + Future.delayed(Duration(milliseconds: 3000), () { + currentPlayingLuckGift = null; + showLuckGiftBigHead = true; + notifyListeners(); + playLuckGiftBackCoins(); + }); + } + + void updateNotificationCount(int count) { + notifcationUnReadCount = 0; + allUnReadCount = + messageUnReadCount + notifcationUnReadCount + activityUnReadCount; + notifyListeners(); + } + + void updateActivityCount(int count) { + activityUnReadCount = 0; + allUnReadCount = + messageUnReadCount + notifcationUnReadCount + activityUnReadCount; + notifyListeners(); + } + + void updateSystemCount(int count) { + conversationMap[SCGlobalConfig.imAdmin]?.unreadCount = 0; + systemUnReadCount = 0; + notifyListeners(); + } + + void updateCustomerCount(int count) { + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = 0; + customerUnReadCount = 0; + notifyListeners(); + } + + void clearC2CHistoryMessage(String conversationID, bool needShowToast) async { + // 清空单聊本地及云端的消息(不删除会话) + + V2TimCallback clearC2CHistoryMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getConversationManager() + .deleteConversation(conversationID: conversationID); // 需要清空记录的用户id + if (clearC2CHistoryMessageRes.code == 0) { + // 清除成功 + if (needShowToast) { + SCTts.show(SCAppLocalizations.of(context!)!.operationSuccessful); + } + initConversation(); + } else { + // 清除失败,可以查看 clearC2CHistoryMessageRes.desc 获取错误描述 + } + } +} diff --git a/lib/services/auth/authentication_manager.dart b/lib/services/auth/authentication_manager.dart new file mode 100644 index 0000000..43361a5 --- /dev/null +++ b/lib/services/auth/authentication_manager.dart @@ -0,0 +1,113 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_google_auth_utils.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_version_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; + +import '../../shared/data_sources/models/enum/sc_auth_type.dart'; + +class SocialChatAuthenticationManager extends ChangeNotifier { + String authType = ""; + String uid = ""; + User? _user; + + User? get user => _user; + + bool get isSignedIn => _user != null; + + SocialChatAuthenticationManager() { + _initializeAuthentication(); + } + + Future _initializeAuthentication() async { + await SCGoogleAuthService.initialize(); + await _verifyExistingSession(); + } + + Future _verifyExistingSession() async { + final user = await SCGoogleAuthService.signInSilently(); + _user = user; + notifyListeners(); + } + + Future authenticateUser(BuildContext context, String authType) async { + if (authType == SCAuthType.GOOGLE.name) { + this.authType = authType; + try { + _user ??= await SCGoogleAuthService.signInWithGoogle(); + if (_user == null) { + return; + } + uid = _user!.uid; + SCLoadingManager.show(); + if (uid.isNotEmpty) { + final res = await Future.wait([ + SCAccountRepository().loginForChannel(authType, uid), + SCVersionUtils.checkReview(), + ]); + var user = (res[0] as SocialChatLoginRes); + AccountStorage().setCurrentUser(user); + SCLoadingManager.hide(); + notifyListeners(); + SCNavigatorUtils.push(context, SCRoutes.home, replace: true); + } + } catch (e) { + SCLoadingManager.hide(); + _showAuthError(e); + } + } else if (authType == SCAuthType.APPLE.name) { + this.authType = authType; + try { + final appleCredential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.fullName, // 请求姓名权限 + ], + ); + uid = appleCredential.userIdentifier ?? ""; + SCLoadingManager.show(); + if (uid.isNotEmpty) { + final res = await Future.wait([ + SCAccountRepository().loginForChannel(authType, uid), + SCVersionUtils.checkReview(), + ]); + var user = (res[0] as SocialChatLoginRes); + AccountStorage().setCurrentUser(user); + SCLoadingManager.hide(); + notifyListeners(); + SCNavigatorUtils.push(context, SCRoutes.home, replace: true); + } + } catch (e) { + SCLoadingManager.hide(); + _showAuthError(e); + } + } + } + + Future logoutUser() async { + await SCGoogleAuthService.signOut(); + _user = null; + notifyListeners(); + } + + void _showAuthError(Object error) { + debugPrint("authenticateUser error: $error"); + + if (error is DioException) { + return; + } + + final message = error.toString().replaceFirst("Exception: ", "").trim(); + if (message.isNotEmpty) { + SCTts.show(message); + } + } +} diff --git a/lib/services/auth/user_profile_manager.dart b/lib/services/auth/user_profile_manager.dart new file mode 100644 index 0000000..b5672eb --- /dev/null +++ b/lib/services/auth/user_profile_manager.dart @@ -0,0 +1,178 @@ +import 'package:flutter/cupertino.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart'; +import 'package:yumi/shared/business_logic/models/res/country_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart'; + +class SocialChatUserProfileManager extends ChangeNotifier { + SCUserProfileCmd? editUser; + + void updateUserAvatar(String avatar) { + editUser ??= SCUserProfileCmd(); + editUser!.setUserAvatar = avatar; + notifyListeners(); + } + + void updateUserSex(num sex) { + editUser ??= SCUserProfileCmd(); + editUser!.setUserSex = sex; + } + + void updateUserNickname(String nickname) { + editUser ??= SCUserProfileCmd(); + editUser!.setUserNickname = nickname; + } + + void updateBornYear(num bornYear) { + editUser ??= SCUserProfileCmd(); + editUser!.setBornYear = bornYear; + } + + void updateBornMonth(num bornMonth) { + editUser ??= SCUserProfileCmd(); + editUser!.setBornMonth = bornMonth; + } + + void updateBornDay(num bornDay) { + editUser ??= SCUserProfileCmd(); + editUser!.setBornDay = bornDay; + } + + void updateAge(num age) { + editUser ??= SCUserProfileCmd(); + editUser!.setAge = age; + } + + void setCountry(Country country) { + editUser ??= SCUserProfileCmd(); + editUser!.setCountryCode = country.alphaTwo!; + editUser!.setCountryId = country.id!; + editUser!.setCountryName = country.countryName!; + } + + void resetEditUserData() { + editUser = null; + notifyListeners(); + } + + Future fetchUserProfileData({ + bool loadGuardCount = true, + bool refreshFamilyData = false, + }) async { + var us = AccountStorage().getCurrentUser(); + String userId = us?.userProfile?.id ?? ""; + var userInfo = await SCAccountRepository().loadUserInfo(userId); + userProfile = userInfo; + us?.setUserProfile(userInfo); + AccountStorage().setCurrentUser(us!); + notifyListeners(); + } + + SCUserIdentityRes? userIdentity; + + void getUserIdentity() async { + userIdentity = await SCAccountRepository().userIdentity(); + notifyListeners(); + } + + ///用户信息详情个人页 + SocialChatUserProfile? userProfile; + + void getUserInfoById(String userId) async { + userProfile = null; + userProfile = await SCAccountRepository().loadUserInfo(userId); + + notifyListeners(); + } + + ///房间资料卡 + RoomUserCardRes? userCardInfo; + + // List? userCountGuardResList; + + void roomUserCard(String roomId, String userId) async { + userCardInfo = null; + userCardInfo = await SCChatRoomRepository().roomUserCard(roomId, userId); + notifyListeners(); + } + + void followUser(String userId) async { + var result = await SCAccountRepository().followUser(userId); + if (result) { + if (userCardInfo != null) { + userCardInfo = userCardInfo?.copyWith(follow: !userCardInfo!.follow!); + notifyListeners(); + } + } + } + + double myBalance = 0.0; + + void balance() async { + myBalance = await SCAccountRepository().balance(); + notifyListeners(); + } + + updateBalance(double m) { + myBalance = m; + notifyListeners(); + } + + void inviteHostOpt(BuildContext ct, String id, String status) async { + try { + await SCAccountRepository().inviteHost(id, status); + getUserIdentity(); + SCTts.show(SCAppLocalizations.of(ct)!.operationSuccessful); + } catch (e) {} + } + + void inviteAgentOpt(BuildContext ct, String id, String status) async { + try { + await SCAccountRepository().inviteAgent(id, status); + getUserIdentity(); + SCTts.show(SCAppLocalizations.of(ct)!.operationSuccessful); + } catch (e) {} + } + + void inviteBDOpt(BuildContext ct, String id, String status) async { + try { + await SCAccountRepository().inviteBD(id, status); + getUserIdentity(); + SCTts.show(SCAppLocalizations.of(ct)!.operationSuccessful); + } catch (e) {} + } + + void inviteBDLeader(BuildContext ct, String id, String status) async { + try { + await SCAccountRepository().inviteBDLeader(id, status); + getUserIdentity(); + SCTts.show(SCAppLocalizations.of(ct)!.operationSuccessful); + } catch (e) {} + } + + void inviteRechargeAgent(BuildContext ct, String id, String status) async { + try { + await SCAccountRepository().inviteRechargeAgent(id, status); + getUserIdentity(); + SCTts.show(SCAppLocalizations.of(ct)!.operationSuccessful); + } catch (e) {} + } + + void cpRlationshipProcessApply( + BuildContext ct, + String id, + bool status, + ) async { + try { + await SCAccountRepository().cpRelationshipProcessApply(id, status); + fetchUserProfileData(loadGuardCount: false); + SCTts.show(SCAppLocalizations.of(ct)!.operationSuccessful); + } catch (e) {} + } +} diff --git a/lib/services/general/sc_app_general_manager.dart b/lib/services/general/sc_app_general_manager.dart new file mode 100644 index 0000000..cd02a7a --- /dev/null +++ b/lib/services/general/sc_app_general_manager.dart @@ -0,0 +1,272 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/data_sources/models/country_mode.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.dart'; +import 'package:yumi/shared/business_logic/models/res/country_res.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart'; + +import '../../shared/data_sources/models/enum/sc_banner_type.dart'; + +class SCAppGeneralManager extends ChangeNotifier { + List countryModeList = []; + Map> _countryMap = {}; + Map _countryByNameMap = {}; + bool _isFetchingCountryList = false; + + ///礼物 + List giftResList = []; + Map _giftByIdMap = {}; + + Map> giftByTab = {}; + Map> activityGiftByTab = {}; + + ///选中的国家 + Country? selectCountryInfo; + + List activityList = []; + + SCAppGeneralManager() { + fetchCountryList(); + giftList(); // 使用默认参数 + giftActivityList(); + // giftBackpack(); + // configLevel(); + } + + ///加载国家列表 + Future fetchCountryList() async { + if (_isFetchingCountryList) { + return; + } + if (countryModeList.isNotEmpty) { + clearCountrySelection(); + notifyListeners(); + return; + } + + _isFetchingCountryList = true; + try { + await Future.delayed(Duration(milliseconds: 550)); + final result = await SCConfigRepositoryImp().loadCountry(); + _countryMap.clear(); + _countryByNameMap.clear(); + countryModeList.clear(); + + for (final c in result.openCountry ?? const []) { + final prefix = c.aliasName?.substring(0, 1) ?? ""; + if (prefix.isNotEmpty) { + final countryList = _countryMap[prefix] ?? []; + countryList.add(c.copyWith()); + _countryMap[prefix] = countryList; + } + if ((c.countryName ?? "").isNotEmpty) { + _countryByNameMap[c.countryName!] = c; + } + } + + _countryMap.forEach((k, v) { + countryModeList.add(CountryMode(k, v)); + }); + + ///排序 + countryModeList.sort((a, b) => a.prefix.compareTo(b.prefix)); + notifyListeners(); + } catch (_) { + notifyListeners(); + } finally { + _isFetchingCountryList = false; + } + } + + ///国家国家名称获取国家 + Country? findCountryByName(String countryName) { + return _countryByNameMap[countryName]; + } + + ///选择国家 + Country? chooseCountry(int subIndex, int cIndex) { + var selectCm = countryModeList[subIndex].prefixCountrys[cIndex]; + if (selectCountryInfo != null) { + selectCountryInfo = null; + } else { + selectCountryInfo = selectCm; + } + return selectCountryInfo; + } + + void updateCurrentCountry(Country? countryInfo) { + selectCountryInfo = countryInfo; + notifyListeners(); + } + + void clearCountrySelection() { + selectCountryInfo = null; + } + + void openDrawer(BuildContext context) { + Scaffold.of(context).openDrawer(); + } + + void closeDrawer(BuildContext context) { + Scaffold.of(context).closeDrawer(); + } + + void giftList({bool includeCustomized = true}) async { + try { + giftResList = await SCChatRoomRepository().giftList(); + giftByTab.clear(); + for (var gift in giftResList) { + giftByTab[gift.giftTab]; + var gmap = giftByTab[gift.giftTab]; + gmap ??= []; + gmap.add(gift); + giftByTab[gift.giftTab!] = gmap; + var gAllMap = giftByTab["ALL"]; + gAllMap ??= []; + if (gift.giftTab != "NSCIONAL_FLAG" && + gift.giftTab != "ACTIVITY" && + gift.giftTab != "LUCKY_GIFT" && + gift.giftTab != "CP" && + gift.giftTab != "CUSTOMIZED" && + gift.giftTab != "MAGIC") { + gAllMap.add(gift); + } + giftByTab["ALL"] = gAllMap; + _giftByIdMap[gift.id!] = gift; + } + + if (includeCustomized) { + giftByTab["CUSTOMIZED"] ??= []; + + ///定制礼物需要一个占位符,显示规则的 + giftByTab["CUSTOMIZED"]?.insert(0, SocialChatGiftRes(id: "-1000")); + } else { + giftByTab.remove("CUSTOMIZED"); + } + + notifyListeners(); + + ///先去预下载 + downLoad(giftResList); + } catch (e) {} + } + + void giftActivityList() async { + try { + activityList.clear(); + activityList = await SCChatRoomRepository().giftActivityList(); + activityGiftByTab.clear(); + for (var gift in activityList) { + var gmap = activityGiftByTab["${gift.activityId}"]; + gmap ??= []; + gmap.add(gift); + activityGiftByTab["${gift.activityId}"] = gmap; + } + notifyListeners(); + } catch (e) {} + } + + ///礼物背包 + void giftBackpack() async { + try { + var result = await SCChatRoomRepository().giftBackpack(); + } catch (e) {} + } + + ///礼物id获取礼物 + SocialChatGiftRes? getGiftById(String id) { + return _giftByIdMap[id]; + } + + void downLoad(List giftResList) { + for (var gift in giftResList) { + if (gift.giftSourceUrl != null) { + if (SCPathUtils.getFileExtension(gift.giftSourceUrl!).toLowerCase() == + ".svga") { + ///预解析 + if (!SCGiftVapSvgaManager().videoItemCache.containsKey( + gift.giftSourceUrl, + )) { + SVGAParser.shared.decodeFromURL(gift.giftSourceUrl!).then((entity) { + entity.autorelease = false; + SCGiftVapSvgaManager().videoItemCache[gift.giftSourceUrl!] = + entity; + }); + } + } else { + if ((gift.giftSourceUrl ?? "").isNotEmpty) { + FileCacheManager.getInstance().getFile(url: gift.giftSourceUrl!); + } + } + } + } + } + + List homeBanners = []; + List exploreBanners = []; + List roomBanners = []; + List gameBanners = []; + + ///首页banner + void loadMainBanner() async { + try { + var banners = await SCConfigRepositoryImp().getBanner(); + homeBanners.clear(); + roomBanners.clear(); + gameBanners.clear(); + exploreBanners.clear(); + for (var v in banners) { + if ((v.displayPosition ?? "").contains( + SCBannerType.EXPLORE_PAGE.name, + )) { + exploreBanners.add(v); + } + if ((v.displayPosition ?? "").contains(SCBannerType.ROOM.name)) { + roomBanners.add(v); + } + + if ((v.displayPosition ?? "").contains(SCBannerType.HOME_ALERT.name)) { + homeBanners.add(v); + } + if ((v.displayPosition ?? "").contains(SCBannerType.GAME.name)) { + gameBanners.add(v); + } + } + notifyListeners(); + } catch (e) {} + } + + Map> emojiByTab = {}; + + ///emoji表情 + void emojiAll() async { + var roomEmojis = await SCChatRoomRepository().emojiAll(); + for (var value in roomEmojis) { + emojiByTab[value.groupName!] = value.emojis ?? []; + } + notifyListeners(); + } + + ///加载等级资源 + void configLevel() { + SCConfigRepositoryImp().configLevel(); + } + + SCBannerLeaderboardRes? appLeaderResult; + + ///查询榜单前三名 + void appLeaderboard() async { + try { + appLeaderResult = await SCChatRoomRepository().appLeaderboard(); + notifyListeners(); + } catch (e) {} + } +} diff --git a/lib/services/gift/gift_animation_manager.dart b/lib/services/gift/gift_animation_manager.dart new file mode 100644 index 0000000..34aadc3 --- /dev/null +++ b/lib/services/gift/gift_animation_manager.dart @@ -0,0 +1,66 @@ +import 'dart:collection'; + +import 'package:flutter/cupertino.dart'; + +import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; + +class GiftAnimationManager extends ChangeNotifier { + Queue pendingAnimationsQueue = Queue(); + List animationControllerList = []; + + //每个控件正在播放的动画 + Map giftMap = {0: null, 1: null, 2: null, 3: null}; + + void enqueueGiftAnimation(LGiftModel giftModel) { + pendingAnimationsQueue.add(giftModel); + proceedToNextAnimation(); + } + + ///开始播放 + proceedToNextAnimation() { + if (pendingAnimationsQueue.isEmpty) { + return; + } + var playGift = pendingAnimationsQueue.first; + for (var key in giftMap.keys) { + var value = giftMap[key]; + if (value == null) { + giftMap[key] = playGift; + pendingAnimationsQueue.removeFirst(); + notifyListeners(); + animationControllerList[key].controller.forward(from: 0); + break; + } else { + if (value.labelId == playGift.labelId) { + playGift.giftCount = value.giftCount + playGift.giftCount; + giftMap[key] = playGift; + pendingAnimationsQueue.removeFirst(); + notifyListeners(); + animationControllerList[key].controller.forward(from: 0.45); + break; + } + } + } + } + + void attachAnimationControllers(List anins) { + animationControllerList = anins; + } + + void cleanupAnimationResources() { + pendingAnimationsQueue.clear(); + giftMap[0] = null; + giftMap[1] = null; + giftMap[2] = null; + giftMap[3] = null; + for (var element in animationControllerList) { + element.controller.dispose(); + } + } + + void markAnimationAsFinished(int index) { + giftMap[index] = null; + notifyListeners(); + proceedToNextAnimation(); + } +} diff --git a/lib/services/gift/gift_system_manager.dart b/lib/services/gift/gift_system_manager.dart new file mode 100644 index 0000000..edbdb64 --- /dev/null +++ b/lib/services/gift/gift_system_manager.dart @@ -0,0 +1,179 @@ +import 'package:flutter/cupertino.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; + +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; + +typedef GiftProvider = SocialChatGiftSystemManager; + +class SocialChatGiftSystemManager extends ChangeNotifier { + ///幸运礼物就不显示这个特效 + bool hideLGiftAnimal = false; + + ///挡位是否已经播放了 + Map isPlayed = { + 10: false, + 20: false, + 30: false, + 50: false, + 66: false, + 88: false, + 100: false, + 200: false, + 300: false, + 400: false, + 500: false, + 666: false, + 777: false, + 888: false, + 1000: false, + 1500: false, + 2000: false, + 3000: false, + 5000: false, + 10000: false, + 15000: false, + 20000: false, + 25000: false, + 30000: false, + 35000: false, + 40000: false, + 45000: false, + 50000: false, + 55000: false, + 60000: false, + 65000: false, + 70000: false, + 75000: false, + 80000: false, + 85000: false, + 90000: false, + 95000: false, + 100000: false, + }; + + ///幸运礼物获得的金币总额 + num luckGiftObtainCoins = 0; + + ///单次中奖的金币 + num awardAmount = 0; + + ///赠送的数量 + num number = 0; + + bool isManyPeople = false; + + MicRes? toUser; + + SocialChatGiftRes? gift; + + double giftAnimSize = 1; + double obtainCoinsAnimSize = 1; + + double awardAmountAnimSize = 1; + + void toggleGiftAnimationVisibility(bool isHide) { + hideLGiftAnimal = isHide; + notifyListeners(); + } + + void clearAllGiftData() { + gift = null; + luckGiftObtainCoins = 0; + number = 0; + isManyPeople = false; + toUser = null; + + isPlayed = { + 10: false, + 20: false, + 30: false, + 50: false, + 66: false, + 88: false, + 100: false, + 200: false, + 300: false, + 400: false, + 500: false, + 666: false, + 777: false, + 888: false, + 1000: false, + 1500: false, + 2000: false, + 3000: false, + 5000: false, + 10000: false, + 15000: false, + 20000: false, + 25000: false, + 30000: false, + 35000: false, + 40000: false, + 45000: false, + 50000: false, + 60000: false, + 65000: false, + 70000: false, + 75000: false, + 80000: false, + 85000: false, + 90000: false, + 95000: false, + 100000: false, + }; + notifyListeners(); + } + + void modifyLuckyGiftCount( + num n, + bool isManyPeople, + MicRes first, + SocialChatGiftRes? checkedGift, + ) { + this.number = number + n; + this.isManyPeople = isManyPeople; + this.toUser = first; + this.gift = checkedGift; + giftAnimSize = 1.4; + notifyListeners(); + startGiftAnimation(); + } + + void updateLuckyRewardAmount(num n) { + this.awardAmount = n; + this.luckGiftObtainCoins = luckGiftObtainCoins + n; + this.obtainCoinsAnimSize = 1.4; + this.awardAmountAnimSize = 1.4; + notifyListeners(); + } + + void startGiftAnimation() { + isPlayed.forEach((k, v) { + if (k < number + 1 && !v) { + playVisualEffect(k); + } + }); + } + + void playVisualEffect(num n) { + if (!(isPlayed[n] ?? false)) { + if (SCGlobalConfig.isLuckGiftSpecialEffects) { + if (n > 9999) { + SCGiftVapSvgaManager().play( + "sc_images/room/anim/luck_gift_count_5000_mor.mp4", + priority: 200, + ); + } else { + SCGiftVapSvgaManager().play( + "sc_images/room/anim/luck_gift_count_$n.mp4", + priority: 200, + ); + } + } + } + isPlayed[n] = true; + } +} diff --git a/lib/services/localization/localization_manager.dart b/lib/services/localization/localization_manager.dart new file mode 100644 index 0000000..18e965c --- /dev/null +++ b/lib/services/localization/localization_manager.dart @@ -0,0 +1,54 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; + +class LocalizationManager with ChangeNotifier { + Locale? _locale; + + Locale? get locale => _locale; + + LocalizationManager() { + initializeLanguageSettings(); + } + + initializeLanguageSettings() { + String language = DataPersistence.getLang(); + if (language.isEmpty) { + Locale systemLocale = ui.window.locale; + if (systemLocale.languageCode.startsWith("ar")) { + _locale = Locale('ar', ''); + } else if (systemLocale.languageCode == "en") { + _locale = Locale('en', ''); + } else if (systemLocale.languageCode == "tr") { + _locale = Locale('tr', ''); + } else if (systemLocale.languageCode == "bn") { + _locale = Locale('bn', ''); + } else { + _locale = Locale('en', ''); + } + } else { + if (language.startsWith("ar")) { + _locale = Locale('ar', ''); + } else if (language == "tr") { + _locale = Locale('tr', ''); + } else if (language == "bn") { + _locale = Locale('bn', ''); + } else { + _locale = Locale('en', ''); + } + } + if (_locale != null) { + changeAppLanguage(_locale!); + } + } + + void changeAppLanguage(Locale locale) { + _locale = locale; + SCGlobalConfig.lang = locale.languageCode; + DataPersistence.setLang(locale.languageCode); + notifyListeners(); + } +} diff --git a/lib/services/payment/apple_payment_manager.dart b/lib/services/payment/apple_payment_manager.dart new file mode 100644 index 0000000..0791521 --- /dev/null +++ b/lib/services/payment/apple_payment_manager.dart @@ -0,0 +1,339 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart'; +import 'package:yumi/modules/wallet/recharge/recharge_page.dart'; + +import '../../shared/data_sources/models/enum/sc_erro_code.dart'; + +class IOSPaymentProcessor extends ChangeNotifier { + StreamSubscription>? _subscription; + final InAppPurchase iap = InAppPurchase.instance; + + // 只复制必要的状态变量 + bool _isAvailable = false; + String _errorMessage = ''; + List _products = []; + + late BuildContext context; + Map productMap = {}; + + // 公开访问器 + bool get isAvailable => _isAvailable; + + String get errorMessage => _errorMessage; + + List get products => _products; + + // 初始化 - 只处理iOS逻辑 + Future initializePaymentProcessor(BuildContext context) async { + this.context = context; + + // 如果不是iOS,直接返回 + if (!Platform.isIOS) { + _errorMessage = 'This provider is for iOS only'; + return; + } + + try { + SCLoadingManager.show(context: context); + // 检查支付是否可用 + _isAvailable = await iap.isAvailable(); + if (!_isAvailable) { + _errorMessage = 'App Store payment service is unavailable'; + SCTts.show(_errorMessage); + return; + } + _subscription?.cancel(); + // 设置购买流监听 + _subscription = iap.purchaseStream.listen( + _handlePurchase, + onError: (error, stackTrace) => _handleError(error, stackTrace), + ); + + // 获取iOS商品配置 + await _loadIOSProductConfig(); + + // 获取苹果商品信息 + await _fetchIOSProducts(); + } catch (e) { + SCTts.show("Apple Pay init fail: $e"); + _errorMessage = 'Apple Pay init fail: ${e.toString()}'; + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + /// 加载iOS商品配置 + Future _loadIOSProductConfig() async { + productMap.clear(); + // 假设你的接口支持按平台获取配置 + var productConfigs = await SCConfigRepositoryImp().productConfig(); + + for (var value in productConfigs) { + productMap[value.productPackage!] = value; + } + notifyListeners(); + } + + // 获取iOS商品信息 + Future _fetchIOSProducts() async { + try { + SCLoadingManager.show(context: context); + notifyListeners(); + + Set productIds = productMap.keys.toSet(); + + ProductDetailsResponse response = await iap.queryProductDetails( + productIds, + ); + + if (response.notFoundIDs.isNotEmpty) { + _errorMessage = '未找到商品: ${response.notFoundIDs.join(', ')}'; + debugPrint(_errorMessage); + } + + var details = response.productDetails; + // 保持你的排序逻辑 + if (details.isNotEmpty) { + // 这里可以添加iOS特定的排序逻辑 + } + + _products.clear(); + for (var v in details) { + _products.add(SelecteProductConfig(v, _products.isEmpty)); // 第一个选中 + } + } catch (e) { + SCTts.show("Failed to retrieve iOS products: $e"); + _errorMessage = 'Failed to get iOS goods: ${e.toString()}'; + debugPrint(_errorMessage); + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + // 发起iOS购买 + Future processPurchase() async { + ProductDetails? product; + _products.forEach((d) { + if (d.isSelecte) { + product = d.produc; + } + }); + + if (product == null) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem); + return; + } + + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.areYouRureRoRecharge, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _goIOSBuy(product!); + }, + ); + }, + ); + } + + // iOS购买流程 + void _goIOSBuy(ProductDetails product) async { + try { + SCLoadingManager.show(); + notifyListeners(); + + final PurchaseParam purchaseParam = PurchaseParam( + productDetails: product, + applicationUserName: null, + ); + + // iOS统一使用buyNonConsumable,但实际类型由App Store Connect配置决定 + bool success = await iap.buyConsumable(purchaseParam: purchaseParam); + + if (!success) { + _errorMessage = + 'If the purchase fails to start, check your network connection'; + SCTts.show(_errorMessage); + } + } catch (e) { + SCTts.show("iOS Purchase failed: $e"); + _errorMessage = 'iOS purchase failed: ${e.toString()}'; + debugPrint(_errorMessage); + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + // 处理iOS购买结果 + void _handlePurchase(List purchases) { + for (var purchase in purchases) { + switch (purchase.status) { + case PurchaseStatus.purchased: + _verifyIOSPayment(purchase); + break; + + case PurchaseStatus.error: + if (purchase.error != null) { + _handleIOSError(purchase.error!); + } + break; + + case PurchaseStatus.pending: + debugPrint('iOS支付处理中: ${purchase.productID}'); + SCTts.show('Payment is being processed, please wait...'); + break; + case PurchaseStatus.restored: + // SmartDialog.show( + // tag: "showConfirmDialog", + // alignment: Alignment.center, + // animationType: SmartAnimationType.fade, + // builder: (_) { + // return MsgDialog( + // title: SCAppLocalizations.of(context)!.tips, + // msg: SCAppLocalizations.of(context)!.restorePurchasesTips, + // btnText: SCAppLocalizations.of(context)!.confirm, + // onEnsure: () { + // + // }, + // ); + // }, + // ); + _verifyIOSPayment(purchase); // 恢复的购买也需要验证 + break; + case PurchaseStatus.canceled: + break; + } + } + notifyListeners(); + } + + // 验证iOS支付 + Future _verifyIOSPayment(PurchaseDetails purchase) async { + SCLoadingManager.show(); + try { + String receiptData = purchase.verificationData.serverVerificationData; + if (receiptData.isEmpty) { + receiptData = purchase.verificationData.localVerificationData; + } + // iOS验证 - 使用服务器收据验证 + try { + await SCConfigRepositoryImp().applePay( + purchase.productID, + receiptData, + purchase.purchaseID ?? "", + ); + // 🔥 iOS关键:必须完成交易 + await iap.completePurchase(purchase); + // 更新用户信息 + Provider.of(context, listen: false).fetchUserProfileData(); + Provider.of(context, listen: false).balance(); + SCTts.show('Purchase is successful!'); + debugPrint('iOS购买成功: ${purchase.productID}'); + } catch (e) { + if (e.toString().endsWith("${SCErroCode.orderExistsCreated.code}")) { + SCTts.show('The order already exists!'); + await iap.completePurchase(purchase); + } + } + } catch (e) { + SCTts.show("ios verification fails $e"); + _errorMessage = 'iOS verification failed: ${e.toString()}'; + debugPrint(_errorMessage); + // 即使验证失败,也要完成交易,否则会卡住 + if (purchase.pendingCompletePurchase) { + await iap.completePurchase(purchase); + } + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + // 交付商品(与安卓相同) + Future _deliverProduct(String productId) async { + debugPrint('交付iOS商品: $productId'); + // 使用相同的业务逻辑 + } + + // iOS错误处理 + void _handleIOSError(IAPError error) { + _errorMessage = 'iOS payment error: ${error.message} (${error.code})'; + + switch (error.code) { + case 'SKErrorPaymentCancelled': + SCTts.show('the purchase has been canceled'); + break; + case 'SKErrorPaymentNotAllowed': + SCTts.show('the device does not support in app purchases'); + break; + default: + SCTts.show('ios payment failed: ${error.message}'); + break; + } + + debugPrint(_errorMessage); + SCLoadingManager.hide(); + notifyListeners(); + } + + void _handleError(Object error, StackTrace stackTrace) { + if (error is IAPError) { + _handleIOSError(error); + } else { + _errorMessage = 'iOS Unknown Error: ${error.toString()}'; + SCTts.show(_errorMessage); + } + } + + // iOS恢复购买 + Future recoverTransactions() async { + try { + SCLoadingManager.show(context: context); + notifyListeners(); + + SCTts.show('recovering ios purchases...'); + await iap.restorePurchases(); + } catch (e) { + _errorMessage = 'Failed to restore iOS purchases: ${e.toString()}'; + SCTts.show(_errorMessage); + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + void chooseProductConfig(int index) { + for (var v in _products) { + v.isSelecte = false; + } + _products[index].isSelecte = true; + notifyListeners(); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } +} diff --git a/lib/services/payment/google_payment_manager.dart b/lib/services/payment/google_payment_manager.dart new file mode 100644 index 0000000..fe05e99 --- /dev/null +++ b/lib/services/payment/google_payment_manager.dart @@ -0,0 +1,496 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/shared/tools/sc_string_utils.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart'; +import 'package:yumi/modules/wallet/recharge/recharge_page.dart'; + +class AndroidPaymentProcessor extends ChangeNotifier { + StreamSubscription>? _subscription; + final InAppPurchase iap = InAppPurchase.instance; + + // 状态管理变量 + bool _isAvailable = false; + String _errorMessage = ''; + List _products = []; + List _purchases = []; + + ///商品列表 + List productConfigs = []; + + // 维护商品类型映射 + final Map _productTypeMap = {}; + + // 公开访问器 + bool get isAvailable => _isAvailable; + + String get errorMessage => _errorMessage; + + List get products => _products; + + List get purchases => _purchases; + late BuildContext context; + Map productMap = {}; + + Map productTypes = {}; + + // 初始化支付系统 + Future initializePaymentProcessor(BuildContext context) async { + this.context = context; + try { + SCLoadingManager.show(context: context); + + // 检查支付是否可用 + _isAvailable = await iap.isAvailable(); + if (!_isAvailable) { + _errorMessage = 'Google Play payment service is unavailable'; + SCTts.show("Google Play payment service is unavailable"); + return; + } + _subscription?.cancel(); + // 设置购买流监听 + _subscription = iap.purchaseStream.listen( + _handlePurchase, + onError: (error, stackTrace) => _handleError(error, stackTrace), + ); + + // 先获取商品配置 + await fetchProductConfiguration(); + + // 然后获取谷歌商品信息 + await _fetchProducts(); + + // 最后恢复购买,处理未完成交易 + await recoverTransactions(); + } catch (e) { + SCTts.show("init fail: $e"); + _errorMessage = 'init fail: ${e.toString()}'; + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + ///用户获取商店配置的商品列表 + Future fetchProductConfiguration() async { + productMap.clear(); + productConfigs = await SCConfigRepositoryImp().productConfig(); + int index = 0; + for (var value in productConfigs) { + productMap[value.productPackage!] = value; + productTypes[value.productPackage!] = "consumable"; + index = index + 1; + } + notifyListeners(); + } + + // 获取商品信息 + Future _fetchProducts() async { + try { + SCLoadingManager.show(context: context); + notifyListeners(); + + Set productIds = productTypes.keys.toSet(); + + ProductDetailsResponse response = await iap.queryProductDetails( + productIds, + ); + if (response.notFoundIDs.isNotEmpty) { + _errorMessage = '未找到商品: ${response.notFoundIDs.join(', ')}'; + debugPrint(_errorMessage); + } + var dtails = response.productDetails; + if (dtails.isNotEmpty) { + dtails.sort((a, b) { + int ia = 0; + int ib = 0; + if (a.id.contains(".")) { + ia = SCStringUtils.convertToInteger(a.id.split(".").last); + } + if (b.id.contains(".")) { + ib = SCStringUtils.convertToInteger(b.id.split(".").last); + } + return ia.compareTo(ib); + }); + } + int index = 0; + _products.clear(); + for (var v in dtails) { + if (index == 0) { + _products.add(SelecteProductConfig(v, true)); + } else { + _products.add(SelecteProductConfig(v, false)); + } + index++; + } + + // 初始化商品类型映射 + for (var product in _products) { + if (productTypes.containsKey(product.produc.id)) { + _productTypeMap[product.produc.id] = productTypes[product.produc.id]!; + } else { + _productTypeMap[product.produc.id] = 'nonConsumable'; // 默认类型 + } + } + + // 打印商品信息用于调试 + for (var product in _products) { + debugPrint( + '商品: ${product.produc.id}, 类型: ${_productTypeMap[product.produc.id]}, 价格: ${product.produc.price}', + ); + } + } catch (e) { + SCTts.show("Failed to retrieve the product: $e"); + _errorMessage = '获取商品失败: ${e.toString()}'; + debugPrint(_errorMessage); + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + // 获取商品类型 + String _getProductType(String productId) { + return _productTypeMap[productId] ?? 'nonConsumable'; + } + + // 发起购买 + Future processPurchase() async { + ProductDetails? product; + _products.forEach((d) { + if (d.isSelecte) { + product = d.produc; + } + }); + if (product == null) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem); + return; + } + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.areYouRureRoRecharge, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + _goBuy(product!); + }, + ); + }, + ); + } + + // 处理购买结果 + void _handlePurchase(List purchases) { + _purchases = purchases; + + for (var purchase in purchases) { + switch (purchase.status) { + case PurchaseStatus.purchased: + _verifyPayment(purchase); + break; + + case PurchaseStatus.error: + if (purchase.error != null) { + _handleIAPError(purchase.error!); + // 特别处理 "item-already-owned" 错误 + if (purchase.error!.code == 'item-already-owned') { + _handleAlreadyOwnedItem(purchase); + } + } + break; + + case PurchaseStatus.pending: + debugPrint('支付处理中: ${purchase.productID}'); + break; + + case PurchaseStatus.restored: + // 处理恢复的购买 + _verifyPayment(purchase); + break; + default: + break; + } + } + + notifyListeners(); + } + + // 新增:处理已拥有商品的情况 + Future _handleAlreadyOwnedItem(PurchaseDetails purchase) async { + try { + debugPrint('检测到已拥有商品: ${purchase.productID},尝试消耗'); + + // 如果是消耗型商品,尝试消耗 + if (_getProductType(purchase.productID) == 'consumable') { + await consumePurchase(purchase); + // 消耗后重新获取余额 + Provider.of(context, listen: false).balance(); + SCTts.show('outstanding purchases have been reinstated'); + } + } catch (e) { + debugPrint('处理已拥有商品失败: $e'); + } + } + + // 验证支付凭证 + Future _verifyPayment(PurchaseDetails purchase) async { + try { + // 1. 基本验证 + if (purchase.verificationData.serverVerificationData.isEmpty) { + _errorMessage = '无效的支付凭证'; + SCTts.show("Invalid payment voucher"); + return; + } + + // 2. 检查是否已经处理过这个购买 + if (!purchase.pendingCompletePurchase) { + debugPrint('购买已处理过: ${purchase.productID}'); + return; + } + + // 3. 获取购买数据 + String purchaseData = purchase.verificationData.localVerificationData; + String signature = ""; + + if (purchase is GooglePlayPurchaseDetails) { + signature = purchase.billingClientPurchase.signature; + } + + // 4. 服务器验证 + await SCConfigRepositoryImp().googlePay( + purchase.productID, + signature, + purchaseData, + ); + + // 5. 交付商品 + await _deliverProduct(purchase.productID); + + // 6. 完成购买 + await iap.completePurchase(purchase); + + // 7. 如果是消耗型商品,确保消耗 + if (_getProductType(purchase.productID) == 'consumable') { + await _ensurePurchaseConsumed(purchase); + } + + // 8. 更新用户余额 + Provider.of(context, listen: false).fetchUserProfileData(); + Provider.of(context, listen: false).balance(); + + SCTts.show('purchase successful'); + debugPrint('购买成功: ${purchase.productID}'); + } catch (e) { + SCTts.show("verification failed: $e"); + _errorMessage = '验证失败: ${e.toString()}'; + debugPrint(_errorMessage); + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + // 交付商品 + Future _deliverProduct(String productId) async { + debugPrint('交付商品: $productId'); + // 实现您的业务逻辑 + // 例如:增加用户余额、解锁功能等 + } + + // 新增:确保购买被消耗 + Future _ensurePurchaseConsumed(PurchaseDetails purchase) async { + try { + // 等待一段时间确保购买完成 + await Future.delayed(Duration(seconds: 1)); + + // 尝试消耗购买 + await consumePurchase(purchase); + + debugPrint('商品已成功消耗: ${purchase.productID}'); + } catch (e) { + debugPrint('消耗商品失败,可能已自动消耗: $e'); + } + } + + void _handleError(Object error, StackTrace stackTrace) { + SCTts.show("Payment error: $error"); + if (error is IAPError) { + // 处理IAP特定错误 + _handleIAPError(error); + } else { + // 处理其他类型的错误 + _errorMessage = '未知错误: ${error.toString()}'; + SCTts.show(_errorMessage); + } + } + + // 添加了专门的IAP错误处理方法 + void _handleIAPError(IAPError error) { + _errorMessage = '支付错误: ${error.message} (${error.code})'; + + // 处理特定错误代码 + switch (error.code) { + case 'payment-invalid': + debugPrint('支付无效: 商品ID可能配置错误'); + SCTts.show( + 'Payment configuration error, please contact customer service', + ); + break; + case 'item-already-owned': + debugPrint('商品已拥有,尝试消耗商品'); + SCTts.show('Unfinished purchase detected, processing...'); + break; + case 'user-cancelled': + debugPrint('用户取消购买'); + SCTts.show('Purchase cancelled'); + break; + case 'service-timeout': + case 'service-unavailable': + debugPrint('Google Play服务不可用'); + SCTts.show( + 'Google Play services are temporarily unavailable, please try again later', + ); + break; + default: + SCTts.show('Payment failed: ${error.message}'); + break; + } + + debugPrint(_errorMessage); + SCLoadingManager.hide(); + notifyListeners(); + } + + // 恢复购买 + Future recoverTransactions() async { + try { + SCLoadingManager.show(context: context); + notifyListeners(); + + await iap.restorePurchases(); + + // 给恢复操作一些时间 + await Future.delayed(Duration(milliseconds: 1000)); + } catch (e) { + _errorMessage = '恢复购买失败: ${e.toString()}'; + debugPrint(_errorMessage); + SCTts.show("Failed to restore purchase: ${e.toString()}"); + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + // 消耗型商品手动消耗 + Future consumePurchase(PurchaseDetails purchase) async { + try { + if (Platform.isAndroid) { + final InAppPurchaseAndroidPlatformAddition androidAddition = + iap.getPlatformAddition(); + await androidAddition.consumePurchase(purchase); + debugPrint('已消耗商品: ${purchase.productID}'); + } + } catch (e) { + _errorMessage = '消耗商品失败: ${e.toString()}'; + debugPrint(_errorMessage); + } + } + + // 购买流程 + void _goBuy(ProductDetails product) async { + try { + SCLoadingManager.show(context: context); + notifyListeners(); + + // 购买前检查是否有未完成的购买 + await _checkPendingPurchases(); + + final PurchaseParam purchaseParam = PurchaseParam( + productDetails: product, + applicationUserName: null, + ); + + String productType = _getProductType(product.id); + + bool success; + if (productType == 'consumable') { + success = await iap.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: kReleaseMode, // 生产环境自动消耗,调试时手动控制 + ); + } else { + success = await iap.buyNonConsumable(purchaseParam: purchaseParam); + } + + if (!success) { + _errorMessage = '购买启动失败,请检查网络连接'; + SCTts.show(_errorMessage); + } + } catch (e) { + SCTts.show("Purchase failed: $e"); + _errorMessage = '购买失败: ${e.toString()}'; + debugPrint(_errorMessage); + } finally { + SCLoadingManager.hide(); + notifyListeners(); + } + } + + // 新增:检查未完成的购买 + Future _checkPendingPurchases() async { + try { + // 恢复购买以获取所有未完成交易 + await iap.restorePurchases(); + + // 给一点时间处理恢复的购买 + await Future.delayed(Duration(milliseconds: 550)); + } catch (e) { + debugPrint('检查未完成购买时出错: $e'); + } + } + + // 添加调试方法 + void logCurrentPurchaseStatus() { + debugPrint('=== 当前购买状态 ==='); + debugPrint('可用商品数量: ${_products.length}'); + debugPrint('未完成购买数量: ${_purchases.length}'); + + for (var purchase in _purchases) { + debugPrint('商品: ${purchase.productID}, 状态: ${purchase.status}'); + debugPrint('待完成: ${purchase.pendingCompletePurchase}'); + } + debugPrint('=================='); + } + + // 释放资源 + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + void chooseProductConfig(int index) { + for (var v in _products) { + v.isSelecte = false; + } + _products[index].isSelecte = true; + notifyListeners(); + } +} diff --git a/lib/services/room/rc_room_manager.dart b/lib/services/room/rc_room_manager.dart new file mode 100644 index 0000000..8340453 --- /dev/null +++ b/lib/services/room/rc_room_manager.dart @@ -0,0 +1,52 @@ +import 'package:flutter/cupertino.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_edit_room_info_res.dart'; + +import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_contribute_level_res.dart'; +class SocialChatRoomManager extends ChangeNotifier { + MyRoomRes? myRoom; + SCRoomContributeLevelRes? roomContributeLevelRes; + + void fetchMyRoomData() async { + myRoom = null; + myRoom = await SCAccountRepository().myProfile(); + notifyListeners(); + } + + void updateMyRoomInfo(SCEditRoomInfoRes roomInfo) { + if (myRoom != null && myRoom!.id == roomInfo.id) { + myRoom?.setRoomName = roomInfo.roomName ?? ""; + myRoom?.setRoomDesc = roomInfo.roomDesc ?? ""; + notifyListeners(); + } + } + + void createNewRoom(BuildContext context, {String? customName}) async { + SCLoadingManager.show(context: context); + // 差异化:添加自定义名称参数(暂未使用,为未来扩展预留) + if (customName != null) { + // TODO: 实现自定义房间名称 + print('创建房间使用自定义名称: $customName'); + } + await SCAccountRepository().createRoom(); + myRoom = await SCAccountRepository().myProfile(); + SCTts.show(SCAppLocalizations.of(context)!.createRoomSuccsess); + notifyListeners(); + SCLoadingManager.hide(); + } + + void fetchContributionLevelData(String roomId, {bool forceRefresh = false}) { + if (forceRefresh) { + roomContributeLevelRes = null; + } + SCChatRoomRepository().roomContributionActivity(roomId).then((value){ + roomContributeLevelRes = value; + notifyListeners(); + }); + } +} diff --git a/lib/services/shop/shop_manager.dart b/lib/services/shop/shop_manager.dart new file mode 100644 index 0000000..cff5b90 --- /dev/null +++ b/lib/services/shop/shop_manager.dart @@ -0,0 +1,26 @@ +import 'package:flutter/cupertino.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import '../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../shared/data_sources/models/enum/sc_props_type.dart'; + +class ShopManager extends ChangeNotifier { + List headdressList = []; + List mountainsList = []; + + ///头饰 + void fetchHeaddressItems() async { + headdressList = await SCStoreRepositoryImp().storeList( + SCCurrencyType.GOLD.name, + SCPropsType.AVATAR_FRAME.name, + ); + } + + ///坐骑 + void fetchMountainsItems() async { + mountainsList = await SCStoreRepositoryImp().storeList( + SCCurrencyType.GOLD.name, + SCPropsType.RIDE.name, + ); + } +} diff --git a/lib/services/theme/theme_manager.dart b/lib/services/theme/theme_manager.dart new file mode 100644 index 0000000..bbb34cb --- /dev/null +++ b/lib/services/theme/theme_manager.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +class ThemeManager with ChangeNotifier { + ThemeMode _themeMode = ThemeMode.system; + + ThemeMode get themeMode => _themeMode; + + ThemeData get currentTheme { + switch (_themeMode) { + case ThemeMode.dark: + return SocialChatTheme.darkTheme; + case ThemeMode.light: + case ThemeMode.system: + default: + return SocialChatTheme.lightTheme; + } + } + + void updateThemeMode(ThemeMode mode) { + _themeMode = mode; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/req/sc_give_away_gift_room_acceptscmd.dart b/lib/shared/business_logic/models/req/sc_give_away_gift_room_acceptscmd.dart new file mode 100644 index 0000000..9c11dc4 --- /dev/null +++ b/lib/shared/business_logic/models/req/sc_give_away_gift_room_acceptscmd.dart @@ -0,0 +1,49 @@ +/// acceptUserId : 0 +/// reqTraceId : "" +/// seatIndex : 0 + +class SCGiveAwayGiftRoomAcceptsCmd { + SCGiveAwayGiftRoomAcceptsCmd({ + String? acceptUserId, + String? reqTraceId, + num? seatIndex, + }) { + _acceptUserId = acceptUserId; + _reqTraceId = reqTraceId; + _seatIndex = seatIndex; + } + + SCGiveAwayGiftRoomAcceptsCmd.fromJson(dynamic json) { + _acceptUserId = json['acceptUserId']; + _reqTraceId = json['reqTraceId']; + _seatIndex = json['seatIndex']; + } + + String? _acceptUserId; + String? _reqTraceId; + num? _seatIndex; + + SCGiveAwayGiftRoomAcceptsCmd copyWith({ + String? acceptUserId, + String? reqTraceId, + num? seatIndex, + }) => SCGiveAwayGiftRoomAcceptsCmd( + acceptUserId: acceptUserId ?? _acceptUserId, + reqTraceId: reqTraceId ?? _reqTraceId, + seatIndex: seatIndex ?? _seatIndex, + ); + + String? get acceptUserId => _acceptUserId; + + String? get reqTraceId => _reqTraceId; + + num? get seatIndex => _seatIndex; + + Map toJson() { + final map = {}; + map['acceptUserId'] = _acceptUserId; + map['reqTraceId'] = _reqTraceId; + map['seatIndex'] = _seatIndex; + return map; + } +} diff --git a/lib/shared/business_logic/models/req/sc_mobile_auth_cmd.dart b/lib/shared/business_logic/models/req/sc_mobile_auth_cmd.dart new file mode 100644 index 0000000..f36468e --- /dev/null +++ b/lib/shared/business_logic/models/req/sc_mobile_auth_cmd.dart @@ -0,0 +1,51 @@ +/// code : "522" +/// phoneNumber : "13570191401" +/// phonePrefix : 5477 +/// pwd : "z88888888" + +class SCMobileAuthCmd { + SCMobileAuthCmd({ + String? code, + String? phoneNumber, + num? phonePrefix, + String? pwd,}){ + _code = code; + _phoneNumber = phoneNumber; + _phonePrefix = phonePrefix; + _pwd = pwd; + } + + SCMobileAuthCmd.fromJson(dynamic json) { + _code = json['code']; + _phoneNumber = json['phoneNumber']; + _phonePrefix = json['phonePrefix']; + _pwd = json['pwd']; + } + String? _code; + String? _phoneNumber; + num? _phonePrefix; + String? _pwd; + SCMobileAuthCmd copyWith({ String? code, + String? phoneNumber, + num? phonePrefix, + String? pwd, + }) => SCMobileAuthCmd( code: code ?? _code, + phoneNumber: phoneNumber ?? _phoneNumber, + phonePrefix: phonePrefix ?? _phonePrefix, + pwd: pwd ?? _pwd, + ); + String? get code => _code; + String? get phoneNumber => _phoneNumber; + num? get phonePrefix => _phonePrefix; + String? get pwd => _pwd; + + Map toJson() { + final map = {}; + map['code'] = _code; + map['phoneNumber'] = _phoneNumber; + map['phonePrefix'] = _phonePrefix; + map['pwd'] = _pwd; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/req/sc_user_profile_cmd.dart b/lib/shared/business_logic/models/req/sc_user_profile_cmd.dart new file mode 100644 index 0000000..c4a2489 --- /dev/null +++ b/lib/shared/business_logic/models/req/sc_user_profile_cmd.dart @@ -0,0 +1,126 @@ +/// id : 0 +/// userNickname : "string" +/// userAvatar : "string" +/// userSex : 0 +/// bornYear : 0 +/// bornMonth : 0 +/// bornDay : 0 +/// age : 0 +/// countryCode : "string" +/// countryId : 0 +/// countryName : "string" + +class SCUserProfileCmd { + SCUserProfileCmd({ + num? id, + String? userNickname, + String? userAvatar, + num? userSex=0, + num? bornYear, + num? bornMonth, + num? bornDay, + num? age, + String? countryCode, + String? countryId, + String? countryName,}){ + _id = id; + _userNickname = userNickname; + _userAvatar = userAvatar; + _userSex = userSex; + _bornYear = bornYear; + _bornMonth = bornMonth; + _bornDay = bornDay; + _age = age; + _countryCode = countryCode; + _countryId = countryId; + _countryName = countryName; + } + + SCUserProfileCmd.fromJson(dynamic json) { + _id = json['id']; + _userNickname = json['userNickname']; + _userAvatar = json['userAvatar']; + _userSex = json['userSex']; + _bornYear = json['bornYear']; + _bornMonth = json['bornMonth']; + _bornDay = json['bornDay']; + _age = json['age']; + _countryCode = json['countryCode']; + _countryId = json['countryId']; + _countryName = json['countryName']; + } + num? _id; + String? _userNickname; + String? _userAvatar; + num? _userSex; + num? _bornYear; + num? _bornMonth; + num? _bornDay; + num? _age; + String? _countryCode; + String? _countryId; + String? _countryName; + SCUserProfileCmd copyWith({ num? id, + String? userNickname, + String? userAvatar, + num? userSex, + num? bornYear, + num? bornMonth, + num? bornDay, + num? age, + String? countryCode, + String? countryId, + String? countryName, + }) => SCUserProfileCmd( id: id ?? _id, + userNickname: userNickname ?? _userNickname, + userAvatar: userAvatar ?? _userAvatar, + userSex: userSex ?? _userSex, + bornYear: bornYear ?? _bornYear, + bornMonth: bornMonth ?? _bornMonth, + bornDay: bornDay ?? _bornDay, + age: age ?? _age, + countryCode: countryCode ?? _countryCode, + countryId: countryId ?? _countryId, + countryName: countryName ?? _countryName, + ); + num? get id => _id; + String? get userNickname => _userNickname; + String? get userAvatar => _userAvatar; + num? get userSex => _userSex; + num? get bornYear => _bornYear; + num? get bornMonth => _bornMonth; + num? get bornDay => _bornDay; + num? get age => _age; + String? get countryCode => _countryCode; + String? get countryId => _countryId; + String? get countryName => _countryName; + + set setUserAvatar(String value) => _userAvatar = value; + set setUserNickname(String value) => _userNickname = value; + set setUserSex(num value) => _userSex = value; + set setBornYear(num value) => _bornYear = value; + set setBornMonth(num value) => _bornMonth = value; + set setBornDay(num value) => _bornDay = value; + set setAge(num value) => _age = value; + set setCountryCode(String value) => _countryCode = value; + set setCountryId(String value) => _countryId = value; + set setCountryName(String value) => _countryName = value; + + + Map toJson() { + final map = {}; + map['id'] = _id; + map['userNickname'] = _userNickname; + map['userAvatar'] = _userAvatar; + map['userSex'] = _userSex; + map['bornYear'] = _bornYear; + map['bornMonth'] = _bornMonth; + map['bornDay'] = _bornDay; + map['age'] = _age; + map['countryCode'] = _countryCode; + map['countryId'] = _countryId; + map['countryName'] = _countryName; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/bags_list_res.dart b/lib/shared/business_logic/models/res/bags_list_res.dart new file mode 100644 index 0000000..f0b0ec4 --- /dev/null +++ b/lib/shared/business_logic/models/res/bags_list_res.dart @@ -0,0 +1,86 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// userId : "" +/// type : "" +/// expireTime : 0 +/// useProps : false +/// propsResources : {"id":"","type":"","code":"","name":"","cover":"","sourceUrl":"","expand":"","amount":0.0} +/// allowGive : false + +class BagsListRes { + BagsListRes({ + String? userId, + String? type, + int? expireTime, + bool? useProps, + PropsResources? propsResources, + bool? allowGive, + }) { + _userId = userId; + _type = type; + _expireTime = expireTime; + _useProps = useProps; + _propsResources = propsResources; + _allowGive = allowGive; + } + + BagsListRes.fromJson(dynamic json) { + _userId = json['userId']; + _type = json['type']; + _expireTime = json['expireTime']; + _useProps = json['useProps']; + _propsResources = + json['propsResources'] != null + ? PropsResources.fromJson(json['propsResources']) + : null; + _allowGive = json['allowGive']; + } + + String? _userId; + String? _type; + int? _expireTime; + bool? _useProps; + PropsResources? _propsResources; + bool? _allowGive; + + BagsListRes copyWith({ + String? userId, + String? type, + int? expireTime, + bool? useProps, + PropsResources? propsResources, + bool? allowGive, + }) => BagsListRes( + userId: userId ?? _userId, + type: type ?? _type, + expireTime: expireTime ?? _expireTime, + useProps: useProps ?? _useProps, + propsResources: propsResources ?? _propsResources, + allowGive: allowGive ?? _allowGive, + ); + + String? get userId => _userId; + + String? get type => _type; + + int? get expireTime => _expireTime; + + bool? get useProps => _useProps; + + PropsResources? get propsResources => _propsResources; + + bool? get allowGive => _allowGive; + + Map toJson() { + final map = {}; + map['userId'] = _userId; + map['type'] = _type; + map['expireTime'] = _expireTime; + map['useProps'] = _useProps; + if (_propsResources != null) { + map['propsResources'] = _propsResources?.toJson(); + } + map['allowGive'] = _allowGive; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/broad_cast_mic_change_push.dart b/lib/shared/business_logic/models/res/broad_cast_mic_change_push.dart new file mode 100644 index 0000000..db10ddd --- /dev/null +++ b/lib/shared/business_logic/models/res/broad_cast_mic_change_push.dart @@ -0,0 +1,75 @@ +/// data : {"mics":[{"micIndex":0,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":1,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":2,"micLock":false,"micMute":false,"roomId":"1954021581748412417","user":{"account":"8826517","charmLevel":17,"cpUserId":"0","heartbeatVal":0,"id":"1954021558348390402","roles":"HOMEOWNER","useProps":[{"propsResources":{"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-f77b1efa-ae00-4935-872d-cbb5db3d2fb0.svga","type":"AVATAR_FRAME"}},{"propsResources":{"code":"10026","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-c0508d8c-8393-46e8-84c2-3319be893ba7.png","type":"NOBLE_VIP"}}],"userAvatar":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/avatar/750355c0-cfa7-4447-b453-f622bd2e3859.jpg","userNickname":"JacksoUI","userSex":1}},{"micIndex":3,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":4,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":5,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":6,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":7,"micLock":false,"micMute":true,"roomId":"1954021581748412417"},{"micIndex":8,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":9,"micLock":false,"micMute":true,"roomId":"1954021581748412417"}],"timeId":"1968592383197442050"} +/// reqTraceId : "GL-f45a6e89405a2fff611dbb94f02a8d84" +/// type : "MIC_CHANGE" + +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; + +class BroadCastMicChangePush { + BroadCastMicChangePush({ + Data? data, + String? reqTraceId, + String? type,}){ + _data = data; + _reqTraceId = reqTraceId; + _type = type; +} + + BroadCastMicChangePush.fromJson(dynamic json) { + _data = json['data'] != null ? Data.fromJson(json['data']) : null; + _reqTraceId = json['reqTraceId']; + _type = json['type']; + } + Data? _data; + String? _reqTraceId; + String? _type; + Data? get data => _data; + String? get reqTraceId => _reqTraceId; + String? get type => _type; + + Map toJson() { + final map = {}; + if (_data != null) { + map['data'] = _data?.toJson(); + } + map['reqTraceId'] = _reqTraceId; + map['type'] = _type; + return map; + } + +} + +/// mics : [{"micIndex":0,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":1,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":2,"micLock":false,"micMute":false,"roomId":"1954021581748412417","user":{"account":"8826517","charmLevel":17,"cpUserId":"0","heartbeatVal":0,"id":"1954021558348390402","roles":"HOMEOWNER","useProps":[{"propsResources":{"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-f77b1efa-ae00-4935-872d-cbb5db3d2fb0.svga","type":"AVATAR_FRAME"}},{"propsResources":{"code":"10026","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-c0508d8c-8393-46e8-84c2-3319be893ba7.png","type":"NOBLE_VIP"}}],"userAvatar":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/avatar/750355c0-cfa7-4447-b453-f622bd2e3859.jpg","userNickname":"JacksoUI","userSex":1}},{"micIndex":3,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":4,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":5,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":6,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":7,"micLock":false,"micMute":true,"roomId":"1954021581748412417"},{"micIndex":8,"micLock":false,"micMute":false,"roomId":"1954021581748412417"},{"micIndex":9,"micLock":false,"micMute":true,"roomId":"1954021581748412417"}] +/// timeId : "1968592383197442050" + +class Data { + Data({ + List? mics, + String? timeId,}){ + _mics = mics; + _timeId = timeId; +} + + Data.fromJson(dynamic json) { + if (json['mics'] != null) { + _mics = []; + json['mics'].forEach((v) { + _mics?.add(MicRes.fromJson(v)); + }); + } + _timeId = json['timeId']; + } + List? _mics; + String? _timeId; + List? get mics => _mics; + String? get timeId => _timeId; + + Map toJson() { + final map = {}; + if (_mics != null) { + map['mics'] = _mics?.map((v) => v.toJson()).toList(); + } + map['timeId'] = _timeId; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/country_res.dart b/lib/shared/business_logic/models/res/country_res.dart new file mode 100644 index 0000000..63fb893 --- /dev/null +++ b/lib/shared/business_logic/models/res/country_res.dart @@ -0,0 +1,158 @@ +/// openCountry : [{"aliasName":"India","alphaThree":"IND","alphaTwo":"IN","countryName":"India","enName":"India","id":"1231833304232112130","nationalFlag":"https://dev.file.momooline.com/other/manager-2d956fa5-3a17-4af7-95c4-42d4b4f1161c.png","open":true,"phonePrefix":91,"top":false},{"aliasName":"Iran","alphaThree":"IRN","alphaTwo":"IR","countryName":"Iran, Islamic Republic of","enName":"Iran","id":"1231833306144714754","nationalFlag":"https://dev.file.momooline.com/other/manager-108be79c-0c8e-4b19-9147-fb2514e1828e.png","open":true,"phonePrefix":98,"top":false},{"aliasName":"Italy","alphaThree":"ITA","alphaTwo":"IT","countryName":"Italy","enName":"Italy","id":"1231833307491086337","nationalFlag":"https://dev.file.momooline.com/other/manager-0d7a19f5-e2e8-4b22-839f-791782112db0.png","open":true,"phonePrefix":39,"top":false},{"aliasName":"UAE 01- Abu Dhabi","alphaThree":"ARE","alphaTwo":"AE","countryName":"UAE 01- Abu Dhabi","enName":"UAE 01- Abu Dhabi","id":"1231833232115249154","nationalFlag":"https://dev.file.momooline.com/other/manager-41ffec46-d569-4235-89c8-a42f928b3f71.png","open":true,"phonePrefix":971,"top":false},{"aliasName":"Kuwait","alphaThree":"KWT","alphaTwo":"KW","countryName":"Kuwait","enName":"Kuwait","id":"1231833316571754497","nationalFlag":"https://dev.file.momooline.com/other/manager-9d7c61a8-c0a4-4923-9688-1f3eb65e1083.png","open":true,"phonePrefix":965,"top":false},{"aliasName":"Sri Lanka","alphaThree":"LKA","alphaTwo":"LK","countryName":"Sri Lanka","enName":"Sri Lanka","id":"1231833320958996482","nationalFlag":"https://dev.file.momooline.com/other/manager-369085b4-d012-4d49-b2df-85948ddd73d2.png","open":true,"phonePrefix":94,"top":false}] +/// tops : [{"aliasName":"KSA 02- Jeddah","alphaThree":"SAU","alphaTwo":"SP","countryName":"KSA 02- Jeddah","enName":"KSA 02- Jeddah","id":"1231833361190760450","nationalFlag":"https://dev.file.momooline.com/other/manager-5b5277f2-6ea7-4d2c-aa27-1435a6793905.png","open":true,"phonePrefix":966,"top":true},{"aliasName":"KSA 01- Abu Dhabi","alphaThree":"SAU","alphaTwo":"SA","countryName":"KSA 01- Abu Dhabi","enName":"KSA 01- Abu Dhabi","id":"1231833361190760449","nationalFlag":"https://dev.file.momooline.com/other/manager-5b5277f2-6ea7-4d2c-aa27-1435a6793905.png","open":true,"phonePrefix":966,"top":true},{"aliasName":"Egypt 02- Alexandria","alphaThree":"EGY","alphaTwo":"EY","countryName":"Egypt 02- Alexandria","enName":"Egypt 02- Alexandria","id":"1231833276281270275","nationalFlag":"https://dev.file.momooline.com/other/manager-2a1785b7-dd47-40ec-a1d9-18a505563dfe.png","open":true,"phonePrefix":20,"top":true},{"aliasName":"Egypt","alphaThree":"EGY","alphaTwo":"EG","countryName":"Egypt","enName":"Egypt","id":"1231833276281270274","nationalFlag":"https://dev.file.momooline.com/other/manager-2a1785b7-dd47-40ec-a1d9-18a505563dfe.png","open":true,"phonePrefix":20,"top":true}] + +class CountryRes { + CountryRes({ + List? openCountry, + List? tops,}){ + _openCountry = openCountry; + _tops = tops; + } + CountryRes.fromJson(dynamic json) { + if (json['openCountry'] != null) { + _openCountry = []; + json['openCountry'].forEach((v) { + _openCountry?.add(Country.fromJson(v)); + }); + } + if (json['tops'] != null) { + _tops = []; + json['tops'].forEach((v) { + _tops?.add(Country.fromJson(v)); + }); + } + } + List? _openCountry; + List? _tops; + CountryRes copyWith({ List? openCountry, + List? tops, + }) => CountryRes( openCountry: openCountry ?? _openCountry, + tops: tops ?? _tops, + ); + List? get openCountry => _openCountry; + List? get tops => _tops; + + Map toJson() { + final map = {}; + if (_openCountry != null) { + map['openCountry'] = _openCountry?.map((v) => v.toJson()).toList(); + } + if (_tops != null) { + map['tops'] = _tops?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// aliasName : "KSA 02- Jeddah" +/// alphaThree : "SAU" +/// alphaTwo : "SP" +/// countryName : "KSA 02- Jeddah" +/// enName : "KSA 02- Jeddah" +/// id : "1231833361190760450" +/// nationalFlag : "https://dev.file.momooline.com/other/manager-5b5277f2-6ea7-4d2c-aa27-1435a6793905.png" +/// open : true +/// phonePrefix : 966 +/// top : true + + +class Country { + Country({ + String? aliasName, + String? alphaThree, + String? alphaTwo, + String? countryName, + String? enName, + String? id, + String? nationalFlag, + bool? open, + num? phonePrefix, + bool isSelect = false, + bool? top,}){ + _aliasName = aliasName; + _alphaThree = alphaThree; + _alphaTwo = alphaTwo; + _countryName = countryName; + _enName = enName; + _id = id; + _nationalFlag = nationalFlag; + _open = open; + _phonePrefix = phonePrefix; + _top = top; + _isSelect = isSelect; + } + + Country.fromJson(dynamic json) { + _aliasName = json['aliasName']; + _alphaThree = json['alphaThree']; + _alphaTwo = json['alphaTwo']; + _countryName = json['countryName']; + _enName = json['enName']; + _id = json['id']; + _nationalFlag = json['nationalFlag']; + _open = json['open']; + _phonePrefix = json['phonePrefix']; + _top = json['top']; + } + String? _aliasName; + String? _alphaThree; + String? _alphaTwo; + String? _countryName; + String? _enName; + String? _id; + String? _nationalFlag; + bool? _open; + num? _phonePrefix; + bool? _top; + bool _isSelect = false; + Country copyWith({ String? aliasName, + String? alphaThree, + String? alphaTwo, + String? countryName, + String? enName, + String? id, + String? nationalFlag, + bool? open, + num? phonePrefix, + bool? top, + }) => Country( aliasName: aliasName ?? _aliasName, + alphaThree: alphaThree ?? _alphaThree, + alphaTwo: alphaTwo ?? _alphaTwo, + countryName: countryName ?? _countryName, + enName: enName ?? _enName, + id: id ?? _id, + nationalFlag: nationalFlag ?? _nationalFlag, + open: open ?? _open, + phonePrefix: phonePrefix ?? _phonePrefix, + top: top ?? _top, + ); + String? get aliasName => _aliasName; + String? get alphaThree => _alphaThree; + String? get alphaTwo => _alphaTwo; + String? get countryName => _countryName; + String? get enName => _enName; + String? get id => _id; + String? get nationalFlag => _nationalFlag; + bool? get open => _open; + num? get phonePrefix => _phonePrefix; + bool? get top => _top; + + + + Map toJson() { + final map = {}; + map['aliasName'] = _aliasName; + map['alphaThree'] = _alphaThree; + map['alphaTwo'] = _alphaTwo; + map['countryName'] = _countryName; + map['enName'] = _enName; + map['id'] = _id; + map['nationalFlag'] = _nationalFlag; + map['open'] = _open; + map['phonePrefix'] = _phonePrefix; + map['top'] = _top; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/follow_room_res.dart b/lib/shared/business_logic/models/res/follow_room_res.dart new file mode 100644 index 0000000..b9ec7c2 --- /dev/null +++ b/lib/shared/business_logic/models/res/follow_room_res.dart @@ -0,0 +1,515 @@ +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; + +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// id : "0" +/// roomProfile : {"countryCode":"","countryName":"","event":"","hotRoom":false,"id":"0","layoutKey":"","nationalFlag":"","roomAccount":"","roomBadgeIcons":[""],"roomCover":"","roomDesc":"","roomGameIcon":"","roomName":"","sysOrigin":"","userId":"0","userProfile":{"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"s","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]},"userSVipLevel":""} + +class FollowRoomRes { + FollowRoomRes({ + String? id, + RoomProfile? roomProfile,}){ + _id = id; + _roomProfile = roomProfile; +} + + FollowRoomRes.fromJson(dynamic json) { + _id = json['id']; + _roomProfile = json['roomProfile'] != null ? RoomProfile.fromJson(json['roomProfile']) : null; + } + String? _id; + RoomProfile? _roomProfile; +FollowRoomRes copyWith({ String? id, + RoomProfile? roomProfile, +}) => FollowRoomRes( id: id ?? _id, + roomProfile: roomProfile ?? _roomProfile, +); + String? get id => _id; + RoomProfile? get roomProfile => _roomProfile; + + Map toJson() { + final map = {}; + map['id'] = _id; + if (_roomProfile != null) { + map['roomProfile'] = _roomProfile?.toJson(); + } + return map; + } + +} + +/// countryCode : "" +/// countryName : "" +/// event : "" +/// hotRoom : false +/// id : "0" +/// layoutKey : "" +/// nationalFlag : "" +/// roomAccount : "" +/// roomBadgeIcons : [""] +/// roomCover : "" +/// roomDesc : "" +/// roomGameIcon : "" +/// roomName : "" +/// sysOrigin : "" +/// userId : "0" +/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"s","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} +/// userSVipLevel : "" + +class RoomProfile { + RoomProfile({ + String? countryCode, + String? countryName, + String? event, + bool? hotRoom, + String? id, + String? layoutKey, + String? nationalFlag, + String? roomAccount, + List? roomBadgeIcons, + String? roomCover, + String? roomDesc, + String? roomGameIcon, + String? roomName, + String? sysOrigin, + String? userId, + SocialChatUserProfile? userProfile, + String? userSVipLevel,ExtValues? extValues,}){ + _countryCode = countryCode; + _countryName = countryName; + _event = event; + _hotRoom = hotRoom; + _id = id; + _layoutKey = layoutKey; + _nationalFlag = nationalFlag; + _roomAccount = roomAccount; + _roomBadgeIcons = roomBadgeIcons; + _roomCover = roomCover; + _roomDesc = roomDesc; + _roomGameIcon = roomGameIcon; + _roomName = roomName; + _sysOrigin = sysOrigin; + _userId = userId; + _userProfile = userProfile; + _userSVipLevel = userSVipLevel; + _extValues = extValues; +} + + RoomProfile.fromJson(dynamic json) { + _countryCode = json['countryCode']; + _countryName = json['countryName']; + _event = json['event']; + _hotRoom = json['hotRoom']; + _id = json['id']; + _layoutKey = json['layoutKey']; + _nationalFlag = json['nationalFlag']; + _roomAccount = json['roomAccount']; + _roomBadgeIcons = json['roomBadgeIcons'] != null ? json['roomBadgeIcons'].cast() : []; + _roomCover = json['roomCover']; + _roomDesc = json['roomDesc']; + _roomGameIcon = json['roomGameIcon']; + _roomName = json['roomName']; + _sysOrigin = json['sysOrigin']; + _userId = json['userId']; + _userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null; + _userSVipLevel = json['userSVipLevel']; + _extValues = + json['extValues'] != null + ? ExtValues.fromJson(json['extValues']) + : null; + } + String? _countryCode; + String? _countryName; + String? _event; + bool? _hotRoom; + String? _id; + String? _layoutKey; + String? _nationalFlag; + String? _roomAccount; + List? _roomBadgeIcons; + String? _roomCover; + String? _roomDesc; + String? _roomGameIcon; + String? _roomName; + String? _sysOrigin; + String? _userId; + SocialChatUserProfile? _userProfile; + String? _userSVipLevel; + ExtValues? _extValues; +RoomProfile copyWith({ String? countryCode, + String? countryName, + String? event, + bool? hotRoom, + String? id, + String? layoutKey, + String? nationalFlag, + String? roomAccount, + List? roomBadgeIcons, + String? roomCover, + String? roomDesc, + String? roomGameIcon, + String? roomName, + String? sysOrigin, + String? userId, + SocialChatUserProfile? userProfile, + String? userSVipLevel, + ExtValues? extValues, +}) => RoomProfile( countryCode: countryCode ?? _countryCode, + countryName: countryName ?? _countryName, + event: event ?? _event, + hotRoom: hotRoom ?? _hotRoom, + id: id ?? _id, + layoutKey: layoutKey ?? _layoutKey, + nationalFlag: nationalFlag ?? _nationalFlag, + roomAccount: roomAccount ?? _roomAccount, + roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons, + roomCover: roomCover ?? _roomCover, + roomDesc: roomDesc ?? _roomDesc, + roomGameIcon: roomGameIcon ?? _roomGameIcon, + roomName: roomName ?? _roomName, + sysOrigin: sysOrigin ?? _sysOrigin, + userId: userId ?? _userId, + userProfile: userProfile ?? _userProfile, + userSVipLevel: userSVipLevel ?? _userSVipLevel, + extValues: extValues ?? _extValues, +); + String? get countryCode => _countryCode; + String? get countryName => _countryName; + String? get event => _event; + bool? get hotRoom => _hotRoom; + String? get id => _id; + String? get layoutKey => _layoutKey; + String? get nationalFlag => _nationalFlag; + String? get roomAccount => _roomAccount; + List? get roomBadgeIcons => _roomBadgeIcons; + String? get roomCover => _roomCover; + String? get roomDesc => _roomDesc; + String? get roomGameIcon => _roomGameIcon; + String? get roomName => _roomName; + String? get sysOrigin => _sysOrigin; + String? get userId => _userId; + SocialChatUserProfile? get userProfile => _userProfile; + String? get userSVipLevel => _userSVipLevel; + ExtValues? get extValues => _extValues; + + Map toJson() { + final map = {}; + map['countryCode'] = _countryCode; + map['countryName'] = _countryName; + map['event'] = _event; + map['hotRoom'] = _hotRoom; + map['id'] = _id; + map['layoutKey'] = _layoutKey; + map['nationalFlag'] = _nationalFlag; + map['roomAccount'] = _roomAccount; + map['roomBadgeIcons'] = _roomBadgeIcons; + map['roomCover'] = _roomCover; + map['roomDesc'] = _roomDesc; + map['roomGameIcon'] = _roomGameIcon; + map['roomName'] = _roomName; + map['sysOrigin'] = _sysOrigin; + map['userId'] = _userId; + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + map['userSVipLevel'] = _userSVipLevel; + if (_extValues != null) { + map['extValues'] = _extValues?.toJson(); + } + return map; + } + +} + +/// account : "" +/// accountStatus : "" +/// age : 0 +/// bornDay : 0 +/// bornMonth : 0 +/// bornYear : 0 +/// countryCode : "" +/// countryId : 0 +/// countryName : "" +/// createTime : 0 +/// del : false +/// freezingTime : 0 +/// id : "s" +/// originSys : "" +/// ownSpecialId : {"account":"","expiredTime":0} +/// sameRegion : false +/// sysOriginChild : "" +/// useProps : [{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}] +/// userAvatar : "" +/// userNickname : "" +/// userSex : 0 +/// wearBadge : [{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}] + +class WearBadge { + WearBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId,}){ + _animationUrl = animationUrl; + _badgeKey = badgeKey; + _badgeLevel = badgeLevel; + _badgeName = badgeName; + _expireTime = expireTime; + _id = id; + _milestone = milestone; + _notSelectUrl = notSelectUrl; + _selectUrl = selectUrl; + _type = type; + _userId = userId; +} + + WearBadge.fromJson(dynamic json) { + _animationUrl = json['animationUrl']; + _badgeKey = json['badgeKey']; + _badgeLevel = json['badgeLevel']; + _badgeName = json['badgeName']; + _expireTime = json['expireTime']; + _id = json['id']; + _milestone = json['milestone']; + _notSelectUrl = json['notSelectUrl']; + _selectUrl = json['selectUrl']; + _type = json['type']; + _userId = json['userId']; + } + String? _animationUrl; + String? _badgeKey; + num? _badgeLevel; + String? _badgeName; + num? _expireTime; + num? _id; + num? _milestone; + String? _notSelectUrl; + String? _selectUrl; + String? _type; + num? _userId; +WearBadge copyWith({ String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId, +}) => WearBadge( animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, +); + String? get animationUrl => _animationUrl; + String? get badgeKey => _badgeKey; + num? get badgeLevel => _badgeLevel; + String? get badgeName => _badgeName; + num? get expireTime => _expireTime; + num? get id => _id; + num? get milestone => _milestone; + String? get notSelectUrl => _notSelectUrl; + String? get selectUrl => _selectUrl; + String? get type => _type; + num? get userId => _userId; + + Map toJson() { + final map = {}; + map['animationUrl'] = _animationUrl; + map['badgeKey'] = _badgeKey; + map['badgeLevel'] = _badgeLevel; + map['badgeName'] = _badgeName; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['milestone'] = _milestone; + map['notSelectUrl'] = _notSelectUrl; + map['selectUrl'] = _selectUrl; + map['type'] = _type; + map['userId'] = _userId; + return map; + } + +} + +/// expireTime : 0 +/// propsResources : {"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""} +/// userId : 0 + +class UseProps { + UseProps({ + num? expireTime, + PropsResources? propsResources, + num? userId,}){ + _expireTime = expireTime; + _propsResources = propsResources; + _userId = userId; +} + + UseProps.fromJson(dynamic json) { + _expireTime = json['expireTime']; + _propsResources = json['propsResources'] != null ? PropsResources.fromJson(json['propsResources']) : null; + _userId = json['userId']; + } + num? _expireTime; + PropsResources? _propsResources; + num? _userId; +UseProps copyWith({ num? expireTime, + PropsResources? propsResources, + num? userId, +}) => UseProps( expireTime: expireTime ?? _expireTime, + propsResources: propsResources ?? _propsResources, + userId: userId ?? _userId, +); + num? get expireTime => _expireTime; + PropsResources? get propsResources => _propsResources; + num? get userId => _userId; + + Map toJson() { + final map = {}; + map['expireTime'] = _expireTime; + if (_propsResources != null) { + map['propsResources'] = _propsResources?.toJson(); + } + map['userId'] = _userId; + return map; + } + +} + +/// amount : 0.0 +/// code : "" +/// cover : "" +/// expand : "" +/// id : 0 +/// name : "" +/// sourceUrl : "" +/// type : "" + +class PropsResources { + PropsResources({ + num? amount, + String? code, + String? cover, + String? expand, + num? id, + String? name, + String? sourceUrl, + String? type,}){ + _amount = amount; + _code = code; + _cover = cover; + _expand = expand; + _id = id; + _name = name; + _sourceUrl = sourceUrl; + _type = type; +} + + PropsResources.fromJson(dynamic json) { + _amount = json['amount']; + _code = json['code']; + _cover = json['cover']; + _expand = json['expand']; + _id = json['id']; + _name = json['name']; + _sourceUrl = json['sourceUrl']; + _type = json['type']; + } + num? _amount; + String? _code; + String? _cover; + String? _expand; + num? _id; + String? _name; + String? _sourceUrl; + String? _type; +PropsResources copyWith({ num? amount, + String? code, + String? cover, + String? expand, + num? id, + String? name, + String? sourceUrl, + String? type, +}) => PropsResources( amount: amount ?? _amount, + code: code ?? _code, + cover: cover ?? _cover, + expand: expand ?? _expand, + id: id ?? _id, + name: name ?? _name, + sourceUrl: sourceUrl ?? _sourceUrl, + type: type ?? _type, +); + num? get amount => _amount; + String? get code => _code; + String? get cover => _cover; + String? get expand => _expand; + num? get id => _id; + String? get name => _name; + String? get sourceUrl => _sourceUrl; + String? get type => _type; + + Map toJson() { + final map = {}; + map['amount'] = _amount; + map['code'] = _code; + map['cover'] = _cover; + map['expand'] = _expand; + map['id'] = _id; + map['name'] = _name; + map['sourceUrl'] = _sourceUrl; + map['type'] = _type; + return map; + } + +} + +/// account : "" +/// expiredTime : 0 + +class OwnSpecialId { + OwnSpecialId({ + String? account, + num? expiredTime,}){ + _account = account; + _expiredTime = expiredTime; +} + + OwnSpecialId.fromJson(dynamic json) { + _account = json['account']; + _expiredTime = json['expiredTime']; + } + String? _account; + num? _expiredTime; +OwnSpecialId copyWith({ String? account, + num? expiredTime, +}) => OwnSpecialId( account: account ?? _account, + expiredTime: expiredTime ?? _expiredTime, +); + String? get account => _account; + num? get expiredTime => _expiredTime; + + Map toJson() { + final map = {}; + map['account'] = _account; + map['expiredTime'] = _expiredTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/follow_user_res.dart b/lib/shared/business_logic/models/res/follow_user_res.dart new file mode 100644 index 0000000..9ca2707 --- /dev/null +++ b/lib/shared/business_logic/models/res/follow_user_res.dart @@ -0,0 +1,245 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// id : 0 +/// mutualRelations : false +/// useProps : [{"allowGive":false,"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","createTime":0,"del":false,"expand":"","id":0,"imNotOpenedUrl":"","imOpenedUrl":"","imOpenedUrlTwo":"","imSendCoverUrl":"","name":"","roomNotOpenedUrl":"","roomOpenedUrl":"","roomSendCoverUrl":"","sourceUrl":"","sysOrigin":"","type":"","updateTime":0},"userId":0}] +/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} + +class FollowUserRes { + FollowUserRes({ + String? id, + bool? mutualRelations, + List? useProps, + SocialChatUserProfile? userProfile,}){ + _id = id; + _mutualRelations = mutualRelations; + _useProps = useProps; + _userProfile = userProfile; +} + + FollowUserRes.fromJson(dynamic json) { + _id = json['id']; + _mutualRelations = json['mutualRelations']; + if (json['useProps'] != null) { + _useProps = []; + json['useProps'].forEach((v) { + _useProps?.add(UseProps.fromJson(v)); + }); + } + _userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null; + } + String? _id; + bool? _mutualRelations; + List? _useProps; + SocialChatUserProfile? _userProfile; +FollowUserRes copyWith({ String? id, + bool? mutualRelations, + List? useProps, + SocialChatUserProfile? userProfile, +}) => FollowUserRes( id: id ?? _id, + mutualRelations: mutualRelations ?? _mutualRelations, + useProps: useProps ?? _useProps, + userProfile: userProfile ?? _userProfile, +); + String? get id => _id; + bool? get mutualRelations => _mutualRelations; + List? get useProps => _useProps; + SocialChatUserProfile? get userProfile => _userProfile; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['mutualRelations'] = _mutualRelations; + if (_useProps != null) { + map['useProps'] = _useProps?.map((v) => v.toJson()).toList(); + } + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + return map; + } + +} + +/// account : "" +/// accountStatus : "" +/// age : 0 +/// bornDay : 0 +/// bornMonth : 0 +/// bornYear : 0 +/// countryCode : "" +/// countryId : 0 +/// countryName : "" +/// createTime : 0 +/// del : false +/// freezingTime : 0 +/// id : 0 +/// originSys : "" +/// ownSpecialId : {"account":"","expiredTime":0} +/// sameRegion : false +/// sysOriginChild : "" +/// useProps : [{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}] +/// userAvatar : "" +/// userNickname : "" +/// userSex : 0 +/// wearBadge : [{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}] + +/// animationUrl : "" +/// badgeKey : "" +/// badgeLevel : 0 +/// badgeName : "" +/// expireTime : 0 +/// id : 0 +/// milestone : 0 +/// notSelectUrl : "" +/// selectUrl : "" +/// type : "" +/// userId : 0 + +class WearBadge { + WearBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + String? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId,}){ + _animationUrl = animationUrl; + _badgeKey = badgeKey; + _badgeLevel = badgeLevel; + _badgeName = badgeName; + _expireTime = expireTime; + _id = id; + _milestone = milestone; + _notSelectUrl = notSelectUrl; + _selectUrl = selectUrl; + _type = type; + _userId = userId; +} + + WearBadge.fromJson(dynamic json) { + _animationUrl = json['animationUrl']; + _badgeKey = json['badgeKey']; + _badgeLevel = json['badgeLevel']; + _badgeName = json['badgeName']; + _expireTime = json['expireTime']; + _id = json['id']; + _milestone = json['milestone']; + _notSelectUrl = json['notSelectUrl']; + _selectUrl = json['selectUrl']; + _type = json['type']; + _userId = json['userId']; + } + String? _animationUrl; + String? _badgeKey; + num? _badgeLevel; + String? _badgeName; + String? _expireTime; + num? _id; + num? _milestone; + String? _notSelectUrl; + String? _selectUrl; + String? _type; + String? _userId; +WearBadge copyWith({ String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + String? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, +}) => WearBadge( animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, +); + String? get animationUrl => _animationUrl; + String? get badgeKey => _badgeKey; + num? get badgeLevel => _badgeLevel; + String? get badgeName => _badgeName; + String? get expireTime => _expireTime; + num? get id => _id; + num? get milestone => _milestone; + String? get notSelectUrl => _notSelectUrl; + String? get selectUrl => _selectUrl; + String? get type => _type; + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['animationUrl'] = _animationUrl; + map['badgeKey'] = _badgeKey; + map['badgeLevel'] = _badgeLevel; + map['badgeName'] = _badgeName; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['milestone'] = _milestone; + map['notSelectUrl'] = _notSelectUrl; + map['selectUrl'] = _selectUrl; + map['type'] = _type; + map['userId'] = _userId; + return map; + } + +} + +/// expireTime : 0 +/// propsResources : {"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""} +/// userId : 0 + +class UseProps { + UseProps({ + String? expireTime, + PropsResources? propsResources, + String? userId,}){ + _expireTime = expireTime; + _propsResources = propsResources; + _userId = userId; +} + + UseProps.fromJson(dynamic json) { + _expireTime = json['expireTime']; + _propsResources = json['propsResources'] != null ? PropsResources.fromJson(json['propsResources']) : null; + _userId = json['userId']; + } + String? _expireTime; + PropsResources? _propsResources; + String? _userId; +UseProps copyWith({ String? expireTime, + PropsResources? propsResources, + String? userId, +}) => UseProps( expireTime: expireTime ?? _expireTime, + propsResources: propsResources ?? _propsResources, + userId: userId ?? _userId, +); + String? get expireTime => _expireTime; + PropsResources? get propsResources => _propsResources; + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['expireTime'] = _expireTime; + if (_propsResources != null) { + map['propsResources'] = _propsResources?.toJson(); + } + map['userId'] = _userId; + return map; + } + +} diff --git a/lib/shared/business_logic/models/res/gift_backpack_res.dart b/lib/shared/business_logic/models/res/gift_backpack_res.dart new file mode 100644 index 0000000..b4453bc --- /dev/null +++ b/lib/shared/business_logic/models/res/gift_backpack_res.dart @@ -0,0 +1,117 @@ +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; + +/// id : "0" +/// quantity : 0 +/// giftLimit : {"id":"0","userId":"0","backId":"0","giftTab":"","content":""} +/// giftConfig : {"id":"0","giftCode":"","giftPhoto":"","giftSourceUrl":"","giftCandy":0.0,"giftIntegral":0.0,"special":"","type":"","giftTab":"","standardId":"0","explanationGift":false,"account":"","userId":"0","giftName":"","expiredTime":0} + +class SocialChatGiftBackpackRes { + SocialChatGiftBackpackRes({ + String? id, + num? quantity, + GiftLimit? giftLimit, + SocialChatGiftRes? giftConfig,}){ + _id = id; + _quantity = quantity; + _giftLimit = giftLimit; + _giftConfig = giftConfig; +} + + SocialChatGiftBackpackRes.fromJson(dynamic json) { + _id = json['id']; + _quantity = json['quantity']; + _giftLimit = json['giftLimit'] != null ? GiftLimit.fromJson(json['giftLimit']) : null; + _giftConfig = json['giftConfig'] != null ? SocialChatGiftRes.fromJson(json['giftConfig']) : null; + } + String? _id; + num? _quantity; + GiftLimit? _giftLimit; + SocialChatGiftRes? _giftConfig; + + String? get id => _id; + num? get quantity => _quantity; + GiftLimit? get giftLimit => _giftLimit; + SocialChatGiftRes? get giftConfig => _giftConfig; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['quantity'] = _quantity; + if (_giftLimit != null) { + map['giftLimit'] = _giftLimit?.toJson(); + } + if (_giftConfig != null) { + map['giftConfig'] = _giftConfig?.toJson(); + } + return map; + } + +} + +/// id : "0" +/// giftCode : "" +/// giftPhoto : "" +/// giftSourceUrl : "" +/// giftCandy : 0.0 +/// giftIntegral : 0.0 +/// special : "" +/// type : "" +/// giftTab : "" +/// standardId : "0" +/// explanationGift : false +/// account : "" +/// userId : "0" +/// giftName : "" +/// expiredTime : 0 + + +/// id : "0" +/// userId : "0" +/// backId : "0" +/// giftTab : "" +/// content : "" + +class GiftLimit { + GiftLimit({ + String? id, + String? userId, + String? backId, + String? giftTab, + String? content,}){ + _id = id; + _userId = userId; + _backId = backId; + _giftTab = giftTab; + _content = content; +} + + GiftLimit.fromJson(dynamic json) { + _id = json['id']; + _userId = json['userId']; + _backId = json['backId']; + _giftTab = json['giftTab']; + _content = json['content']; + } + String? _id; + String? _userId; + String? _backId; + String? _giftTab; + String? _content; + + String? get id => _id; + String? get userId => _userId; + String? get backId => _backId; + String? get giftTab => _giftTab; + String? get content => _content; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['userId'] = _userId; + map['backId'] = _backId; + map['giftTab'] = _giftTab; + map['content'] = _content; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/gift_by_group_res.dart b/lib/shared/business_logic/models/res/gift_by_group_res.dart new file mode 100644 index 0000000..bd0c686 --- /dev/null +++ b/lib/shared/business_logic/models/res/gift_by_group_res.dart @@ -0,0 +1,42 @@ +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; + +/// giftConfigs : [{"account":"","expiredTime":0,"explanationGift":false,"giftCandy":0.0,"giftCode":"","giftIntegral":0.0,"giftName":"","giftPhoto":"","giftSourceUrl":"","giftTab":"","id":0,"special":"","standardId":0,"type":"","userId":0}] +/// giftTab : "" + +class GiftByGroupRes { + GiftByGroupRes({ + List? giftConfigs, + String? giftTab,}){ + _giftConfigs = giftConfigs; + _giftTab = giftTab; +} + + GiftByGroupRes.fromJson(dynamic json) { + if (json['giftConfigs'] != null) { + _giftConfigs = []; + json['giftConfigs'].forEach((v) { + _giftConfigs?.add(SocialChatGiftRes.fromJson(v)); + }); + } + _giftTab = json['giftTab']; + } + List? _giftConfigs; + String? _giftTab; +GiftByGroupRes copyWith({ List? giftConfigs, + String? giftTab, +}) => GiftByGroupRes( giftConfigs: giftConfigs ?? _giftConfigs, + giftTab: giftTab ?? _giftTab, +); + List? get giftConfigs => _giftConfigs; + String? get giftTab => _giftTab; + + Map toJson() { + final map = {}; + if (_giftConfigs != null) { + map['giftConfigs'] = _giftConfigs?.map((v) => v.toJson()).toList(); + } + map['giftTab'] = _giftTab; + return map; + } + +} diff --git a/lib/shared/business_logic/models/res/gift_res.dart b/lib/shared/business_logic/models/res/gift_res.dart new file mode 100644 index 0000000..a35d53d --- /dev/null +++ b/lib/shared/business_logic/models/res/gift_res.dart @@ -0,0 +1,182 @@ +/// id : 0 +/// giftCode : "" +/// giftPhoto : "" +/// giftSourceUrl : "" +/// giftCandy : 0.0 +/// giftIntegral : 0.0 +/// special : "" +/// type : "" +/// giftTab : "" +/// standardId : 0 +/// explanationGift : false +/// account : "" +/// userId : 0 +/// giftName : "" +/// expiredTime : 0 + +class SocialChatGiftRes { + SocialChatGiftRes({ + String? id, + String? giftCode, + String? giftPhoto, + String? giftSourceUrl, + num? giftCandy, + num? activityId, + num? giftIntegral, + String? special, + String? type, + String? giftTab, + String? standardId, + String? jumpUrl, + String? bannerUrl, + bool? explanationGift, + String? account, + String? userId, + String? giftName, + String? quantity, + num? expiredTime,}){ + _id = id; + _giftCode = giftCode; + _giftPhoto = giftPhoto; + _giftSourceUrl = giftSourceUrl; + _giftCandy = giftCandy; + _activityId = activityId; + _giftIntegral = giftIntegral; + _special = special; + _type = type; + _giftTab = giftTab; + _standardId = standardId; + _jumpUrl = jumpUrl; + _bannerUrl = bannerUrl; + _explanationGift = explanationGift; + _account = account; + _userId = userId; + _giftName = giftName; + _quantity = quantity; + _expiredTime = expiredTime; +} + + SocialChatGiftRes.fromJson(dynamic json) { + _id = json['id']; + _giftCode = json['giftCode']; + _giftPhoto = json['giftPhoto']; + _giftSourceUrl = json['giftSourceUrl']; + _giftCandy = json['giftCandy']; + _activityId = json['activityId']; + _giftIntegral = json['giftIntegral']; + _special = json['special']; + _type = json['type']; + _giftTab = json['giftTab']; + _standardId = json['standardId']; + _jumpUrl = json['jumpUrl']; + _bannerUrl = json['bannerUrl']; + _explanationGift = json['explanationGift']; + _account = json['account']; + _userId = json['userId']; + _giftName = json['giftName']; + _quantity = json['quantity']; + _expiredTime = json['expiredTime']; + } + String? _id; + String? _giftCode; + String? _giftPhoto; + String? _giftSourceUrl; + num? _giftCandy; + num? _activityId; + num? _giftIntegral; + String? _special; + String? _type; + String? _giftTab; + String? _standardId; + String? _jumpUrl; + String? _bannerUrl; + bool? _explanationGift; + String? _account; + String? _userId; + String? _giftName; + String? _quantity; + num? _expiredTime; +SocialChatGiftRes copyWith({ String? id, + String? giftCode, + String? giftPhoto, + String? giftSourceUrl, + num? giftCandy, + num? activityId, + num? giftIntegral, + String? special, + String? type, + String? giftTab, + String? standardId, + String? jumpUrl, + String? bannerUrl, + bool? explanationGift, + String? account, + String? userId, + String? giftName, + String? quantity, + num? expiredTime, +}) => SocialChatGiftRes( id: id ?? _id, + giftCode: giftCode ?? _giftCode, + giftPhoto: giftPhoto ?? _giftPhoto, + giftSourceUrl: giftSourceUrl ?? _giftSourceUrl, + giftCandy: giftCandy ?? _giftCandy, + activityId: activityId ?? _activityId, + giftIntegral: giftIntegral ?? _giftIntegral, + special: special ?? _special, + type: type ?? _type, + giftTab: giftTab ?? _giftTab, + standardId: standardId ?? _standardId, + jumpUrl: jumpUrl ?? _jumpUrl, + bannerUrl: bannerUrl ?? _bannerUrl, + explanationGift: explanationGift ?? _explanationGift, + account: account ?? _account, + userId: userId ?? _userId, + giftName: giftName ?? _giftName, + quantity: quantity ?? _quantity, + expiredTime: expiredTime ?? _expiredTime, +); + String? get id => _id; + String? get giftCode => _giftCode; + String? get giftPhoto => _giftPhoto; + String? get giftSourceUrl => _giftSourceUrl; + num? get giftCandy => _giftCandy; + num? get activityId => _activityId; + num? get giftIntegral => _giftIntegral; + String? get special => _special; + String? get type => _type; + String? get giftTab => _giftTab; + String? get standardId => _standardId; + String? get jumpUrl => _jumpUrl; + String? get bannerUrl => _bannerUrl; + bool? get explanationGift => _explanationGift; + String? get account => _account; + String? get userId => _userId; + String? get giftName => _giftName; + String? get quantity => _quantity; + num? get expiredTime => _expiredTime; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['giftCode'] = _giftCode; + map['giftPhoto'] = _giftPhoto; + map['giftSourceUrl'] = _giftSourceUrl; + map['giftCandy'] = _giftCandy; + map['activityId'] = _activityId; + map['giftIntegral'] = _giftIntegral; + map['special'] = _special; + map['type'] = _type; + map['giftTab'] = _giftTab; + map['standardId'] = _standardId; + map['jumpUrl'] = _jumpUrl; + map['bannerUrl'] = _bannerUrl; + map['explanationGift'] = _explanationGift; + map['account'] = _account; + map['userId'] = _userId; + map['giftName'] = _giftName; + map['quantity'] = _quantity; + map['expiredTime'] = _expiredTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/join_room_res.dart b/lib/shared/business_logic/models/res/join_room_res.dart new file mode 100644 index 0000000..1ef4cae --- /dev/null +++ b/lib/shared/business_logic/models/res/join_room_res.dart @@ -0,0 +1,959 @@ +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; + +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// entrants : {"level":{"charmLevel":0,"userId":0,"wealthLevel":0},"nobleVipAbility":[""],"roles":"","roomToken":""} +/// roomProfile : {"regionCode":"","roomCounter":{"adminCount":0,"memberCount":0},"roomProfile":{"activeTime":0,"countryCode":"","countryName":"","createTime":0,"del":false,"event":"","id":"0","langCode":"","nationalFlag":"","roomAccount":"","roomCover":"","roomDesc":"","roomName":"","sysOrigin":"","updateTime":0,"userId":"0"},"roomSetting":{"adminLockSeat":false,"allowMusic":false,"joinGolds":0,"lockLocation":"","maxAdmin":0,"maxMember":0,"mikeSize":0,"openKtvMode":false,"password":"","roomId":0,"roomSpecialMikeType":"","showHeartbeat":false,"takeMicRole":"","touristMike":false,"touristMsg":false,"userId":"0"},"userProfile":{"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}} +/// roomProps : {"layoutCode":"","roomTheme":{"expireTime":0,"id":0,"themeBack":"","themeStatus":"","useTheme":false}} + +class JoinRoomRes { + JoinRoomRes({ + Entrants? entrants, + RoomProfile? roomProfile, + RoomProps? roomProps, + }) { + _entrants = entrants; + _roomProfile = roomProfile; + _roomProps = roomProps; + } + + JoinRoomRes.fromJson(dynamic json) { + _entrants = + json['entrants'] != null ? Entrants.fromJson(json['entrants']) : null; + _roomProfile = + json['roomProfile'] != null + ? RoomProfile.fromJson(json['roomProfile']) + : null; + _roomProps = + json['roomProps'] != null + ? RoomProps.fromJson(json['roomProps']) + : null; + } + + Entrants? _entrants; + RoomProfile? _roomProfile; + RoomProps? _roomProps; + + JoinRoomRes copyWith({ + Entrants? entrants, + RoomProfile? roomProfile, + RoomProps? roomProps, + }) => JoinRoomRes( + entrants: entrants ?? _entrants, + roomProfile: roomProfile ?? _roomProfile, + roomProps: roomProps ?? _roomProps, + ); + + Entrants? get entrants => _entrants; + + RoomProfile? get roomProfile => _roomProfile; + + RoomProps? get roomProps => _roomProps; + + Map toJson() { + final map = {}; + if (_entrants != null) { + map['entrants'] = _entrants?.toJson(); + } + if (_roomProfile != null) { + map['roomProfile'] = _roomProfile?.toJson(); + } + if (_roomProps != null) { + map['roomProps'] = _roomProps?.toJson(); + } + return map; + } +} + +/// layoutCode : "" +/// roomTheme : {"expireTime":0,"id":0,"themeBack":"","themeStatus":"","useTheme":false} + +class RoomProps { + RoomProps({String? layoutCode, RoomTheme? roomTheme}) { + _layoutCode = layoutCode; + _roomTheme = roomTheme; + } + + RoomProps.fromJson(dynamic json) { + _layoutCode = json['layoutCode']; + _roomTheme = + json['roomTheme'] != null + ? RoomTheme.fromJson(json['roomTheme']) + : null; + } + + String? _layoutCode; + RoomTheme? _roomTheme; + + RoomProps copyWith({String? layoutCode, RoomTheme? roomTheme}) => RoomProps( + layoutCode: layoutCode ?? _layoutCode, + roomTheme: roomTheme ?? _roomTheme, + ); + + String? get layoutCode => _layoutCode; + + RoomTheme? get roomTheme => _roomTheme; + + Map toJson() { + final map = {}; + map['layoutCode'] = _layoutCode; + if (_roomTheme != null) { + map['roomTheme'] = _roomTheme?.toJson(); + } + return map; + } + + void setRoomTheme(RoomTheme? roomTheme) { + _roomTheme = roomTheme; + } +} + +/// expireTime : 0 +/// id : 0 +/// themeBack : "" +/// themeStatus : "" +/// useTheme : false + +class RoomTheme { + RoomTheme({ + int? expireTime, + String? id, + String? themeBack, + String? themeStatus, + bool? useTheme, + }) { + _expireTime = expireTime; + _id = id; + _themeBack = themeBack; + _themeStatus = themeStatus; + _useTheme = useTheme; + } + + RoomTheme.fromJson(dynamic json) { + _expireTime = json['expireTime']; + _id = json['id']; + _themeBack = json['themeBack']; + _themeStatus = json['themeStatus']; + _useTheme = json['useTheme']; + } + + int? _expireTime; + String? _id; + String? _themeBack; + String? _themeStatus; + bool? _useTheme; + + RoomTheme copyWith({ + int? expireTime, + String? id, + String? themeBack, + String? themeStatus, + bool? useTheme, + }) => RoomTheme( + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + themeBack: themeBack ?? _themeBack, + themeStatus: themeStatus ?? _themeStatus, + useTheme: useTheme ?? _useTheme, + ); + + int? get expireTime => _expireTime; + + String? get id => _id; + + String? get themeBack => _themeBack; + + String? get themeStatus => _themeStatus; + + bool? get useTheme => _useTheme; + + Map toJson() { + final map = {}; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['themeBack'] = _themeBack; + map['themeStatus'] = _themeStatus; + map['useTheme'] = _useTheme; + return map; + } +} + +/// regionCode : "" +/// roomCounter : {"adminCount":0,"memberCount":0} +/// roomProfile : {"activeTime":0,"countryCode":"","countryName":"","createTime":0,"del":false,"event":"","id":"0","langCode":"","nationalFlag":"","roomAccount":"","roomCover":"","roomDesc":"","roomName":"","sysOrigin":"","updateTime":0,"userId":"0"} +/// roomSetting : {"adminLockSeat":false,"allowMusic":false,"joinGolds":0,"lockLocation":"","maxAdmin":0,"maxMember":0,"mikeSize":0,"openKtvMode":false,"password":"","roomId":0,"roomSpecialMikeType":"","showHeartbeat":false,"takeMicRole":"","touristMike":false,"touristMsg":false,"userId":"0"} +/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} + +class RoomProfile { + RoomProfile({ + String? regionCode, + RoomCounter? roomCounter, + RoomProfile2? roomProfile, + RoomSetting? roomSetting, + SocialChatUserProfile? userProfile, + }) { + _regionCode = regionCode; + _roomCounter = roomCounter; + _roomProfile = roomProfile; + _roomSetting = roomSetting; + _userProfile = userProfile; + } + + RoomProfile.fromJson(dynamic json) { + _regionCode = json['regionCode']; + _roomCounter = + json['roomCounter'] != null + ? RoomCounter.fromJson(json['roomCounter']) + : null; + _roomProfile = + json['roomProfile'] != null + ? RoomProfile2.fromJson(json['roomProfile']) + : null; + _roomSetting = + json['roomSetting'] != null + ? RoomSetting.fromJson(json['roomSetting']) + : null; + _userProfile = + json['userProfile'] != null + ? SocialChatUserProfile.fromJson(json['userProfile']) + : null; + } + + String? _regionCode; + RoomCounter? _roomCounter; + RoomProfile2? _roomProfile; + RoomSetting? _roomSetting; + SocialChatUserProfile? _userProfile; + + RoomProfile copyWith({ + String? regionCode, + RoomCounter? roomCounter, + RoomProfile2? roomProfile, + RoomSetting? roomSetting, + SocialChatUserProfile? userProfile, + }) => RoomProfile( + regionCode: regionCode ?? _regionCode, + roomCounter: roomCounter ?? _roomCounter, + roomProfile: roomProfile ?? _roomProfile, + roomSetting: roomSetting ?? _roomSetting, + userProfile: userProfile ?? _userProfile, + ); + + String? get regionCode => _regionCode; + + RoomCounter? get roomCounter => _roomCounter; + + RoomProfile2? get roomProfile => _roomProfile; + + RoomSetting? get roomSetting => _roomSetting; + + SocialChatUserProfile? get userProfile => _userProfile; + + Map toJson() { + final map = {}; + map['regionCode'] = _regionCode; + if (_roomCounter != null) { + map['roomCounter'] = _roomCounter?.toJson(); + } + if (_roomProfile != null) { + map['roomProfile'] = _roomProfile?.toJson(); + } + if (_roomSetting != null) { + map['roomSetting'] = _roomSetting?.toJson(); + } + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + return map; + } + + void setRoomSetting(RoomSetting roomSetting) { + _roomSetting = roomSetting; + } +} + +/// account : "" +/// accountStatus : "" +/// age : 0 +/// bornDay : 0 +/// bornMonth : 0 +/// bornYear : 0 +/// countryCode : "" +/// countryId : 0 +/// countryName : "" +/// createTime : 0 +/// del : false +/// freezingTime : 0 +/// id : "0" +/// originSys : "" +/// ownSpecialId : {"account":"","expiredTime":0} +/// sameRegion : false +/// sysOriginChild : "" +/// useProps : [{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}] +/// userAvatar : "" +/// userNickname : "" +/// userSex : 0 +/// wearBadge : [{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}] + +/// animationUrl : "" +/// badgeKey : "" +/// badgeLevel : 0 +/// badgeName : "" +/// expireTime : 0 +/// id : 0 +/// milestone : 0 +/// notSelectUrl : "" +/// selectUrl : "" +/// type : "" +/// userId : 0 + +class WearBadge { + WearBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId, + }) { + _animationUrl = animationUrl; + _badgeKey = badgeKey; + _badgeLevel = badgeLevel; + _badgeName = badgeName; + _expireTime = expireTime; + _id = id; + _milestone = milestone; + _notSelectUrl = notSelectUrl; + _selectUrl = selectUrl; + _type = type; + _userId = userId; + } + + WearBadge.fromJson(dynamic json) { + _animationUrl = json['animationUrl']; + _badgeKey = json['badgeKey']; + _badgeLevel = json['badgeLevel']; + _badgeName = json['badgeName']; + _expireTime = json['expireTime']; + _id = json['id']; + _milestone = json['milestone']; + _notSelectUrl = json['notSelectUrl']; + _selectUrl = json['selectUrl']; + _type = json['type']; + _userId = json['userId']; + } + + String? _animationUrl; + String? _badgeKey; + num? _badgeLevel; + String? _badgeName; + num? _expireTime; + num? _id; + num? _milestone; + String? _notSelectUrl; + String? _selectUrl; + String? _type; + num? _userId; + + WearBadge copyWith({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId, + }) => WearBadge( + animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, + ); + + String? get animationUrl => _animationUrl; + + String? get badgeKey => _badgeKey; + + num? get badgeLevel => _badgeLevel; + + String? get badgeName => _badgeName; + + num? get expireTime => _expireTime; + + num? get id => _id; + + num? get milestone => _milestone; + + String? get notSelectUrl => _notSelectUrl; + + String? get selectUrl => _selectUrl; + + String? get type => _type; + + num? get userId => _userId; + + Map toJson() { + final map = {}; + map['animationUrl'] = _animationUrl; + map['badgeKey'] = _badgeKey; + map['badgeLevel'] = _badgeLevel; + map['badgeName'] = _badgeName; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['milestone'] = _milestone; + map['notSelectUrl'] = _notSelectUrl; + map['selectUrl'] = _selectUrl; + map['type'] = _type; + map['userId'] = _userId; + return map; + } +} + +/// expireTime : 0 +/// propsResources : {"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""} +/// userId : 0 + +class UseProps { + UseProps({ + String? expireTime, + PropsResources? propsResources, + String? userId, + }) { + _expireTime = expireTime; + _propsResources = propsResources; + _userId = userId; + } + + UseProps.fromJson(dynamic json) { + _expireTime = json['expireTime']; + _propsResources = + json['propsResources'] != null + ? PropsResources.fromJson(json['propsResources']) + : null; + _userId = json['userId']; + } + + String? _expireTime; + PropsResources? _propsResources; + String? _userId; + + UseProps copyWith({ + String? expireTime, + PropsResources? propsResources, + String? userId, + }) => UseProps( + expireTime: expireTime ?? _expireTime, + propsResources: propsResources ?? _propsResources, + userId: userId ?? _userId, + ); + + String? get expireTime => _expireTime; + + PropsResources? get propsResources => _propsResources; + + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['expireTime'] = _expireTime; + if (_propsResources != null) { + map['propsResources'] = _propsResources?.toJson(); + } + map['userId'] = _userId; + return map; + } +} + +/// amount : 0.0 +/// code : "" +/// cover : "" +/// expand : "" +/// id : 0 +/// name : "" +/// sourceUrl : "" +/// type : "" + +class PropsResources { + PropsResources({ + num? amount, + String? code, + String? cover, + String? expand, + String? id, + String? name, + String? sourceUrl, + String? type, + }) { + _amount = amount; + _code = code; + _cover = cover; + _expand = expand; + _id = id; + _name = name; + _sourceUrl = sourceUrl; + _type = type; + } + + PropsResources.fromJson(dynamic json) { + _amount = json['amount']; + _code = json['code']; + _cover = json['cover']; + _expand = json['expand']; + _id = json['id']; + _name = json['name']; + _sourceUrl = json['sourceUrl']; + _type = json['type']; + } + + num? _amount; + String? _code; + String? _cover; + String? _expand; + String? _id; + String? _name; + String? _sourceUrl; + String? _type; + + PropsResources copyWith({ + num? amount, + String? code, + String? cover, + String? expand, + String? id, + String? name, + String? sourceUrl, + String? type, + }) => PropsResources( + amount: amount ?? _amount, + code: code ?? _code, + cover: cover ?? _cover, + expand: expand ?? _expand, + id: id ?? _id, + name: name ?? _name, + sourceUrl: sourceUrl ?? _sourceUrl, + type: type ?? _type, + ); + + num? get amount => _amount; + + String? get code => _code; + + String? get cover => _cover; + + String? get expand => _expand; + + String? get id => _id; + + String? get name => _name; + + String? get sourceUrl => _sourceUrl; + + String? get type => _type; + + Map toJson() { + final map = {}; + map['amount'] = _amount; + map['code'] = _code; + map['cover'] = _cover; + map['expand'] = _expand; + map['id'] = _id; + map['name'] = _name; + map['sourceUrl'] = _sourceUrl; + map['type'] = _type; + return map; + } +} + +/// account : "" +/// expiredTime : 0 + +class OwnSpecialId { + OwnSpecialId({String? account, num? expiredTime}) { + _account = account; + _expiredTime = expiredTime; + } + + OwnSpecialId.fromJson(dynamic json) { + _account = json['account']; + _expiredTime = json['expiredTime']; + } + + String? _account; + num? _expiredTime; + + OwnSpecialId copyWith({String? account, num? expiredTime}) => OwnSpecialId( + account: account ?? _account, + expiredTime: expiredTime ?? _expiredTime, + ); + + String? get account => _account; + + num? get expiredTime => _expiredTime; + + Map toJson() { + final map = {}; + map['account'] = _account; + map['expiredTime'] = _expiredTime; + return map; + } +} + +/// adminLockSeat : false +/// allowMusic : false +/// joinGolds : 0 +/// lockLocation : "" +/// maxAdmin : 0 +/// maxMember : 0 +/// mikeSize : 0 +/// openKtvMode : false +/// password : "" +/// roomId : 0 +/// roomSpecialMikeType : "" +/// showHeartbeat : false +/// takeMicRole : "" +/// touristMike : false +/// touristMsg : false +/// userId : "0" + +/// activeTime : 0 +/// countryCode : "" +/// countryName : "" +/// createTime : 0 +/// del : false +/// event : "" +/// id : "0" +/// langCode : "" +/// nationalFlag : "" +/// roomAccount : "" +/// roomCover : "" +/// roomDesc : "" +/// roomName : "" +/// sysOrigin : "" +/// updateTime : 0 +/// userId : "0" + +class RoomProfile2 { + RoomProfile2({ + num? activeTime, + String? countryCode, + String? countryName, + num? createTime, + bool? del, + String? event, + String? id, + String? langCode, + String? nationalFlag, + String? roomAccount, + String? roomCover, + String? roomDesc, + String? roomName, + String? sysOrigin, + num? updateTime, + String? userId, + }) { + _activeTime = activeTime; + _countryCode = countryCode; + _countryName = countryName; + _createTime = createTime; + _del = del; + _event = event; + _id = id; + _langCode = langCode; + _nationalFlag = nationalFlag; + _roomAccount = roomAccount; + _roomCover = roomCover; + _roomDesc = roomDesc; + _roomName = roomName; + _sysOrigin = sysOrigin; + _updateTime = updateTime; + _userId = userId; + } + + RoomProfile2.fromJson(dynamic json) { + _activeTime = json['activeTime']; + _countryCode = json['countryCode']; + _countryName = json['countryName']; + _createTime = json['createTime']; + _del = json['del']; + _event = json['event']; + _id = json['id']; + _langCode = json['langCode']; + _nationalFlag = json['nationalFlag']; + _roomAccount = json['roomAccount']; + _roomCover = json['roomCover']; + _roomDesc = json['roomDesc']; + _roomName = json['roomName']; + _sysOrigin = json['sysOrigin']; + _updateTime = json['updateTime']; + _userId = json['userId']; + } + + num? _activeTime; + String? _countryCode; + String? _countryName; + num? _createTime; + bool? _del; + String? _event; + String? _id; + String? _langCode; + String? _nationalFlag; + String? _roomAccount; + String? _roomCover; + String? _roomDesc; + String? _roomName; + String? _sysOrigin; + num? _updateTime; + String? _userId; + + RoomProfile2 copyWith({ + num? activeTime, + String? countryCode, + String? countryName, + num? createTime, + bool? del, + String? event, + String? id, + String? langCode, + String? nationalFlag, + String? roomAccount, + String? roomCover, + String? roomDesc, + String? roomName, + String? sysOrigin, + num? updateTime, + String? userId, + }) => RoomProfile2( + activeTime: activeTime ?? _activeTime, + countryCode: countryCode ?? _countryCode, + countryName: countryName ?? _countryName, + createTime: createTime ?? _createTime, + del: del ?? _del, + event: event ?? _event, + id: id ?? _id, + langCode: langCode ?? _langCode, + nationalFlag: nationalFlag ?? _nationalFlag, + roomAccount: roomAccount ?? _roomAccount, + roomCover: roomCover ?? _roomCover, + roomDesc: roomDesc ?? _roomDesc, + roomName: roomName ?? _roomName, + sysOrigin: sysOrigin ?? _sysOrigin, + updateTime: updateTime ?? _updateTime, + userId: userId ?? _userId, + ); + + num? get activeTime => _activeTime; + + String? get countryCode => _countryCode; + + String? get countryName => _countryName; + + num? get createTime => _createTime; + + bool? get del => _del; + + String? get event => _event; + + String? get id => _id; + + String? get langCode => _langCode; + + String? get nationalFlag => _nationalFlag; + + String? get roomAccount => _roomAccount; + + String? get roomCover => _roomCover; + + String? get roomDesc => _roomDesc; + + String? get roomName => _roomName; + + String? get sysOrigin => _sysOrigin; + + num? get updateTime => _updateTime; + + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['activeTime'] = _activeTime; + map['countryCode'] = _countryCode; + map['countryName'] = _countryName; + map['createTime'] = _createTime; + map['del'] = _del; + map['event'] = _event; + map['id'] = _id; + map['langCode'] = _langCode; + map['nationalFlag'] = _nationalFlag; + map['roomAccount'] = _roomAccount; + map['roomCover'] = _roomCover; + map['roomDesc'] = _roomDesc; + map['roomName'] = _roomName; + map['sysOrigin'] = _sysOrigin; + map['updateTime'] = _updateTime; + map['userId'] = _userId; + return map; + } +} + +/// adminCount : 0 +/// memberCount : 0 + +class RoomCounter { + RoomCounter({num? adminCount, num? memberCount}) { + _adminCount = adminCount; + _memberCount = memberCount; + } + + RoomCounter.fromJson(dynamic json) { + _adminCount = json['adminCount']; + _memberCount = json['memberCount']; + } + + num? _adminCount; + num? _memberCount; + + RoomCounter copyWith({num? adminCount, num? memberCount}) => RoomCounter( + adminCount: adminCount ?? _adminCount, + memberCount: memberCount ?? _memberCount, + ); + + num? get adminCount => _adminCount; + + num? get memberCount => _memberCount; + + Map toJson() { + final map = {}; + map['adminCount'] = _adminCount; + map['memberCount'] = _memberCount; + return map; + } +} + +/// level : {"charmLevel":0,"userId":0,"wealthLevel":0} +/// nobleVipAbility : [""] +/// roles : "" +/// roomToken : "" + +class Entrants { + Entrants({ + Level? level, + List? nobleVipAbility, + String? roles, + String? roomToken, + }) { + _level = level; + _nobleVipAbility = nobleVipAbility; + _roles = roles; + _roomToken = roomToken; + } + + Entrants.fromJson(dynamic json) { + _level = json['level'] != null ? Level.fromJson(json['level']) : null; + _nobleVipAbility = + json['nobleVipAbility'] != null + ? (json['nobleVipAbility'] as List) + .map((item) => item as String) + .toList() + : null; + _roles = json['roles']; + _roomToken = json['roomToken']; + } + + Level? _level; + List? _nobleVipAbility; + String? _roles; + String? _roomToken; + + Entrants copyWith({ + Level? level, + List? nobleVipAbility, + String? roles, + String? roomToken, + }) => Entrants( + level: level ?? _level, + nobleVipAbility: nobleVipAbility ?? _nobleVipAbility, + roles: roles ?? _roles, + roomToken: roomToken ?? _roomToken, + ); + + Level? get level => _level; + + List? get nobleVipAbility => _nobleVipAbility; + + String? get roles => _roles; + + String? get roomToken => _roomToken; + + Map toJson() { + final map = {}; + if (_level != null) { + map['level'] = _level?.toJson(); + } + map['nobleVipAbility'] = _nobleVipAbility; + map['roles'] = _roles; + map['roomToken'] = _roomToken; + return map; + } + + void setRoles(String? roles) { + _roles = roles; + } +} + +/// charmLevel : 0 +/// userId : 0 +/// wealthLevel : 0 + +class Level { + Level({num? charmLevel, num? userId, num? wealthLevel}) { + _charmLevel = charmLevel; + _userId = userId; + _wealthLevel = wealthLevel; + } + + Level.fromJson(dynamic json) { + _charmLevel = json['charmLevel']; + _userId = json['userId']; + _wealthLevel = json['wealthLevel']; + } + + num? _charmLevel; + num? _userId; + num? _wealthLevel; + + Level copyWith({num? charmLevel, num? userId, num? wealthLevel}) => Level( + charmLevel: charmLevel ?? _charmLevel, + userId: userId ?? _userId, + wealthLevel: wealthLevel ?? _wealthLevel, + ); + + num? get charmLevel => _charmLevel; + + num? get userId => _userId; + + num? get wealthLevel => _wealthLevel; + + Map toJson() { + final map = {}; + map['charmLevel'] = _charmLevel; + map['userId'] = _userId; + map['wealthLevel'] = _wealthLevel; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart new file mode 100644 index 0000000..abe512e --- /dev/null +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -0,0 +1,1230 @@ +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; + +import '../../../data_sources/models/enum/sc_props_type.dart'; +/// token : "" +/// userCredential : {"expireTime":0,"releaseTime":0,"sign":"","sysOrigin":"","userId":0,"version":""} +/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} +/// userSig : "" + +class SocialChatLoginRes { + SocialChatLoginRes({ + String? token, + SocialChatUserCredential? userCredential, + SocialChatUserProfile? userProfile, + String? userSig, + }) { + _token = token; + _userCredential = userCredential; + _userProfile = userProfile; + _userSig = userSig; + } + + SocialChatLoginRes.fromJson(dynamic json) { + _token = json['token']; + _userCredential = + json['userCredential'] != null + ? SocialChatUserCredential.fromJson(json['userCredential']) + : null; + _userProfile = + json['userProfile'] != null + ? SocialChatUserProfile.fromJson(json['userProfile']) + : null; + _userSig = json['userSig']; + } + + String? _token; + SocialChatUserCredential? _userCredential; + SocialChatUserProfile? _userProfile; + String? _userSig; + + SocialChatLoginRes copyWith({ + String? token, + SocialChatUserCredential? userCredential, + SocialChatUserProfile? userProfile, + String? userSig, + }) => SocialChatLoginRes( + token: token ?? _token, + userCredential: userCredential ?? _userCredential, + userProfile: userProfile ?? _userProfile, + userSig: userSig ?? _userSig, + ); + + String? get token => _token; + + SocialChatUserCredential? get userCredential => _userCredential; + + SocialChatUserProfile? get userProfile => _userProfile; + + String? get userSig => _userSig; + + Map toJson() { + final map = {}; + map['token'] = _token; + if (_userCredential != null) { + map['userCredential'] = _userCredential?.toJson(); + } + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + map['userSig'] = _userSig; + return map; + } + + void setUserProfile(SocialChatUserProfile userInfo) { + _userProfile = userInfo; + } +} + +/// account : "" +/// accountStatus : "" +/// age : 0 +/// bornDay : 0 +/// bornMonth : 0 +/// bornYear : 0 +/// countryCode : "" +/// countryId : 0 +/// countryName : "" +/// createTime : 0 +/// del : false +/// freezingTime : 0 +/// id : 0 +/// originSys : "" +/// ownSpecialId : {"account":"","expiredTime":0} +/// sameRegion : false +/// sysOriginChild : "" +/// useProps : [{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}] +/// userAvatar : "" +/// userNickname : "" +/// userSex : 0 +/// wearBadge : [{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}] + +class SocialChatUserProfile { + SocialChatUserProfile({ + String? account, + String? accountStatus, + num? age, + num? bornDay, + num? bornMonth, + num? bornYear, + String? countryCode, + String? countryId, + String? countryName, + num? createTime, + bool? del, + num? freezingTime, + String? id, + bool? firstRecharge, + num? charmLevel, + num? firstRechargeAmount, + num? wealthLevel, + num? heartbeatVal, + String? originSys, + OwnSpecialId? ownSpecialId, + bool? sameRegion, + int? firstRechargeEndTime, + String? sysOriginChild, + List? useProps, + String? userAvatar, + String? roles, + String? inRoomId, + String? userNickname, + String? hobby, + String? familyId, + String? autograph, + num? userSex, + List? wearBadge, + List? wearHonor, + List? backgroundPhotos, + List? personalPhotos, + List? cpList, + bool? isCpRelation, + }) { + _account = account; + _accountStatus = accountStatus; + _age = age; + _bornDay = bornDay; + _inRoomId = inRoomId; + _bornMonth = bornMonth; + _bornYear = bornYear; + _countryCode = countryCode; + _countryId = countryId; + _countryName = countryName; + _createTime = createTime; + _del = del; + _freezingTime = freezingTime; + _id = id; + _originSys = originSys; + _charmLevel = charmLevel; + _firstRechargeAmount = firstRechargeAmount; + _wealthLevel = wealthLevel; + _heartbeatVal = heartbeatVal; + _ownSpecialId = ownSpecialId; + _sameRegion = sameRegion; + _isCpRelation = isCpRelation; + _firstRechargeEndTime = firstRechargeEndTime; + _firstRecharge = firstRecharge; + _sysOriginChild = sysOriginChild; + _useProps = useProps; + _userAvatar = userAvatar; + _roles = roles; + _userNickname = userNickname; + _hobby = hobby; + _familyId = familyId; + _autograph = autograph; + _userSex = userSex; + _wearBadge = wearBadge; + _wearHonor = wearHonor; + _backgroundPhotos = backgroundPhotos; + _personalPhotos = personalPhotos; + _cpList = cpList; + } + + SocialChatUserProfile.fromJson(dynamic json) { + _account = json['account']; + _accountStatus = json['accountStatus']; + _age = json['age']; + _bornDay = json['bornDay']; + _inRoomId = json['inRoomId']; + _bornMonth = json['bornMonth']; + _bornYear = json['bornYear']; + _countryCode = json['countryCode']; + _countryId = json['countryId']; + _countryName = json['countryName']; + _createTime = json['createTime']; + _del = json['del']; + _freezingTime = json['freezingTime']; + _id = json['id']; + _originSys = json['originSys']; + _charmLevel = json['charmLevel']; + _firstRechargeAmount = json['firstRechargeAmount']; + _wealthLevel = json['wealthLevel']; + _heartbeatVal = json['heartbeatVal']; + _ownSpecialId = + json['ownSpecialId'] != null + ? OwnSpecialId.fromJson(json['ownSpecialId']) + : null; + _sameRegion = json['sameRegion']; + _isCpRelation = json['isCpRelation']; + _firstRechargeEndTime = json['firstRechargeEndTime']; + _firstRecharge = json['firstRecharge']; + _sysOriginChild = json['sysOriginChild']; + if (json['useProps'] != null) { + _useProps = []; + json['useProps'].forEach((v) { + _useProps?.add(UseProps.fromJson(v)); + }); + } + _userAvatar = json['userAvatar']; + _roles = json['roles']; + _userNickname = json['userNickname']; + _hobby = json['hobby']; + _familyId = json['familyId']; + _autograph = json['autograph']; + _userSex = json['userSex']; + if (json['wearBadge'] != null) { + _wearBadge = []; + json['wearBadge'].forEach((v) { + _wearBadge?.add(WearBadge.fromJson(v)); + }); + } + if (json['wearHonor'] != null) { + _wearHonor = []; + json['wearHonor'].forEach((v) { + _wearHonor?.add(WearBadge.fromJson(v)); + }); + } + if (json['backgroundPhotos'] != null) { + _backgroundPhotos = []; + json['backgroundPhotos'].forEach((v) { + _backgroundPhotos?.add(PersonPhoto.fromJson(v)); + }); + } + if (json['personalPhotos'] != null) { + _personalPhotos = []; + json['personalPhotos'].forEach((v) { + _personalPhotos?.add(PersonPhoto.fromJson(v)); + }); + } + if (json['cpList'] != null) { + _cpList = []; + json['cpList'].forEach((v) { + _cpList?.add(CPRes.fromJson(v)); + }); + } + } + + String? _account; + String? _accountStatus; + num? _age; + num? _bornDay; + num? _bornMonth; + num? _bornYear; + String? _countryCode; + String? _countryId; + String? _inRoomId; + String? _countryName; + num? _createTime; + bool? _del; + num? _freezingTime; + String? _id; + String? _originSys; + num? _charmLevel; + num? _firstRechargeAmount; + num? _wealthLevel; + num? _heartbeatVal; + OwnSpecialId? _ownSpecialId; + bool? _sameRegion; + bool? _isCpRelation; + int? _firstRechargeEndTime; + bool? _firstRecharge; + String? _sysOriginChild; + List? _useProps; + String? _userAvatar; + String? _roles; + String? _userNickname; + String? _hobby; + String? _familyId; + String? _autograph; + num? _userSex; + List? _wearBadge; + List? _wearHonor; + List? _backgroundPhotos; + List? _personalPhotos; + List? _cpList; + + SocialChatUserProfile copyWith({ + String? account, + String? accountStatus, + num? age, + num? bornDay, + num? bornMonth, + num? bornYear, + String? countryCode, + String? countryId, + String? countryName, + num? createTime, + bool? del, + num? freezingTime, + String? id, + String? originSys, + String? inRoomId, + num? charmLevel, + num? firstRechargeAmount, + num? wealthLevel, + num? heartbeatVal, + OwnSpecialId? ownSpecialId, + bool? sameRegion, + bool? isCpRelation, + int? firstRechargeEndTime, + bool? firstRecharge, + String? sysOriginChild, + List? useProps, + String? userAvatar, + String? roles, + String? userNickname, + String? hobby, + String? familyId, + String? autograph, + num? userSex, + List? wearBadge, + List? wearHonor, + List? backgroundPhotos, + List? personalPhotos, + List? cpList, + }) => SocialChatUserProfile( + account: account ?? _account, + accountStatus: accountStatus ?? _accountStatus, + age: age ?? _age, + bornDay: bornDay ?? _bornDay, + bornMonth: bornMonth ?? _bornMonth, + bornYear: bornYear ?? _bornYear, + inRoomId: inRoomId ?? _inRoomId, + countryCode: countryCode ?? _countryCode, + countryId: countryId ?? _countryId, + countryName: countryName ?? _countryName, + createTime: createTime ?? _createTime, + del: del ?? _del, + freezingTime: freezingTime ?? _freezingTime, + id: id ?? _id, + originSys: originSys ?? _originSys, + charmLevel: charmLevel ?? _charmLevel, + firstRechargeAmount: firstRechargeAmount ?? _firstRechargeAmount, + wealthLevel: wealthLevel ?? _wealthLevel, + heartbeatVal: heartbeatVal ?? _heartbeatVal, + ownSpecialId: ownSpecialId ?? _ownSpecialId, + sameRegion: sameRegion ?? _sameRegion, + isCpRelation: isCpRelation ?? _isCpRelation, + firstRechargeEndTime: firstRechargeEndTime ?? _firstRechargeEndTime, + firstRecharge: firstRecharge ?? _firstRecharge, + sysOriginChild: sysOriginChild ?? _sysOriginChild, + useProps: useProps ?? _useProps, + userAvatar: userAvatar ?? _userAvatar, + roles: roles ?? _roles, + userNickname: userNickname ?? _userNickname, + hobby: hobby ?? _hobby, + familyId: familyId ?? _familyId, + autograph: autograph ?? _autograph, + userSex: userSex ?? _userSex, + wearBadge: wearBadge ?? _wearBadge, + wearHonor: wearHonor ?? _wearHonor, + backgroundPhotos: backgroundPhotos ?? _backgroundPhotos, + personalPhotos: personalPhotos ?? _personalPhotos, + cpList: cpList ?? _cpList, + ); + + String? get account => _account; + + String? get inRoomId => _inRoomId; + + String? get accountStatus => _accountStatus; + + num? get age => (_age ?? 0) < 1 ? 18 : _age; + + num? get bornDay => _bornDay; + + num? get bornMonth => _bornMonth; + + num? get bornYear => _bornYear; + + String? get countryCode => _countryCode; + + String? get countryId => _countryId; + + String? get countryName => _countryName; + + num? get createTime => _createTime; + + bool? get del => _del; + + num? get freezingTime => _freezingTime; + + String? get id => _id; + + String? get originSys => _originSys; + + num? get charmLevel => _charmLevel; + + num? get firstRechargeAmount => _firstRechargeAmount; + + num? get wealthLevel => _wealthLevel; + + num? get heartbeatVal => _heartbeatVal; + + OwnSpecialId? get ownSpecialId => _ownSpecialId; + + bool? get sameRegion => _sameRegion; + + bool? get isCpRelation => _isCpRelation; + + int? get firstRechargeEndTime => _firstRechargeEndTime; + + bool? get firstRecharge => _firstRecharge; + + String? get sysOriginChild => _sysOriginChild; + + List? get useProps => _useProps; + + String? get userAvatar => _userAvatar; + + String? get roles => _roles; + + String? get userNickname => _userNickname; + + String? get hobby => _hobby; + + String? get familyId => _familyId; + + String? get autograph => _autograph; + + num? get userSex => _userSex; + + List? get wearBadge => _wearBadge; + + List? get wearHonor => _wearHonor; + + List? get backgroundPhotos => _backgroundPhotos; + + List? get personalPhotos => _personalPhotos; + + List? get cpList => _cpList; + + void setFamilyId(String? familyId) { + _familyId = familyId; + AccountStorage().setCurrentUser(AccountStorage().getCurrentUser()!); + } + + ///清除荣耀信息,避免发送消息个人信息数据太多超12kb,注意这个只在房间发消息的时候才用,因为荣耀这个属性数据在房间里面不用展示 + void cleanWearHonor() { + _wearHonor?.clear(); + } + + ///同上 + void cleanWearBadge() { + _wearBadge?.clear(); + } + void cleanUseProps() { + _useProps?.clear(); + } + + void cleanPhotos() { + _backgroundPhotos?.clear(); + _personalPhotos?.clear(); + _cpList?.clear(); + } + + PropsResources? getHeaddress() { + PropsResources? pr; + _useProps?.forEach((value) { + if (value.propsResources?.type == SCPropsType.AVATAR_FRAME.name) { + pr = value.propsResources; + } + }); + return pr; + } + + PropsResources? getChatBox() { + PropsResources? pr; + _useProps?.forEach((value) { + if (value.propsResources?.type == SCPropsType.CHAT_BUBBLE.name) { + pr = value.propsResources; + } + }); + return pr; + } + + PropsResources? getMountains() { + PropsResources? pr; + _useProps?.forEach((value) { + if (value.propsResources?.type == SCPropsType.RIDE.name) { + pr = value.propsResources; + } + }); + return pr; + } + + PropsResources? getVIP() { + return null; + } + + PropsResources? getDataCard() { + PropsResources? pr; + _useProps?.forEach((value) { + if (value.propsResources?.type == SCPropsType.DATA_CARD.name) { + pr = value.propsResources; + } + }); + return pr; + } + + Map toJson() { + final map = {}; + map['account'] = _account; + map['accountStatus'] = _accountStatus; + map['age'] = _age; + map['bornDay'] = _bornDay; + map['bornMonth'] = _bornMonth; + map['bornYear'] = _bornYear; + map['countryCode'] = _countryCode; + map['countryId'] = _countryId; + map['countryName'] = _countryName; + map['createTime'] = _createTime; + map['del'] = _del; + map['freezingTime'] = _freezingTime; + map['id'] = _id; + map['originSys'] = _originSys; + map['charmLevel'] = _charmLevel; + map['firstRechargeAmount'] = _firstRechargeAmount; + map['wealthLevel'] = _wealthLevel; + map['heartbeatVal'] = _heartbeatVal; + if (_ownSpecialId != null) { + map['ownSpecialId'] = _ownSpecialId?.toJson(); + } + map['sameRegion'] = _sameRegion; + map['isCpRelation'] = _isCpRelation; + map['firstRecharge'] = _firstRecharge; + map['firstRechargeEndTime'] = _firstRechargeEndTime; + map['sysOriginChild'] = _sysOriginChild; + if (_useProps != null) { + map['useProps'] = _useProps?.map((v) => v.toJson()).toList(); + } + map['userAvatar'] = _userAvatar; + map['roles'] = _roles; + map['userNickname'] = _userNickname; + map['hobby'] = _hobby; + map['familyId'] = _familyId; + map['autograph'] = _autograph; + map['userSex'] = _userSex; + if (_wearBadge != null) { + map['wearBadge'] = _wearBadge?.map((v) => v.toJson()).toList(); + } + if (_wearHonor != null) { + map['wearHonor'] = _wearHonor?.map((v) => v.toJson()).toList(); + } + return map; + } + + ///获取Id,有靓号显示靓号 + String getID() { + String uid = account ?? ""; + if (_ownSpecialId != null) { + uid = _ownSpecialId?.account ?? ""; + } + return uid; + } + + ///是否有靓号 + bool hasSpecialId() { + if (_ownSpecialId != null) { + if ((_ownSpecialId?.expiredTime ?? 0) > + DateTime.now().millisecondsSinceEpoch) { + return true; + } + } + return false; + } + + void setHeartbeatVal(num? heartbeatVal) { + _heartbeatVal = (_heartbeatVal ?? 0) + (heartbeatVal ?? 0); + } +} + +/// animationUrl : "" +/// badgeKey : "" +/// badgeLevel : 0 +/// badgeName : "" +/// expireTime : 0 +/// id : 0 +/// milestone : 0 +/// notSelectUrl : "" +/// selectUrl : "" +/// type : "" +/// userId : 0 + +class WearBadge { + WearBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + int? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, + bool? use, + }) { + _animationUrl = animationUrl; + _badgeKey = badgeKey; + _badgeLevel = badgeLevel; + _badgeName = badgeName; + _expireTime = expireTime; + _id = id; + _use = use; + _milestone = milestone; + _notSelectUrl = notSelectUrl; + _selectUrl = selectUrl; + _type = type; + _userId = userId; + } + + WearBadge.fromJson(dynamic json) { + _animationUrl = json['animationUrl']; + _badgeKey = json['badgeKey']; + _badgeLevel = json['badgeLevel']; + _badgeName = json['badgeName']; + _expireTime = json['expireTime']; + _id = json['id']; + _use = json['use']; + _milestone = json['milestone']; + _notSelectUrl = json['notSelectUrl']; + _selectUrl = json['selectUrl']; + _type = json['type']; + _userId = json['userId']; + } + + String? _animationUrl; + String? _badgeKey; + num? _badgeLevel; + String? _badgeName; + int? _expireTime; + String? _id; + bool? _use; + String? _milestone; + String? _notSelectUrl; + String? _selectUrl; + String? _type; + String? _userId; + + WearBadge copyWith({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + int? expireTime, + String? id, + bool? use, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, + }) => WearBadge( + animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + use: use ?? _use, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, + ); + + String? get animationUrl => _animationUrl; + + String? get badgeKey => _badgeKey; + + num? get badgeLevel => _badgeLevel; + + bool? get use => _use; + + String? get badgeName => _badgeName; + + int? get expireTime => _expireTime; + + String? get id => _id; + + String? get milestone => _milestone; + + String? get notSelectUrl => _notSelectUrl; + + String? get selectUrl => _selectUrl; + + String? get type => _type; + + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['animationUrl'] = _animationUrl; + map['badgeKey'] = _badgeKey; + map['badgeLevel'] = _badgeLevel; + map['badgeName'] = _badgeName; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['use'] = _use; + map['milestone'] = _milestone; + map['notSelectUrl'] = _notSelectUrl; + map['selectUrl'] = _selectUrl; + map['type'] = _type; + map['userId'] = _userId; + return map; + } +} + +class PersonPhoto { + PersonPhoto({String? url, num? status}) { + _url = url; + _status = status; + } + + PersonPhoto.fromJson(dynamic json) { + _url = json['url']; + _status = json['status']; + } + + String? _url; + num? _status; + + PersonPhoto copyWith({String? url, num? status}) => + PersonPhoto(url: url ?? _url, status: status ?? _status); + + String? get url => _url; + + num? get status => _status; + + Map toJson() { + final map = {}; + map['url'] = _url; + map['status'] = _status; + return map; + } +} + +class CPRes { + CPRes({ + String? meUserId, + String? meAccount, + String? meUserAvatar, + String? meUserNickname, + String? cpUserId, + String? cpUserNickname, + String? cpAccount, + String? cpUserAvatar, + String? days, + String? firstDay, + double? cpValue, + }) { + _meUserId = meUserId; + _meAccount = meAccount; + _meUserAvatar = meUserAvatar; + _meUserNickname = meUserNickname; + _cpUserId = cpUserId; + _cpUserNickname = cpUserNickname; + _cpAccount = cpAccount; + _cpUserAvatar = cpUserAvatar; + _days = days; + _firstDay = firstDay; + _cpValue = cpValue; + } + + CPRes.fromJson(dynamic json) { + _meUserId = json['meUserId']; + _meAccount = json['meAccount']; + _meUserAvatar = json['meUserAvatar']; + _meUserNickname = json['meUserNickname']; + _cpUserId = json['cpUserId']; + _cpUserNickname = json['cpUserNickname']; + _cpAccount = json['cpAccount']; + _cpUserAvatar = json['cpUserAvatar']; + _days = json['days']; + _firstDay = json['firstDay']; + _cpValue = json['cpValue']; + } + + String? _meUserId; + String? _meAccount; + String? _meUserAvatar; + String? _meUserNickname; + String? _cpUserId; + String? _cpUserNickname; + String? _cpAccount; + String? _cpUserAvatar; + String? _days; + String? _firstDay; + double? _cpValue; + + String? get meUserId => _meUserId; + + String? get meAccount => _meAccount; + + String? get meUserAvatar => _meUserAvatar; + + String? get meUserNickname => _meUserNickname; + + String? get cpUserId => _cpUserId; + + String? get cpUserNickname => _cpUserNickname; + + String? get cpAccount => _cpAccount; + + String? get cpUserAvatar => _cpUserAvatar; + + String? get days => _days; + + String? get firstDay => _firstDay; + + double? get cpValue => _cpValue; + + Map toJson() { + final map = {}; + map['meUserId'] = _meUserId; + map['meAccount'] = _meAccount; + map['meUserAvatar'] = _meUserAvatar; + map['meUserNickname'] = _meUserNickname; + map['cpUserId'] = _cpUserId; + map['cpUserNickname'] = _cpUserNickname; + map['cpAccount'] = _cpAccount; + map['cpUserAvatar'] = _cpUserAvatar; + map['days'] = _days; + map['firstDay'] = _firstDay; + map['cpValue'] = _cpValue; + return map; + } +} + +/// expireTime : 0 +/// propsResources : {"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""} +/// userId : 0 + +class UseProps { + UseProps({ + bool? allowGive, + String? expireTime, + PropsResources? propsResources, + String? userId, + }) { + _allowGive = allowGive; + _expireTime = expireTime; + _propsResources = propsResources; + _userId = userId; + } + + UseProps.fromJson(dynamic json) { + _allowGive = json['allowGive']; + _expireTime = json['expireTime']; + _propsResources = + json['propsResources'] != null + ? PropsResources.fromJson(json['propsResources']) + : null; + _userId = json['userId']; + } + + bool? _allowGive; + String? _expireTime; + PropsResources? _propsResources; + String? _userId; + + UseProps copyWith({ + bool? allowGive, + String? expireTime, + PropsResources? propsResources, + String? userId, + }) => UseProps( + allowGive: allowGive ?? _allowGive, + expireTime: expireTime ?? _expireTime, + propsResources: propsResources ?? _propsResources, + userId: userId ?? _userId, + ); + + bool? get allowGive => _allowGive; + + String? get expireTime => _expireTime; + + PropsResources? get propsResources => _propsResources; + + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['allowGive'] = _allowGive; + map['expireTime'] = _expireTime; + if (_propsResources != null) { + map['propsResources'] = _propsResources?.toJson(); + } + map['userId'] = _userId; + return map; + } + + void setPropsResources(PropsResources? pr) { + _propsResources = pr; + } +} + +/// amount : 0.0 +/// code : "" +/// cover : "" +/// expand : "" +/// id : 0 +/// name : "" +/// sourceUrl : "" +/// type : "" + +class PropsResources { + PropsResources({ + num? amount, + String? code, + String? cover, + num? createTime, + bool? del, + String? expand, + String? id, + String? imNotOpenedUrl, + String? imOpenedUrl, + String? imOpenedUrlTwo, + String? imSendCoverUrl, + String? name, + String? roomNotOpenedUrl, + String? roomOpenedUrl, + String? roomSendCoverUrl, + String? sourceUrl, + String? sysOrigin, + String? type, + num? updateTime, + }) { + _amount = amount; + _code = code; + _cover = cover; + _createTime = createTime; + _del = del; + _expand = expand; + _id = id; + _imNotOpenedUrl = imNotOpenedUrl; + _imOpenedUrl = imOpenedUrl; + _imOpenedUrlTwo = imOpenedUrlTwo; + _imSendCoverUrl = imSendCoverUrl; + _name = name; + _roomNotOpenedUrl = roomNotOpenedUrl; + _roomOpenedUrl = roomOpenedUrl; + _roomSendCoverUrl = roomSendCoverUrl; + _sourceUrl = sourceUrl; + _sysOrigin = sysOrigin; + _type = type; + _updateTime = updateTime; + } + + PropsResources.fromJson(dynamic json) { + _amount = json['amount']; + _code = json['code']; + _cover = json['cover']; + _createTime = json['createTime']; + _del = json['del']; + _expand = json['expand']; + _id = json['id']; + _imNotOpenedUrl = json['imNotOpenedUrl']; + _imOpenedUrl = json['imOpenedUrl']; + _imOpenedUrlTwo = json['imOpenedUrlTwo']; + _imSendCoverUrl = json['imSendCoverUrl']; + _name = json['name']; + _roomNotOpenedUrl = json['roomNotOpenedUrl']; + _roomOpenedUrl = json['roomOpenedUrl']; + _roomSendCoverUrl = json['roomSendCoverUrl']; + _sourceUrl = json['sourceUrl']; + _sysOrigin = json['sysOrigin']; + _type = json['type']; + _updateTime = json['updateTime']; + } + + num? _amount; + String? _code; + String? _cover; + num? _createTime; + bool? _del; + String? _expand; + String? _id; + String? _imNotOpenedUrl; + String? _imOpenedUrl; + String? _imOpenedUrlTwo; + String? _imSendCoverUrl; + String? _name; + String? _roomNotOpenedUrl; + String? _roomOpenedUrl; + String? _roomSendCoverUrl; + String? _sourceUrl; + String? _sysOrigin; + String? _type; + num? _updateTime; + + PropsResources copyWith({ + num? amount, + String? code, + String? cover, + num? createTime, + bool? del, + String? expand, + String? id, + String? imNotOpenedUrl, + String? imOpenedUrl, + String? imOpenedUrlTwo, + String? imSendCoverUrl, + String? name, + String? roomNotOpenedUrl, + String? roomOpenedUrl, + String? roomSendCoverUrl, + String? sourceUrl, + String? sysOrigin, + String? type, + num? updateTime, + }) => PropsResources( + amount: amount ?? _amount, + code: code ?? _code, + cover: cover ?? _cover, + createTime: createTime ?? _createTime, + del: del ?? _del, + expand: expand ?? _expand, + id: id ?? _id, + imNotOpenedUrl: imNotOpenedUrl ?? _imNotOpenedUrl, + imOpenedUrl: imOpenedUrl ?? _imOpenedUrl, + imOpenedUrlTwo: imOpenedUrlTwo ?? _imOpenedUrlTwo, + imSendCoverUrl: imSendCoverUrl ?? _imSendCoverUrl, + name: name ?? _name, + roomNotOpenedUrl: roomNotOpenedUrl ?? _roomNotOpenedUrl, + roomOpenedUrl: roomOpenedUrl ?? _roomOpenedUrl, + roomSendCoverUrl: roomSendCoverUrl ?? _roomSendCoverUrl, + sourceUrl: sourceUrl ?? _sourceUrl, + sysOrigin: sysOrigin ?? _sysOrigin, + type: type ?? _type, + updateTime: updateTime ?? _updateTime, + ); + + num? get amount => _amount; + + String? get code => _code; + + String? get cover => _cover; + + num? get createTime => _createTime; + + bool? get del => _del; + + String? get expand => _expand; + + String? get id => _id; + + String? get imNotOpenedUrl => _imNotOpenedUrl; + + String? get imOpenedUrl => _imOpenedUrl; + + String? get imOpenedUrlTwo => _imOpenedUrlTwo; + + String? get imSendCoverUrl => _imSendCoverUrl; + + String? get name => _name; + + String? get roomNotOpenedUrl => _roomNotOpenedUrl; + + String? get roomOpenedUrl => _roomOpenedUrl; + + String? get roomSendCoverUrl => _roomSendCoverUrl; + + String? get sourceUrl => _sourceUrl; + + String? get sysOrigin => _sysOrigin; + + String? get type => _type; + + num? get updateTime => _updateTime; + + Map toJson() { + final map = {}; + map['amount'] = _amount; + map['code'] = _code; + map['cover'] = _cover; + map['createTime'] = _createTime; + map['del'] = _del; + map['expand'] = _expand; + map['id'] = _id; + map['imNotOpenedUrl'] = _imNotOpenedUrl; + map['imOpenedUrl'] = _imOpenedUrl; + map['imOpenedUrlTwo'] = _imOpenedUrlTwo; + map['imSendCoverUrl'] = _imSendCoverUrl; + map['name'] = _name; + map['roomNotOpenedUrl'] = _roomNotOpenedUrl; + map['roomOpenedUrl'] = _roomOpenedUrl; + map['roomSendCoverUrl'] = _roomSendCoverUrl; + map['sourceUrl'] = _sourceUrl; + map['sysOrigin'] = _sysOrigin; + map['type'] = _type; + map['updateTime'] = _updateTime; + return map; + } +} + +/// account : "" +/// expiredTime : 0 + +class OwnSpecialId { + OwnSpecialId({String? account, num? expiredTime}) { + _account = account; + _expiredTime = expiredTime; + } + + OwnSpecialId.fromJson(dynamic json) { + _account = json['account']; + _expiredTime = json['expiredTime']; + } + + String? _account; + num? _expiredTime; + + OwnSpecialId copyWith({String? account, num? expiredTime}) => OwnSpecialId( + account: account ?? _account, + expiredTime: expiredTime ?? _expiredTime, + ); + + String? get account => _account; + + num? get expiredTime => _expiredTime; + + Map toJson() { + final map = {}; + map['account'] = _account; + map['expiredTime'] = _expiredTime; + return map; + } +} + +/// expireTime : 0 +/// releaseTime : 0 +/// sign : "" +/// sysOrigin : "" +/// userId : 0 +/// version : "" + +class SocialChatUserCredential { + SocialChatUserCredential({ + String? expireTime, + String? releaseTime, + String? sign, + String? sysOrigin, + String? userId, + String? version, + }) { + _expireTime = expireTime; + _releaseTime = releaseTime; + _sign = sign; + _sysOrigin = sysOrigin; + _userId = userId; + _version = version; + } + + SocialChatUserCredential.fromJson(dynamic json) { + _expireTime = json['expireTime']; + _releaseTime = json['releaseTime']; + _sign = json['sign']; + _sysOrigin = json['sysOrigin']; + _userId = json['userId']; + _version = json['version']; + } + + String? _expireTime; + String? _releaseTime; + String? _sign; + String? _sysOrigin; + String? _userId; + String? _version; + + SocialChatUserCredential copyWith({ + String? expireTime, + String? releaseTime, + String? sign, + String? sysOrigin, + String? userId, + String? version, + }) => SocialChatUserCredential( + expireTime: expireTime ?? _expireTime, + releaseTime: releaseTime ?? _releaseTime, + sign: sign ?? _sign, + sysOrigin: sysOrigin ?? _sysOrigin, + userId: userId ?? _userId, + version: version ?? _version, + ); + + String? get expireTime => _expireTime; + + String? get releaseTime => _releaseTime; + + String? get sign => _sign; + + String? get sysOrigin => _sysOrigin; + + String? get userId => _userId; + + String? get version => _version; + + Map toJson() { + final map = {}; + map['expireTime'] = _expireTime; + map['releaseTime'] = _releaseTime; + map['sign'] = _sign; + map['sysOrigin'] = _sysOrigin; + map['userId'] = _userId; + map['version'] = _version; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/message_friend_user_res.dart b/lib/shared/business_logic/models/res/message_friend_user_res.dart new file mode 100644 index 0000000..738774c --- /dev/null +++ b/lib/shared/business_logic/models/res/message_friend_user_res.dart @@ -0,0 +1,40 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +class MessageFriendUserRes{ + SocialChatLoginRes({ + String? id, + SocialChatUserProfile? userProfile, + + }) { + _userProfile = userProfile; + _id = id; + } + + MessageFriendUserRes.fromJson(dynamic json) { + _id = json['id']; + _userProfile = + json['userProfile'] != null + ? SocialChatUserProfile.fromJson(json['userProfile']) + : null; + } + SocialChatUserProfile? _userProfile; + String? _id; + + + String? get id => _id; + SocialChatUserProfile? get userProfile => _userProfile; + + + Map toJson() { + final map = {}; + map['id'] = _id; + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + return map; + } + + void setUserProfile(SocialChatUserProfile userInfo) { + _userProfile = userInfo; + } +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/mic_res.dart b/lib/shared/business_logic/models/res/mic_res.dart new file mode 100644 index 0000000..a65331a --- /dev/null +++ b/lib/shared/business_logic/models/res/mic_res.dart @@ -0,0 +1,115 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// roomId : 0 +/// micIndex : 0 +/// micLock : false +/// micMute : false +/// user : {"id":"0","account":"","userAvatar":"","userNickname":"","userSex":0,"ownSpecialId":{"account":"","expiredTime":0},"useProps":[{"userId":"0","propsResources":{"id":"0","type":"","code":"","name":"","cover":"","sourceUrl":"","expand":"","amount":0.0},"expireTime":0}],"roles":"","charmLevel":0,"heartbeatVal":0} +/// roomToken : "" + +class MicRes { + MicRes({ + String? roomId, + num? micIndex, + bool? micLock, + bool? micMute, + String? emojiPath, + SocialChatUserProfile? user, + String? type, + String? roomToken, + String?number + }) { + _roomId = roomId; + _micIndex = micIndex; + _micLock = micLock; + _micMute = micMute; + _user = user; + _roomToken = roomToken; + _emojiPath = emojiPath; + _type = type; + _number = number; + } + + MicRes.fromJson(dynamic json) { + _roomId = json['roomId']; + _micIndex = json['micIndex']; + _micLock = json['micLock']; + _micMute = json['micMute']; + _user = json['user'] != null ? SocialChatUserProfile.fromJson(json['user']) : null; + _roomToken = json['roomToken']; + _type = json['type']; + _number = json['number']; + } + + String? _roomId; + num? _micIndex; + bool? _micLock; + bool? _micMute; + SocialChatUserProfile? _user; + String? _roomToken; + int? _volume = 0; + String? _type = ""; + String? _emojiPath; + String? _number; + + MicRes copyWith({ + String? roomId, + num? micIndex, + bool? micLock, + bool? micMute, + String? type, + SocialChatUserProfile? user, + String? roomToken, + String? emojiPath, + String? number, + }) => MicRes( + roomId: roomId ?? _roomId, + micIndex: micIndex ?? _micIndex, + micLock: micLock ?? _micLock, + micMute: micMute ?? _micMute, + user: user ?? _user, + roomToken: roomToken ?? _roomToken, + emojiPath: emojiPath ?? _emojiPath, + type: type ?? _type, + number: number ?? _number, + ); + + String? get roomId => _roomId; + + num? get micIndex => _micIndex; + + bool? get micLock => _micLock; + + bool? get micMute => _micMute; + + SocialChatUserProfile? get user => _user; + + String? get roomToken => _roomToken; + + String? get type => _type; + + String? get emojiPath => _emojiPath; + + String? get number => _number; + + set setUser(SocialChatUserProfile? value) => _user = value; + + set setVolume(int? value) => _volume = value; + + set setEmojiPath(String? value) => _emojiPath = value; + + Map toJson() { + final map = {}; + map['roomId'] = _roomId; + map['micIndex'] = _micIndex; + map['micLock'] = _micLock; + map['micMute'] = _micMute; + map['type'] = _type; + map['number'] = _number; + if (_user != null) { + map['user'] = _user?.toJson(); + } + map['roomToken'] = _roomToken; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/my_room_res.dart b/lib/shared/business_logic/models/res/my_room_res.dart new file mode 100644 index 0000000..8c5659d --- /dev/null +++ b/lib/shared/business_logic/models/res/my_room_res.dart @@ -0,0 +1,320 @@ +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; + +import 'join_room_res.dart' show RoomCounter; + +/// id : 0 +/// roomAccount : "" +/// userId : 0 +/// roomCover : "" +/// roomName : "" +/// roomDesc : "" +/// event : "" +/// sysOrigin : "" +/// countryCode : "" +/// countryName : "" +/// nationalFlag : "" +/// langCode : "" +/// del : false +/// createTime : 0 +/// updateTime : 0 +/// activeTime : 0 +/// setting : {"password":"","takeMicRole":"","touristMike":false,"touristMsg":false,"maxMember":0,"maxAdmin":0,"joinGolds":0,"mikeSize":0,"allowMusic":false,"adminLockSeat":false,"showHeartbeat":false,"roomSpecialMikeType":"","roomSpecialMikeExpireTime":0,"roomSpecialMikeExpireType":""} +/// counter : {"memberCount":0,"adminCount":0} +/// customizeSetting : {"":""} +/// useBadges : [{"id":0,"userId":0,"badgeLevel":0,"milestone":0,"badgeName":"","type":"","badgeKey":"","selectUrl":"","notSelectUrl":"","animationUrl":"","expireTime":0}] + +class MyRoomRes { + MyRoomRes({ + String? id, + String? roomAccount, + String? userId, + String? roomCover, + String? roomName, + String? roomDesc, + String? event, + String? sysOrigin, + String? countryCode, + String? countryName, + String? nationalFlag, + String? langCode, + bool? del, + num? createTime, + num? updateTime, + num? activeTime, + RoomSetting? setting, + RoomCounter? counter, + List? useBadges,}){ + _id = id; + _roomAccount = roomAccount; + _userId = userId; + _roomCover = roomCover; + _roomName = roomName; + _roomDesc = roomDesc; + _event = event; + _sysOrigin = sysOrigin; + _countryCode = countryCode; + _countryName = countryName; + _nationalFlag = nationalFlag; + _langCode = langCode; + _del = del; + _createTime = createTime; + _updateTime = updateTime; + _activeTime = activeTime; + _setting = setting; + _counter = counter; + _useBadges = useBadges; + } + + MyRoomRes.fromJson(dynamic json) { + _id = json['id']; + _roomAccount = json['roomAccount']; + _userId = json['userId']; + _roomCover = json['roomCover']; + _roomName = json['roomName']; + _roomDesc = json['roomDesc']; + _event = json['event']; + _sysOrigin = json['sysOrigin']; + _countryCode = json['countryCode']; + _countryName = json['countryName']; + _nationalFlag = json['nationalFlag']; + _langCode = json['langCode']; + _del = json['del']; + _createTime = json['createTime']; + _updateTime = json['updateTime']; + _activeTime = json['activeTime']; + _setting = json['setting'] != null ? RoomSetting.fromJson(json['setting']) : null; + _counter = json['counter'] != null ? RoomCounter.fromJson(json['counter']) : null; + if (json['useBadges'] != null) { + _useBadges = []; + json['useBadges'].forEach((v) { + _useBadges?.add(UseBadges.fromJson(v)); + }); + } + } + String? _id; + String? _roomAccount; + String? _userId; + String? _roomCover; + String? _roomName; + String? _roomDesc; + String? _event; + String? _sysOrigin; + String? _countryCode; + String? _countryName; + String? _nationalFlag; + String? _langCode; + bool? _del; + num? _createTime; + num? _updateTime; + num? _activeTime; + RoomSetting? _setting; + RoomCounter? _counter; + List? _useBadges; + MyRoomRes copyWith({ String? id, + String? roomAccount, + String? userId, + String? roomCover, + String? roomName, + String? roomDesc, + String? event, + String? sysOrigin, + String? countryCode, + String? countryName, + String? nationalFlag, + String? langCode, + bool? del, + num? createTime, + num? updateTime, + num? activeTime, + RoomSetting? setting, + RoomCounter? counter, + List? useBadges, + }) => MyRoomRes( id: id ?? _id, + roomAccount: roomAccount ?? _roomAccount, + userId: userId ?? _userId, + roomCover: roomCover ?? _roomCover, + roomName: roomName ?? _roomName, + roomDesc: roomDesc ?? _roomDesc, + event: event ?? _event, + sysOrigin: sysOrigin ?? _sysOrigin, + countryCode: countryCode ?? _countryCode, + countryName: countryName ?? _countryName, + nationalFlag: nationalFlag ?? _nationalFlag, + langCode: langCode ?? _langCode, + del: del ?? _del, + createTime: createTime ?? _createTime, + updateTime: updateTime ?? _updateTime, + activeTime: activeTime ?? _activeTime, + setting: setting ?? _setting, + counter: counter ?? _counter, + useBadges: useBadges ?? _useBadges, + ); + String? get id => _id; + String? get roomAccount => _roomAccount; + String? get userId => _userId; + String? get roomCover => _roomCover; + String? get roomName => _roomName; + String? get roomDesc => _roomDesc; + String? get event => _event; + String? get sysOrigin => _sysOrigin; + String? get countryCode => _countryCode; + String? get countryName => _countryName; + String? get nationalFlag => _nationalFlag; + String? get langCode => _langCode; + bool? get del => _del; + num? get createTime => _createTime; + num? get updateTime => _updateTime; + num? get activeTime => _activeTime; + RoomSetting? get setting => _setting; + RoomCounter? get counter => _counter; + List? get useBadges => _useBadges; + set setRoomCover(String value) => _roomCover = value; + set setRoomName(String value) => _roomName = value; + set setRoomDesc(String value) => _roomDesc = value; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['roomAccount'] = _roomAccount; + map['userId'] = _userId; + map['roomCover'] = _roomCover; + map['roomName'] = _roomName; + map['roomDesc'] = _roomDesc; + map['event'] = _event; + map['sysOrigin'] = _sysOrigin; + map['countryCode'] = _countryCode; + map['countryName'] = _countryName; + map['nationalFlag'] = _nationalFlag; + map['langCode'] = _langCode; + map['del'] = _del; + map['createTime'] = _createTime; + map['updateTime'] = _updateTime; + map['activeTime'] = _activeTime; + if (_setting != null) { + map['setting'] = _setting?.toJson(); + } + if (_counter != null) { + map['counter'] = _counter?.toJson(); + } + if (_useBadges != null) { + map['useBadges'] = _useBadges?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// id : 0 +/// userId : 0 +/// badgeLevel : 0 +/// milestone : 0 +/// badgeName : "" +/// type : "" +/// badgeKey : "" +/// selectUrl : "" +/// notSelectUrl : "" +/// animationUrl : "" +/// expireTime : 0 + +class UseBadges { + UseBadges({ + num? id, + num? userId, + num? badgeLevel, + num? milestone, + String? badgeName, + String? type, + String? badgeKey, + String? selectUrl, + String? notSelectUrl, + String? animationUrl, + String? expireTime,}){ + _id = id; + _userId = userId; + _badgeLevel = badgeLevel; + _milestone = milestone; + _badgeName = badgeName; + _type = type; + _badgeKey = badgeKey; + _selectUrl = selectUrl; + _notSelectUrl = notSelectUrl; + _animationUrl = animationUrl; + _expireTime = expireTime; + } + + UseBadges.fromJson(dynamic json) { + _id = json['id']; + _userId = json['userId']; + _badgeLevel = json['badgeLevel']; + _milestone = json['milestone']; + _badgeName = json['badgeName']; + _type = json['type']; + _badgeKey = json['badgeKey']; + _selectUrl = json['selectUrl']; + _notSelectUrl = json['notSelectUrl']; + _animationUrl = json['animationUrl']; + _expireTime = json['expireTime']; + } + num? _id; + num? _userId; + num? _badgeLevel; + num? _milestone; + String? _badgeName; + String? _type; + String? _badgeKey; + String? _selectUrl; + String? _notSelectUrl; + String? _animationUrl; + String? _expireTime; + UseBadges copyWith({ num? id, + num? userId, + num? badgeLevel, + num? milestone, + String? badgeName, + String? type, + String? badgeKey, + String? selectUrl, + String? notSelectUrl, + String? animationUrl, + String? expireTime, + }) => UseBadges( id: id ?? _id, + userId: userId ?? _userId, + badgeLevel: badgeLevel ?? _badgeLevel, + milestone: milestone ?? _milestone, + badgeName: badgeName ?? _badgeName, + type: type ?? _type, + badgeKey: badgeKey ?? _badgeKey, + selectUrl: selectUrl ?? _selectUrl, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + animationUrl: animationUrl ?? _animationUrl, + expireTime: expireTime ?? _expireTime, + ); + num? get id => _id; + num? get userId => _userId; + num? get badgeLevel => _badgeLevel; + num? get milestone => _milestone; + String? get badgeName => _badgeName; + String? get type => _type; + String? get badgeKey => _badgeKey; + String? get selectUrl => _selectUrl; + String? get notSelectUrl => _notSelectUrl; + String? get animationUrl => _animationUrl; + String? get expireTime => _expireTime; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['userId'] = _userId; + map['badgeLevel'] = _badgeLevel; + map['milestone'] = _milestone; + map['badgeName'] = _badgeName; + map['type'] = _type; + map['badgeKey'] = _badgeKey; + map['selectUrl'] = _selectUrl; + map['notSelectUrl'] = _notSelectUrl; + map['animationUrl'] = _animationUrl; + map['expireTime'] = _expireTime; + return map; + } + +} diff --git a/lib/shared/business_logic/models/res/room_black_list_res.dart b/lib/shared/business_logic/models/res/room_black_list_res.dart new file mode 100644 index 0000000..a701aa2 --- /dev/null +++ b/lib/shared/business_logic/models/res/room_black_list_res.dart @@ -0,0 +1,72 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// blacklistUserProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"inRoomId":"","isUpdateCountry":"","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"regionCode":"","roomIcon":"","sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} +/// createTime : 0 +/// expiredTime : 0 +/// id : 0 +/// optUserProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"inRoomId":"","isUpdateCountry":"","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"regionCode":"","roomIcon":"","sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} +/// roomId : 0 + +class RoomBlackListRes { + RoomBlackListRes({ + SocialChatUserProfile? blacklistUserProfile, + num? createTime, + num? expiredTime, + String? id, + SocialChatUserProfile? optUserProfile, + String? roomId,}){ + _blacklistUserProfile = blacklistUserProfile; + _createTime = createTime; + _expiredTime = expiredTime; + _id = id; + _optUserProfile = optUserProfile; + _roomId = roomId; +} + + RoomBlackListRes.fromJson(dynamic json) { + _blacklistUserProfile = json['blacklistUserProfile'] != null ? SocialChatUserProfile.fromJson(json['blacklistUserProfile']) : null; + _createTime = json['createTime']; + _expiredTime = json['expiredTime']; + _id = json['id']; + _optUserProfile = json['optUserProfile'] != null ? SocialChatUserProfile.fromJson(json['optUserProfile']) : null; + _roomId = json['roomId']; + } + SocialChatUserProfile? _blacklistUserProfile; + num? _createTime; + num? _expiredTime; + String? _id; + SocialChatUserProfile? _optUserProfile; + String? _roomId; +RoomBlackListRes copyWith({ SocialChatUserProfile? blacklistUserProfile, + num? createTime, + num? expiredTime, + String? id, + SocialChatUserProfile? optUserProfile, + String? roomId, +}) => RoomBlackListRes( blacklistUserProfile: blacklistUserProfile ?? _blacklistUserProfile, + createTime: createTime ?? _createTime, + expiredTime: expiredTime ?? _expiredTime, + id: id ?? _id, + optUserProfile: optUserProfile ?? _optUserProfile, + roomId: roomId ?? _roomId, +); + SocialChatUserProfile? get blacklistUserProfile => _blacklistUserProfile; + num? get createTime => _createTime; + num? get expiredTime => _expiredTime; + String? get id => _id; + SocialChatUserProfile? get optUserProfile => _optUserProfile; + String? get roomId => _roomId; + + Map toJson() { + final map = {}; + map['blacklistUserProfile'] = _blacklistUserProfile; + map['createTime'] = _createTime; + map['expiredTime'] = _expiredTime; + map['id'] = _id; + if (_optUserProfile != null) { + map['optUserProfile'] = _optUserProfile?.toJson(); + } + map['roomId'] = _roomId; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/room_gift_rank_res.dart b/lib/shared/business_logic/models/res/room_gift_rank_res.dart new file mode 100644 index 0000000..5ff076c --- /dev/null +++ b/lib/shared/business_logic/models/res/room_gift_rank_res.dart @@ -0,0 +1,46 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// userProfile : {"id":0,"account":"","userAvatar":"","userNickname":"","userSex":0,"age":0,"accountStatus":"","freezingTime":0,"countryId":0,"countryName":"","countryCode":"","originSys":"","sysOriginChild":"","del":false,"createTime":0,"bornYear":0,"bornMonth":0,"bornDay":0,"ownSpecialId":{"account":"","expiredTime":0},"useProps":[{"userId":0,"propsResources":{"id":0,"type":"","code":"","name":"","cover":"","sourceUrl":"","expand":"","amount":0.0},"expireTime":0}],"sameRegion":false,"wearBadge":[{"id":0,"userId":0,"badgeLevel":0,"milestone":0,"badgeName":"","type":"","badgeKey":"","selectUrl":"","notSelectUrl":"","animationUrl":"","expireTime":0}]} +/// totalFormat : "" +/// total : 0 + +class RoomGiftRankRes { + RoomGiftRankRes({ + SocialChatUserProfile? userProfile, + String? totalFormat, + String? total,}){ + _userProfile = userProfile; + _totalFormat = totalFormat; + _total = total; +} + + RoomGiftRankRes.fromJson(dynamic json) { + _userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null; + _totalFormat = json['totalFormat']; + _total = json['total']; + } + SocialChatUserProfile? _userProfile; + String? _totalFormat; + String? _total; +RoomGiftRankRes copyWith({ SocialChatUserProfile? userProfile, + String? totalFormat, + String? total, +}) => RoomGiftRankRes( userProfile: userProfile ?? _userProfile, + totalFormat: totalFormat ?? _totalFormat, + total: total ?? _total, +); + SocialChatUserProfile? get userProfile => _userProfile; + String? get totalFormat => _totalFormat; + String? get total => _total; + + Map toJson() { + final map = {}; + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + map['totalFormat'] = _totalFormat; + map['total'] = _total; + return map; + } + +} diff --git a/lib/shared/business_logic/models/res/room_member_res.dart b/lib/shared/business_logic/models/res/room_member_res.dart new file mode 100644 index 0000000..0412a3a --- /dev/null +++ b/lib/shared/business_logic/models/res/room_member_res.dart @@ -0,0 +1,52 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// id : 0 +/// userProfile : {"id":0,"account":"","userAvatar":"","userNickname":"","userSex":0,"age":0,"accountStatus":"","freezingTime":0,"countryId":0,"countryName":"","countryCode":"","originSys":"","sysOriginChild":"","del":false,"createTime":0,"bornYear":0,"bornMonth":0,"bornDay":0,"ownSpecialId":{"account":"","expiredTime":0},"useProps":[{"userId":0,"propsResources":{"id":0,"type":"","code":"","name":"","cover":"","sourceUrl":"","expand":"","amount":0.0},"expireTime":0}],"sameRegion":false,"wearBadge":[{"id":0,"userId":0,"badgeLevel":0,"milestone":0,"badgeName":"","type":"","badgeKey":"","selectUrl":"","notSelectUrl":"","animationUrl":"","expireTime":0}]} +/// roles : "" + +class SocialChatRoomMemberRes { + SocialChatRoomMemberRes({String? id, SocialChatUserProfile? userProfile, String? roles}) { + _id = id; + _userProfile = userProfile; + _roles = roles; + } + + SocialChatRoomMemberRes.fromJson(dynamic json) { + _id = json['id']; + _userProfile = + json['userProfile'] != null + ? SocialChatUserProfile.fromJson(json['userProfile']) + : null; + _roles = json['roles']; + } + + String? _id; + SocialChatUserProfile? _userProfile; + String? _roles; + + SocialChatRoomMemberRes copyWith({ + String? id, + SocialChatUserProfile? userProfile, + String? roles, + }) => SocialChatRoomMemberRes( + id: id ?? _id, + userProfile: userProfile ?? _userProfile, + roles: roles ?? _roles, + ); + + String? get id => _id; + + SocialChatUserProfile? get userProfile => _userProfile; + + String? get roles => _roles; + + Map toJson() { + final map = {}; + map['id'] = _id; + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + map['roles'] = _roles; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/room_res.dart b/lib/shared/business_logic/models/res/room_res.dart new file mode 100644 index 0000000..f4f0f33 --- /dev/null +++ b/lib/shared/business_logic/models/res/room_res.dart @@ -0,0 +1,742 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// countryCode : "" +/// countryName : "" +/// event : "" +/// hotRoom : false +/// id : 0 +/// layoutKey : "" +/// nationalFlag : "" +/// roomAccount : "" +/// roomBadgeIcons : [""] +/// roomCover : "" +/// roomDesc : "" +/// roomGameIcon : "" +/// roomName : "" +/// sysOrigin : "" +/// userId : 0 +/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} +/// userSVipLevel : "" + +class SocialChatRoomRes { + SocialChatRoomRes({ + String? countryCode, + String? countryName, + String? event, + bool? hotRoom, + String? id, + String? layoutKey, + String? nationalFlag, + String? roomAccount, + List? roomBadgeIcons, + String? roomCover, + String? roomDesc, + String? roomGameIcon, + String? roomName, + String? sysOrigin, + String? userId, + SocialChatUserProfile? userProfile, + String? userSVipLevel, + ExtValues? extValues, + }) { + _countryCode = countryCode; + _countryName = countryName; + _event = event; + _hotRoom = hotRoom; + _id = id; + _layoutKey = layoutKey; + _nationalFlag = nationalFlag; + _roomAccount = roomAccount; + _roomBadgeIcons = roomBadgeIcons; + _roomCover = roomCover; + _roomDesc = roomDesc; + _roomGameIcon = roomGameIcon; + _roomName = roomName; + _sysOrigin = sysOrigin; + _userId = userId; + _userProfile = userProfile; + _userSVipLevel = userSVipLevel; + _extValues = extValues; + } + + SocialChatRoomRes.fromJson(dynamic json) { + _countryCode = json['countryCode']; + _countryName = json['countryName']; + _event = json['event']; + _hotRoom = json['hotRoom']; + _id = json['id']; + _layoutKey = json['layoutKey']; + _nationalFlag = json['nationalFlag']; + _roomAccount = json['roomAccount']; + _roomBadgeIcons = + json['roomBadgeIcons'] != null + ? (json['roomBadgeIcons'] as List) + .map((item) => item as String) + .toList() + : null; + _roomCover = json['roomCover']; + _roomDesc = json['roomDesc']; + _roomGameIcon = json['roomGameIcon']; + _roomName = json['roomName']; + _sysOrigin = json['sysOrigin']; + _userId = json['userId']; + _userProfile = + json['userProfile'] != null + ? SocialChatUserProfile.fromJson(json['userProfile']) + : null; + _userSVipLevel = json['userSVipLevel']; + _extValues = + json['extValues'] != null + ? ExtValues.fromJson(json['extValues']) + : null; + } + + String? _countryCode; + String? _countryName; + String? _event; + bool? _hotRoom; + String? _id; + String? _layoutKey; + String? _nationalFlag; + String? _roomAccount; + List? _roomBadgeIcons; + String? _roomCover; + String? _roomDesc; + String? _roomGameIcon; + String? _roomName; + String? _sysOrigin; + String? _userId; + SocialChatUserProfile? _userProfile; + String? _userSVipLevel; + ExtValues? _extValues; + + SocialChatRoomRes copyWith({ + String? countryCode, + String? countryName, + String? event, + bool? hotRoom, + String? id, + String? layoutKey, + String? nationalFlag, + String? roomAccount, + List? roomBadgeIcons, + String? roomCover, + String? roomDesc, + String? roomGameIcon, + String? roomName, + String? sysOrigin, + String? userId, + SocialChatUserProfile? userProfile, + String? userSVipLevel, + ExtValues? extValues, + }) => SocialChatRoomRes( + countryCode: countryCode ?? _countryCode, + countryName: countryName ?? _countryName, + event: event ?? _event, + hotRoom: hotRoom ?? _hotRoom, + id: id ?? _id, + layoutKey: layoutKey ?? _layoutKey, + nationalFlag: nationalFlag ?? _nationalFlag, + roomAccount: roomAccount ?? _roomAccount, + roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons, + roomCover: roomCover ?? _roomCover, + roomDesc: roomDesc ?? _roomDesc, + roomGameIcon: roomGameIcon ?? _roomGameIcon, + roomName: roomName ?? _roomName, + sysOrigin: sysOrigin ?? _sysOrigin, + userId: userId ?? _userId, + userProfile: userProfile ?? _userProfile, + userSVipLevel: userSVipLevel ?? _userSVipLevel, + extValues: extValues ?? _extValues, + ); + + String? get countryCode => _countryCode; + + String? get countryName => _countryName; + + String? get event => _event; + + bool? get hotRoom => _hotRoom; + + String? get id => _id; + + String? get layoutKey => _layoutKey; + + String? get nationalFlag => _nationalFlag; + + String? get roomAccount => _roomAccount; + + List? get roomBadgeIcons => _roomBadgeIcons; + + String? get roomCover => _roomCover; + + String? get roomDesc => _roomDesc; + + String? get roomGameIcon => _roomGameIcon; + + String? get roomName => _roomName; + + String? get sysOrigin => _sysOrigin; + + String? get userId => _userId; + + SocialChatUserProfile? get userProfile => _userProfile; + + String? get userSVipLevel => _userSVipLevel; + + ExtValues? get extValues => _extValues; + + Map toJson() { + final map = {}; + map['countryCode'] = _countryCode; + map['countryName'] = _countryName; + map['event'] = _event; + map['hotRoom'] = _hotRoom; + map['id'] = _id; + map['layoutKey'] = _layoutKey; + map['nationalFlag'] = _nationalFlag; + map['roomAccount'] = _roomAccount; + map['roomBadgeIcons'] = _roomBadgeIcons; + map['roomCover'] = _roomCover; + map['roomDesc'] = _roomDesc; + map['roomGameIcon'] = _roomGameIcon; + map['roomName'] = _roomName; + map['sysOrigin'] = _sysOrigin; + map['userId'] = _userId; + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + map['userSVipLevel'] = _userSVipLevel; + if (_extValues != null) { + map['extValues'] = _extValues?.toJson(); + } + return map; + } +} + +/// account : "" +/// accountStatus : "" +/// age : 0 +/// bornDay : 0 +/// bornMonth : 0 +/// bornYear : 0 +/// countryCode : "" +/// countryId : 0 +/// countryName : "" +/// createTime : 0 +/// del : false +/// freezingTime : 0 +/// id : 0 +/// originSys : "" +/// ownSpecialId : {"account":"","expiredTime":0} +/// sameRegion : false +/// sysOriginChild : "" +/// useProps : [{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}] +/// userAvatar : "" +/// userNickname : "" +/// userSex : 0 +/// wearBadge : [{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}] +/// animationUrl : "" +/// badgeKey : "" +/// badgeLevel : 0 +/// badgeName : "" +/// expireTime : 0 +/// id : 0 +/// milestone : 0 +/// notSelectUrl : "" +/// selectUrl : "" +/// type : "" +/// userId : 0 + +class WearBadge { + WearBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, + }) { + _animationUrl = animationUrl; + _badgeKey = badgeKey; + _badgeLevel = badgeLevel; + _badgeName = badgeName; + _expireTime = expireTime; + _id = id; + _milestone = milestone; + _notSelectUrl = notSelectUrl; + _selectUrl = selectUrl; + _type = type; + _userId = userId; + } + + WearBadge.fromJson(dynamic json) { + _animationUrl = json['animationUrl']; + _badgeKey = json['badgeKey']; + _badgeLevel = json['badgeLevel']; + _badgeName = json['badgeName']; + _expireTime = json['expireTime']; + _id = json['id']; + _milestone = json['milestone']; + _notSelectUrl = json['notSelectUrl']; + _selectUrl = json['selectUrl']; + _type = json['type']; + _userId = json['userId']; + } + + String? _animationUrl; + String? _badgeKey; + num? _badgeLevel; + String? _badgeName; + num? _expireTime; + String? _id; + String? _milestone; + String? _notSelectUrl; + String? _selectUrl; + String? _type; + String? _userId; + + WearBadge copyWith({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, + }) => WearBadge( + animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, + ); + + String? get animationUrl => _animationUrl; + + String? get badgeKey => _badgeKey; + + num? get badgeLevel => _badgeLevel; + + String? get badgeName => _badgeName; + + num? get expireTime => _expireTime; + + String? get id => _id; + + String? get milestone => _milestone; + + String? get notSelectUrl => _notSelectUrl; + + String? get selectUrl => _selectUrl; + + String? get type => _type; + + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['animationUrl'] = _animationUrl; + map['badgeKey'] = _badgeKey; + map['badgeLevel'] = _badgeLevel; + map['badgeName'] = _badgeName; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['milestone'] = _milestone; + map['notSelectUrl'] = _notSelectUrl; + map['selectUrl'] = _selectUrl; + map['type'] = _type; + map['userId'] = _userId; + return map; + } +} + +class ExtValues { + ExtValues({ + bool? existsPassword, + num? rocketLevel, + String? memberQuantity, + RoomSetting? roomSetting, + }) { + _existsPassword = existsPassword; + _rocketLevel = rocketLevel; + _memberQuantity = memberQuantity; + _roomSetting = roomSetting; + } + + ExtValues.fromJson(dynamic json) { + _existsPassword = json['existsPassword']; + _rocketLevel = json['rocketLevel']; + _memberQuantity = json['memberQuantity']; + _roomSetting = + json['roomSetting'] != null + ? RoomSetting.fromJson(json['roomSetting']) + : null; + } + + bool? _existsPassword; + num? _rocketLevel; + String? _memberQuantity; + RoomSetting? _roomSetting; + + ExtValues copyWith({ + bool? existsPassword, + num? rocketLevel, + String? memberQuantity, + RoomSetting? roomSetting, + }) => ExtValues( + existsPassword: existsPassword ?? _existsPassword, + rocketLevel: rocketLevel ?? _rocketLevel, + memberQuantity: memberQuantity ?? _memberQuantity, + roomSetting: roomSetting ?? _roomSetting, + ); + + bool? get existsPassword => _existsPassword; + + num? get rocketLevel => _rocketLevel; + + String? get memberQuantity => _memberQuantity; + + RoomSetting? get roomSetting => _roomSetting; + + Map toJson() { + final map = {}; + map['existsPassword'] = _existsPassword; + map['rocketLevel'] = _rocketLevel; + map['memberQuantity'] = _memberQuantity; + if (_roomSetting != null) { + map['roomSetting'] = _roomSetting?.toJson(); + } + return map; + } +} + +class RoomSetting { + RoomSetting({ + String? password, + String? takeMicRole, + bool? touristMike, + bool? touristMsg, + num? maxMember, + num? maxAdmin, + num? joinGolds, + num? mikeSize, + bool? allowMusic, + bool? adminLockSeat, + bool? showHeartbeat, + String? roomSpecialMikeType, + num? roomSpecialMikeExpireTime, + String? roomSpecialMikeExpireType,}){ + _password = password; + _takeMicRole = takeMicRole; + _touristMike = touristMike; + _touristMsg = touristMsg; + _maxMember = maxMember; + _maxAdmin = maxAdmin; + _joinGolds = joinGolds; + _mikeSize = mikeSize; + _allowMusic = allowMusic; + _adminLockSeat = adminLockSeat; + _showHeartbeat = showHeartbeat; + _roomSpecialMikeType = roomSpecialMikeType; + _roomSpecialMikeExpireTime = roomSpecialMikeExpireTime; + _roomSpecialMikeExpireType = roomSpecialMikeExpireType; + } + + RoomSetting.fromJson(dynamic json) { + _password = json['password']; + _takeMicRole = json['takeMicRole']; + _touristMike = json['touristMike']; + _touristMsg = json['touristMsg']; + _maxMember = json['maxMember']; + _maxAdmin = json['maxAdmin']; + _joinGolds = json['joinGolds']; + _mikeSize = json['mikeSize']; + _allowMusic = json['allowMusic']; + _adminLockSeat = json['adminLockSeat']; + _showHeartbeat = json['showHeartbeat']; + _roomSpecialMikeType = json['roomSpecialMikeType']; + _roomSpecialMikeExpireTime = json['roomSpecialMikeExpireTime']; + _roomSpecialMikeExpireType = json['roomSpecialMikeExpireType']; + } + String? _password; + String? _takeMicRole; + bool? _touristMike; + bool? _touristMsg; + num? _maxMember; + num? _maxAdmin; + num? _joinGolds; + num? _mikeSize; + bool? _allowMusic; + bool? _adminLockSeat; + bool? _showHeartbeat; + String? _roomSpecialMikeType; + num? _roomSpecialMikeExpireTime; + String? _roomSpecialMikeExpireType; + RoomSetting copyWith({ String? password, + String? takeMicRole, + bool? touristMike, + bool? touristMsg, + num? maxMember, + num? maxAdmin, + num? joinGolds, + num? mikeSize, + bool? allowMusic, + bool? adminLockSeat, + bool? showHeartbeat, + String? roomSpecialMikeType, + num? roomSpecialMikeExpireTime, + String? roomSpecialMikeExpireType, + }) => RoomSetting( password: password ?? _password, + takeMicRole: takeMicRole ?? _takeMicRole, + touristMike: touristMike ?? _touristMike, + touristMsg: touristMsg ?? _touristMsg, + maxMember: maxMember ?? _maxMember, + maxAdmin: maxAdmin ?? _maxAdmin, + joinGolds: joinGolds ?? _joinGolds, + mikeSize: mikeSize ?? _mikeSize, + allowMusic: allowMusic ?? _allowMusic, + adminLockSeat: adminLockSeat ?? _adminLockSeat, + showHeartbeat: showHeartbeat ?? _showHeartbeat, + roomSpecialMikeType: roomSpecialMikeType ?? _roomSpecialMikeType, + roomSpecialMikeExpireTime: roomSpecialMikeExpireTime ?? _roomSpecialMikeExpireTime, + roomSpecialMikeExpireType: roomSpecialMikeExpireType ?? _roomSpecialMikeExpireType, + ); + String? get password => _password; + String? get takeMicRole => _takeMicRole; + bool? get touristMike => _touristMike; + bool? get touristMsg => _touristMsg; + num? get maxMember => _maxMember; + num? get maxAdmin => _maxAdmin; + num? get joinGolds => _joinGolds; + num? get mikeSize => _mikeSize; + bool? get allowMusic => _allowMusic; + bool? get adminLockSeat => _adminLockSeat; + bool? get showHeartbeat => _showHeartbeat; + String? get roomSpecialMikeType => _roomSpecialMikeType; + num? get roomSpecialMikeExpireTime => _roomSpecialMikeExpireTime; + String? get roomSpecialMikeExpireType => _roomSpecialMikeExpireType; + + Map toJson() { + final map = {}; + map['password'] = _password; + map['takeMicRole'] = _takeMicRole; + map['touristMike'] = _touristMike; + map['touristMsg'] = _touristMsg; + map['maxMember'] = _maxMember; + map['maxAdmin'] = _maxAdmin; + map['joinGolds'] = _joinGolds; + map['mikeSize'] = _mikeSize; + map['allowMusic'] = _allowMusic; + map['adminLockSeat'] = _adminLockSeat; + map['showHeartbeat'] = _showHeartbeat; + map['roomSpecialMikeType'] = _roomSpecialMikeType; + map['roomSpecialMikeExpireTime'] = _roomSpecialMikeExpireTime; + map['roomSpecialMikeExpireType'] = _roomSpecialMikeExpireType; + return map; + } + +} +/// expireTime : 0 +/// propsResources : {"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""} +/// userId : 0 + +class UseProps { + UseProps({ + String? expireTime, + PropsResources? propsResources, + String? userId, + }) { + _expireTime = expireTime; + _propsResources = propsResources; + _userId = userId; + } + + UseProps.fromJson(dynamic json) { + _expireTime = json['expireTime']; + _propsResources = + json['propsResources'] != null + ? PropsResources.fromJson(json['propsResources']) + : null; + _userId = json['userId']; + } + + String? _expireTime; + PropsResources? _propsResources; + String? _userId; + + UseProps copyWith({ + String? expireTime, + PropsResources? propsResources, + String? userId, + }) => UseProps( + expireTime: expireTime ?? _expireTime, + propsResources: propsResources ?? _propsResources, + userId: userId ?? _userId, + ); + + String? get expireTime => _expireTime; + + PropsResources? get propsResources => _propsResources; + + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['expireTime'] = _expireTime; + if (_propsResources != null) { + map['propsResources'] = _propsResources?.toJson(); + } + map['userId'] = _userId; + return map; + } +} + +/// amount : 0.0 +/// code : "" +/// cover : "" +/// expand : "" +/// id : 0 +/// name : "" +/// sourceUrl : "" +/// type : "" + +class PropsResources { + PropsResources({ + num? amount, + String? code, + String? cover, + String? expand, + String? id, + String? name, + String? sourceUrl, + String? type, + }) { + _amount = amount; + _code = code; + _cover = cover; + _expand = expand; + _id = id; + _name = name; + _sourceUrl = sourceUrl; + _type = type; + } + + PropsResources.fromJson(dynamic json) { + _amount = json['amount']; + _code = json['code']; + _cover = json['cover']; + _expand = json['expand']; + _id = json['id']; + _name = json['name']; + _sourceUrl = json['sourceUrl']; + _type = json['type']; + } + + num? _amount; + String? _code; + String? _cover; + String? _expand; + String? _id; + String? _name; + String? _sourceUrl; + String? _type; + + PropsResources copyWith({ + num? amount, + String? code, + String? cover, + String? expand, + String? id, + String? name, + String? sourceUrl, + String? type, + }) => PropsResources( + amount: amount ?? _amount, + code: code ?? _code, + cover: cover ?? _cover, + expand: expand ?? _expand, + id: id ?? _id, + name: name ?? _name, + sourceUrl: sourceUrl ?? _sourceUrl, + type: type ?? _type, + ); + + num? get amount => _amount; + + String? get code => _code; + + String? get cover => _cover; + + String? get expand => _expand; + + String? get id => _id; + + String? get name => _name; + + String? get sourceUrl => _sourceUrl; + + String? get type => _type; + + Map toJson() { + final map = {}; + map['amount'] = _amount; + map['code'] = _code; + map['cover'] = _cover; + map['expand'] = _expand; + map['id'] = _id; + map['name'] = _name; + map['sourceUrl'] = _sourceUrl; + map['type'] = _type; + return map; + } +} + +/// account : "" +/// expiredTime : 0 + +class OwnSpecialId { + OwnSpecialId({String? account, num? expiredTime}) { + _account = account; + _expiredTime = expiredTime; + } + + OwnSpecialId.fromJson(dynamic json) { + _account = json['account']; + _expiredTime = json['expiredTime']; + } + + String? _account; + num? _expiredTime; + + OwnSpecialId copyWith({String? account, num? expiredTime}) => OwnSpecialId( + account: account ?? _account, + expiredTime: expiredTime ?? _expiredTime, + ); + + String? get account => _account; + + num? get expiredTime => _expiredTime; + + Map toJson() { + final map = {}; + map['account'] = _account; + map['expiredTime'] = _expiredTime; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/room_user_card_res.dart b/lib/shared/business_logic/models/res/room_user_card_res.dart new file mode 100644 index 0000000..3fe7811 --- /dev/null +++ b/lib/shared/business_logic/models/res/room_user_card_res.dart @@ -0,0 +1,406 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// agent : false +/// anchor : false +/// follow : false +/// friend : false +/// ktvIntegral : "" +/// roomRole : "" +/// supperVip : "" +/// useBadge : [{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}] +/// useProps : [{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}] +/// userLevel : {"charmLevel":0,"extValues":{"":{}},"wealthLevel":0} +/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} + +class RoomUserCardRes { + RoomUserCardRes({ + bool? agent, + bool? anchor, + bool? follow, + bool? friend, + String? ktvIntegral, + String? roomRole, + String? supperVip, + List? useBadge, + List? useProps, + UserLevel? userLevel, + SocialChatUserProfile? userProfile,}){ + _agent = agent; + _anchor = anchor; + _follow = follow; + _friend = friend; + _ktvIntegral = ktvIntegral; + _roomRole = roomRole; + _supperVip = supperVip; + _useBadge = useBadge; + _useProps = useProps; + _userLevel = userLevel; + _userProfile = userProfile; +} + + RoomUserCardRes.fromJson(dynamic json) { + _agent = json['agent']; + _anchor = json['anchor']; + _follow = json['follow']; + _friend = json['friend']; + _ktvIntegral = json['ktvIntegral']; + _roomRole = json['roomRole']; + _supperVip = json['supperVip']; + if (json['useBadge'] != null) { + _useBadge = []; + json['useBadge'].forEach((v) { + _useBadge?.add(UseBadge.fromJson(v)); + }); + } + if (json['useProps'] != null) { + _useProps = []; + json['useProps'].forEach((v) { + _useProps?.add(UseProps.fromJson(v)); + }); + } + _userLevel = json['userLevel'] != null ? UserLevel.fromJson(json['userLevel']) : null; + _userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null; + } + bool? _agent; + bool? _anchor; + bool? _follow; + bool? _friend; + String? _ktvIntegral; + String? _roomRole; + String? _supperVip; + List? _useBadge; + List? _useProps; + UserLevel? _userLevel; + SocialChatUserProfile? _userProfile; +RoomUserCardRes copyWith({ bool? agent, + bool? anchor, + bool? follow, + bool? friend, + String? ktvIntegral, + String? roomRole, + String? supperVip, + List? useBadge, + List? useProps, + UserLevel? userLevel, + SocialChatUserProfile? userProfile, +}) => RoomUserCardRes( agent: agent ?? _agent, + anchor: anchor ?? _anchor, + follow: follow ?? _follow, + friend: friend ?? _friend, + ktvIntegral: ktvIntegral ?? _ktvIntegral, + roomRole: roomRole ?? _roomRole, + supperVip: supperVip ?? _supperVip, + useBadge: useBadge ?? _useBadge, + useProps: useProps ?? _useProps, + userLevel: userLevel ?? _userLevel, + userProfile: userProfile ?? _userProfile, +); + bool? get agent => _agent; + bool? get anchor => _anchor; + bool? get follow => _follow; + bool? get friend => _friend; + String? get ktvIntegral => _ktvIntegral; + String? get roomRole => _roomRole; + String? get supperVip => _supperVip; + List? get useBadge => _useBadge; + List? get useProps => _useProps; + UserLevel? get userLevel => _userLevel; + SocialChatUserProfile? get userProfile => _userProfile; + + Map toJson() { + final map = {}; + map['agent'] = _agent; + map['anchor'] = _anchor; + map['follow'] = _follow; + map['friend'] = _friend; + map['ktvIntegral'] = _ktvIntegral; + map['roomRole'] = _roomRole; + map['supperVip'] = _supperVip; + if (_useBadge != null) { + map['useBadge'] = _useBadge?.map((v) => v.toJson()).toList(); + } + if (_useProps != null) { + map['useProps'] = _useProps?.map((v) => v.toJson()).toList(); + } + if (_userLevel != null) { + map['userLevel'] = _userLevel?.toJson(); + } + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + return map; + } + +} + +/// account : "" +/// accountStatus : "" +/// age : 0 +/// bornDay : 0 +/// bornMonth : 0 +/// bornYear : 0 +/// countryCode : "" +/// countryId : 0 +/// countryName : "" +/// createTime : 0 +/// del : false +/// freezingTime : 0 +/// id : "0" +/// originSys : "" +/// ownSpecialId : {"account":"","expiredTime":0} +/// sameRegion : false +/// sysOriginChild : "" +/// useProps : [{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}] +/// userAvatar : "" +/// userNickname : "" +/// userSex : 0 +/// wearBadge : [{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}] + +/// animationUrl : "" +/// badgeKey : "" +/// badgeLevel : 0 +/// badgeName : "" +/// expireTime : 0 +/// id : 0 +/// milestone : 0 +/// notSelectUrl : "" +/// selectUrl : "" +/// type : "" +/// userId : 0 + +class WearBadge { + WearBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId,}){ + _animationUrl = animationUrl; + _badgeKey = badgeKey; + _badgeLevel = badgeLevel; + _badgeName = badgeName; + _expireTime = expireTime; + _id = id; + _milestone = milestone; + _notSelectUrl = notSelectUrl; + _selectUrl = selectUrl; + _type = type; + _userId = userId; +} + + WearBadge.fromJson(dynamic json) { + _animationUrl = json['animationUrl']; + _badgeKey = json['badgeKey']; + _badgeLevel = json['badgeLevel']; + _badgeName = json['badgeName']; + _expireTime = json['expireTime']; + _id = json['id']; + _milestone = json['milestone']; + _notSelectUrl = json['notSelectUrl']; + _selectUrl = json['selectUrl']; + _type = json['type']; + _userId = json['userId']; + } + String? _animationUrl; + String? _badgeKey; + num? _badgeLevel; + String? _badgeName; + num? _expireTime; + num? _id; + num? _milestone; + String? _notSelectUrl; + String? _selectUrl; + String? _type; + num? _userId; +WearBadge copyWith({ String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + num? id, + num? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + num? userId, +}) => WearBadge( animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, +); + String? get animationUrl => _animationUrl; + String? get badgeKey => _badgeKey; + num? get badgeLevel => _badgeLevel; + String? get badgeName => _badgeName; + num? get expireTime => _expireTime; + num? get id => _id; + num? get milestone => _milestone; + String? get notSelectUrl => _notSelectUrl; + String? get selectUrl => _selectUrl; + String? get type => _type; + num? get userId => _userId; + + Map toJson() { + final map = {}; + map['animationUrl'] = _animationUrl; + map['badgeKey'] = _badgeKey; + map['badgeLevel'] = _badgeLevel; + map['badgeName'] = _badgeName; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['milestone'] = _milestone; + map['notSelectUrl'] = _notSelectUrl; + map['selectUrl'] = _selectUrl; + map['type'] = _type; + map['userId'] = _userId; + return map; + } + +} + +class UserLevel { + UserLevel({ + num? charmLevel, + num? wealthLevel,}){ + _charmLevel = charmLevel; + _wealthLevel = wealthLevel; +} + + UserLevel.fromJson(dynamic json) { + _charmLevel = json['charmLevel']; + _wealthLevel = json['wealthLevel']; + } + num? _charmLevel; + num? _wealthLevel; +UserLevel copyWith({ num? charmLevel, + num? wealthLevel, +}) => UserLevel( charmLevel: charmLevel ?? _charmLevel, + wealthLevel: wealthLevel ?? _wealthLevel, +); + num? get charmLevel => _charmLevel; + num? get wealthLevel => _wealthLevel; + + Map toJson() { + final map = {}; + map['charmLevel'] = _charmLevel; + map['wealthLevel'] = _wealthLevel; + return map; + } + +} + +class UseBadge { + UseBadge({ + String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId,}){ + _animationUrl = animationUrl; + _badgeKey = badgeKey; + _badgeLevel = badgeLevel; + _badgeName = badgeName; + _expireTime = expireTime; + _id = id; + _milestone = milestone; + _notSelectUrl = notSelectUrl; + _selectUrl = selectUrl; + _type = type; + _userId = userId; +} + + UseBadge.fromJson(dynamic json) { + _animationUrl = json['animationUrl']; + _badgeKey = json['badgeKey']; + _badgeLevel = json['badgeLevel']; + _badgeName = json['badgeName']; + _expireTime = json['expireTime']; + _id = json['id']; + _milestone = json['milestone']; + _notSelectUrl = json['notSelectUrl']; + _selectUrl = json['selectUrl']; + _type = json['type']; + _userId = json['userId']; + } + String? _animationUrl; + String? _badgeKey; + num? _badgeLevel; + String? _badgeName; + num? _expireTime; + String? _id; + String? _milestone; + String? _notSelectUrl; + String? _selectUrl; + String? _type; + String? _userId; +UseBadge copyWith({ String? animationUrl, + String? badgeKey, + num? badgeLevel, + String? badgeName, + num? expireTime, + String? id, + String? milestone, + String? notSelectUrl, + String? selectUrl, + String? type, + String? userId, +}) => UseBadge( animationUrl: animationUrl ?? _animationUrl, + badgeKey: badgeKey ?? _badgeKey, + badgeLevel: badgeLevel ?? _badgeLevel, + badgeName: badgeName ?? _badgeName, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + milestone: milestone ?? _milestone, + notSelectUrl: notSelectUrl ?? _notSelectUrl, + selectUrl: selectUrl ?? _selectUrl, + type: type ?? _type, + userId: userId ?? _userId, +); + String? get animationUrl => _animationUrl; + String? get badgeKey => _badgeKey; + num? get badgeLevel => _badgeLevel; + String? get badgeName => _badgeName; + num? get expireTime => _expireTime; + String? get id => _id; + String? get milestone => _milestone; + String? get notSelectUrl => _notSelectUrl; + String? get selectUrl => _selectUrl; + String? get type => _type; + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['animationUrl'] = _animationUrl; + map['badgeKey'] = _badgeKey; + map['badgeLevel'] = _badgeLevel; + map['badgeName'] = _badgeName; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['milestone'] = _milestone; + map['notSelectUrl'] = _notSelectUrl; + map['selectUrl'] = _selectUrl; + map['type'] = _type; + map['userId'] = _userId; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_banner_leaderboard_res.dart b/lib/shared/business_logic/models/res/sc_banner_leaderboard_res.dart new file mode 100644 index 0000000..b13e63a --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_banner_leaderboard_res.dart @@ -0,0 +1,281 @@ +/// giftsReceivedLeaderboard : {"daily":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}],"monthly":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}],"weekly":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}]} +/// giftsSendLeaderboard : {"daily":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}],"monthly":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}],"weekly":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}]} +/// roomGiftsLeaderboard : {"daily":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}],"monthly":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}],"weekly":[{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}]} + +class SCBannerLeaderboardRes { + SCBannerLeaderboardRes({ + GiftsReceivedLeaderboard? giftsReceivedLeaderboard, + GiftsSendLeaderboard? giftsSendLeaderboard, + RoomGiftsLeaderboard? roomGiftsLeaderboard,}){ + _giftsReceivedLeaderboard = giftsReceivedLeaderboard; + _giftsSendLeaderboard = giftsSendLeaderboard; + _roomGiftsLeaderboard = roomGiftsLeaderboard; +} + + SCBannerLeaderboardRes.fromJson(dynamic json) { + _giftsReceivedLeaderboard = json['giftsReceivedLeaderboard'] != null ? GiftsReceivedLeaderboard.fromJson(json['giftsReceivedLeaderboard']) : null; + _giftsSendLeaderboard = json['giftsSendLeaderboard'] != null ? GiftsSendLeaderboard.fromJson(json['giftsSendLeaderboard']) : null; + _roomGiftsLeaderboard = json['roomGiftsLeaderboard'] != null ? RoomGiftsLeaderboard.fromJson(json['roomGiftsLeaderboard']) : null; + } + GiftsReceivedLeaderboard? _giftsReceivedLeaderboard; + GiftsSendLeaderboard? _giftsSendLeaderboard; + RoomGiftsLeaderboard? _roomGiftsLeaderboard; + + GiftsReceivedLeaderboard? get giftsReceivedLeaderboard => _giftsReceivedLeaderboard; + GiftsSendLeaderboard? get giftsSendLeaderboard => _giftsSendLeaderboard; + RoomGiftsLeaderboard? get roomGiftsLeaderboard => _roomGiftsLeaderboard; + + Map toJson() { + final map = {}; + if (_giftsReceivedLeaderboard != null) { + map['giftsReceivedLeaderboard'] = _giftsReceivedLeaderboard?.toJson(); + } + if (_giftsSendLeaderboard != null) { + map['giftsSendLeaderboard'] = _giftsSendLeaderboard?.toJson(); + } + if (_roomGiftsLeaderboard != null) { + map['roomGiftsLeaderboard'] = _roomGiftsLeaderboard?.toJson(); + } + return map; + } + +} + +/// daily : [{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}] +/// monthly : [{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}] +/// weekly : [{"avatar":"","id":0,"nickname":"","quantity":0,"quantityFormat":""}] + +class RoomGiftsLeaderboard { + RoomGiftsLeaderboard({ + List? daily, + List? monthly, + List? weekly,}){ + _daily = daily; + _monthly = monthly; + _weekly = weekly; +} + + RoomGiftsLeaderboard.fromJson(dynamic json) { + if (json['daily'] != null) { + _daily = []; + json['daily'].forEach((v) { + _daily?.add(LeaderboardUser.fromJson(v)); + }); + } + if (json['monthly'] != null) { + _monthly = []; + json['monthly'].forEach((v) { + _monthly?.add(LeaderboardUser.fromJson(v)); + }); + } + if (json['weekly'] != null) { + _weekly = []; + json['weekly'].forEach((v) { + _weekly?.add(LeaderboardUser.fromJson(v)); + }); + } + } + List? _daily; + List? _monthly; + List? _weekly; + + List? get daily => _daily; + List? get monthly => _monthly; + List? get weekly => _weekly; + + Map toJson() { + final map = {}; + if (_daily != null) { + map['daily'] = _daily?.map((v) => v.toJson()).toList(); + } + if (_monthly != null) { + map['monthly'] = _monthly?.map((v) => v.toJson()).toList(); + } + if (_weekly != null) { + map['weekly'] = _weekly?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// avatar : "" +/// id : 0 +/// nickname : "" +/// quantity : 0 +/// quantityFormat : "" + +class LeaderboardUser { + LeaderboardUser({ + String? avatar, + String? id, + String? nickname, + String? quantity, + String? quantityFormat,}){ + _avatar = avatar; + _id = id; + _nickname = nickname; + _quantity = quantity; + _quantityFormat = quantityFormat; +} + + LeaderboardUser.fromJson(dynamic json) { + _avatar = json['avatar']; + _id = json['id']; + _nickname = json['nickname']; + _quantity = json['quantity']; + _quantityFormat = json['quantityFormat']; + } + String? _avatar; + String? _id; + String? _nickname; + String? _quantity; + String? _quantityFormat; + + String? get avatar => _avatar; + String? get id => _id; + String? get nickname => _nickname; + String? get quantity => _quantity; + String? get quantityFormat => _quantityFormat; + + Map toJson() { + final map = {}; + map['avatar'] = _avatar; + map['id'] = _id; + map['nickname'] = _nickname; + map['quantity'] = _quantity; + map['quantityFormat'] = _quantityFormat; + return map; + } + +} + +class GiftsSendLeaderboard { + GiftsSendLeaderboard({ + List? daily, + List? monthly, + List? weekly,}){ + _daily = daily; + _monthly = monthly; + _weekly = weekly; +} + + GiftsSendLeaderboard.fromJson(dynamic json) { + if (json['daily'] != null) { + _daily = []; + json['daily'].forEach((v) { + _daily?.add(LeaderboardUser.fromJson(v)); + }); + } + if (json['monthly'] != null) { + _monthly = []; + json['monthly'].forEach((v) { + _monthly?.add(LeaderboardUser.fromJson(v)); + }); + } + if (json['weekly'] != null) { + _weekly = []; + json['weekly'].forEach((v) { + _weekly?.add(LeaderboardUser.fromJson(v)); + }); + } + } + List? _daily; + List? _monthly; + List? _weekly; + + List? get daily => _daily; + List? get monthly => _monthly; + List? get weekly => _weekly; + + Map toJson() { + final map = {}; + if (_daily != null) { + map['daily'] = _daily?.map((v) => v.toJson()).toList(); + } + if (_monthly != null) { + map['monthly'] = _monthly?.map((v) => v.toJson()).toList(); + } + if (_weekly != null) { + map['weekly'] = _weekly?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// avatar : "" +/// id : 0 +/// nickname : "" +/// quantity : 0 +/// quantityFormat : "" + + +/// avatar : "" +/// id : 0 +/// nickname : "" +/// quantity : 0 +/// quantityFormat : "" + + +class GiftsReceivedLeaderboard { + GiftsReceivedLeaderboard({ + List? daily, + List? monthly, + List? weekly,}){ + _daily = daily; + _monthly = monthly; + _weekly = weekly; +} + + GiftsReceivedLeaderboard.fromJson(dynamic json) { + if (json['daily'] != null) { + _daily = []; + json['daily'].forEach((v) { + _daily?.add(LeaderboardUser.fromJson(v)); + }); + } + if (json['monthly'] != null) { + _monthly = []; + json['monthly'].forEach((v) { + _monthly?.add(LeaderboardUser.fromJson(v)); + }); + } + if (json['weekly'] != null) { + _weekly = []; + json['weekly'].forEach((v) { + _weekly?.add(LeaderboardUser.fromJson(v)); + }); + } + } + List? _daily; + List? _monthly; + List? _weekly; + + List? get daily => _daily; + List? get monthly => _monthly; + List? get weekly => _weekly; + + Map toJson() { + final map = {}; + if (_daily != null) { + map['daily'] = _daily?.map((v) => v.toJson()).toList(); + } + if (_monthly != null) { + map['monthly'] = _monthly?.map((v) => v.toJson()).toList(); + } + if (_weekly != null) { + map['weekly'] = _weekly?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// avatar : "" +/// id : 0 +/// nickname : "" +/// quantity : 0 +/// quantityFormat : "" + + diff --git a/lib/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart b/lib/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart new file mode 100644 index 0000000..6b8aa0d --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart @@ -0,0 +1,190 @@ +/// data : {"acceptNickname":"Sukabuliete","acceptUserId":"1957345312961527809","account":"8826602","avatarFrameCover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-33adbddc-b68c-407d-8cbb-3c7b9878b953.png","avatarFrameSvg":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-97edd7e3-0335-4521-8eb0-5a84b009d2a9.svga","balance":1611358.0,"giftCandy":1000.0,"giftCover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/gifts/manager-effb4f1c-872d-4abf-8797-9987bf206006.png","giftQuantity":1,"globalNews":true,"multiple":1,"multipleType":"WIN","nickname":"Sukabuliete","regionCode":"AF","roomAccount":"8826602","roomId":"1957345511792508929","sendUserId":"1957345312961527809","sysOrigin":"YUMI","userAvatar":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/avatar/0e031d7e-d269-44fe-98eb-543060d37f07.jpg"} +/// type : "GAME_LUCKY_GIFT" + +class SCBroadCastLuckGiftPush { + SCBroadCastLuckGiftPush({ + Data? data, + String? type,}){ + _data = data; + _type = type; +} + + SCBroadCastLuckGiftPush.fromJson(dynamic json) { + _data = json['data'] != null ? Data.fromJson(json['data']) : null; + _type = json['type']; + } + Data? _data; + String? _type; + Data? get data => _data; + String? get type => _type; + + Map toJson() { + final map = {}; + if (_data != null) { + map['data'] = _data?.toJson(); + } + map['type'] = _type; + return map; + } + +} + +/// acceptNickname : "Sukabuliete" +/// acceptUserId : "1957345312961527809" +/// account : "8826602" +/// avatarFrameCover : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-33adbddc-b68c-407d-8cbb-3c7b9878b953.png" +/// avatarFrameSvg : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-97edd7e3-0335-4521-8eb0-5a84b009d2a9.svga" +/// balance : 1611358.0 +/// giftCandy : 1000.0 +/// giftCover : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/gifts/manager-effb4f1c-872d-4abf-8797-9987bf206006.png" +/// giftQuantity : 1 +/// globalNews : true +/// multiple : 倍率 +/// multipleType : "WIN" +/// nickname : "Sukabuliete" +/// regionCode : "AF" +/// roomAccount : "8826602" +/// roomId : "1957345511792508929" +/// sendUserId : "1957345312961527809" +/// sysOrigin : "YUMI" +/// userAvatar : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/avatar/0e031d7e-d269-44fe-98eb-543060d37f07.jpg" + +class Data { + Data({ + String? acceptNickname, + String? acceptUserId, + String? account, + String? avatarFrameCover, + String? avatarFrameSvg, + num? balance, + num? awardAmount, + num? giftCandy, + String? giftCover, + num? giftQuantity, + bool? globalNews, + num? multiple, + String? multipleType, + String? nickname, + String? regionCode, + String? roomAccount, + String? roomId, + String? giftId, + String? sendUserId, + String? sysOrigin, + String? userAvatar,}){ + _acceptNickname = acceptNickname; + _acceptUserId = acceptUserId; + _account = account; + _avatarFrameCover = avatarFrameCover; + _avatarFrameSvg = avatarFrameSvg; + _balance = balance; + _awardAmount = awardAmount; + _giftCandy = giftCandy; + _giftCover = giftCover; + _giftQuantity = giftQuantity; + _globalNews = globalNews; + _multiple = multiple; + _multipleType = multipleType; + _nickname = nickname; + _regionCode = regionCode; + _roomAccount = roomAccount; + _roomId = roomId; + _sendUserId = sendUserId; + _giftId = giftId; + _sysOrigin = sysOrigin; + _userAvatar = userAvatar; +} + + Data.fromJson(dynamic json) { + _acceptNickname = json['acceptNickname']; + _acceptUserId = json['acceptUserId']; + _account = json['account']; + _avatarFrameCover = json['avatarFrameCover']; + _avatarFrameSvg = json['avatarFrameSvg']; + _balance = json['balance']; + _awardAmount = json['awardAmount']; + _giftCandy = json['giftCandy']; + _giftCover = json['giftCover']; + _giftQuantity = json['giftQuantity']; + _globalNews = json['globalNews']; + _multiple = json['multiple']; + _multipleType = json['multipleType']; + _nickname = json['nickname']; + _regionCode = json['regionCode']; + _roomAccount = json['roomAccount']; + _roomId = json['roomId']; + _sendUserId = json['sendUserId']; + _sysOrigin = json['sysOrigin']; + _userAvatar = json['userAvatar']; + _giftId = json['giftId']; + } + String? _acceptNickname; + String? _acceptUserId; + String? _account; + String? _avatarFrameCover; + String? _avatarFrameSvg; + num? _balance; + num? _awardAmount; + num? _giftCandy; + String? _giftCover; + num? _giftQuantity; + bool? _globalNews; + num? _multiple; + String? _multipleType; + String? _nickname; + String? _regionCode; + String? _roomAccount; + String? _roomId; + String? _sendUserId; + String? _giftId; + String? _sysOrigin; + String? _userAvatar; + String? get acceptNickname => _acceptNickname; + String? get acceptUserId => _acceptUserId; + String? get account => _account; + String? get avatarFrameCover => _avatarFrameCover; + String? get avatarFrameSvg => _avatarFrameSvg; + num? get balance => _balance; + num? get awardAmount => _awardAmount; + num? get giftCandy => _giftCandy; + String? get giftCover => _giftCover; + String? get giftId => _giftId; + num? get giftQuantity => _giftQuantity; + bool? get globalNews => _globalNews; + num? get multiple => _multiple; + String? get multipleType => _multipleType; + String? get nickname => _nickname; + String? get regionCode => _regionCode; + String? get roomAccount => _roomAccount; + String? get roomId => _roomId; + String? get sendUserId => _sendUserId; + String? get sysOrigin => _sysOrigin; + String? get userAvatar => _userAvatar; + + Map toJson() { + final map = {}; + map['acceptNickname'] = _acceptNickname; + map['acceptUserId'] = _acceptUserId; + map['account'] = _account; + map['avatarFrameCover'] = _avatarFrameCover; + map['avatarFrameSvg'] = _avatarFrameSvg; + map['balance'] = _balance; + map['awardAmount'] = _awardAmount; + map['giftCandy'] = _giftCandy; + map['giftCover'] = _giftCover; + map['giftQuantity'] = _giftQuantity; + map['globalNews'] = _globalNews; + map['multiple'] = _multiple; + map['multipleType'] = _multipleType; + map['nickname'] = _nickname; + map['regionCode'] = _regionCode; + map['roomAccount'] = _roomAccount; + map['roomId'] = _roomId; + map['sendUserId'] = _sendUserId; + map['sysOrigin'] = _sysOrigin; + map['userAvatar'] = _userAvatar; + map['giftId'] = _giftId; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_comment_list_res.dart b/lib/shared/business_logic/models/res/sc_comment_list_res.dart new file mode 100644 index 0000000..e264d6e --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_comment_list_res.dart @@ -0,0 +1,218 @@ +/// records : [{"id":"","dynamicId":"","userAvatar":"","userNickname":"","userSex":0,"userId":"","toUserId":"","toUserNickname":"","content":"","likeStrQuantity":"","createTime":0,"like":false,"rootCommentId":0}] +/// total : 0 +/// size : 0 +/// current : 0 + +class SCCommentListRes { + SCCommentListRes({ + List? records, + num? total, + num? size, + num? current, + }) { + _records = records; + _total = total; + _size = size; + _current = current; + } + + SCCommentListRes.fromJson(dynamic json) { + if (json['records'] != null) { + _records = []; + json['records'].forEach((v) { + _records?.add(CommentListRecords.fromJson(v)); + }); + } + _total = json['total']; + _size = json['size']; + _current = json['current']; + } + + List? _records; + num? _total; + num? _size; + num? _current; + + List? get records => _records; + + num? get total => _total; + + num? get size => _size; + + num? get current => _current; + + Map toJson() { + final map = {}; + if (_records != null) { + map['records'] = _records?.map((v) => v.toJson()).toList(); + } + map['total'] = _total; + map['size'] = _size; + map['current'] = _current; + return map; + } +} + +/// id : "" +/// dynamicId : "" +/// userAvatar : "" +/// userNickname : "" +/// userSex : 0 +/// userId : "" +/// toUserId : "" +/// toUserNickname : "" +/// content : "" +/// likeStrQuantity : "" +/// createTime : 0 +/// like : false +/// rootCommentId : 0 + +class CommentListRecords { + CommentListRecords({ + String? id, + String? dynamicId, + String? userAvatar, + String? userNickname, + num? userSex, + String? userId, + String? toUserId, + String? toUserNickname, + String? content, + String? likeStrQuantity, + String? commentStrQuantity, + int? createTime, + bool? like, + num? rootCommentId, + num? replyCommentId, + num? childCommentCount, + }) { + _id = id; + _dynamicId = dynamicId; + _userAvatar = userAvatar; + _userNickname = userNickname; + _userSex = userSex; + _userId = userId; + _toUserId = toUserId; + _toUserNickname = toUserNickname; + _content = content; + _likeStrQuantity = likeStrQuantity; + _commentStrQuantity = commentStrQuantity; + _createTime = createTime; + _like = like; + _rootCommentId = rootCommentId; + _replyCommentId = replyCommentId; + _childCommentCount = childCommentCount; + } + + CommentListRecords.fromJson(dynamic json) { + _id = json['id']; + _dynamicId = json['dynamicId']; + _userAvatar = json['userAvatar']; + _userNickname = json['userNickname']; + _userSex = json['userSex']; + _userId = json['userId']; + _toUserId = json['toUserId']; + _toUserNickname = json['toUserNickname']; + _content = json['content']; + _likeStrQuantity = json['likeStrQuantity']; + _commentStrQuantity = json['commentStrQuantity']; + _createTime = json['createTime']; + _like = json['like']; + _rootCommentId = json['rootCommentId']; + _replyCommentId = json['replyCommentId']; + _childCommentCount = json['childCommentCount']; + } + + String? _id; + String? _dynamicId; + String? _userAvatar; + String? _userNickname; + num? _userSex; + String? _userId; + String? _toUserId; + String? _toUserNickname; + String? _content; + String? _likeStrQuantity; + String? _commentStrQuantity; + int? _createTime; + bool? _like; + num? _rootCommentId; + num? _replyCommentId; + num? _childCommentCount; + + String? get id => _id; + + String? get dynamicId => _dynamicId; + + String? get userAvatar => _userAvatar; + + String? get userNickname => _userNickname; + + num? get userSex => _userSex; + + String? get userId => _userId; + + String? get toUserId => _toUserId; + + String? get toUserNickname => _toUserNickname; + + String? get content => _content; + + String? get likeStrQuantity => _likeStrQuantity; + + String? get commentStrQuantity => _commentStrQuantity; + + int? get createTime => _createTime; + + bool? get like => _like; + + num? get rootCommentId => _rootCommentId; + + num? get replyCommentId => _replyCommentId; + + num? get childCommentCount => _childCommentCount; + + setChildCommentCount(num count) { + _childCommentCount = count; + } + + setLike(bool like) { + _like = like; + } + + setLikeStrQuantity(String likeStrQuantity) { + _likeStrQuantity = likeStrQuantity; + } + + setUserNickname(String name) { + _userNickname = name; + } + + setUserAvatar(String avatar) { + _userAvatar = avatar; + } + + setUserSex(num sex) { + _userSex = sex; + } + + Map toJson() { + final map = {}; + map['id'] = _id; + map['dynamicId'] = _dynamicId; + map['userAvatar'] = _userAvatar; + map['userNickname'] = _userNickname; + map['userSex'] = _userSex; + map['userId'] = _userId; + map['toUserId'] = _toUserId; + map['toUserNickname'] = _toUserNickname; + map['content'] = _content; + map['likeStrQuantity'] = _likeStrQuantity; + map['commentStrQuantity'] = _commentStrQuantity; + map['createTime'] = _createTime; + map['like'] = _like; + map['rootCommentId'] = _rootCommentId; + map['childCommentCount'] = _childCommentCount; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart b/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart new file mode 100644 index 0000000..534afa0 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart @@ -0,0 +1,159 @@ +/// id : 0 +/// roomAccount : "" +/// userId : 0 +/// roomCover : "" +/// roomName : "" +/// roomDesc : "" +/// event : "" +/// sysOrigin : "" +/// countryCode : "" +/// countryName : "" +/// nationalFlag : "" +/// langCode : "" +/// del : false +/// createTime : 0 +/// updateTime : 0 +/// activeTime : 0 + +class SCEditRoomInfoRes { + SCEditRoomInfoRes({ + String? id, + String? roomAccount, + String? userId, + String? roomCover, + String? roomName, + String? roomDesc, + String? event, + String? sysOrigin, + String? countryCode, + String? countryName, + String? nationalFlag, + String? langCode, + bool? del, + num? createTime, + num? updateTime, + num? activeTime,}){ + _id = id; + _roomAccount = roomAccount; + _userId = userId; + _roomCover = roomCover; + _roomName = roomName; + _roomDesc = roomDesc; + _event = event; + _sysOrigin = sysOrigin; + _countryCode = countryCode; + _countryName = countryName; + _nationalFlag = nationalFlag; + _langCode = langCode; + _del = del; + _createTime = createTime; + _updateTime = updateTime; + _activeTime = activeTime; +} + + SCEditRoomInfoRes.fromJson(dynamic json) { + _id = json['id']; + _roomAccount = json['roomAccount']; + _userId = json['userId']; + _roomCover = json['roomCover']; + _roomName = json['roomName']; + _roomDesc = json['roomDesc']; + _event = json['event']; + _sysOrigin = json['sysOrigin']; + _countryCode = json['countryCode']; + _countryName = json['countryName']; + _nationalFlag = json['nationalFlag']; + _langCode = json['langCode']; + _del = json['del']; + _createTime = json['createTime']; + _updateTime = json['updateTime']; + _activeTime = json['activeTime']; + } + String? _id; + String? _roomAccount; + String? _userId; + String? _roomCover; + String? _roomName; + String? _roomDesc; + String? _event; + String? _sysOrigin; + String? _countryCode; + String? _countryName; + String? _nationalFlag; + String? _langCode; + bool? _del; + num? _createTime; + num? _updateTime; + num? _activeTime; +SCEditRoomInfoRes copyWith({ String? id, + String? roomAccount, + String? userId, + String? roomCover, + String? roomName, + String? roomDesc, + String? event, + String? sysOrigin, + String? countryCode, + String? countryName, + String? nationalFlag, + String? langCode, + bool? del, + num? createTime, + num? updateTime, + num? activeTime, +}) => SCEditRoomInfoRes( id: id ?? _id, + roomAccount: roomAccount ?? _roomAccount, + userId: userId ?? _userId, + roomCover: roomCover ?? _roomCover, + roomName: roomName ?? _roomName, + roomDesc: roomDesc ?? _roomDesc, + event: event ?? _event, + sysOrigin: sysOrigin ?? _sysOrigin, + countryCode: countryCode ?? _countryCode, + countryName: countryName ?? _countryName, + nationalFlag: nationalFlag ?? _nationalFlag, + langCode: langCode ?? _langCode, + del: del ?? _del, + createTime: createTime ?? _createTime, + updateTime: updateTime ?? _updateTime, + activeTime: activeTime ?? _activeTime, +); + String? get id => _id; + String? get roomAccount => _roomAccount; + String? get userId => _userId; + String? get roomCover => _roomCover; + String? get roomName => _roomName; + String? get roomDesc => _roomDesc; + String? get event => _event; + String? get sysOrigin => _sysOrigin; + String? get countryCode => _countryCode; + String? get countryName => _countryName; + String? get nationalFlag => _nationalFlag; + String? get langCode => _langCode; + bool? get del => _del; + num? get createTime => _createTime; + num? get updateTime => _updateTime; + num? get activeTime => _activeTime; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['roomAccount'] = _roomAccount; + map['userId'] = _userId; + map['roomCover'] = _roomCover; + map['roomName'] = _roomName; + map['roomDesc'] = _roomDesc; + map['event'] = _event; + map['sysOrigin'] = _sysOrigin; + map['countryCode'] = _countryCode; + map['countryName'] = _countryName; + map['nationalFlag'] = _nationalFlag; + map['langCode'] = _langCode; + map['del'] = _del; + map['createTime'] = _createTime; + map['updateTime'] = _updateTime; + map['activeTime'] = _activeTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_famaily_leader_board_res.dart b/lib/shared/business_logic/models/res/sc_famaily_leader_board_res.dart new file mode 100644 index 0000000..53508af --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_famaily_leader_board_res.dart @@ -0,0 +1,172 @@ +/// hostWeekRank : [{"userId":"","nickname":"","avatar":"","exp":"","isHost":false}] +/// supporterWeekRank : [{"userId":"","nickname":"","avatar":"","exp":"","isHost":false}] + +class SCFamailyLeaderBoardRes { + SCFamailyLeaderBoardRes({ + List? hostWeekRank, + HostWeekRank? myRankInfo, + List? supporterWeekRank,}){ + _hostWeekRank = hostWeekRank; + _supporterWeekRank = supporterWeekRank; + _myRankInfo = myRankInfo; +} + + SCFamailyLeaderBoardRes.fromJson(dynamic json) { + if (json['hostWeekRank'] != null) { + _hostWeekRank = []; + json['hostWeekRank'].forEach((v) { + _hostWeekRank?.add(HostWeekRank.fromJson(v)); + }); + } + if (json['supporterWeekRank'] != null) { + _supporterWeekRank = []; + json['supporterWeekRank'].forEach((v) { + _supporterWeekRank?.add(SupporterWeekRank.fromJson(v)); + }); + } + _myRankInfo = json['myRankInfo'] != null ? HostWeekRank.fromJson(json['myRankInfo']) : null; + } + List? _hostWeekRank; + HostWeekRank? _myRankInfo; + List? _supporterWeekRank; + + List? get hostWeekRank => _hostWeekRank; + HostWeekRank? get myRankInfo => _myRankInfo; + List? get supporterWeekRank => _supporterWeekRank; + + Map toJson() { + final map = {}; + if (_hostWeekRank != null) { + map['hostWeekRank'] = _hostWeekRank?.map((v) => v.toJson()).toList(); + } + if (_supporterWeekRank != null) { + map['supporterWeekRank'] = _supporterWeekRank?.map((v) => v.toJson()).toList(); + } + + if (_myRankInfo != null) { + map['myRankInfo'] = _myRankInfo?.toJson(); + } + return map; + } + +} + +/// userId : "" +/// nickname : "" +/// avatar : "" +/// exp : "" +/// isHost : false + +class SupporterWeekRank { + SupporterWeekRank({ + String? userId, + String? account, + String? nickname, + String? avatar, + String? exp, + bool? isHost,}){ + _userId = userId; + _account = account; + _nickname = nickname; + _avatar = avatar; + _exp = exp; + _isHost = isHost; +} + + SupporterWeekRank.fromJson(dynamic json) { + _userId = json['userId']; + _account = json['account']; + _nickname = json['nickname']; + _avatar = json['avatar']; + _exp = json['exp']; + _isHost = json['isHost']; + } + String? _userId; + String? _account; + String? _nickname; + String? _avatar; + String? _exp; + bool? _isHost; + + String? get userId => _userId; + String? get nickname => _nickname; + String? get account => _account; + String? get avatar => _avatar; + String? get exp => _exp; + bool? get isHost => _isHost; + + Map toJson() { + final map = {}; + map['userId'] = _userId; + map['account'] = _account; + map['nickname'] = _nickname; + map['avatar'] = _avatar; + map['exp'] = _exp; + map['isHost'] = _isHost; + return map; + } + +} + +/// userId : "" +/// nickname : "" +/// avatar : "" +/// exp : "" +/// isHost : false + +class HostWeekRank { + HostWeekRank({ + String? userId, + String? account, + String? nickname, + String? avatar, + String? exp, + String? rank, + bool? isHost,}){ + _userId = userId; + _account = account; + _nickname = nickname; + _avatar = avatar; + _exp = exp; + _rank = rank; + _isHost = isHost; +} + + HostWeekRank.fromJson(dynamic json) { + _userId = json['userId']; + _account = json['account']; + _nickname = json['nickname']; + _avatar = json['avatar']; + _exp = json['exp']; + _rank = json['rank']; + _isHost = json['isHost']; + } + String? _userId; + String? _account; + String? _nickname; + String? _avatar; + String? _exp; + String? _rank; + bool? _isHost; + + String? get userId => _userId; + String? get account => _account; + String? get nickname => _nickname; + String? get avatar => _avatar; + String? get exp => _exp; + String? get rank => _rank; + bool? get isHost => _isHost; + + Map toJson() { + final map = {}; + map['userId'] = _userId; + map['account'] = _account; + map['nickname'] = _nickname; + map['avatar'] = _avatar; + map['exp'] = _exp; + map['rank'] = _rank; + map['isHost'] = _isHost; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_gold_record_res.dart b/lib/shared/business_logic/models/res/sc_gold_record_res.dart new file mode 100644 index 0000000..c60d011 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_gold_record_res.dart @@ -0,0 +1,69 @@ +/// id : 0 +/// title : "" +/// type : 0 +/// quantity : 0.0 +/// createTime : "" + +class SCGoldRecordRes { + SCGoldRecordRes({ + String? id, + String? title, + num? type, + num? quantity, + int? createTime, + }) { + _id = id; + _title = title; + _type = type; + _quantity = quantity; + _createTime = createTime; + } + + SCGoldRecordRes.fromJson(dynamic json) { + _id = json['id']; + _title = json['title']; + _type = json['type']; + _quantity = json['quantity']; + _createTime = json['createTime']; + } + + String? _id; + String? _title; + num? _type; + num? _quantity; + int? _createTime; + + SCGoldRecordRes copyWith({ + String? id, + String? title, + num? type, + num? quantity, + int? createTime, + }) => SCGoldRecordRes( + id: id ?? _id, + title: title ?? _title, + type: type ?? _type, + quantity: quantity ?? _quantity, + createTime: createTime ?? _createTime, + ); + + String? get id => _id; + + String? get title => _title; + + num? get type => _type; + + num? get quantity => _quantity; + + int? get createTime => _createTime; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['title'] = _title; + map['type'] = _type; + map['quantity'] = _quantity; + map['createTime'] = _createTime; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_google_pay_res.dart b/lib/shared/business_logic/models/res/sc_google_pay_res.dart new file mode 100644 index 0000000..7e086fb --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_google_pay_res.dart @@ -0,0 +1,51 @@ +/// productId : "" +/// productGroup : "" +/// obtainAmount : 0.0 +/// balanceAmount : 0.0 + +class SCGooglePayRes { + SCGooglePayRes({ + String? productId, + String? productGroup, + num? obtainAmount, + num? balanceAmount,}){ + _productId = productId; + _productGroup = productGroup; + _obtainAmount = obtainAmount; + _balanceAmount = balanceAmount; +} + + SCGooglePayRes.fromJson(dynamic json) { + _productId = json['productId']; + _productGroup = json['productGroup']; + _obtainAmount = json['obtainAmount']; + _balanceAmount = json['balanceAmount']; + } + String? _productId; + String? _productGroup; + num? _obtainAmount; + num? _balanceAmount; +SCGooglePayRes copyWith({ String? productId, + String? productGroup, + num? obtainAmount, + num? balanceAmount, +}) => SCGooglePayRes( productId: productId ?? _productId, + productGroup: productGroup ?? _productGroup, + obtainAmount: obtainAmount ?? _obtainAmount, + balanceAmount: balanceAmount ?? _balanceAmount, +); + String? get productId => _productId; + String? get productGroup => _productGroup; + num? get obtainAmount => _obtainAmount; + num? get balanceAmount => _balanceAmount; + + Map toJson() { + final map = {}; + map['productId'] = _productId; + map['productGroup'] = _productGroup; + map['obtainAmount'] = _obtainAmount; + map['balanceAmount'] = _balanceAmount; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_index_banner_res.dart b/lib/shared/business_logic/models/res/sc_index_banner_res.dart new file mode 100644 index 0000000..c8123c4 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_index_banner_res.dart @@ -0,0 +1,105 @@ +/// alertCover : "" +/// content : "" +/// cover : "" +/// displayPosition : "" +/// expiredTime : 0 +/// id : 0 +/// params : "" +/// smallCover : "" +/// sysOrigin : "" +/// type : "" + +class SCIndexBannerRes { + SCIndexBannerRes({ + String? alertCover, + String? content, + String? cover, + String? displayPosition, + num? expiredTime, + String? id, + String? params, + String? smallCover, + String? sysOrigin, + String? type,}){ + _alertCover = alertCover; + _content = content; + _cover = cover; + _displayPosition = displayPosition; + _expiredTime = expiredTime; + _id = id; + _params = params; + _smallCover = smallCover; + _sysOrigin = sysOrigin; + _type = type; +} + + SCIndexBannerRes.fromJson(dynamic json) { + _alertCover = json['alertCover']; + _content = json['content']; + _cover = json['cover']; + _displayPosition = json['displayPosition']; + _expiredTime = json['expiredTime']; + _id = json['id']; + _params = json['params']; + _smallCover = json['smallCover']; + _sysOrigin = json['sysOrigin']; + _type = json['type']; + } + String? _alertCover; + String? _content; + String? _cover; + String? _displayPosition; + num? _expiredTime; + String? _id; + String? _params; + String? _smallCover; + String? _sysOrigin; + String? _type; +SCIndexBannerRes copyWith({ String? alertCover, + String? content, + String? cover, + String? displayPosition, + num? expiredTime, + String? id, + String? params, + String? smallCover, + String? sysOrigin, + String? type, +}) => SCIndexBannerRes( alertCover: alertCover ?? _alertCover, + content: content ?? _content, + cover: cover ?? _cover, + displayPosition: displayPosition ?? _displayPosition, + expiredTime: expiredTime ?? _expiredTime, + id: id ?? _id, + params: params ?? _params, + smallCover: smallCover ?? _smallCover, + sysOrigin: sysOrigin ?? _sysOrigin, + type: type ?? _type, +); + String? get alertCover => _alertCover; + String? get content => _content; + String? get cover => _cover; + String? get displayPosition => _displayPosition; + num? get expiredTime => _expiredTime; + String? get id => _id; + String? get params => _params; + String? get smallCover => _smallCover; + String? get sysOrigin => _sysOrigin; + String? get type => _type; + + Map toJson() { + final map = {}; + map['alertCover'] = _alertCover; + map['content'] = _content; + map['cover'] = _cover; + map['displayPosition'] = _displayPosition; + map['expiredTime'] = _expiredTime; + map['id'] = _id; + map['params'] = _params; + map['smallCover'] = _smallCover; + map['sysOrigin'] = _sysOrigin; + map['type'] = _type; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_is_follow_room_res.dart b/lib/shared/business_logic/models/res/sc_is_follow_room_res.dart new file mode 100644 index 0000000..7cb2f6a --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_is_follow_room_res.dart @@ -0,0 +1,33 @@ +/// joinRoom : false +/// followRoom : false + +class SCIsFollowRoomRes { + SCIsFollowRoomRes({ + bool? joinRoom, + bool? followRoom,}){ + _joinRoom = joinRoom; + _followRoom = followRoom; +} + + SCIsFollowRoomRes.fromJson(dynamic json) { + _joinRoom = json['joinRoom']; + _followRoom = json['followRoom']; + } + bool? _joinRoom; + bool? _followRoom; +SCIsFollowRoomRes copyWith({ bool? joinRoom, + bool? followRoom, +}) => SCIsFollowRoomRes( joinRoom: joinRoom ?? _joinRoom, + followRoom: followRoom ?? _followRoom, +); + bool? get joinRoom => _joinRoom; + bool? get followRoom => _followRoom; + + Map toJson() { + final map = {}; + map['joinRoom'] = _joinRoom; + map['followRoom'] = _followRoom; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_level_config_res.dart b/lib/shared/business_logic/models/res/sc_level_config_res.dart new file mode 100644 index 0000000..9e1ef23 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_level_config_res.dart @@ -0,0 +1,180 @@ +/// charm : {"backCover":"","resources":[{"levelIcon":"","levelRange":"","levelSpecial":""}],"ribbon":""} +/// wealth : {"backCover":"","resources":[{"levelIcon":"","levelRange":"","levelSpecial":""}],"ribbon":""} + +class SCLevelConfigRes { + SCLevelConfigRes({ + Charm? charm, + Wealth? wealth,}){ + _charm = charm; + _wealth = wealth; +} + + SCLevelConfigRes.fromJson(dynamic json) { + _charm = json['charm'] != null ? Charm.fromJson(json['charm']) : null; + _wealth = json['wealth'] != null ? Wealth.fromJson(json['wealth']) : null; + } + Charm? _charm; + Wealth? _wealth; +SCLevelConfigRes copyWith({ Charm? charm, + Wealth? wealth, +}) => SCLevelConfigRes( charm: charm ?? _charm, + wealth: wealth ?? _wealth, +); + Charm? get charm => _charm; + Wealth? get wealth => _wealth; + + Map toJson() { + final map = {}; + if (_charm != null) { + map['charm'] = _charm?.toJson(); + } + if (_wealth != null) { + map['wealth'] = _wealth?.toJson(); + } + return map; + } + +} + +/// backCover : "" +/// resources : [{"levelIcon":"","levelRange":"","levelSpecial":""}] +/// ribbon : "" + +class Wealth { + Wealth({ + String? backCover, + List? resources, + String? ribbon,}){ + _backCover = backCover; + _resources = resources; + _ribbon = ribbon; +} + + Wealth.fromJson(dynamic json) { + _backCover = json['backCover']; + if (json['resources'] != null) { + _resources = []; + json['resources'].forEach((v) { + _resources?.add(Resources.fromJson(v)); + }); + } + _ribbon = json['ribbon']; + } + String? _backCover; + List? _resources; + String? _ribbon; +Wealth copyWith({ String? backCover, + List? resources, + String? ribbon, +}) => Wealth( backCover: backCover ?? _backCover, + resources: resources ?? _resources, + ribbon: ribbon ?? _ribbon, +); + String? get backCover => _backCover; + List? get resources => _resources; + String? get ribbon => _ribbon; + + Map toJson() { + final map = {}; + map['backCover'] = _backCover; + if (_resources != null) { + map['resources'] = _resources?.map((v) => v.toJson()).toList(); + } + map['ribbon'] = _ribbon; + return map; + } + +} + +/// levelIcon : "" +/// levelRange : "" +/// levelSpecial : "" + +class Resources { + Resources({ + String? levelIcon, + String? levelRange, + String? levelSpecial,}){ + _levelIcon = levelIcon; + _levelRange = levelRange; + _levelSpecial = levelSpecial; +} + + Resources.fromJson(dynamic json) { + _levelIcon = json['levelIcon']; + _levelRange = json['levelRange']; + _levelSpecial = json['levelSpecial']; + } + String? _levelIcon; + String? _levelRange; + String? _levelSpecial; +Resources copyWith({ String? levelIcon, + String? levelRange, + String? levelSpecial, +}) => Resources( levelIcon: levelIcon ?? _levelIcon, + levelRange: levelRange ?? _levelRange, + levelSpecial: levelSpecial ?? _levelSpecial, +); + String? get levelIcon => _levelIcon; + String? get levelRange => _levelRange; + String? get levelSpecial => _levelSpecial; + + Map toJson() { + final map = {}; + map['levelIcon'] = _levelIcon; + map['levelRange'] = _levelRange; + map['levelSpecial'] = _levelSpecial; + return map; + } + +} + +/// backCover : "" +/// resources : [{"levelIcon":"","levelRange":"","levelSpecial":""}] +/// ribbon : "" + +class Charm { + Charm({ + String? backCover, + List? resources, + String? ribbon,}){ + _backCover = backCover; + _resources = resources; + _ribbon = ribbon; +} + + Charm.fromJson(dynamic json) { + _backCover = json['backCover']; + if (json['resources'] != null) { + _resources = []; + json['resources'].forEach((v) { + _resources?.add(Resources.fromJson(v)); + }); + } + _ribbon = json['ribbon']; + } + String? _backCover; + List? _resources; + String? _ribbon; +Charm copyWith({ String? backCover, + List? resources, + String? ribbon, +}) => Charm( backCover: backCover ?? _backCover, + resources: resources ?? _resources, + ribbon: ribbon ?? _ribbon, +); + String? get backCover => _backCover; + List? get resources => _resources; + String? get ribbon => _ribbon; + + Map toJson() { + final map = {}; + map['backCover'] = _backCover; + if (_resources != null) { + map['resources'] = _resources?.map((v) => v.toJson()).toList(); + } + map['ribbon'] = _ribbon; + return map; + } + +} diff --git a/lib/shared/business_logic/models/res/sc_mic_go_up_res.dart b/lib/shared/business_logic/models/res/sc_mic_go_up_res.dart new file mode 100644 index 0000000..50c6118 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_mic_go_up_res.dart @@ -0,0 +1,353 @@ +/// micIndex : 0 +/// micLock : false +/// micMute : false +/// roomId : 0 +/// roomToken : "" +/// user : {"account":"","charmLevel":0,"heartbeatVal":0,"id":0,"ownSpecialId":{"account":"","expiredTime":0},"roles":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0} + +class SCMicGoUpRes { + SCMicGoUpRes({ + num? micIndex, + bool? micLock, + bool? micMute, + String? roomId, + String? roomToken, + User? user,}){ + _micIndex = micIndex; + _micLock = micLock; + _micMute = micMute; + _roomId = roomId; + _roomToken = roomToken; + _user = user; +} + + SCMicGoUpRes.fromJson(dynamic json) { + _micIndex = json['micIndex']; + _micLock = json['micLock']; + _micMute = json['micMute']; + _roomId = json['roomId']; + _roomToken = json['roomToken']; + _user = json['user'] != null ? User.fromJson(json['user']) : null; + } + num? _micIndex; + bool? _micLock; + bool? _micMute; + String? _roomId; + String? _roomToken; + User? _user; +SCMicGoUpRes copyWith({ num? micIndex, + bool? micLock, + bool? micMute, + String? roomId, + String? roomToken, + User? user, +}) => SCMicGoUpRes( micIndex: micIndex ?? _micIndex, + micLock: micLock ?? _micLock, + micMute: micMute ?? _micMute, + roomId: roomId ?? _roomId, + roomToken: roomToken ?? _roomToken, + user: user ?? _user, +); + num? get micIndex => _micIndex; + bool? get micLock => _micLock; + bool? get micMute => _micMute; + String? get roomId => _roomId; + String? get roomToken => _roomToken; + User? get user => _user; + + Map toJson() { + final map = {}; + map['micIndex'] = _micIndex; + map['micLock'] = _micLock; + map['micMute'] = _micMute; + map['roomId'] = _roomId; + map['roomToken'] = _roomToken; + if (_user != null) { + map['user'] = _user?.toJson(); + } + return map; + } + +} + +/// account : "" +/// charmLevel : 0 +/// heartbeatVal : 0 +/// id : 0 +/// ownSpecialId : {"account":"","expiredTime":0} +/// roles : "" +/// useProps : [{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}] +/// userAvatar : "" +/// userNickname : "" +/// userSex : 0 + +class User { + User({ + String? account, + num? charmLevel, + num? heartbeatVal, + String? id, + OwnSpecialId? ownSpecialId, + String? roles, + List? useProps, + String? userAvatar, + String? userNickname, + num? userSex,}){ + _account = account; + _charmLevel = charmLevel; + _heartbeatVal = heartbeatVal; + _id = id; + _ownSpecialId = ownSpecialId; + _roles = roles; + _useProps = useProps; + _userAvatar = userAvatar; + _userNickname = userNickname; + _userSex = userSex; +} + + User.fromJson(dynamic json) { + _account = json['account']; + _charmLevel = json['charmLevel']; + _heartbeatVal = json['heartbeatVal']; + _id = json['id']; + _ownSpecialId = json['ownSpecialId'] != null ? OwnSpecialId.fromJson(json['ownSpecialId']) : null; + _roles = json['roles']; + if (json['useProps'] != null) { + _useProps = []; + json['useProps'].forEach((v) { + _useProps?.add(UseProps.fromJson(v)); + }); + } + _userAvatar = json['userAvatar']; + _userNickname = json['userNickname']; + _userSex = json['userSex']; + } + String? _account; + num? _charmLevel; + num? _heartbeatVal; + String? _id; + OwnSpecialId? _ownSpecialId; + String? _roles; + List? _useProps; + String? _userAvatar; + String? _userNickname; + num? _userSex; +User copyWith({ String? account, + num? charmLevel, + num? heartbeatVal, + String? id, + OwnSpecialId? ownSpecialId, + String? roles, + List? useProps, + String? userAvatar, + String? userNickname, + num? userSex, +}) => User( account: account ?? _account, + charmLevel: charmLevel ?? _charmLevel, + heartbeatVal: heartbeatVal ?? _heartbeatVal, + id: id ?? _id, + ownSpecialId: ownSpecialId ?? _ownSpecialId, + roles: roles ?? _roles, + useProps: useProps ?? _useProps, + userAvatar: userAvatar ?? _userAvatar, + userNickname: userNickname ?? _userNickname, + userSex: userSex ?? _userSex, +); + String? get account => _account; + num? get charmLevel => _charmLevel; + num? get heartbeatVal => _heartbeatVal; + String? get id => _id; + OwnSpecialId? get ownSpecialId => _ownSpecialId; + String? get roles => _roles; + List? get useProps => _useProps; + String? get userAvatar => _userAvatar; + String? get userNickname => _userNickname; + num? get userSex => _userSex; + + Map toJson() { + final map = {}; + map['account'] = _account; + map['charmLevel'] = _charmLevel; + map['heartbeatVal'] = _heartbeatVal; + map['id'] = _id; + if (_ownSpecialId != null) { + map['ownSpecialId'] = _ownSpecialId?.toJson(); + } + map['roles'] = _roles; + if (_useProps != null) { + map['useProps'] = _useProps?.map((v) => v.toJson()).toList(); + } + map['userAvatar'] = _userAvatar; + map['userNickname'] = _userNickname; + map['userSex'] = _userSex; + return map; + } + +} + +/// expireTime : 0 +/// propsResources : {"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""} +/// userId : 0 + +class UseProps { + UseProps({ + String? expireTime, + PropsResources? propsResources, + String? userId,}){ + _expireTime = expireTime; + _propsResources = propsResources; + _userId = userId; +} + + UseProps.fromJson(dynamic json) { + _expireTime = json['expireTime']; + _propsResources = json['propsResources'] != null ? PropsResources.fromJson(json['propsResources']) : null; + _userId = json['userId']; + } + String? _expireTime; + PropsResources? _propsResources; + String? _userId; +UseProps copyWith({ String? expireTime, + PropsResources? propsResources, + String? userId, +}) => UseProps( expireTime: expireTime ?? _expireTime, + propsResources: propsResources ?? _propsResources, + userId: userId ?? _userId, +); + String? get expireTime => _expireTime; + PropsResources? get propsResources => _propsResources; + String? get userId => _userId; + + Map toJson() { + final map = {}; + map['expireTime'] = _expireTime; + if (_propsResources != null) { + map['propsResources'] = _propsResources?.toJson(); + } + map['userId'] = _userId; + return map; + } + +} + +/// amount : 0.0 +/// code : "" +/// cover : "" +/// expand : "" +/// id : 0 +/// name : "" +/// sourceUrl : "" +/// type : "" + +class PropsResources { + PropsResources({ + num? amount, + String? code, + String? cover, + String? expand, + num? id, + String? name, + String? sourceUrl, + String? type,}){ + _amount = amount; + _code = code; + _cover = cover; + _expand = expand; + _id = id; + _name = name; + _sourceUrl = sourceUrl; + _type = type; +} + + PropsResources.fromJson(dynamic json) { + _amount = json['amount']; + _code = json['code']; + _cover = json['cover']; + _expand = json['expand']; + _id = json['id']; + _name = json['name']; + _sourceUrl = json['sourceUrl']; + _type = json['type']; + } + num? _amount; + String? _code; + String? _cover; + String? _expand; + num? _id; + String? _name; + String? _sourceUrl; + String? _type; +PropsResources copyWith({ num? amount, + String? code, + String? cover, + String? expand, + num? id, + String? name, + String? sourceUrl, + String? type, +}) => PropsResources( amount: amount ?? _amount, + code: code ?? _code, + cover: cover ?? _cover, + expand: expand ?? _expand, + id: id ?? _id, + name: name ?? _name, + sourceUrl: sourceUrl ?? _sourceUrl, + type: type ?? _type, +); + num? get amount => _amount; + String? get code => _code; + String? get cover => _cover; + String? get expand => _expand; + num? get id => _id; + String? get name => _name; + String? get sourceUrl => _sourceUrl; + String? get type => _type; + + Map toJson() { + final map = {}; + map['amount'] = _amount; + map['code'] = _code; + map['cover'] = _cover; + map['expand'] = _expand; + map['id'] = _id; + map['name'] = _name; + map['sourceUrl'] = _sourceUrl; + map['type'] = _type; + return map; + } + +} + +/// account : "" +/// expiredTime : 0 + +class OwnSpecialId { + OwnSpecialId({ + String? account, + num? expiredTime,}){ + _account = account; + _expiredTime = expiredTime; +} + + OwnSpecialId.fromJson(dynamic json) { + _account = json['account']; + _expiredTime = json['expiredTime']; + } + String? _account; + num? _expiredTime; +OwnSpecialId copyWith({ String? account, + num? expiredTime, +}) => OwnSpecialId( account: account ?? _account, + expiredTime: expiredTime ?? _expiredTime, +); + String? get account => _account; + num? get expiredTime => _expiredTime; + + Map toJson() { + final map = {}; + map['account'] = _account; + map['expiredTime'] = _expiredTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_product_config_res.dart b/lib/shared/business_logic/models/res/sc_product_config_res.dart new file mode 100644 index 0000000..f677015 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_product_config_res.dart @@ -0,0 +1,69 @@ +/// id : 0 +/// sysOrigin : "" +/// productPackage : "" +/// unitPrice : 0.0 +/// obtainCandy : 0.0 +/// rewardCandy : 0.0 + +class SCProductConfigRes { + SCProductConfigRes({ + String? id, + String? sysOrigin, + String? productPackage, + num? unitPrice, + num? obtainCandy, + num? rewardCandy,}){ + _id = id; + _sysOrigin = sysOrigin; + _productPackage = productPackage; + _unitPrice = unitPrice; + _obtainCandy = obtainCandy; + _rewardCandy = rewardCandy; +} + + SCProductConfigRes.fromJson(dynamic json) { + _id = json['id']; + _sysOrigin = json['sysOrigin']; + _productPackage = json['productPackage']; + _unitPrice = json['unitPrice']; + _obtainCandy = json['obtainCandy']; + _rewardCandy = json['rewardCandy']; + } + String? _id; + String? _sysOrigin; + String? _productPackage; + num? _unitPrice; + num? _obtainCandy; + num? _rewardCandy; +SCProductConfigRes copyWith({ String? id, + String? sysOrigin, + String? productPackage, + num? unitPrice, + num? obtainCandy, + num? rewardCandy, +}) => SCProductConfigRes( id: id ?? _id, + sysOrigin: sysOrigin ?? _sysOrigin, + productPackage: productPackage ?? _productPackage, + unitPrice: unitPrice ?? _unitPrice, + obtainCandy: obtainCandy ?? _obtainCandy, + rewardCandy: rewardCandy ?? _rewardCandy, +); + String? get id => _id; + String? get sysOrigin => _sysOrigin; + String? get productPackage => _productPackage; + num? get unitPrice => _unitPrice; + num? get obtainCandy => _obtainCandy; + num? get rewardCandy => _rewardCandy; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['sysOrigin'] = _sysOrigin; + map['productPackage'] = _productPackage; + map['unitPrice'] = _unitPrice; + map['obtainCandy'] = _obtainCandy; + map['rewardCandy'] = _rewardCandy; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_prop_coupon_list_res.dart b/lib/shared/business_logic/models/res/sc_prop_coupon_list_res.dart new file mode 100644 index 0000000..050499d --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_prop_coupon_list_res.dart @@ -0,0 +1,177 @@ +/// records : [{"id":"0","couponNo":"","couponType":0,"couponTypeName":"","propId":0,"propName":"","propIcon":"","validDays":0,"status":0,"statusName":"","source":0,"sourceName":"","expireTime":"","useTime":"","createTime":""}] +/// total : 0 +/// size : 0 +/// current : 0 + +class SCPropCouponListRes { + SCPropCouponListRes({ + List? records, + num? total, + num? size, + num? current,}){ + _records = records; + _total = total; + _size = size; + _current = current; +} + + SCPropCouponListRes.fromJson(dynamic json) { + if (json['records'] != null) { + _records = []; + json['records'].forEach((v) { + _records?.add(Records.fromJson(v)); + }); + } + _total = json['total']; + _size = json['size']; + _current = json['current']; + } + List? _records; + num? _total; + num? _size; + num? _current; + + List? get records => _records; + num? get total => _total; + num? get size => _size; + num? get current => _current; + + Map toJson() { + final map = {}; + if (_records != null) { + map['records'] = _records?.map((v) => v.toJson()).toList(); + } + map['total'] = _total; + map['size'] = _size; + map['current'] = _current; + return map; + } + +} + +/// id : "0" +/// couponNo : "" +/// couponType : 0 +/// couponTypeName : "" +/// propId : 0 +/// propName : "" +/// propIcon : "" +/// validDays : 0 +/// status : 0 +/// statusName : "" +/// source : 0 +/// sourceName : "" +/// expireTime : "" +/// useTime : "" +/// createTime : "" + +class Records { + Records({ + num? id, + String? couponNo, + num? couponType, + String? couponTypeName, + num? propId, + String? propName, + String? propIcon, + num? validDays, + num? propDays, + num? status, + String? statusName, + num? source, + String? sourceName, + int? expireTime, + int? useTime, + int? createTime,}){ + _id = id; + _couponNo = couponNo; + _couponType = couponType; + _couponTypeName = couponTypeName; + _propId = propId; + _propName = propName; + _propIcon = propIcon; + _validDays = validDays; + _propDays = propDays; + _status = status; + _statusName = statusName; + _source = source; + _sourceName = sourceName; + _expireTime = expireTime; + _useTime = useTime; + _createTime = createTime; +} + + Records.fromJson(dynamic json) { + _id = json['id']; + _couponNo = json['couponNo']; + _couponType = json['couponType']; + _couponTypeName = json['couponTypeName']; + _propId = json['propId']; + _propName = json['propName']; + _propIcon = json['propIcon']; + _validDays = json['validDays']; + _propDays = json['propDays']; + _status = json['status']; + _statusName = json['statusName']; + _source = json['source']; + _sourceName = json['sourceName']; + _expireTime = json['expireTime']; + _useTime = json['useTime']; + _createTime = json['createTime']; + } + num? _id; + String? _couponNo; + num? _couponType; + String? _couponTypeName; + num? _propId; + String? _propName; + String? _propIcon; + num? _validDays; + num? _propDays; + num? _status; + String? _statusName; + num? _source; + String? _sourceName; + int? _expireTime; + int? _useTime; + int? _createTime; + + num? get id => _id; + String? get couponNo => _couponNo; + num? get couponType => _couponType; + String? get couponTypeName => _couponTypeName; + num? get propId => _propId; + String? get propName => _propName; + String? get propIcon => _propIcon; + num? get validDays => _validDays; + num? get propDays => _propDays; + num? get status => _status; + String? get statusName => _statusName; + num? get source => _source; + String? get sourceName => _sourceName; + int? get expireTime => _expireTime; + int? get useTime => _useTime; + int? get createTime => _createTime; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['couponNo'] = _couponNo; + map['couponType'] = _couponType; + map['couponTypeName'] = _couponTypeName; + map['propId'] = _propId; + map['propName'] = _propName; + map['propIcon'] = _propIcon; + map['validDays'] = _validDays; + map['propDays'] = _propDays; + map['status'] = _status; + map['statusName'] = _statusName; + map['source'] = _source; + map['sourceName'] = _sourceName; + map['expireTime'] = _expireTime; + map['useTime'] = _useTime; + map['createTime'] = _createTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_prop_coupon_record_list_res.dart b/lib/shared/business_logic/models/res/sc_prop_coupon_record_list_res.dart new file mode 100644 index 0000000..f38e8ec --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_prop_coupon_record_list_res.dart @@ -0,0 +1,179 @@ +/// records : [{"id":0,"couponNo":"","couponType":0,"couponTypeName":"","propId":0,"propName":"","validDays":0,"actionType":0,"actionTypeName":"","receiverId":0,"receiverNickname":"","remark":"","createTime":""}] +/// total : 0 +/// size : 0 +/// current : 0 + +class SCPropCouponRecordListRes { + SCPropCouponRecordListRes({ + List? records, + num? total, + num? size, + num? current,}){ + _records = records; + _total = total; + _size = size; + _current = current; +} + + SCPropCouponRecordListRes.fromJson(dynamic json) { + if (json['records'] != null) { + _records = []; + json['records'].forEach((v) { + _records?.add(Records.fromJson(v)); + }); + } + _total = json['total']; + _size = json['size']; + _current = json['current']; + } + List? _records; + num? _total; + num? _size; + num? _current; + + List? get records => _records; + num? get total => _total; + num? get size => _size; + num? get current => _current; + + Map toJson() { + final map = {}; + if (_records != null) { + map['records'] = _records?.map((v) => v.toJson()).toList(); + } + map['total'] = _total; + map['size'] = _size; + map['current'] = _current; + return map; + } + +} + +/// id : 0 +/// couponNo : "" +/// couponType : 0 +/// couponTypeName : "" +/// propId : 0 +/// propName : "" +/// validDays : 0 +/// actionType : 0 +/// actionTypeName : "" +/// receiverId : 0 +/// receiverNickname : "" +/// remark : "" +/// createTime : "" + +class Records { + Records({ + num? id, + String? couponNo, + String? propIcon, + num? couponType, + String? couponTypeName, + num? propId, + String? propName, + num? validDays, + num? propDays, + num? actionType, + String? actionTypeName, + num? receiverId, + String? receiverAccount, + String? receiverNickname, + String? remark, + int? createTime, + int? expireTime,}){ + _id = id; + _couponNo = couponNo; + _propIcon = propIcon; + _couponType = couponType; + _couponTypeName = couponTypeName; + _propId = propId; + _propName = propName; + _validDays = validDays; + _propDays = propDays; + _actionType = actionType; + _actionTypeName = actionTypeName; + _receiverId = receiverId; + _receiverAccount = receiverAccount; + _receiverNickname = receiverNickname; + _remark = remark; + _createTime = createTime; + _expireTime = expireTime; +} + + Records.fromJson(dynamic json) { + _id = json['id']; + _couponNo = json['couponNo']; + _propIcon = json['propIcon']; + _couponType = json['couponType']; + _couponTypeName = json['couponTypeName']; + _propId = json['propId']; + _propName = json['propName']; + _propDays = json['propDays']; + _actionType = json['actionType']; + _actionTypeName = json['actionTypeName']; + _receiverId = json['receiverId']; + _receiverAccount = json['receiverAccount']; + _receiverNickname = json['receiverNickname']; + _remark = json['remark']; + _createTime = json['createTime']; + _expireTime = json['expireTime']; + } + num? _id; + String? _couponNo; + String? _propIcon; + num? _couponType; + String? _couponTypeName; + num? _propId; + String? _propName; + num? _validDays; + num? _propDays; + num? _actionType; + String? _actionTypeName; + num? _receiverId; + String? _receiverAccount; + String? _receiverNickname; + String? _remark; + int? _createTime; + int? _expireTime; + + num? get id => _id; + String? get propIcon => _propIcon; + num? get couponType => _couponType; + String? get couponTypeName => _couponTypeName; + num? get propId => _propId; + String? get propName => _propName; + num? get validDays => _validDays; + num? get propDays => _propDays; + num? get actionType => _actionType; + String? get actionTypeName => _actionTypeName; + num? get receiverId => _receiverId; + String? get receiverAccount => _receiverAccount; + String? get receiverNickname => _receiverNickname; + String? get remark => _remark; + int? get createTime => _createTime; + int? get expireTime => _expireTime; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['couponNo'] = _couponNo; + map['propIcon'] = _propIcon; + map['couponType'] = _couponType; + map['couponTypeName'] = _couponTypeName; + map['propId'] = _propId; + map['propName'] = _propName; + map['validDays'] = _validDays; + map['propDays'] = _propDays; + map['actionType'] = _actionType; + map['actionTypeName'] = _actionTypeName; + map['receiverId'] = _receiverId; + map['receiverAccount'] = _receiverAccount; + map['receiverNickname'] = _receiverNickname; + map['remark'] = _remark; + map['createTime'] = _createTime; + map['expireTime'] = _expireTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_public_message_page_res.dart b/lib/shared/business_logic/models/res/sc_public_message_page_res.dart new file mode 100644 index 0000000..d55fcfc --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_public_message_page_res.dart @@ -0,0 +1,175 @@ +/// records : [{"id":"","messageId":"","type":"","title":"","content":"","imageUrl":"","extraData":{"":{}},"createdAt":0,"expiresAt":0,"status":""}] +/// total : 0 +/// size : 0 +/// current : 0 + +class SCPublicMessagePageRes { + SCPublicMessagePageRes({ + List? records, + num? total, + num? size, + num? current, + }) { + _records = records; + _total = total; + _size = size; + _current = current; + } + + SCPublicMessagePageRes.fromJson(dynamic json) { + if (json['records'] != null) { + _records = []; + json['records'].forEach((v) { + _records?.add(Records.fromJson(v)); + }); + } + _total = json['total']; + _size = json['size']; + _current = json['current']; + } + + List? _records; + num? _total; + num? _size; + num? _current; + + List? get records => _records; + + num? get total => _total; + + num? get size => _size; + + num? get current => _current; + + Map toJson() { + final map = {}; + if (_records != null) { + map['records'] = _records?.map((v) => v.toJson()).toList(); + } + map['total'] = _total; + map['size'] = _size; + map['current'] = _current; + return map; + } +} + +/// id : "" +/// messageId : "" +/// type : "" +/// title : "" +/// content : "" +/// imageUrl : "" +/// extraData : {"":{}} +/// createdAt : 0 +/// expiresAt : 0 +/// status : "" + +class Records { + Records({ + String? id, + String? messageId, + String? type, + String? title, + String? content, + String? imageUrl, + String? createdAt, + String? expiresAt, + String? status, + ExtraData? extraData, + }) { + _id = id; + _messageId = messageId; + _type = type; + _title = title; + _content = content; + _imageUrl = imageUrl; + _createdAt = createdAt; + _expiresAt = expiresAt; + _status = status; + _extraData = extraData; + } + + Records.fromJson(dynamic json) { + _id = json['id']; + _messageId = json['messageId']; + _type = json['type']; + _title = json['title']; + _content = json['content']; + _imageUrl = json['imageUrl']; + _createdAt = json['createdAt']; + _expiresAt = json['expiresAt']; + _status = json['status']; + _extraData = + json['extraData'] != null + ? ExtraData.fromJson(json['extraData']) + : null; + } + + String? _id; + String? _messageId; + String? _type; + String? _title; + String? _content; + String? _imageUrl; + String? _createdAt; + String? _expiresAt; + String? _status; + ExtraData? _extraData; + + String? get id => _id; + + String? get messageId => _messageId; + + String? get type => _type; + + String? get title => _title; + + String? get content => _content; + + String? get imageUrl => _imageUrl; + + String? get createdAt => _createdAt; + + String? get expiresAt => _expiresAt; + + String? get status => _status; + + ExtraData? get extraData => _extraData; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['messageId'] = _messageId; + map['type'] = _type; + map['title'] = _title; + map['content'] = _content; + map['imageUrl'] = _imageUrl; + map['createdAt'] = _createdAt; + map['expiresAt'] = _expiresAt; + map['status'] = _status; + if (_extraData != null) { + map['extraData'] = _extraData?.toJson(); + } + return map; + } +} + +class ExtraData { + ExtraData({String? link}) { + _link = link; + } + + String? _link; + + String? get link => _link; + + ExtraData.fromJson(dynamic json) { + _link = json['link']; + } + + Map toJson() { + final map = {}; + map['link'] = _link; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_room_contribute_level_res.dart b/lib/shared/business_logic/models/res/sc_room_contribute_level_res.dart new file mode 100644 index 0000000..a74c62e --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_contribute_level_res.dart @@ -0,0 +1,87 @@ +/// level : 0 +/// thatExperience : 0 +/// thatExperienceFormat : "" +/// nextExperience : 0 +/// nextExperienceFormat : "" + +class SCRoomContributeLevelRes { + SCRoomContributeLevelRes({ + num? level, + String? thatExperience, + String? thatExperienceFormat, + num? thisWeekAmount, + String? thisWeekIntegral, + String? nextExperience, + String? nextExperienceFormat, + }) { + _level = level; + _thatExperience = thatExperience; + _thatExperienceFormat = thatExperienceFormat; + _thisWeekAmount = thisWeekAmount; + _thisWeekIntegral = thisWeekIntegral; + _nextExperience = nextExperience; + _nextExperienceFormat = nextExperienceFormat; + } + + SCRoomContributeLevelRes.fromJson(dynamic json) { + _level = json['level']; + _thatExperience = json['thatExperience']; + _thatExperienceFormat = json['thatExperienceFormat']; + _thisWeekAmount = json['thisWeekAmount']; + _thisWeekIntegral = json['thisWeekIntegral']; + _nextExperience = json['nextExperience']; + _nextExperienceFormat = json['nextExperienceFormat']; + } + + num? _level; + num? _thisWeekAmount; + String? _thatExperience; + String? _thatExperienceFormat; + String? _thisWeekIntegral; + String? _nextExperience; + String? _nextExperienceFormat; + + SCRoomContributeLevelRes copyWith({ + num? level, + num? thisWeekAmount, + String? thatExperience, + String? thatExperienceFormat, + String? thisWeekIntegral, + String? nextExperience, + String? nextExperienceFormat, + }) => SCRoomContributeLevelRes( + level: level ?? _level, + thisWeekAmount: thisWeekAmount ?? _thisWeekAmount, + thatExperience: thatExperience ?? _thatExperience, + thatExperienceFormat: thatExperienceFormat ?? _thatExperienceFormat, + thisWeekIntegral: thisWeekIntegral ?? _thisWeekIntegral, + nextExperience: nextExperience ?? _nextExperience, + nextExperienceFormat: nextExperienceFormat ?? _nextExperienceFormat, + ); + + num? get level => _level; + + num? get thisWeekAmount => _thisWeekAmount; + + String? get thatExperience => _thatExperience; + + String? get thatExperienceFormat => _thatExperienceFormat; + + String? get thisWeekIntegral => _thisWeekIntegral; + + String? get nextExperience => _nextExperience; + + String? get nextExperienceFormat => _nextExperienceFormat; + + Map toJson() { + final map = {}; + map['level'] = _level; + map['thatExperience'] = _thatExperience; + map['thatExperienceFormat'] = _thatExperienceFormat; + map['thisWeekAmount'] = _thisWeekAmount; + map['thisWeekIntegral'] = _thisWeekIntegral; + map['nextExperience'] = _nextExperience; + map['nextExperienceFormat'] = _nextExperienceFormat; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_room_emoji_res.dart b/lib/shared/business_logic/models/res/sc_room_emoji_res.dart new file mode 100644 index 0000000..92775f4 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_emoji_res.dart @@ -0,0 +1,146 @@ +/// id : 0 +/// sysOrigin : "" +/// groupCode : "" +/// groupName : "" +/// cover : "" +/// have : false +/// amount : 0.0 +/// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}] + +class SCRoomEmojiRes { + SCRoomEmojiRes({ + String? id, + String? sysOrigin, + String? groupCode, + String? groupName, + String? cover, + bool? have, + num? amount, + List? emojis,}){ + _id = id; + _sysOrigin = sysOrigin; + _groupCode = groupCode; + _groupName = groupName; + _cover = cover; + _have = have; + _amount = amount; + _emojis = emojis; +} + + SCRoomEmojiRes.fromJson(dynamic json) { + _id = json['id']; + _sysOrigin = json['sysOrigin']; + _groupCode = json['groupCode']; + _groupName = json['groupName']; + _cover = json['cover']; + _have = json['have']; + _amount = json['amount']; + if (json['emojis'] != null) { + _emojis = []; + json['emojis'].forEach((v) { + _emojis?.add(Emojis.fromJson(v)); + }); + } + } + String? _id; + String? _sysOrigin; + String? _groupCode; + String? _groupName; + String? _cover; + bool? _have; + num? _amount; + List? _emojis; +SCRoomEmojiRes copyWith({ String? id, + String? sysOrigin, + String? groupCode, + String? groupName, + String? cover, + bool? have, + num? amount, + List? emojis, +}) => SCRoomEmojiRes( id: id ?? _id, + sysOrigin: sysOrigin ?? _sysOrigin, + groupCode: groupCode ?? _groupCode, + groupName: groupName ?? _groupName, + cover: cover ?? _cover, + have: have ?? _have, + amount: amount ?? _amount, + emojis: emojis ?? _emojis, +); + String? get id => _id; + String? get sysOrigin => _sysOrigin; + String? get groupCode => _groupCode; + String? get groupName => _groupName; + String? get cover => _cover; + bool? get have => _have; + num? get amount => _amount; + List? get emojis => _emojis; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['sysOrigin'] = _sysOrigin; + map['groupCode'] = _groupCode; + map['groupName'] = _groupName; + map['cover'] = _cover; + map['have'] = _have; + map['amount'] = _amount; + if (_emojis != null) { + map['emojis'] = _emojis?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// sourceUrl : "" +/// sourceType : 0 +/// coverUrl : "" +/// type : "" + +class Emojis { + Emojis({ + String? sourceUrl, + num? sourceType, + String? coverUrl, + String? type,}){ + _sourceUrl = sourceUrl; + _sourceType = sourceType; + _coverUrl = coverUrl; + _type = type; +} + + Emojis.fromJson(dynamic json) { + _sourceUrl = json['sourceUrl']; + _sourceType = json['sourceType']; + _coverUrl = json['coverUrl']; + _type = json['type']; + } + String? _sourceUrl; + num? _sourceType; + String? _coverUrl; + String? _type; +Emojis copyWith({ String? sourceUrl, + num? sourceType, + String? coverUrl, + String? type, +}) => Emojis( sourceUrl: sourceUrl ?? _sourceUrl, + sourceType: sourceType ?? _sourceType, + coverUrl: coverUrl ?? _coverUrl, + type: type ?? _type, +); + String? get sourceUrl => _sourceUrl; + num? get sourceType => _sourceType; + String? get coverUrl => _coverUrl; + String? get type => _type; + + Map toJson() { + final map = {}; + map['sourceUrl'] = _sourceUrl; + map['sourceType'] = _sourceType; + map['coverUrl'] = _coverUrl; + map['type'] = _type; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_room_join_black_list_res.dart b/lib/shared/business_logic/models/res/sc_room_join_black_list_res.dart new file mode 100644 index 0000000..cd818c0 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_join_black_list_res.dart @@ -0,0 +1,42 @@ +/// roomId : 0 +/// roles : "" +/// remark : "" + +class SCRoomJoinBlackListRes { + SCRoomJoinBlackListRes({ + String? roomId, + String? roles, + String? remark,}){ + _roomId = roomId; + _roles = roles; + _remark = remark; +} + + SCRoomJoinBlackListRes.fromJson(dynamic json) { + _roomId = json['roomId']; + _roles = json['roles']; + _remark = json['remark']; + } + String? _roomId; + String? _roles; + String? _remark; +SCRoomJoinBlackListRes copyWith({ String? roomId, + String? roles, + String? remark, +}) => SCRoomJoinBlackListRes( roomId: roomId ?? _roomId, + roles: roles ?? _roles, + remark: remark ?? _remark, +); + String? get roomId => _roomId; + String? get roles => _roles; + String? get remark => _remark; + + Map toJson() { + final map = {}; + map['roomId'] = _roomId; + map['roles'] = _roles; + map['remark'] = _remark; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_room_red_packet_detail_res.dart b/lib/shared/business_logic/models/res/sc_room_red_packet_detail_res.dart new file mode 100644 index 0000000..e7a928d --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_red_packet_detail_res.dart @@ -0,0 +1,198 @@ +/// packetId : "" +/// userId : 0 +/// userName : "" +/// userAvatar : "" +/// packetType : 0 +/// packetTypeDesc : "" +/// totalAmount : 0 +/// totalCount : 0 +/// remainAmount : 0 +/// remainCount : 0 +/// status : 0 +/// statusDesc : "" +/// myAmount : 0 +/// grabbed : false +/// records : [{"recordId":"","userId":0,"userName":"","userAvatar":"","amount":0,"grabTime":""}] +/// createTime : "" + +class SCRoomRedPacketDetailRes { + SCRoomRedPacketDetailRes({ + String? packetId, + num? userId, + String? userName, + String? userAvatar, + num? packetType, + String? packetTypeDesc, + num? totalAmount, + num? totalCount, + num? remainAmount, + num? remainCount, + num? status, + String? statusDesc, + num? myAmount, + bool? grabbed, + List? records, + String? createTime,}){ + _packetId = packetId; + _userId = userId; + _userName = userName; + _userAvatar = userAvatar; + _packetType = packetType; + _packetTypeDesc = packetTypeDesc; + _totalAmount = totalAmount; + _totalCount = totalCount; + _remainAmount = remainAmount; + _remainCount = remainCount; + _status = status; + _statusDesc = statusDesc; + _myAmount = myAmount; + _grabbed = grabbed; + _records = records; + _createTime = createTime; +} + + SCRoomRedPacketDetailRes.fromJson(dynamic json) { + _packetId = json['packetId']; + _userId = json['userId']; + _userName = json['userName']; + _userAvatar = json['userAvatar']; + _packetType = json['packetType']; + _packetTypeDesc = json['packetTypeDesc']; + _totalAmount = json['totalAmount']; + _totalCount = json['totalCount']; + _remainAmount = json['remainAmount']; + _remainCount = json['remainCount']; + _status = json['status']; + _statusDesc = json['statusDesc']; + _myAmount = json['myAmount']; + _grabbed = json['grabbed']; + if (json['records'] != null) { + _records = []; + json['records'].forEach((v) { + _records?.add(Records.fromJson(v)); + }); + } + _createTime = json['createTime']; + } + String? _packetId; + num? _userId; + String? _userName; + String? _userAvatar; + num? _packetType; + String? _packetTypeDesc; + num? _totalAmount; + num? _totalCount; + num? _remainAmount; + num? _remainCount; + num? _status; + String? _statusDesc; + num? _myAmount; + bool? _grabbed; + List? _records; + String? _createTime; + + String? get packetId => _packetId; + num? get userId => _userId; + String? get userName => _userName; + String? get userAvatar => _userAvatar; + num? get packetType => _packetType; + String? get packetTypeDesc => _packetTypeDesc; + num? get totalAmount => _totalAmount; + num? get totalCount => _totalCount; + num? get remainAmount => _remainAmount; + num? get remainCount => _remainCount; + num? get status => _status; + String? get statusDesc => _statusDesc; + num? get myAmount => _myAmount; + bool? get grabbed => _grabbed; + List? get records => _records; + String? get createTime => _createTime; + + Map toJson() { + final map = {}; + map['packetId'] = _packetId; + map['userId'] = _userId; + map['userName'] = _userName; + map['userAvatar'] = _userAvatar; + map['packetType'] = _packetType; + map['packetTypeDesc'] = _packetTypeDesc; + map['totalAmount'] = _totalAmount; + map['totalCount'] = _totalCount; + map['remainAmount'] = _remainAmount; + map['remainCount'] = _remainCount; + map['status'] = _status; + map['statusDesc'] = _statusDesc; + map['myAmount'] = _myAmount; + map['grabbed'] = _grabbed; + if (_records != null) { + map['records'] = _records?.map((v) => v.toJson()).toList(); + } + map['createTime'] = _createTime; + return map; + } + +} + +/// recordId : "" +/// userId : 0 +/// userName : "" +/// userAvatar : "" +/// amount : 0 +/// grabTime : "" + +class Records { + Records({ + String? recordId, + num? userId, + String? userName, + String? account, + String? userAvatar, + num? amount, + String? grabTime,}){ + _recordId = recordId; + _userId = userId; + _userName = userName; + _account = account; + _userAvatar = userAvatar; + _amount = amount; + _grabTime = grabTime; +} + + Records.fromJson(dynamic json) { + _recordId = json['recordId']; + _userId = json['userId']; + _userName = json['userName']; + _account = json['account']; + _userAvatar = json['userAvatar']; + _amount = json['amount']; + _grabTime = json['grabTime']; + } + String? _recordId; + num? _userId; + String? _userName; + String? _account; + String? _userAvatar; + num? _amount; + String? _grabTime; + + String? get recordId => _recordId; + num? get userId => _userId; + String? get userName => _userName; + String? get account => _account; + String? get userAvatar => _userAvatar; + num? get amount => _amount; + String? get grabTime => _grabTime; + + Map toJson() { + final map = {}; + map['recordId'] = _recordId; + map['userId'] = _userId; + map['userName'] = _userName; + map['account'] = _account; + map['userAvatar'] = _userAvatar; + map['amount'] = _amount; + map['grabTime'] = _grabTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_room_red_packet_grab_res.dart b/lib/shared/business_logic/models/res/sc_room_red_packet_grab_res.dart new file mode 100644 index 0000000..3e47adc --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_red_packet_grab_res.dart @@ -0,0 +1,50 @@ +/// packetId : "" +/// amount : 0 +/// grabTime : "" +/// remainCount : 0 +/// totalCount : 0 + +class SCRoomRedPacketGrabRes { + SCRoomRedPacketGrabRes({ + String? packetId, + num? amount, + String? grabTime, + num? remainCount, + num? totalCount,}){ + _packetId = packetId; + _amount = amount; + _grabTime = grabTime; + _remainCount = remainCount; + _totalCount = totalCount; +} + + SCRoomRedPacketGrabRes.fromJson(dynamic json) { + _packetId = json['packetId']; + _amount = json['amount']; + _grabTime = json['grabTime']; + _remainCount = json['remainCount']; + _totalCount = json['totalCount']; + } + String? _packetId; + num? _amount; + String? _grabTime; + num? _remainCount; + num? _totalCount; + + String? get packetId => _packetId; + num? get amount => _amount; + String? get grabTime => _grabTime; + num? get remainCount => _remainCount; + num? get totalCount => _totalCount; + + Map toJson() { + final map = {}; + map['packetId'] = _packetId; + map['amount'] = _amount; + map['grabTime'] = _grabTime; + map['remainCount'] = _remainCount; + map['totalCount'] = _totalCount; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_room_red_packet_list_res.dart b/lib/shared/business_logic/models/res/sc_room_red_packet_list_res.dart new file mode 100644 index 0000000..5f3dbd4 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_red_packet_list_res.dart @@ -0,0 +1,167 @@ +/// packetId : "" +/// roomId : 0 +/// userId : 0 +/// userName : "" +/// userAvatar : "" +/// packetType : 0 +/// packetTypeDesc : "" +/// sourceType : 0 +/// totalAmount : 0 +/// totalCount : 0 +/// remainAmount : 0 +/// remainCount : 0 +/// expireMinutes : 0 +/// expireTime : "" +/// countdown : 0 +/// status : 0 +/// statusDesc : "" +/// grabbed : false +/// createTime : "" + +class SCRoomRedPacketListRes { + SCRoomRedPacketListRes({ + String? packetId, + String? roomId, + num? userId, + String? userName, + String? userAvatar, + num? packetType, + String? packetTypeDesc, + num? sourceType, + num? totalAmount, + num? totalCount, + num? remainAmount, + num? remainCount, + num? expireMinutes, + String? expireTime, + int? countdown, + num? status, + String? statusDesc, + bool? grabbed, + String? createTime, + }) { + _packetId = packetId; + _roomId = roomId; + _userId = userId; + _userName = userName; + _userAvatar = userAvatar; + _packetType = packetType; + _packetTypeDesc = packetTypeDesc; + _sourceType = sourceType; + _totalAmount = totalAmount; + _totalCount = totalCount; + _remainAmount = remainAmount; + _remainCount = remainCount; + _expireMinutes = expireMinutes; + _expireTime = expireTime; + _countdown = countdown; + _status = status; + _statusDesc = statusDesc; + _grabbed = grabbed; + _createTime = createTime; + } + + SCRoomRedPacketListRes.fromJson(dynamic json) { + _packetId = json['packetId']; + _roomId = json['roomId']; + _userId = json['userId']; + _userName = json['userName']; + _userAvatar = json['userAvatar']; + _packetType = json['packetType']; + _packetTypeDesc = json['packetTypeDesc']; + _sourceType = json['sourceType']; + _totalAmount = json['totalAmount']; + _totalCount = json['totalCount']; + _remainAmount = json['remainAmount']; + _remainCount = json['remainCount']; + _expireMinutes = json['expireMinutes']; + _expireTime = json['expireTime']; + _countdown = json['countdown']; + _status = json['status']; + _statusDesc = json['statusDesc']; + _grabbed = json['grabbed']; + _createTime = json['createTime']; + } + + String? _packetId; + String? _roomId; + num? _userId; + String? _userName; + String? _userAvatar; + num? _packetType; + String? _packetTypeDesc; + num? _sourceType; + num? _totalAmount; + num? _totalCount; + num? _remainAmount; + num? _remainCount; + num? _expireMinutes; + String? _expireTime; + int? _countdown; + num? _status; + String? _statusDesc; + bool? _grabbed; + String? _createTime; + + String? get packetId => _packetId; + + String? get roomId => _roomId; + + num? get userId => _userId; + + String? get userName => _userName; + + String? get userAvatar => _userAvatar; + + num? get packetType => _packetType; + + String? get packetTypeDesc => _packetTypeDesc; + + num? get sourceType => _sourceType; + + num? get totalAmount => _totalAmount; + + num? get totalCount => _totalCount; + + num? get remainAmount => _remainAmount; + + num? get remainCount => _remainCount; + + num? get expireMinutes => _expireMinutes; + + String? get expireTime => _expireTime; + + int? get countdown => _countdown; + + num? get status => _status; + + String? get statusDesc => _statusDesc; + + bool? get grabbed => _grabbed; + + String? get createTime => _createTime; + + Map toJson() { + final map = {}; + map['packetId'] = _packetId; + map['roomId'] = _roomId; + map['userId'] = _userId; + map['userName'] = _userName; + map['userAvatar'] = _userAvatar; + map['packetType'] = _packetType; + map['packetTypeDesc'] = _packetTypeDesc; + map['sourceType'] = _sourceType; + map['totalAmount'] = _totalAmount; + map['totalCount'] = _totalCount; + map['remainAmount'] = _remainAmount; + map['remainCount'] = _remainCount; + map['expireMinutes'] = _expireMinutes; + map['expireTime'] = _expireTime; + map['countdown'] = _countdown; + map['status'] = _status; + map['statusDesc'] = _statusDesc; + map['grabbed'] = _grabbed; + map['createTime'] = _createTime; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_room_reward_info_res.dart b/lib/shared/business_logic/models/res/sc_room_reward_info_res.dart new file mode 100644 index 0000000..681ecea --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_reward_info_res.dart @@ -0,0 +1,142 @@ +/// countdown : 0 +/// currentProgress : {"amount":0,"level":0} +/// lastWeekProgress : {"amount":0,"level":0,"ownerIncome":0,"ownerIncomeReceived":false,"rewardCoins":0,"rewardCoinsSent":false} + +class SCRoomRewardInfoRes { + SCRoomRewardInfoRes({ + int? countdown, + CurrentProgress? currentProgress, + LastWeekProgress? lastWeekProgress, + }) { + _countdown = countdown; + _currentProgress = currentProgress; + _lastWeekProgress = lastWeekProgress; + } + + SCRoomRewardInfoRes.fromJson(dynamic json) { + _countdown = json['countdown']; + _currentProgress = + json['currentProgress'] != null + ? CurrentProgress.fromJson(json['currentProgress']) + : null; + _lastWeekProgress = + json['lastWeekProgress'] != null + ? LastWeekProgress.fromJson(json['lastWeekProgress']) + : null; + } + + int? _countdown; + CurrentProgress? _currentProgress; + LastWeekProgress? _lastWeekProgress; + + int? get countdown => _countdown; + + CurrentProgress? get currentProgress => _currentProgress; + + LastWeekProgress? get lastWeekProgress => _lastWeekProgress; + + Map toJson() { + final map = {}; + map['countdown'] = _countdown; + if (_currentProgress != null) { + map['currentProgress'] = _currentProgress?.toJson(); + } + if (_lastWeekProgress != null) { + map['lastWeekProgress'] = _lastWeekProgress?.toJson(); + } + return map; + } +} + +/// amount : 0 +/// level : 0 +/// ownerIncome : 0 +/// ownerIncomeReceived : false +/// rewardCoins : 0 +/// rewardCoinsSent : false + +class LastWeekProgress { + LastWeekProgress({ + num? amount, + num? level, + num? ownerIncome, + bool? ownerIncomeReceived, + num? rewardCoins, + bool? rewardCoinsSent, + }) { + _amount = amount; + _level = level; + _ownerIncome = ownerIncome; + _ownerIncomeReceived = ownerIncomeReceived; + _rewardCoins = rewardCoins; + _rewardCoinsSent = rewardCoinsSent; + } + + LastWeekProgress.fromJson(dynamic json) { + _amount = json['amount']; + _level = json['level']; + _ownerIncome = json['ownerIncome']; + _ownerIncomeReceived = json['ownerIncomeReceived']; + _rewardCoins = json['rewardCoins']; + _rewardCoinsSent = json['rewardCoinsSent']; + } + + num? _amount; + num? _level; + num? _ownerIncome; + bool? _ownerIncomeReceived; + num? _rewardCoins; + bool? _rewardCoinsSent; + + num? get amount => _amount; + + num? get level => _level; + + num? get ownerIncome => _ownerIncome; + + bool? get ownerIncomeReceived => _ownerIncomeReceived; + + num? get rewardCoins => _rewardCoins; + + bool? get rewardCoinsSent => _rewardCoinsSent; + + Map toJson() { + final map = {}; + map['amount'] = _amount; + map['level'] = _level; + map['ownerIncome'] = _ownerIncome; + map['ownerIncomeReceived'] = _ownerIncomeReceived; + map['rewardCoins'] = _rewardCoins; + map['rewardCoinsSent'] = _rewardCoinsSent; + return map; + } +} + +/// amount : 0 +/// level : 0 + +class CurrentProgress { + CurrentProgress({num? amount, num? level}) { + _amount = amount; + _level = level; + } + + CurrentProgress.fromJson(dynamic json) { + _amount = json['amount']; + _level = json['level']; + } + + num? _amount; + num? _level; + + num? get amount => _amount; + + num? get level => _level; + + Map toJson() { + final map = {}; + map['amount'] = _amount; + map['level'] = _level; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_config_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_config_res.dart new file mode 100644 index 0000000..ed709c8 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_rocket_config_res.dart @@ -0,0 +1,186 @@ +/// enabled : true +/// id : 3 +/// level : 3 +/// levelName : "三级火箭" +/// maxEnergy : 1000000 +/// normalRewardConfig : [] +/// rewardConfig : [{"amount":1969014438069481474,"content":"1969014438069481474","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-5058b89a-8654-4895-866e-fe4295e88408.png","createTime":1758286290000,"detailType":"AVATAR_FRAME","groupId":1966024426660499457,"id":1969021529953873921,"name":"yumi-KingTOP1头饰","quantity":7,"remark":"","sort":1,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-975e24c5-e87a-4236-9256-799c127a06bc.svga","type":"PROPS","updateTime":1758286290000},{"amount":1969014872674873345,"badgeName":"yumi-King'Medal","content":"1969014872674873345","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/other/manager-fc5ad0a4-5e4a-498b-8494-2aee2efe5b99.png","createTime":1758286290000,"detailType":"BADGE","groupId":1966024426660499457,"id":1969021529953873922,"quantity":7,"remark":"","sort":2,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/other/manager-176fcf7e-787b-4bf6-bdb4-238d8388a5e2.svga","type":"BADGE","updateTime":1758286290000},{"amount":30000,"content":"30000","createTime":1758286290000,"detailType":"GOLD","groupId":1966024426660499457,"id":1969021529953873923,"quantity":0,"remark":"","sort":3,"type":"GOLD","updateTime":1758286290000},{"amount":1962463978629165057,"content":"1962463978629165057","cover":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-6633f4cb-58b8-4b7d-8075-7f0a6f29fae0.png","createTime":1758286290000,"detailType":"RIDE","groupId":1966024426660499457,"id":1969021529953873924,"name":"后续活动座驾","quantity":7,"remark":"","sort":4,"sourceUrl":"https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-536ac482-8e9d-4a21-acff-531d51ff5b66.mp4","type":"PROPS","updateTime":1758286290000}] +/// status : 1 + +class SCRoomRocketConfigRes { + SCRoomRocketConfigRes({ + bool? enabled, + String? id, + num? level, + String? levelName, + int? maxEnergy, + List? rewardConfig, + num? status,}){ + _enabled = enabled; + _id = id; + _level = level; + _levelName = levelName; + _maxEnergy = maxEnergy; + _rewardConfig = rewardConfig; + _status = status; +} + + SCRoomRocketConfigRes.fromJson(dynamic json) { + _enabled = json['enabled']; + _id = json['id']; + _level = json['level']; + _levelName = json['levelName']; + _maxEnergy = json['maxEnergy']; + if (json['rewardConfig'] != null) { + _rewardConfig = []; + json['rewardConfig'].forEach((v) { + _rewardConfig?.add(RewardConfig.fromJson(v)); + }); + } + _status = json['status']; + } + bool? _enabled; + String? _id; + num? _level; + String? _levelName; + int? _maxEnergy; + List? _rewardConfig; + num? _status; + + bool? get enabled => _enabled; + String? get id => _id; + num? get level => _level; + String? get levelName => _levelName; + int? get maxEnergy => _maxEnergy; + List? get rewardConfig => _rewardConfig; + num? get status => _status; + + Map toJson() { + final map = {}; + map['enabled'] = _enabled; + map['id'] = _id; + map['level'] = _level; + map['levelName'] = _levelName; + map['maxEnergy'] = _maxEnergy; + if (_rewardConfig != null) { + map['rewardConfig'] = _rewardConfig?.map((v) => v.toJson()).toList(); + } + map['status'] = _status; + return map; + } + +} + +/// amount : 1969014438069481474 +/// content : "1969014438069481474" +/// cover : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svga_cover/manager-5058b89a-8654-4895-866e-fe4295e88408.png" +/// createTime : 1758286290000 +/// detailType : "AVATAR_FRAME" +/// groupId : 1966024426660499457 +/// id : 1969021529953873921 +/// name : "Yumi-KingTOP1头饰" +/// quantity : 7 +/// remark : "" +/// sort : 1 +/// sourceUrl : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/svgasource/manager-975e24c5-e87a-4236-9256-799c127a06bc.svga" +/// type : "PROPS" +/// updateTime : 1758286290000 + +class RewardConfig { + RewardConfig({ + num? amount, + String? content, + String? cover, + num? createTime, + String? detailType, + String? groupId, + String? id, + String? name, + num? quantity, + String? remark, + num? sort, + String? sourceUrl, + String? type, + num? updateTime,}){ + _amount = amount; + _content = content; + _cover = cover; + _createTime = createTime; + _detailType = detailType; + _groupId = groupId; + _id = id; + _name = name; + _quantity = quantity; + _remark = remark; + _sort = sort; + _sourceUrl = sourceUrl; + _type = type; + _updateTime = updateTime; +} + + RewardConfig.fromJson(dynamic json) { + _amount = json['amount']; + _content = json['content']; + _cover = json['cover']; + _createTime = json['createTime']; + _detailType = json['detailType']; + _groupId = json['groupId']; + _id = json['id']; + _name = json['name']; + _quantity = json['quantity']; + _remark = json['remark']; + _sort = json['sort']; + _sourceUrl = json['sourceUrl']; + _type = json['type']; + _updateTime = json['updateTime']; + } + num? _amount; + String? _content; + String? _cover; + num? _createTime; + String? _detailType; + String? _groupId; + String? _id; + String? _name; + num? _quantity; + String? _remark; + num? _sort; + String? _sourceUrl; + String? _type; + num? _updateTime; + + num? get amount => _amount; + String? get content => _content; + String? get cover => _cover; + num? get createTime => _createTime; + String? get detailType => _detailType; + String? get groupId => _groupId; + String? get id => _id; + String? get name => _name; + num? get quantity => _quantity; + String? get remark => _remark; + num? get sort => _sort; + String? get sourceUrl => _sourceUrl; + String? get type => _type; + num? get updateTime => _updateTime; + + Map toJson() { + final map = {}; + map['amount'] = _amount; + map['content'] = _content; + map['cover'] = _cover; + map['createTime'] = _createTime; + map['detailType'] = _detailType; + map['groupId'] = _groupId; + map['id'] = _id; + map['name'] = _name; + map['quantity'] = _quantity; + map['remark'] = _remark; + map['sort'] = _sort; + map['sourceUrl'] = _sourceUrl; + map['type'] = _type; + map['updateTime'] = _updateTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart new file mode 100644 index 0000000..e522539 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart @@ -0,0 +1,145 @@ +/// roomId : 0 +/// date : "" +/// level : 0 +/// levelName : "" +/// currentEnergy : 0 +/// maxEnergy : 0 +/// energyPercent : 0.0 +/// status : "" +/// statusDesc : "" +/// totalContributors : 0 +/// launchTime : "" +/// claimExpireTime : "" +/// claimCount : 0 +/// canClaim : false +/// hasClaimed : false +/// myContribution : 0 +/// myContributionRate : 0.0 + +class SCRoomRocketStatusRes { + SCRoomRocketStatusRes({ + String? roomId, + String? date, + num? level, + String? levelName, + int? currentEnergy, + int? maxEnergy, + num? energyPercent, + String? status, + String? statusDesc, + num? totalContributors, + String? launchTime, + String? claimExpireTime, + num? claimCount, + bool? canClaim, + bool? hasClaimed, + num? myContribution, + num? myContributionRate, + }) { + _roomId = roomId; + _date = date; + _level = level; + _levelName = levelName; + _currentEnergy = currentEnergy; + _maxEnergy = maxEnergy; + _energyPercent = energyPercent; + _status = status; + _statusDesc = statusDesc; + _totalContributors = totalContributors; + _claimExpireTime = claimExpireTime; + _claimCount = claimCount; + _canClaim = canClaim; + _hasClaimed = hasClaimed; + _myContribution = myContribution; + _myContributionRate = myContributionRate; + } + + SCRoomRocketStatusRes.fromJson(dynamic json) { + _roomId = json['roomId']; + _date = json['date']; + _level = json['level']; + _levelName = json['levelName']; + _currentEnergy = json['currentEnergy']; + _maxEnergy = json['maxEnergy']; + _energyPercent = json['energyPercent']; + _status = json['status']; + _statusDesc = json['statusDesc']; + _totalContributors = json['totalContributors']; + _claimExpireTime = json['claimExpireTime']; + _claimCount = json['claimCount']; + _canClaim = json['canClaim']; + _hasClaimed = json['hasClaimed']; + _myContribution = json['myContribution']; + _myContributionRate = json['myContributionRate']; + } + + String? _roomId; + String? _date; + num? _level; + String? _levelName; + int? _currentEnergy; + int? _maxEnergy; + num? _energyPercent; + String? _status; + String? _statusDesc; + num? _totalContributors; + String? _claimExpireTime; + num? _claimCount; + bool? _canClaim; + bool? _hasClaimed; + num? _myContribution; + num? _myContributionRate; + + String? get roomId => _roomId; + + String? get date => _date; + + num? get level => _level; + + String? get levelName => _levelName; + + int? get currentEnergy => _currentEnergy; + + int? get maxEnergy => _maxEnergy; + + num? get energyPercent => _energyPercent; + + String? get status => _status; + + String? get statusDesc => _statusDesc; + + num? get totalContributors => _totalContributors; + + String? get claimExpireTime => _claimExpireTime; + + num? get claimCount => _claimCount; + + bool? get canClaim => _canClaim; + + bool? get hasClaimed => _hasClaimed; + + num? get myContribution => _myContribution; + + num? get myContributionRate => _myContributionRate; + + Map toJson() { + final map = {}; + map['roomId'] = _roomId; + map['date'] = _date; + map['level'] = _level; + map['levelName'] = _levelName; + map['currentEnergy'] = _currentEnergy; + map['maxEnergy'] = _maxEnergy; + map['energyPercent'] = _energyPercent; + map['status'] = _status; + map['statusDesc'] = _statusDesc; + map['totalContributors'] = _totalContributors; + map['claimExpireTime'] = _claimExpireTime; + map['claimCount'] = _claimCount; + map['canClaim'] = _canClaim; + map['hasClaimed'] = _hasClaimed; + map['myContribution'] = _myContribution; + map['myContributionRate'] = _myContributionRate; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_room_task_claimable_count_res.dart b/lib/shared/business_logic/models/res/sc_room_task_claimable_count_res.dart new file mode 100644 index 0000000..4f32c24 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_task_claimable_count_res.dart @@ -0,0 +1,22 @@ +/// claimableCount : 0 + +class SCRoomTaskClaimableCountRes { + SCRoomTaskClaimableCountRes({ + num? claimableCount,}){ + _claimableCount = claimableCount; +} + + SCRoomTaskClaimableCountRes.fromJson(dynamic json) { + _claimableCount = json['claimableCount']; + } + num? _claimableCount; + + num? get claimableCount => _claimableCount; + + Map toJson() { + final map = {}; + map['claimableCount'] = _claimableCount; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_room_task_list_res.dart b/lib/shared/business_logic/models/res/sc_room_task_list_res.dart new file mode 100644 index 0000000..c59f569 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_task_list_res.dart @@ -0,0 +1,166 @@ +/// countdownSeconds : 0 +/// tasks : [{"taskId":0,"taskCode":"string","taskName":"string","taskCategory":0,"iconUrl":"string","currentValue":0,"tiers":[{"tierIndex":0,"targetValue":0,"targetUnit":"string","rewardValue":0,"isCompleted":true,"isClaimed":true}]}] + +class SCRoomTaskListRes { + SCRoomTaskListRes({ + int? countdownSeconds, + List? tasks,}){ + _countdown = countdown; + _tasks = tasks; +} + + SCRoomTaskListRes.fromJson(dynamic json) { + _countdown = json['countdown']; + if (json['tasks'] != null) { + _tasks = []; + json['tasks'].forEach((v) { + _tasks?.add(Tasks.fromJson(v)); + }); + } + } + int? _countdown; + List? _tasks; + + int? get countdown => _countdown; + List? get tasks => _tasks; + + Map toJson() { + final map = {}; + map['countdown'] = _countdown; + if (_tasks != null) { + map['tasks'] = _tasks?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// taskId : 0 +/// taskCode : "string" +/// taskName : "string" +/// taskCategory : 0 +/// iconUrl : "string" +/// currentValue : 0 +/// tiers : [{"tierIndex":0,"targetValue":0,"targetUnit":"string","rewardValue":0,"isCompleted":true,"isClaimed":true}] + +class Tasks { + Tasks({ + num? taskId, + String? taskCode, + String? taskName, + num? taskCategory, + String? iconUrl, + int? currentValue, + List? tiers,}){ + _taskId = taskId; + _taskCode = taskCode; + _taskName = taskName; + _taskCategory = taskCategory; + _iconUrl = iconUrl; + _currentValue = currentValue; + _tiers = tiers; +} + + Tasks.fromJson(dynamic json) { + _taskId = json['taskId']; + _taskCode = json['taskCode']; + _taskName = json['taskName']; + _taskCategory = json['taskCategory']; + _iconUrl = json['iconUrl']; + _currentValue = json['currentValue']; + if (json['tiers'] != null) { + _tiers = []; + json['tiers'].forEach((v) { + _tiers?.add(Tiers.fromJson(v)); + }); + } + } + num? _taskId; + String? _taskCode; + String? _taskName; + num? _taskCategory; + String? _iconUrl; + int? _currentValue; + List? _tiers; + + num? get taskId => _taskId; + String? get taskCode => _taskCode; + String? get taskName => _taskName; + num? get taskCategory => _taskCategory; + String? get iconUrl => _iconUrl; + int? get currentValue => _currentValue; + List? get tiers => _tiers; + + Map toJson() { + final map = {}; + map['taskId'] = _taskId; + map['taskCode'] = _taskCode; + map['taskName'] = _taskName; + map['taskCategory'] = _taskCategory; + map['iconUrl'] = _iconUrl; + map['currentValue'] = _currentValue; + if (_tiers != null) { + map['tiers'] = _tiers?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// tierIndex : 0 +/// targetValue : 0 +/// targetUnit : "string" +/// rewardValue : 0 +/// isCompleted : true +/// isClaimed : true + +class Tiers { + Tiers({ + num? tierIndex, + int? targetValue, + String? targetUnit, + num? rewardValue, + bool? isCompleted, + bool? isClaimed,}){ + _tierIndex = tierIndex; + _targetValue = targetValue; + _targetUnit = targetUnit; + _rewardValue = rewardValue; + _isCompleted = isCompleted; + _isClaimed = isClaimed; +} + + Tiers.fromJson(dynamic json) { + _tierIndex = json['tierIndex']; + _targetValue = json['targetValue']; + _targetUnit = json['targetUnit']; + _rewardValue = json['rewardValue']; + _isCompleted = json['isCompleted']; + _isClaimed = json['isClaimed']; + } + num? _tierIndex; + int? _targetValue; + String? _targetUnit; + num? _rewardValue; + bool? _isCompleted; + bool? _isClaimed; + + num? get tierIndex => _tierIndex; + int? get targetValue => _targetValue; + String? get targetUnit => _targetUnit; + num? get rewardValue => _rewardValue; + bool? get isCompleted => _isCompleted; + bool? get isClaimed => _isClaimed; + + Map toJson() { + final map = {}; + map['tierIndex'] = _tierIndex; + map['targetValue'] = _targetValue; + map['targetUnit'] = _targetUnit; + map['rewardValue'] = _rewardValue; + map['isCompleted'] = _isCompleted; + map['isClaimed'] = _isClaimed; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_room_theme_list_res.dart b/lib/shared/business_logic/models/res/sc_room_theme_list_res.dart new file mode 100644 index 0000000..0aca7b9 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_room_theme_list_res.dart @@ -0,0 +1,85 @@ +/// id : "" +/// userId : "" +/// themeBack : "" +/// themeStatus : "" +/// themeMoney : 0.0 +/// sysOrigin : "" + +class SCRoomThemeListRes { + SCRoomThemeListRes({ + String? id, + String? userId, + String? themeBack, + String? themeStatus, + num? themeMoney, + int? expireTime, + bool? useTheme, + String? sysOrigin,}){ + _id = id; + _userId = userId; + _themeBack = themeBack; + _themeStatus = themeStatus; + _themeMoney = themeMoney; + _expireTime = expireTime; + _useTheme = useTheme; + _sysOrigin = sysOrigin; +} + + SCRoomThemeListRes.fromJson(dynamic json) { + _id = json['id']; + _userId = json['userId']; + _themeBack = json['themeBack']; + _themeStatus = json['themeStatus']; + _themeMoney = json['themeMoney']; + _expireTime = json['expireTime']; + _sysOrigin = json['sysOrigin']; + _useTheme = json['useTheme']; + } + String? _id; + String? _userId; + String? _themeBack; + String? _themeStatus; + num? _themeMoney; + int? _expireTime; + bool? _useTheme; + String? _sysOrigin; +SCRoomThemeListRes copyWith({ String? id, + String? userId, + String? themeBack, + String? themeStatus, + num? themeMoney, + int? expireTime, + bool? useTheme, + String? sysOrigin, +}) => SCRoomThemeListRes( id: id ?? _id, + userId: userId ?? _userId, + themeBack: themeBack ?? _themeBack, + themeStatus: themeStatus ?? _themeStatus, + themeMoney: themeMoney ?? _themeMoney, + expireTime: expireTime ?? _expireTime, + useTheme: useTheme ?? _useTheme, + sysOrigin: sysOrigin ?? _sysOrigin, +); + String? get id => _id; + String? get userId => _userId; + String? get themeBack => _themeBack; + String? get themeStatus => _themeStatus; + num? get themeMoney => _themeMoney; + int? get expireTime => _expireTime; + bool? get useTheme => _useTheme; + String? get sysOrigin => _sysOrigin; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['userId'] = _userId; + map['themeBack'] = _themeBack; + map['themeStatus'] = _themeStatus; + map['themeMoney'] = _themeMoney; + map['expireTime'] = _expireTime; + map['useTheme'] = _useTheme; + map['sysOrigin'] = _sysOrigin; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_rtc_token_res.dart b/lib/shared/business_logic/models/res/sc_rtc_token_res.dart new file mode 100644 index 0000000..ab784a6 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_rtc_token_res.dart @@ -0,0 +1,33 @@ +/// newServiceToken : "" +/// rtcToken : "" + +class SCRtcTokenRes { + SCRtcTokenRes({ + String? newServiceToken, + String? rtcToken,}){ + _newServiceToken = newServiceToken; + _rtcToken = rtcToken; +} + + SCRtcTokenRes.fromJson(dynamic json) { + _newServiceToken = json['newServiceToken']; + _rtcToken = json['rtcToken']; + } + String? _newServiceToken; + String? _rtcToken; +SCRtcTokenRes copyWith({ String? newServiceToken, + String? rtcToken, +}) => SCRtcTokenRes( newServiceToken: newServiceToken ?? _newServiceToken, + rtcToken: rtcToken ?? _rtcToken, +); + String? get newServiceToken => _newServiceToken; + String? get rtcToken => _rtcToken; + + Map toJson() { + final map = {}; + map['newServiceToken'] = _newServiceToken; + map['rtcToken'] = _rtcToken; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_sign_in_res.dart b/lib/shared/business_logic/models/res/sc_sign_in_res.dart new file mode 100644 index 0000000..0b9653a --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_sign_in_res.dart @@ -0,0 +1,291 @@ +/// days : 0 +/// rewards : [{"extValues":{"":{}},"rule":{"id":0,"sysOrigin":"","activityType":"","resourceGroupId":0,"jsonData":"","sort":0},"propsGroup":{"id":0,"name":"","shelfStatus":false,"activityRewardProps":[{"id":0,"groupId":0,"type":"","detailType":"","content":"","sort":0,"cover":"","quantity":0,"amount":0.0,"remark":""}]}}] + +class SCSignInRes { + SCSignInRes({num? days, List? rewards}) { + _days = days; + _rewards = rewards; + } + + SCSignInRes.fromJson(dynamic json) { + _days = json['days']; + if (json['rewards'] != null) { + _rewards = []; + json['rewards'].forEach((v) { + _rewards?.add(Rewards.fromJson(v)); + }); + } + } + + num? _days; + List? _rewards; + + SCSignInRes copyWith({num? days, List? rewards}) => + SCSignInRes(days: days ?? _days, rewards: rewards ?? _rewards); + + num? get days => _days; + + List? get rewards => _rewards; + + Map toJson() { + final map = {}; + map['days'] = _days; + if (_rewards != null) { + map['rewards'] = _rewards?.map((v) => v.toJson()).toList(); + } + return map; + } +} + +/// extValues : {"":{}} +/// rule : {"id":0,"sysOrigin":"","activityType":"","resourceGroupId":0,"jsonData":"","sort":0} +/// propsGroup : {"id":0,"name":"","shelfStatus":false,"activityRewardProps":[{"id":0,"groupId":0,"type":"","detailType":"","content":"","sort":0,"cover":"","quantity":0,"amount":0.0,"remark":""}]} + +class Rewards { + Rewards({Rule? rule, PropsGroup? propsGroup}) { + _rule = rule; + _propsGroup = propsGroup; + } + + Rewards.fromJson(dynamic json) { + _rule = json['rule'] != null ? Rule.fromJson(json['rule']) : null; + _propsGroup = + json['propsGroup'] != null + ? PropsGroup.fromJson(json['propsGroup']) + : null; + } + + Rule? _rule; + PropsGroup? _propsGroup; + + Rule? get rule => _rule; + + PropsGroup? get propsGroup => _propsGroup; + + Map toJson() { + final map = {}; + if (_rule != null) { + map['rule'] = _rule?.toJson(); + } + if (_propsGroup != null) { + map['propsGroup'] = _propsGroup?.toJson(); + } + return map; + } +} + +/// id : 0 +/// name : "" +/// shelfStatus : false +/// activityRewardProps : [{"id":0,"groupId":0,"type":"","detailType":"","content":"","sort":0,"cover":"","quantity":0,"amount":0.0,"remark":""}] + +class PropsGroup { + PropsGroup({ + String? id, + String? name, + bool? shelfStatus, + List? activityRewardProps, + }) { + _id = id; + _name = name; + _shelfStatus = shelfStatus; + _activityRewardProps = activityRewardProps; + } + + PropsGroup.fromJson(dynamic json) { + _id = json['id']; + _name = json['name']; + _shelfStatus = json['shelfStatus']; + if (json['activityRewardProps'] != null) { + _activityRewardProps = []; + json['activityRewardProps'].forEach((v) { + _activityRewardProps?.add(ActivityRewardProps.fromJson(v)); + }); + } + } + + String? _id; + String? _name; + bool? _shelfStatus; + List? _activityRewardProps; + + String? get id => _id; + + String? get name => _name; + + bool? get shelfStatus => _shelfStatus; + + List? get activityRewardProps => _activityRewardProps; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['name'] = _name; + map['shelfStatus'] = _shelfStatus; + if (_activityRewardProps != null) { + map['activityRewardProps'] = + _activityRewardProps?.map((v) => v.toJson()).toList(); + } + return map; + } +} + +/// id : 0 +/// groupId : 0 +/// type : "" +/// detailType : "" +/// content : "" +/// sort : 0 +/// cover : "" +/// quantity : 0 +/// amount : 0.0 +/// remark : "" + +class ActivityRewardProps { + ActivityRewardProps({ + String? id, + String? groupId, + String? type, + String? detailType, + String? content, + num? sort, + String? cover, + num? quantity, + num? amount, + String? remark, + }) { + _id = id; + _groupId = groupId; + _type = type; + _detailType = detailType; + _content = content; + _sort = sort; + _cover = cover; + _quantity = quantity; + _amount = amount; + _remark = remark; + } + + ActivityRewardProps.fromJson(dynamic json) { + _id = json['id']; + _groupId = json['groupId']; + _type = json['type']; + _detailType = json['detailType']; + _content = json['content']; + _sort = json['sort']; + _cover = json['cover']; + _quantity = json['quantity']; + _amount = json['amount']; + _remark = json['remark']; + } + + String? _id; + String? _groupId; + String? _type; + String? _detailType; + String? _content; + num? _sort; + String? _cover; + num? _quantity; + num? _amount; + String? _remark; + + String? get id => _id; + + String? get groupId => _groupId; + + String? get type => _type; + + String? get detailType => _detailType; + + String? get content => _content; + + num? get sort => _sort; + + String? get cover => _cover; + + num? get quantity => _quantity; + + num? get amount => _amount; + + String? get remark => _remark; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['groupId'] = _groupId; + map['type'] = _type; + map['detailType'] = _detailType; + map['content'] = _content; + map['sort'] = _sort; + map['cover'] = _cover; + map['quantity'] = _quantity; + map['amount'] = _amount; + map['remark'] = _remark; + return map; + } +} + +/// id : 0 +/// sysOrigin : "" +/// activityType : "" +/// resourceGroupId : 0 +/// jsonData : "" +/// sort : 0 + +class Rule { + Rule({ + String? id, + String? sysOrigin, + String? activityType, + String? resourceGroupId, + String? jsonData, + num? sort, + }) { + _id = id; + _sysOrigin = sysOrigin; + _activityType = activityType; + _resourceGroupId = resourceGroupId; + _jsonData = jsonData; + _sort = sort; + } + + Rule.fromJson(dynamic json) { + _id = json['id']; + _sysOrigin = json['sysOrigin']; + _activityType = json['activityType']; + _resourceGroupId = json['resourceGroupId']; + _jsonData = json['jsonData']; + _sort = json['sort']; + } + + String? _id; + String? _sysOrigin; + String? _activityType; + String? _resourceGroupId; + String? _jsonData; + num? _sort; + + String? get id => _id; + + String? get sysOrigin => _sysOrigin; + + String? get activityType => _activityType; + + String? get resourceGroupId => _resourceGroupId; + + String? get jsonData => _jsonData; + + num? get sort => _sort; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['sysOrigin'] = _sysOrigin; + map['activityType'] = _activityType; + map['resourceGroupId'] = _resourceGroupId; + map['jsonData'] = _jsonData; + map['sort'] = _sort; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_start_page_res.dart b/lib/shared/business_logic/models/res/sc_start_page_res.dart new file mode 100644 index 0000000..389489e --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_start_page_res.dart @@ -0,0 +1,124 @@ +/// cover : "" +/// expireTime : 0 +/// id : 0 +/// sysOrigin : "" +/// type : "" + +class SCStartPageRes { + SCStartPageRes({ + String? cover, + num? expireTime, + String? id, + String? sysOrigin, + String? type, + List? user, + }) { + _cover = cover; + _expireTime = expireTime; + _id = id; + _sysOrigin = sysOrigin; + _type = type; + _user = user; + } + + SCStartPageRes.fromJson(dynamic json) { + _cover = json['cover']; + _expireTime = json['expireTime']; + _id = json['id']; + _sysOrigin = json['sysOrigin']; + _type = json['type']; + if (json['user'] != null) { + _user = []; + json['user'].forEach((v) { + _user?.add(StartPageUser.fromJson(v)); + }); + } + } + + String? _cover; + num? _expireTime; + String? _id; + String? _sysOrigin; + String? _type; + List? _user; + + SCStartPageRes copyWith({ + String? cover, + num? expireTime, + String? id, + String? sysOrigin, + String? type, + List? user, + }) => SCStartPageRes( + cover: cover ?? _cover, + expireTime: expireTime ?? _expireTime, + id: id ?? _id, + sysOrigin: sysOrigin ?? _sysOrigin, + type: type ?? _type, + user: user ?? _user, + ); + + String? get cover => _cover; + + num? get expireTime => _expireTime; + + String? get id => _id; + + String? get sysOrigin => _sysOrigin; + + String? get type => _type; + + List? get user => _user; + + Map toJson() { + final map = {}; + map['cover'] = _cover; + map['expireTime'] = _expireTime; + map['id'] = _id; + map['sysOrigin'] = _sysOrigin; + map['type'] = _type; + if (_user != null) { + map['user'] = _user?.map((v) => v.toJson()).toList(); + } + return map; + } +} + +class StartPageUser { + StartPageUser({String? account, String? nickName, String? avatar}) { + _account = account; + _nickName = nickName; + _avatar = avatar; + } + + StartPageUser.fromJson(dynamic json) { + _account = json['account']; + _nickName = json['nickName']; + _avatar = json['avatar']; + } + + String? _account; + String? _nickName; + String? _avatar; + + StartPageUser copyWith({String? account, String? nickName, String? avatar}) => + StartPageUser( + account: account ?? _account, + nickName: nickName ?? _nickName, + avatar: avatar ?? _avatar, + ); + + String? get account => _account; + + String? get nickName => _nickName; + + String? get avatar => _avatar; + + Map toJson() { + final map = {}; + map['account'] = _account; + map['nickName'] = _nickName; + map['avatar'] = _avatar; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_system_invit_message_res.dart b/lib/shared/business_logic/models/res/sc_system_invit_message_res.dart new file mode 100644 index 0000000..bd7f5d1 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_system_invit_message_res.dart @@ -0,0 +1,69 @@ +/// account : "" +/// countryCode : "" +/// countryName : "" +/// userAvatar : "" +/// userNickname : "" +/// actualAccount : "" + +class SCSystemInvitMessageRes { + SCSystemInvitMessageRes({ + String? account, + String? countryCode, + String? countryName, + String? userAvatar, + String? userNickname, + String? applyType, + String? content, + String? actualAccount,}){ + _account = account; + _countryCode = countryCode; + _countryName = countryName; + _userAvatar = userAvatar; + _userNickname = userNickname; + _actualAccount = actualAccount; + _applyType = applyType; + _content = content; +} + + SCSystemInvitMessageRes.fromJson(dynamic json) { + _account = json['account']; + _countryCode = json['countryCode']; + _countryName = json['countryName']; + _userAvatar = json['userAvatar']; + _userNickname = json['userNickname']; + _actualAccount = json['actualAccount']; + _applyType = json['applyType']; + _content = json['content']; + } + String? _account; + String? _countryCode; + String? _countryName; + String? _userAvatar; + String? _userNickname; + String? _actualAccount; + String? _applyType; + String? _content; + + String? get account => _account; + String? get countryCode => _countryCode; + String? get countryName => _countryName; + String? get userAvatar => _userAvatar; + String? get userNickname => _userNickname; + String? get actualAccount => _actualAccount; + String? get applyType => _applyType; + String? get content => _content; + + Map toJson() { + final map = {}; + map['account'] = _account; + map['countryCode'] = _countryCode; + map['countryName'] = _countryName; + map['userAvatar'] = _userAvatar; + map['userNickname'] = _userNickname; + map['actualAccount'] = _actualAccount; + map['applyType'] = _applyType; + map['content'] = _content; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_task_list_res.dart b/lib/shared/business_logic/models/res/sc_task_list_res.dart new file mode 100644 index 0000000..a51e5d6 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_task_list_res.dart @@ -0,0 +1,131 @@ +/// taskType : 0 +/// taskName : "" +/// taskDesc : "" +/// jumpPage : "" +/// conditionType : "" +/// sortOrder : 0 +/// taskId : 0 +/// taskStatus : 0 +/// cover : "" +/// taskIcon : "" +/// isRewardCollected : 0 +/// quantity : 0 + +class SCTaskListRes { + SCTaskListRes({ + num? taskType, + String? taskName, + String? taskDesc, + String? jumpPage, + String? conditionType, + num? sortOrder, + num? taskId, + num? taskStatus, + String? cover, + String? taskIcon, + String? conditionValue, + String? completedValue, + num? isRewardCollected, + num? targetValue, + num? quantity, + }) { + _taskType = taskType; + _taskName = taskName; + _taskDesc = taskDesc; + _jumpPage = jumpPage; + _conditionType = conditionType; + _sortOrder = sortOrder; + _taskId = taskId; + _taskStatus = taskStatus; + _cover = cover; + _taskIcon = taskIcon; + _conditionValue = conditionValue; + _completedValue = completedValue; + _isRewardCollected = isRewardCollected; + _quantity = quantity; + _targetValue = targetValue; + } + + SCTaskListRes.fromJson(dynamic json) { + _taskType = json['taskType']; + _taskName = json['taskName']; + _taskDesc = json['taskDesc']; + _jumpPage = json['jumpPage']; + _conditionType = json['conditionType']; + _sortOrder = json['sortOrder']; + _taskId = json['taskId']; + _taskStatus = json['taskStatus']; + _cover = json['cover']; + _taskIcon = json['taskIcon']; + _conditionValue = json['conditionValue']; + _completedValue = json['completedValue']; + _isRewardCollected = json['isRewardCollected']; + _quantity = json['quantity']; + _targetValue = json['targetValue']; + } + + num? _taskType; + String? _taskName; + String? _taskDesc; + String? _jumpPage; + String? _conditionType; + num? _sortOrder; + num? _taskId; + num? _taskStatus; + String? _cover; + String? _taskIcon; + String? _conditionValue; + String? _completedValue; + num? _isRewardCollected; + num? _quantity; + num? _targetValue; + + num? get taskType => _taskType; + + String? get taskName => _taskName; + + String? get taskDesc => _taskDesc; + + String? get jumpPage => _jumpPage; + + String? get conditionType => _conditionType; + + num? get sortOrder => _sortOrder; + + num? get taskId => _taskId; + + num? get taskStatus => _taskStatus; + + String? get cover => _cover; + + String? get taskIcon => _taskIcon; + + String? get conditionValue => _conditionValue; + + String? get completedValue => _completedValue; + + num? get isRewardCollected => _isRewardCollected; + + num? get quantity => _quantity; + num? get targetValue => _targetValue; + + Map toJson() { + final map = {}; + map['taskType'] = _taskType; + map['taskName'] = _taskName; + map['taskDesc'] = _taskDesc; + map['jumpPage'] = _jumpPage; + map['conditionType'] = _conditionType; + map['sortOrder'] = _sortOrder; + map['taskId'] = _taskId; + map['taskStatus'] = _taskStatus; + map['cover'] = _cover; + map['taskIcon'] = _taskIcon; + map['conditionValue'] = _conditionValue; + map['completedValue'] = _completedValue; + map['isRewardCollected'] = _isRewardCollected; + map['quantity'] = _quantity; + map['targetValue'] = _targetValue; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_top_four_with_reward_res.dart b/lib/shared/business_logic/models/res/sc_top_four_with_reward_res.dart new file mode 100644 index 0000000..22aaabb --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_top_four_with_reward_res.dart @@ -0,0 +1,57 @@ +/// userId : "1957345312961527809" +/// account : "8826602" +/// nickname : "8826602" +/// avatar : "https://tkm-yumi.oss-ap-southeast-1.aliyuncs.com/default_avatar/user.png" +/// rank : 1 +/// quantity : "6K" + +class SCTopFourWithRewardRes { + SCTopFourWithRewardRes({ + String? userId, + String? account, + String? nickname, + String? avatar, + num? rank, + String? quantity,}){ + _userId = userId; + _account = account; + _nickname = nickname; + _avatar = avatar; + _rank = rank; + _quantity = quantity; +} + + SCTopFourWithRewardRes.fromJson(dynamic json) { + _userId = json['userId']; + _account = json['account']; + _nickname = json['nickname']; + _avatar = json['avatar']; + _rank = json['rank']; + _quantity = json['quantity']; + } + String? _userId; + String? _account; + String? _nickname; + String? _avatar; + num? _rank; + String? _quantity; + + String? get userId => _userId; + String? get account => _account; + String? get nickname => _nickname; + String? get avatar => _avatar; + num? get rank => _rank; + String? get quantity => _quantity; + + Map toJson() { + final map = {}; + map['userId'] = _userId; + map['account'] = _account; + map['nickname'] = _nickname; + map['avatar'] = _avatar; + map['rank'] = _rank; + map['quantity'] = _quantity; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_user_counter_res.dart b/lib/shared/business_logic/models/res/sc_user_counter_res.dart new file mode 100644 index 0000000..8c8c449 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_user_counter_res.dart @@ -0,0 +1,45 @@ +/// counterType : "" +/// quantity : 0 +/// formatQuantity : "" + +class SCUserCounterRes { + SCUserCounterRes({String? counterType, String? quantity, String? formatQuantity}) { + _counterType = counterType; + _quantity = quantity; + _formatQuantity = formatQuantity; + } + + SCUserCounterRes.fromJson(dynamic json) { + _counterType = json['counterType']; + _quantity = json['quantity']; + _formatQuantity = json['formatQuantity']; + } + + String? _counterType; + String? _quantity; + String? _formatQuantity; + + SCUserCounterRes copyWith({ + String? counterType, + String? quantity, + String? formatQuantity, + }) => SCUserCounterRes( + counterType: counterType ?? _counterType, + quantity: quantity ?? _quantity, + formatQuantity: formatQuantity ?? _formatQuantity, + ); + + String? get counterType => _counterType; + + String? get quantity => _quantity; + + String? get formatQuantity => _formatQuantity; + + Map toJson() { + final map = {}; + map['counterType'] = _counterType; + map['quantity'] = _quantity; + map['formatQuantity'] = _formatQuantity; + return map; + } +} diff --git a/lib/shared/business_logic/models/res/sc_user_identity_res.dart b/lib/shared/business_logic/models/res/sc_user_identity_res.dart new file mode 100644 index 0000000..7abeba4 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_user_identity_res.dart @@ -0,0 +1,90 @@ +/// agent : true +/// anchor : true +/// bd : false +/// freightAgent : false + +class SCUserIdentityRes { + SCUserIdentityRes({ + bool? agent, + bool? anchor, + bool? bd, + bool? admin, + bool? superAdmin, + bool? bdLeader, + bool? superFreightAgent, + bool? manager, + bool? freightAgent,}){ + _agent = agent; + _superFreightAgent = superFreightAgent; + _anchor = anchor; + _bd = bd; + _manager = manager; + _admin = admin; + _superAdmin = superAdmin; + _bdLeader = bdLeader; + _freightAgent = freightAgent; +} + + SCUserIdentityRes.fromJson(dynamic json) { + _agent = json['agent']; + _anchor = json['anchor']; + _bd = json['bd']; + _superAdmin = json['superAdmin']; + _bdLeader = json['bdLeader']; + _superFreightAgent = json['superFreightAgent']; + _admin = json['admin']; + _freightAgent = json['freightAgent']; + _manager = json['manager']; + } + bool? _agent; + bool? _anchor; + bool? _bd; + bool? _admin; + bool? _superAdmin; + bool? _bdLeader; + bool? _freightAgent; + bool? _manager; + bool? _superFreightAgent; +SCUserIdentityRes copyWith({ bool? agent, + bool? anchor, + bool? bd, + bool? admin, + bool? superAdmin, + bool? manager, + bool? superFreightAgent, + bool? freightAgent, +}) => SCUserIdentityRes( agent: agent ?? _agent, + anchor: anchor ?? _anchor, + bd: bd ?? _bd, + superAdmin: superAdmin ?? _superAdmin, + bdLeader: bdLeader ?? _bdLeader, + admin: admin ?? _admin, + manager: manager ?? _manager, + superFreightAgent: superFreightAgent ?? _superFreightAgent, + freightAgent: freightAgent ?? _freightAgent, +); + bool? get agent => _agent; + bool? get anchor => _anchor; + bool? get bd => _bd; + bool? get admin => _admin; + bool? get manager => _manager; + bool? get superAdmin => _superAdmin; + bool? get bdLeader => _bdLeader; + bool? get superFreightAgent => _superFreightAgent; + bool? get freightAgent => _freightAgent; + + Map toJson() { + final map = {}; + map['agent'] = _agent; + map['anchor'] = _anchor; + map['bd'] = _bd; + map['manager'] = _manager; + map['admin'] = _admin; + map['superAdmin'] = _superAdmin; + map['bdLeader'] = _bdLeader; + map['superFreightAgent'] = _superFreightAgent; + map['freightAgent'] = _freightAgent; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_user_level_exp_res.dart b/lib/shared/business_logic/models/res/sc_user_level_exp_res.dart new file mode 100644 index 0000000..1c6affb --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_user_level_exp_res.dart @@ -0,0 +1,60 @@ +/// level : 0 +/// thatExperience : 0 +/// thatExperienceFormat : "" +/// nextExperience : 0 +/// nextExperienceFormat : "" + +class SCUserLevelExpRes { + SCUserLevelExpRes({ + num? level, + String? thatExperience, + String? thatExperienceFormat, + String? nextExperience, + String? nextExperienceFormat,}){ + _level = level; + _thatExperience = thatExperience; + _thatExperienceFormat = thatExperienceFormat; + _nextExperience = nextExperience; + _nextExperienceFormat = nextExperienceFormat; +} + + SCUserLevelExpRes.fromJson(dynamic json) { + _level = json['level']; + _thatExperience = json['thatExperience']; + _thatExperienceFormat = json['thatExperienceFormat']; + _nextExperience = json['nextExperience']; + _nextExperienceFormat = json['nextExperienceFormat']; + } + num? _level; + String? _thatExperience; + String? _thatExperienceFormat; + String? _nextExperience; + String? _nextExperienceFormat; +SCUserLevelExpRes copyWith({ num? level, + String? thatExperience, + String? thatExperienceFormat, + String? nextExperience, + String? nextExperienceFormat, +}) => SCUserLevelExpRes( level: level ?? _level, + thatExperience: thatExperience ?? _thatExperience, + thatExperienceFormat: thatExperienceFormat ?? _thatExperienceFormat, + nextExperience: nextExperience ?? _nextExperience, + nextExperienceFormat: nextExperienceFormat ?? _nextExperienceFormat, +); + num? get level => _level; + String? get thatExperience => _thatExperience; + String? get thatExperienceFormat => _thatExperienceFormat; + String? get nextExperience => _nextExperience; + String? get nextExperienceFormat => _nextExperienceFormat; + + Map toJson() { + final map = {}; + map['level'] = _level; + map['thatExperience'] = _thatExperience; + map['thatExperienceFormat'] = _thatExperienceFormat; + map['nextExperience'] = _nextExperience; + map['nextExperienceFormat'] = _nextExperienceFormat; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_user_red_packet_grab_res.dart b/lib/shared/business_logic/models/res/sc_user_red_packet_grab_res.dart new file mode 100644 index 0000000..b0a3562 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_user_red_packet_grab_res.dart @@ -0,0 +1,36 @@ +/// packetId : "" +/// amount : 0 +/// senderUserId : 0 + +class SCUserRedPacketGrabRes { + SCUserRedPacketGrabRes({ + String? packetId, + num? amount, + num? senderUserId,}){ + _packetId = packetId; + _amount = amount; + _senderUserId = senderUserId; +} + + SCUserRedPacketGrabRes.fromJson(dynamic json) { + _packetId = json['packetId']; + _amount = json['amount']; + _senderUserId = json['senderUserId']; + } + String? _packetId; + num? _amount; + num? _senderUserId; + + String? get packetId => _packetId; + num? get amount => _amount; + num? get senderUserId => _senderUserId; + + Map toJson() { + final map = {}; + map['packetId'] = _packetId; + map['amount'] = _amount; + map['senderUserId'] = _senderUserId; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_user_red_packet_send_res.dart b/lib/shared/business_logic/models/res/sc_user_red_packet_send_res.dart new file mode 100644 index 0000000..0fab3a7 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_user_red_packet_send_res.dart @@ -0,0 +1,184 @@ +/// packetId : "" +/// senderUserId : "" +/// receiverUserId : "" +/// senderInfo : {"userId":"","account":"","nickName":"","avatar":""} +/// receiverInfo : {"userId":"","account":"","nickName":"","avatar":""} +/// totalAmount : 0 +/// handlingFee : 0 +/// status : 0 +/// expireTime : 0 +/// grabbedAt : 0 +/// createTime : 0 + +class SCUserRedPacketSendRes { + SCUserRedPacketSendRes({ + String? packetId, + num? senderUserId, + num? receiverUserId, + SenderInfo? senderInfo, + ReceiverInfo? receiverInfo, + num? totalAmount, + num? handlingFee, + num? status, + int? expireTime, + int? grabbedAt, + int? createTime,}){ + _packetId = packetId; + _senderUserId = senderUserId; + _receiverUserId = receiverUserId; + _senderInfo = senderInfo; + _receiverInfo = receiverInfo; + _totalAmount = totalAmount; + _handlingFee = handlingFee; + _status = status; + _expireTime = expireTime; + _grabbedAt = grabbedAt; + _createTime = createTime; +} + + SCUserRedPacketSendRes.fromJson(dynamic json) { + _packetId = json['packetId']; + _senderUserId = json['senderUserId']; + _receiverUserId = json['receiverUserId']; + _senderInfo = json['senderInfo'] != null ? SenderInfo.fromJson(json['senderInfo']) : null; + _receiverInfo = json['receiverInfo'] != null ? ReceiverInfo.fromJson(json['receiverInfo']) : null; + _totalAmount = json['totalAmount']; + _handlingFee = json['handlingFee']; + _status = json['status']; + _expireTime = json['expireTime']; + _grabbedAt = json['grabbedAt']; + _createTime = json['createTime']; + } + String? _packetId; + num? _senderUserId; + num? _receiverUserId; + SenderInfo? _senderInfo; + ReceiverInfo? _receiverInfo; + num? _totalAmount; + num? _handlingFee; + num? _status; + int? _expireTime; + int? _grabbedAt; + int? _createTime; + + String? get packetId => _packetId; + num? get senderUserId => _senderUserId; + num? get receiverUserId => _receiverUserId; + SenderInfo? get senderInfo => _senderInfo; + ReceiverInfo? get receiverInfo => _receiverInfo; + num? get totalAmount => _totalAmount; + num? get handlingFee => _handlingFee; + num? get status => _status; + int? get expireTime => _expireTime; + int? get grabbedAt => _grabbedAt; + int? get createTime => _createTime; + + Map toJson() { + final map = {}; + map['packetId'] = _packetId; + map['senderUserId'] = _senderUserId; + map['receiverUserId'] = _receiverUserId; + if (_senderInfo != null) { + map['senderInfo'] = _senderInfo?.toJson(); + } + if (_receiverInfo != null) { + map['receiverInfo'] = _receiverInfo?.toJson(); + } + map['totalAmount'] = _totalAmount; + map['handlingFee'] = _handlingFee; + map['status'] = _status; + map['expireTime'] = _expireTime; + map['grabbedAt'] = _grabbedAt; + map['createTime'] = _createTime; + return map; + } + +} + +/// userId : "" +/// account : "" +/// nickName : "" +/// avatar : "" + +class ReceiverInfo { + ReceiverInfo({ + num? userId, + String? account, + String? nickName, + String? avatar,}){ + _userId = userId; + _account = account; + _nickName = nickName; + _avatar = avatar; +} + + ReceiverInfo.fromJson(dynamic json) { + _userId = json['userId']; + _account = json['account']; + _nickName = json['nickName']; + _avatar = json['avatar']; + } + num? _userId; + String? _account; + String? _nickName; + String? _avatar; + + num? get userId => _userId; + String? get account => _account; + String? get nickName => _nickName; + String? get avatar => _avatar; + + Map toJson() { + final map = {}; + map['userId'] = _userId; + map['account'] = _account; + map['nickName'] = _nickName; + map['avatar'] = _avatar; + return map; + } + +} + +/// userId : "" +/// account : "" +/// nickName : "" +/// avatar : "" + +class SenderInfo { + SenderInfo({ + num? userId, + String? account, + String? nickName, + String? avatar,}){ + _userId = userId; + _account = account; + _nickName = nickName; + _avatar = avatar; +} + + SenderInfo.fromJson(dynamic json) { + _userId = json['userId']; + _account = json['account']; + _nickName = json['nickName']; + _avatar = json['avatar']; + } + num? _userId; + String? _account; + String? _nickName; + String? _avatar; + + num? get userId => _userId; + String? get account => _account; + String? get nickName => _nickName; + String? get avatar => _avatar; + + Map toJson() { + final map = {}; + map['userId'] = _userId; + map['account'] = _account; + map['nickName'] = _nickName; + map['avatar'] = _avatar; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_version_manage_latest_res.dart b/lib/shared/business_logic/models/res/sc_version_manage_latest_res.dart new file mode 100644 index 0000000..dc2ea49 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_version_manage_latest_res.dart @@ -0,0 +1,99 @@ +/// id : 0 +/// platform : "" +/// version : "" +/// forceUpdate : false +/// appType : "" +/// review : false +/// patch : false +/// updateDescribe : "" +/// updateWorshipDescribe : "" +/// downloadUrl : "" +/// buildVersion : 0 +/// createTime : 0 + +class SCVersionManageLatestRes { + SCVersionManageLatestRes({ + String? id, + String? platform, + String? version, + bool? forceUpdate, + String? appType, + bool? review, + bool? patch, + String? updateDescribe, + String? updateWorshipDescribe, + String? downloadUrl, + num? buildVersion, + num? createTime,}){ + _id = id; + _platform = platform; + _version = version; + _forceUpdate = forceUpdate; + _appType = appType; + _review = review; + _patch = patch; + _updateDescribe = updateDescribe; + _updateWorshipDescribe = updateWorshipDescribe; + _downloadUrl = downloadUrl; + _buildVersion = buildVersion; + _createTime = createTime; +} + + SCVersionManageLatestRes.fromJson(dynamic json) { + _id = json['id']; + _platform = json['platform']; + _version = json['version']; + _forceUpdate = json['forceUpdate']; + _appType = json['appType']; + _review = json['review']; + _patch = json['patch']; + _updateDescribe = json['updateDescribe']; + _updateWorshipDescribe = json['updateWorshipDescribe']; + _downloadUrl = json['downloadUrl']; + _buildVersion = json['buildVersion']; + _createTime = json['createTime']; + } + String? _id; + String? _platform; + String? _version; + bool? _forceUpdate; + String? _appType; + bool? _review; + bool? _patch; + String? _updateDescribe; + String? _updateWorshipDescribe; + String? _downloadUrl; + num? _buildVersion; + num? _createTime; + + String? get id => _id; + String? get platform => _platform; + String? get version => _version; + bool? get forceUpdate => _forceUpdate; + String? get appType => _appType; + bool? get review => _review; + bool? get patch => _patch; + String? get updateDescribe => _updateDescribe; + String? get updateWorshipDescribe => _updateWorshipDescribe; + String? get downloadUrl => _downloadUrl; + num? get buildVersion => _buildVersion; + num? get createTime => _createTime; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['platform'] = _platform; + map['version'] = _version; + map['forceUpdate'] = _forceUpdate; + map['appType'] = _appType; + map['review'] = _review; + map['patch'] = _patch; + map['updateDescribe'] = _updateDescribe; + map['updateWorshipDescribe'] = _updateWorshipDescribe; + map['downloadUrl'] = _downloadUrl; + map['buildVersion'] = _buildVersion; + map['createTime'] = _createTime; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/sc_violation_handle_res.dart b/lib/shared/business_logic/models/res/sc_violation_handle_res.dart new file mode 100644 index 0000000..ff424e8 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_violation_handle_res.dart @@ -0,0 +1,57 @@ +/// recordId : 0 +/// bizNo : "" +/// message : "" +/// success : false +/// originalContent : "" +/// adjustedContent : "" + +class SCViolationHandleRes { + SCViolationHandleRes({ + num? recordId, + String? bizNo, + String? message, + bool? success, + String? originalContent, + String? adjustedContent,}){ + _recordId = recordId; + _bizNo = bizNo; + _message = message; + _success = success; + _originalContent = originalContent; + _adjustedContent = adjustedContent; +} + + SCViolationHandleRes.fromJson(dynamic json) { + _recordId = json['recordId']; + _bizNo = json['bizNo']; + _message = json['message']; + _success = json['success']; + _originalContent = json['originalContent']; + _adjustedContent = json['adjustedContent']; + } + num? _recordId; + String? _bizNo; + String? _message; + bool? _success; + String? _originalContent; + String? _adjustedContent; + + num? get recordId => _recordId; + String? get bizNo => _bizNo; + String? get message => _message; + bool? get success => _success; + String? get originalContent => _originalContent; + String? get adjustedContent => _adjustedContent; + + Map toJson() { + final map = {}; + map['recordId'] = _recordId; + map['bizNo'] = _bizNo; + map['message'] = _message; + map['success'] = _success; + map['originalContent'] = _originalContent; + map['adjustedContent'] = _adjustedContent; + return map; + } + +} \ No newline at end of file diff --git a/lib/shared/business_logic/models/res/store_list_res.dart b/lib/shared/business_logic/models/res/store_list_res.dart new file mode 100644 index 0000000..7c4a434 --- /dev/null +++ b/lib/shared/business_logic/models/res/store_list_res.dart @@ -0,0 +1,174 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// id : 0 +/// label : "" +/// discount : 0.0 +/// propsResources : {"id":0,"sysOrigin":"","type":"","code":"","name":"","cover":"","sourceUrl":"","expand":"","amount":0.0,"del":false,"roomSendCoverUrl":"","roomOpenedUrl":"","roomNotOpenedUrl":"","imSendCoverUrl":"","imOpenedUrl":"","imNotOpenedUrl":"","imOpenedUrlTwo":"","createTime":0,"updateTime":0} +/// propsPrices : [{"days":0,"amount":0.0,"discountAmount":0.0}] +/// diamondPrices : [{"days":0,"amount":0.0,"discountAmount":0.0}] + +class StoreListRes { + StoreListRes({ + String? id, + String? label, + num? discount, + PropsResources? propsResources, + List? propsPrices, + List? diamondPrices,}){ + _id = id; + _label = label; + _discount = discount; + _propsResources = propsResources; + _propsPrices = propsPrices; + _diamondPrices = diamondPrices; +} + + StoreListRes.fromJson(dynamic json) { + _id = json['id']; + _label = json['label']; + _discount = json['discount']; + _propsResources = json['propsResources'] != null ? PropsResources.fromJson(json['propsResources']) : null; + if (json['propsPrices'] != null) { + _propsPrices = []; + json['propsPrices'].forEach((v) { + _propsPrices?.add(PropsPrices.fromJson(v)); + }); + } + if (json['diamondPrices'] != null) { + _diamondPrices = []; + json['diamondPrices'].forEach((v) { + _diamondPrices?.add(DiamondPrices.fromJson(v)); + }); + } + } + String? _id; + String? _label; + num? _discount; + PropsResources? _propsResources; + List? _propsPrices; + List? _diamondPrices; +StoreListRes copyWith({ String? id, + String? label, + num? discount, + PropsResources? propsResources, + List? propsPrices, + List? diamondPrices, +}) => StoreListRes( id: id ?? _id, + label: label ?? _label, + discount: discount ?? _discount, + propsResources: propsResources ?? _propsResources, + propsPrices: propsPrices ?? _propsPrices, + diamondPrices: diamondPrices ?? _diamondPrices, +); + String? get id => _id; + String? get label => _label; + num? get discount => _discount; + PropsResources? get propsResources => _propsResources; + List? get propsPrices => _propsPrices; + List? get diamondPrices => _diamondPrices; + + Map toJson() { + final map = {}; + map['id'] = _id; + map['label'] = _label; + map['discount'] = _discount; + if (_propsResources != null) { + map['propsResources'] = _propsResources?.toJson(); + } + if (_propsPrices != null) { + map['propsPrices'] = _propsPrices?.map((v) => v.toJson()).toList(); + } + if (_diamondPrices != null) { + map['diamondPrices'] = _diamondPrices?.map((v) => v.toJson()).toList(); + } + return map; + } + +} + +/// days : 0 +/// amount : 0.0 +/// discountAmount : 0.0 + +class DiamondPrices { + DiamondPrices({ + num? days, + num? amount, + num? discountAmount,}){ + _days = days; + _amount = amount; + _discountAmount = discountAmount; +} + + DiamondPrices.fromJson(dynamic json) { + _days = json['days']; + _amount = json['amount']; + _discountAmount = json['discountAmount']; + } + num? _days; + num? _amount; + num? _discountAmount; +DiamondPrices copyWith({ num? days, + num? amount, + num? discountAmount, +}) => DiamondPrices( days: days ?? _days, + amount: amount ?? _amount, + discountAmount: discountAmount ?? _discountAmount, +); + num? get days => _days; + num? get amount => _amount; + num? get discountAmount => _discountAmount; + + Map toJson() { + final map = {}; + map['days'] = _days; + map['amount'] = _amount; + map['discountAmount'] = _discountAmount; + return map; + } + +} + +/// days : 0 +/// amount : 0.0 +/// discountAmount : 0.0 + +class PropsPrices { + PropsPrices({ + num? days, + num? amount, + num? discountAmount,}){ + _days = days; + _amount = amount; + _discountAmount = discountAmount; +} + + PropsPrices.fromJson(dynamic json) { + _days = json['days']; + _amount = json['amount']; + _discountAmount = json['discountAmount']; + } + num? _days; + num? _amount; + num? _discountAmount; +PropsPrices copyWith({ num? days, + num? amount, + num? discountAmount, +}) => PropsPrices( days: days ?? _days, + amount: amount ?? _amount, + discountAmount: discountAmount ?? _discountAmount, +); + num? get days => _days; + num? get amount => _amount; + num? get discountAmount => _discountAmount; + + Map toJson() { + final map = {}; + map['days'] = _days; + map['amount'] = _amount; + map['discountAmount'] = _discountAmount; + return map; + } + +} + diff --git a/lib/shared/business_logic/models/res/user_count_guard_res.dart b/lib/shared/business_logic/models/res/user_count_guard_res.dart new file mode 100644 index 0000000..088b749 --- /dev/null +++ b/lib/shared/business_logic/models/res/user_count_guard_res.dart @@ -0,0 +1,47 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +/// userProfile : {"id":0,"account":"","userAvatar":"","userNickname":"","userSex":0,"age":0,"accountStatus":"","freezingTime":0,"countryId":0,"countryName":"","countryCode":"","originSys":"","sysOriginChild":"","del":false,"createTime":0,"bornYear":0,"bornMonth":0,"bornDay":0,"ownSpecialId":{"account":"","expiredTime":0},"useProps":[{"userId":0,"propsResources":{"id":0,"type":"","code":"","name":"","cover":"","sourceUrl":"","expand":"","amount":0.0},"expireTime":0}],"sameRegion":false,"wearBadge":[{"id":0,"userId":0,"badgeLevel":0,"milestone":0,"badgeName":"","type":"","badgeKey":"","selectUrl":"","notSelectUrl":"","animationUrl":"","expireTime":0}]} +/// quantity : 0 +/// quantityFormat : "" + +class UserCountGuardRes { + UserCountGuardRes({ + SocialChatUserProfile? userProfile, + String? quantity, + String? quantityFormat,}){ + _userProfile = userProfile; + _quantity = quantity; + _quantityFormat = quantityFormat; +} + + UserCountGuardRes.fromJson(dynamic json) { + _userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null; + _quantity = json['quantity']; + _quantityFormat = json['quantityFormat']; + } + SocialChatUserProfile? _userProfile; + String? _quantity; + String? _quantityFormat; +UserCountGuardRes copyWith({ SocialChatUserProfile? userProfile, + String? quantity, + String? quantityFormat, +}) => UserCountGuardRes( userProfile: userProfile ?? _userProfile, + quantity: quantity ?? _quantity, + quantityFormat: quantityFormat ?? _quantityFormat, +); + SocialChatUserProfile? get userProfile => _userProfile; + String? get quantity => _quantity; + String? get quantityFormat => _quantityFormat; + + Map toJson() { + final map = {}; + if (_userProfile != null) { + map['userProfile'] = _userProfile?.toJson(); + } + map['quantity'] = _quantity; + map['quantityFormat'] = _quantityFormat; + return map; + } + +} + diff --git a/lib/shared/business_logic/models/res/version_manage_lates_review_res.dart b/lib/shared/business_logic/models/res/version_manage_lates_review_res.dart new file mode 100644 index 0000000..74f69b2 --- /dev/null +++ b/lib/shared/business_logic/models/res/version_manage_lates_review_res.dart @@ -0,0 +1,36 @@ +import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart'; + +/// latest : {"id":0,"platform":"","version":"","forceUpdate":false,"appType":"","review":false,"patch":false,"updateDescribe":"","updateWorshipDescribe":"","downloadUrl":"","buildVersion":0,"createTime":0} +/// review : {"id":0,"platform":"","version":"","forceUpdate":false,"appType":"","review":false,"patch":false,"updateDescribe":"","updateWorshipDescribe":"","downloadUrl":"","buildVersion":0,"createTime":0} + +class VersionManageLatesReviewRes { + VersionManageLatesReviewRes({ + SCVersionManageLatestRes? latest, + SCVersionManageLatestRes? review,}){ + _latest = latest; + _review = review; +} + + VersionManageLatesReviewRes.fromJson(dynamic json) { + _latest = json['latest'] != null ? SCVersionManageLatestRes.fromJson(json['latest']) : null; + _review = json['review'] != null ? SCVersionManageLatestRes.fromJson(json['review']) : null; + } + SCVersionManageLatestRes? _latest; + SCVersionManageLatestRes? _review; + + SCVersionManageLatestRes? get latest => _latest; + SCVersionManageLatestRes? get review => _review; + + Map toJson() { + final map = {}; + if (_latest != null) { + map['latest'] = _latest?.toJson(); + } + if (_review != null) { + map['review'] = _review?.toJson(); + } + return map; + } + +} + diff --git a/lib/shared/business_logic/repositories/config_repository.dart b/lib/shared/business_logic/repositories/config_repository.dart new file mode 100644 index 0000000..baa9262 --- /dev/null +++ b/lib/shared/business_logic/repositories/config_repository.dart @@ -0,0 +1,72 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +import 'package:yumi/shared/business_logic/models/res/country_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart'; +import 'package:yumi/shared/business_logic/models/res/version_manage_lates_review_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart'; + +abstract class SocialChatConfigRepository { + ///国家列表 + Future loadCountry(); + + ///推荐国家列表 + Future loadTopSix(); + + ///app启动页. + Future> getStartPage(); + + ///系统参数配置列表. + Future getConfig(); + + ///最近活动通知列表. + Future getNoticeMessage(); + + + + + + ///获得客服. + Future getCustomerService(); + + ///获取平台banner. + Future> getBanner({List?types}); + + ///获取商品配置 + Future> productConfig(); + + ///谷歌内购 + Future googlePay( + String product, + String signature, + String purchaseData, { + String? friendId, + }); + + ///苹果内购 + Future applePay( + String product, + String receipt, + String transaction, { + String? friendId, + }); + + ///等级资源 + Future configLevel(); + + + + ///获取APP最新版本 + Future versionManageLatest(); + + ///获取最新及正在审核版本 + Future versionManageLatestReview(); + + ///获得客服 + Future customerService(); + +} diff --git a/lib/shared/business_logic/repositories/general_repository.dart b/lib/shared/business_logic/repositories/general_repository.dart new file mode 100644 index 0000000..280d8c6 --- /dev/null +++ b/lib/shared/business_logic/repositories/general_repository.dart @@ -0,0 +1,7 @@ + +import 'dart:io'; + +abstract class SocialChatGeneralRepository { + ///上传文件 + Future upload(File image); +} \ No newline at end of file diff --git a/lib/shared/business_logic/repositories/gift_repository.dart b/lib/shared/business_logic/repositories/gift_repository.dart new file mode 100644 index 0000000..8d4587c --- /dev/null +++ b/lib/shared/business_logic/repositories/gift_repository.dart @@ -0,0 +1,6 @@ +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; + +abstract class SocialChatGiftRepository { + ///礼物墙 + Future> giftWall(String userId); +} diff --git a/lib/shared/business_logic/repositories/room_repository.dart b/lib/shared/business_logic/repositories/room_repository.dart new file mode 100644 index 0000000..958e940 --- /dev/null +++ b/lib/shared/business_logic/repositories/room_repository.dart @@ -0,0 +1,249 @@ +import 'package:flutter/cupertino.dart'; + +import 'package:yumi/shared/business_logic/models/req/sc_give_away_gift_room_acceptscmd.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_backpack_res.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_by_group_res.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_is_follow_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_mic_go_up_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_contribute_level_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_join_black_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_member_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_detail_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_grab_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_reward_info_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_task_claimable_count_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_task_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart'; +import 'package:yumi/shared/business_logic/models/res/user_count_guard_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart'; + +abstract class SocialChatRoomRepository { + Future> discovery({bool? allRegion}); + + ///房间麦位列表 + Future> micList(String roomId); + + ///获取房间信息 + Future specific(String roomId); + + ///是否关注了房间 + Future isFollowRoom(String roomId); + + ///房间贡献等级 + Future contributeLevel(String roomId); + + ///房间本周贡献值 + Future roomContributionActivity(String roomId); + + ///上麦 + Future micGoUp(String roomId, num mickIndex, {String? eventType}); + + ///下麦 + Future micGoDown(String roomId, num mickIndex); + + ///这个用户是否可以被踢下麦 + Future kickOffMicrophone(String roomId, String kickUserId); + + ///锁麦 + Future micLock(String roomId, num mickIndex, bool lock); + + ///静音麦克风 + Future micMute(String roomId, num mickIndex, bool mute); + + ///T麦克风(房主&管理员) + Future micKill(String roomId, num mickIndex); + + ///房间在线用户 + Future> roomOnlineUsers(String roomId); + + ///房间用户资料卡 + Future roomUserCard(String roomId, String userId); + + ///礼物列表 + Future> giftList(); + + Future> giftListByGroup(); + + ///赠送礼物 + Future giveGift( + List acceptUserIds, + String giftId, + num quantity, + bool checkCombo, { + String? roomId, + SCGiveAwayGiftRoomAcceptsCmd? accepts, + String? dynamicContentId, + }); + + ///举报 + Future reported( + String reportUserId, + String reportedUserId, + num reportType, { + String? reportedContent, + num? relatedId, + String? imageUrls, + String? videoUrls, + }); + + ///房间搜索 + Future> searchRoom(String roomAccount); + + ///房间用户加入黑名单<踢出房间> + Future joinBlacklist( + String roomId, + String userId, + num outTime, + ); + + ///解除黑名单 + Future removeBlacklist(String roomId, String userId); + + ///守护列表 + Future> userCountGuard(String userId); + + ///麦位表情 + Future> emojiAll(); + + ///设置房间用户身份 + Future changeRoomRole( + String roomId, + String userType, + String changeUserId, + String operatorUserId, + String reason, + ); + + ///房间上锁 + Future roomLocked(String roomId, String password); + + ///房间解锁 + Future roomUnlocked(String roomId); + + ///更改房间设置信息 + Future updateRoomSetting( + String roomId, + String roomAcount, + BuildContext context, { + bool? touristMike, + String? takeMicRole, + bool? touristMsg, + String? joinGolds, + int? mikeSize, + bool? allowMusic, + bool? adminLockSeat, + bool? showHeartbeat, + bool? openKtvMode, + String? roomSpecialMikeType, + }); + + ///房间成员列表 + Future> roomMember(String roomId, {String? lastId}); + + ///房间贡献榜单 + Future> roomContributionRank( + String roomId, + String dataType, { + num size, + }); + + ///赠送幸运礼物 + Future giveLuckyGift( + List acceptUserIds, + String giftId, + num quantity, + bool checkCombo, { + String? roomId, + SCGiveAwayGiftRoomAcceptsCmd? accepts, + }); + + ///查询榜单前三名 + Future appLeaderboard(); + + ///礼物背包 + Future> giftBackpack(); + + ///查询房间火箭状态 + Future rocketStatus(String roomId); + + ///查询火箭配置 + Future> rocketConfigEnabled(); + + ///领取奖励 + Future rocketClaim(String roomId); + + ///处理房间违规 + Future roomViolationHandle( + String roomId, + int violationType, + int operationType, + String description, { + List? imageUrls, + }); + + ///查询房间红包列表 + Future> roomRedPacketList( + String roomId, + int current, { + int size = 20, + }); + + ///发送红包 + Future roomSendRedPacket( + String roomId, + String totalAmount, + String totalCount, + String expireMinutes, + ); + + ///抢红包 + Future roomRedPacketGrab(String packetId); + + ///查询红包详情 + Future roomRedPacketDetail(String packetId); + + ///查询房间奖励信息 + Future roomRewardInfo(String roomId); + + ///领取房间奖励房主收入 + Future roomRewardReceive(String roomId); + + ///发送平台红包 + Future sendPlatformRedPack( + String roomId, + String totalAmount, + String totalCount, + String expireMinutes, + ); + + ///获取活动礼物列表 + Future> giftActivityList(); + + ///获取任务列表 + Future roomTaskList(int taskCategory); + + ///领取任务奖励 + Future roomTaskClaim(String taskCode); + + ///获取当前可领取奖励数量 + Future roomTaskClaimableCount(); + + + + + + + +} diff --git a/lib/shared/business_logic/repositories/store_repository.dart b/lib/shared/business_logic/repositories/store_repository.dart new file mode 100644 index 0000000..202d8fb --- /dev/null +++ b/lib/shared/business_logic/repositories/store_repository.dart @@ -0,0 +1,35 @@ +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; + +abstract class SocialChatStoreRepository { + ///获取商店道具 + Future> storeList(String SCCurrencyType, String SCPropsType); + + ///道具商店购买 + Future storePurchasing( + String propsId, + String type, + String SCCurrencyType, + String days, { + String? acceptUserId, + }); + + ///用户道具背包 + Future> storeBackpack(String userId, String type); + + ///佩戴道具 + Future switchPropsUse(String type, String propsId, bool unload); + + ///切换主题 + Future themeSwitchUse(String themeId, bool unload); + + ///获取最新的自定义主题 + Future themeCustomizeLatest(); + + ///我的主题背包 + Future> roomThemeBackpack(); + + ///上传自定义主题 + Future roomThemeCustomize(String themeUrl); +} diff --git a/lib/shared/business_logic/repositories/user_repository.dart b/lib/shared/business_logic/repositories/user_repository.dart new file mode 100644 index 0000000..f741fdc --- /dev/null +++ b/lib/shared/business_logic/repositories/user_repository.dart @@ -0,0 +1,269 @@ +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart' hide WearBadge; + +import 'package:yumi/shared/business_logic/models/req/sc_mobile_auth_cmd.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_edit_room_info_res.dart'; +import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/sc_gold_record_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/message_friend_user_res.dart'; +import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_record_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_black_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_grab_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_send_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart'; + +abstract class SocialChatUserRepository { + Future getUser(String id); + + ///账号登录 + Future loginForAccount(String account, String pwd); + + ///渠道登录谷歌 苹果 + Future loginForChannel(String authType, String openId); + + ///注册 + Future regist( + String type, + String openId, + SCUserProfileCmd userProfileCmd, { + SCMobileAuthCmd? mobileAuthCmd, + String invitePeople, + }); + + ///关注的房间 + Future> followRoomList({String? lastId}); + + ///历史记录 + Future> trace({String? lastId}); + + ///加入的房间 + Future> joined(); + + ///我的房间 + Future myProfile(); + + ///创建房间 + Future createRoom(); + + ///加入房间 + Future entryRoom(String roomId, {String? pwd}); + + ///发送心跳 + Future heartbeat(String status, bool upMick, {String? roomId}); + + ///上麦心跳 + Future anchorHeartbeat(String roomId); + + ///声网token + Future getRtcToken(String channel, String userId); + + ///退出房间 + Future quitRoom(String roomId); + + ///修改房间信息 + Future editRoomInfo( + String roomId, + String roomCover, + String roomName, + String roomDesc, + String event, + ); + + ///获取指定用户信息 + Future loadUserInfo(String userId); + + ///关注房间 + Future followRoom(String roomId); + + ///关注用户 + Future followUser(String userId); + + ///获取我的金币余额 + Future balance(); + + ///搜索用户 + Future searchUser(String account); + + ///设置密码 + Future pwdReset(String pwd); + + ///账号是否绑定 + Future accountIsBind(); + + ///绑定账号 + Future bind(String pwd); + + ///修改密码 + Future updatePwd(String pwd, String oldPwd); + + ///房间用户黑名单 + Future> roomBlacklist(String roomId, String lastId); + + ///修改用户信息 + Future updateUserInfo({ + String? userAvatar, + String? userNickname, + num? userSex, + num? age, + num? bornYear, + num? bornMonth, + num? bornDay, + String? countryId, + String? hobby, + String? autograph, + List? backgroundPhotos, + List? personalPhotos, + }); + + ///关注列表 + Future> followMyList({String? account, String? lastId}); + + ///粉丝列表 + Future> fansMyList({String? account, String? lastId}); + + ///金币记录 + Future> goldRecord({num? type, String? lastId}); + + ///获取身份 + Future userIdentity({String? userId}); + + ///获取用户关系计数 SUBSCRIPTION:订阅、FANS:粉丝、FRIEND:朋友,访客. + Future> userCounter(String userId); + + + ///用户等级 + Future userLevelConsumptionExp(String userId, String type); + + ///是否已经签到 + Future checkInToday(); + + ///签到列表 + Future sginListAward(); + + ///签到领奖 + Future checkInReceive(String id, String resourceGroupId); + + ///添加代理 + Future teamCreate(String ownUserId); + + ///分页查询公共消息列表. + Future publicMessagePage( + String type, + num pageNo, + num pageSize, + ); + + ///好友列表 + Future> friendList({String? lastId}); + + ///好友搜索 + Future> friendSearch(String account); + + ///代理邀请成为主播-处理 + Future inviteHost(String id, String status); + + ///代理邀请成为Agent-处理 + Future inviteAgent(String id, String status); + + ///代理邀请成为BD-处理 + Future inviteBD(String id, String status); + + ///代理邀请成为BDLeader-处理 + Future inviteBDLeader(String id, String status); + + ///处理邀请-邀请充值代理. + Future inviteRechargeAgent(String id, String status); + + ///检测我是否有关注指定用户. + Future followCheck(String userId); + + ///系统消息专用关注接口 + Future followNew(String userId); + + ///任务列表 + Future> tasks(); + + ///领取奖励 + Future taskReward(num taskId); + + ///我的访客列表 + Future> visitorList(num pageNumber); + + ///查询我的道具券列表 + Future propCouponList( + num size, + num current, { + int? couponType, + }); + + ///查询道具券使用记录 + Future couponRecordList( + num size, + num current, { + int? couponType, + int? status, + }); + + ///使用道具券 + Future couponUse(String couponNo); + + ///赠送道具券 + Future couponSend(String couponNo, String receiverId); + + + ///处理用户违规 + Future userViolationHandle( + String userId, + int violationType, + int operationType, + String description, { + List? imageUrls, + }); + + + ///验证是否是好友 + Future friendRelationCheck(String userId); + + ///发送用户红包 + Future userRedPacketSend( + String receiverUserId, + String totalAmount, + ); + + ///查询红包详情 + Future userRedPacketDetail(String packetId); + + ///领取用户红包 + Future userRedPacketGrab(String packetId); + + ///查询黑名单列表. + Future> userBlacklistList({ + String? account, + String? lastId, + }); + + ///检测是否在黑名单. + Future blacklistCheck(String shieldUserId); + + ///添加黑名单 + Future addUserBlacklist(String shieldUserId); + + ///删除黑名单 + Future deleteUserBlacklist(String shieldUserId); + + ///注销账号 + Future logoutAccount(); + +} diff --git a/lib/shared/business_logic/usecases/custom_tab_selector.dart b/lib/shared/business_logic/usecases/custom_tab_selector.dart new file mode 100644 index 0000000..394aed4 --- /dev/null +++ b/lib/shared/business_logic/usecases/custom_tab_selector.dart @@ -0,0 +1,92 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class CustomTabSelector extends StatefulWidget { + final List options; + final int initialIndex; + final ValueChanged onChanged; + + const CustomTabSelector({ + Key? key, + required this.options, + required this.initialIndex, + required this.onChanged, + }) : super(key: key); + + @override + _CustomTabSelectorState createState() => _CustomTabSelectorState(); +} + +class _CustomTabSelectorState extends State { + late int _selectedIndex; + + @override + void initState() { + super.initState(); + _selectedIndex = widget.initialIndex; + } + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_red_envelope_config_tab_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: + widget.options.asMap().entries.map((entry) { + final index = entry.key; + final title = entry.value; + final isSelected = index == _selectedIndex; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + _selectedIndex = index; + }); + widget.onChanged(index); + }, + child: Container( + width: 72.w, + height: 32.w, + decoration: + isSelected + ? BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/room/sc_icon_red_envelope_config_tab_item_bg.png", + ), + fit: BoxFit.fill, + ), + ) + : null, + child: Container( + alignment: Alignment.center, + child: Text( + title, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: isSelected ? 15.sp : 13.sp, + fontFamily: 'MyCustomFont', + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.w500, + ), + ), + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart b/lib/shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart new file mode 100644 index 0000000..957f9e3 --- /dev/null +++ b/lib/shared/business_logic/usecases/sc_accurate_length_limiting_textInput_formatter.dart @@ -0,0 +1,34 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; + +class SCAccurateLengthLimitingTextInputFormatter extends TextInputFormatter { + SCAccurateLengthLimitingTextInputFormatter(this.maxLength); + + final int maxLength; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final newText = newValue.text; + final newTextCharacters = newText.characters; + + if (newTextCharacters.length > maxLength) { + // 如果新文本超过最大长度,尝试截断 + if (oldValue.text.characters.length == maxLength) { + return oldValue; // 已经达到最大长度,不允许再输入 + } + + // 截取前 maxLength 个字符 + final truncated = newTextCharacters.take(maxLength); + return TextEditingValue( + text: truncated.toString(), + selection: TextSelection.collapsed(offset: truncated.length), + ); + } + + return newValue; + } +} \ No newline at end of file diff --git a/lib/shared/business_logic/usecases/sc_case.dart b/lib/shared/business_logic/usecases/sc_case.dart new file mode 100644 index 0000000..12737ea --- /dev/null +++ b/lib/shared/business_logic/usecases/sc_case.dart @@ -0,0 +1,95 @@ +import 'dart:ui'; + +import 'package:extended_text/extended_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_string_utils.dart'; + +class AtTextSpanBuilder extends SpecialTextSpanBuilder { + final Function(String userId)? onTapCall; + + AtTextSpanBuilder({this.onTapCall}); + + @override + SpecialText? createSpecialText( + String flag, { + TextStyle? textStyle, + SpecialTextGestureTapCallback? onTap, + required int index, + }) { + if (isStart(flag, AtText.flag)) { + return AtText( + textStyle: textStyle!, + start: index - (AtText.flag.length - 1), + onTap: (userId) { + onTapCall?.call(userId); + }, + ); + } + return null; + } +} + +class AtText extends SpecialText { + static const String flag = '用户名< + final RegExp exp = RegExp(r'>([^<]+)<'); + final RegExpMatch? match = exp.firstMatch(atContent); + return match?.group(1)?.trim() ?? ''; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is AtText && + other.start == start && + other.toString() == toString(); + } + + @override + int get hashCode => start.hashCode ^ toString().hashCode; +} diff --git a/lib/shared/business_logic/usecases/sc_custom_filtering_textinput_formatter.dart b/lib/shared/business_logic/usecases/sc_custom_filtering_textinput_formatter.dart new file mode 100644 index 0000000..15de2ca --- /dev/null +++ b/lib/shared/business_logic/usecases/sc_custom_filtering_textinput_formatter.dart @@ -0,0 +1,23 @@ +import 'package:flutter/services.dart'; + +class SCCustomFilteringTextInputFormatter extends TextInputFormatter { + // 定义您想要拒绝的字符正则表达式 + // 这个例子拒绝了空格、@、#、¥、* 以及您添加的其他特殊字符 + static final RegExp _deniedRegExp = RegExp(r'[\s@#¥*$%^&+=<>]'); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + // 如果新值处于输入法组合状态,则直接返回,不进行过滤:cite[8] + if (newValue.isComposingRangeValid) { + return newValue; + } + // 如果新值包含被拒绝的字符,则返回旧值 + if (_deniedRegExp.hasMatch(newValue.text)) { + return oldValue; + } + return newValue; + } +} \ No newline at end of file diff --git a/lib/shared/business_logic/usecases/sc_custom_filtering_textinput_formatter2.dart b/lib/shared/business_logic/usecases/sc_custom_filtering_textinput_formatter2.dart new file mode 100644 index 0000000..afb9043 --- /dev/null +++ b/lib/shared/business_logic/usecases/sc_custom_filtering_textinput_formatter2.dart @@ -0,0 +1,23 @@ +import 'package:flutter/services.dart'; + +class SCCustomFilteringTextInputFormatter2 extends TextInputFormatter { + // 定义您想要拒绝的字符正则表达式 + // 这个例子拒绝了空格、@、#、¥、* 以及您添加的其他特殊字符 + static final RegExp _deniedRegExp = RegExp(r'[@#¥*$%^&+=<>]'); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + // 如果新值处于输入法组合状态,则直接返回,不进行过滤:cite[8] + if (newValue.isComposingRangeValid) { + return newValue; + } + // 如果新值包含被拒绝的字符,则返回旧值 + if (_deniedRegExp.hasMatch(newValue.text)) { + return oldValue; + } + return newValue; + } +} \ No newline at end of file diff --git a/lib/shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart b/lib/shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart new file mode 100644 index 0000000..eccb0d5 --- /dev/null +++ b/lib/shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart @@ -0,0 +1,84 @@ +// 自定义固定宽度指示器(带圆角),支持渐变色 +import 'package:flutter/cupertino.dart'; + +class SCFixedWidthTabIndicator extends Decoration { + final double width; + final Color? color; // 改为可选参数 + final Gradient? gradient; // 新增渐变参数 + final double height; + final double borderRadius; + + const SCFixedWidthTabIndicator({ + required this.width, + this.color, // 颜色和渐变至少提供一个 + this.gradient, + this.height = 3.0, + this.borderRadius = 2.0, + }) : assert(color != null || gradient != null, + '必须提供color或gradient参数'); + + @override + BoxPainter createBoxPainter([VoidCallback? onChanged]) { + return _FixedWidthTabIndicatorPainter( + width: width, + color: color, + gradient: gradient, + height: height, + borderRadius: borderRadius, + onChange: onChanged, + ); + } +} + +class _FixedWidthTabIndicatorPainter extends BoxPainter { + final double width; + final Color? color; + final Gradient? gradient; + final double height; + final double borderRadius; + + _FixedWidthTabIndicatorPainter({ + required this.width, + required this.color, + required this.gradient, + required this.height, + required this.borderRadius, + VoidCallback? onChange, + }) : assert(color != null || gradient != null), + super(onChange); + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + final Paint paint = Paint()..style = PaintingStyle.fill; + + // 设置颜色或渐变 + if (gradient != null) { + final Rect rect = offset & configuration.size!; + final double left = rect.left + (rect.width - width) / 2; + final double top = rect.bottom - height; + final Rect indicatorRect = Rect.fromLTWH(left, top, width, height); + + // 创建渐变着色器 + paint.shader = gradient!.createShader(indicatorRect); + } else { + paint.color = color!; + } + + final Rect rect = offset & configuration.size!; + + // 计算固定宽度的指示器位置(居中) + final double left = rect.left + (rect.width - width) / 2; + final double top = rect.bottom - height; + + final Rect indicatorRect = Rect.fromLTWH(left, top, width, height); + + // 创建圆角矩形 + final RRect roundedRect = RRect.fromRectAndRadius( + indicatorRect, + Radius.circular(borderRadius), + ); + + // 绘制圆角矩形 + canvas.drawRRect(roundedRect, paint); + } +} \ No newline at end of file diff --git a/lib/shared/business_logic/usecases/sc_hollow_circle.dart b/lib/shared/business_logic/usecases/sc_hollow_circle.dart new file mode 100644 index 0000000..59853a0 --- /dev/null +++ b/lib/shared/business_logic/usecases/sc_hollow_circle.dart @@ -0,0 +1,57 @@ +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; + +class SCHollowCircle extends StatelessWidget { + final double size; + final double strokeWidth; + final Gradient gradient; + + const SCHollowCircle({ + super.key, + required this.size, + this.strokeWidth = 3.0, + required this.gradient, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size(size, size), + painter: _HollowCirclePainter( + strokeWidth: strokeWidth, + gradient: gradient, + ), + ); + } +} + +class _HollowCirclePainter extends CustomPainter { + final double strokeWidth; + final Gradient gradient; + + _HollowCirclePainter({ + required this.strokeWidth, + required this.gradient, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = size.width / 2 - strokeWidth / 2; + + final paint = Paint() + ..shader = gradient.createShader(Rect.fromCircle( + center: center, + radius: radius, + )) + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + canvas.drawCircle(center, radius, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} \ No newline at end of file diff --git a/lib/shared/business_logic/usecases/sc_partially_bold_marquee.dart b/lib/shared/business_logic/usecases/sc_partially_bold_marquee.dart new file mode 100644 index 0000000..5d6ac91 --- /dev/null +++ b/lib/shared/business_logic/usecases/sc_partially_bold_marquee.dart @@ -0,0 +1,102 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class SCPartiallyBoldMarquee extends StatefulWidget { + String boldText1 = ""; + String boldText2 = ""; + String normalText1 = ""; + String normalText2 = ""; + + @override + _PartiallyBoldMarqueeState createState() => _PartiallyBoldMarqueeState(); + + SCPartiallyBoldMarquee( + this.boldText1, + this.boldText2, + this.normalText1, + this.normalText2, + ); +} + +class _PartiallyBoldMarqueeState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + bool goMarquee = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: Duration(seconds: 20), // 总滚动时间,需调试 + vsync: this, + )..repeat(); // 无限循环 + _animation = Tween(begin: 0.2, end: -1.0).animate(_controller); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), // 禁用用户手势 + child: AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.translate( + offset: Offset(_animation.value * 650, 0), + // 500为估算的滚动距离,需根据文本宽度精确计算 + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: widget.boldText1, + style: TextStyle( + fontSize: 13.sp, + color: Colors.black, + fontFamily: 'MyCustomFont', + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: widget.normalText1, + style: TextStyle( + fontSize: 12.sp, + color: Colors.black, + fontFamily: 'MyCustomFont', + fontWeight: FontWeight.w600, // 加粗 + ), + ), + TextSpan( + text: widget.boldText2, + style: TextStyle( + fontSize: 13.sp, + color: Colors.black, + fontFamily: 'MyCustomFont', + fontWeight: FontWeight.w900, + ), + ), + TextSpan( + text: widget.normalText2, + style: TextStyle( + fontSize: 12.sp, + color: Colors.black, + fontFamily: 'MyCustomFont', + fontWeight: FontWeight.w600, // 加粗 + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/shared/business_logic/usecases/sc_shining_text.dart b/lib/shared/business_logic/usecases/sc_shining_text.dart new file mode 100644 index 0000000..d9d8ca8 --- /dev/null +++ b/lib/shared/business_logic/usecases/sc_shining_text.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +class SCShiningText extends StatefulWidget { + final String text; + final double fontSize; + final FontWeight fontWeight; + final Color baseColor; + final Color shineColor; + + const SCShiningText({ + super.key, + required this.text, + this.fontSize = 24, + this.fontWeight = FontWeight.bold, + this.baseColor = Colors.grey, + this.shineColor = Colors.white, + }); + + @override + State createState() => _ShiningText1State(); +} + + +class _ShiningText1State extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 3000), + vsync: this, + )..repeat(reverse: false); + + _animation = Tween(begin: -1.0, end: 2.0).animate(_controller); + } + + @override + Widget build(BuildContext context) { + return Center( + child: AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (Rect bounds) { + return LinearGradient( + colors: [ + widget.baseColor, + widget.shineColor, + widget.shineColor, + widget.baseColor, + ], + stops: const [0.0, 0.1, 0.9, 1.0], + transform: _GradientTransform(_animation.value), + ).createShader(bounds); + }, + child: Text( + widget.text, + style: TextStyle( + fontSize: widget.fontSize, + fontWeight: widget.fontWeight, + ), + ), + ); + }, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + +class _GradientTransform extends GradientTransform { + final double value; + + const _GradientTransform(this.value); + + @override + Matrix4 transform(Rect bounds, {TextDirection? textDirection}) { + return Matrix4.translationValues(bounds.width * value, 0.0, 0.0); + } +} diff --git a/lib/shared/data_sources/models/country_mode.dart b/lib/shared/data_sources/models/country_mode.dart new file mode 100644 index 0000000..93d058c --- /dev/null +++ b/lib/shared/data_sources/models/country_mode.dart @@ -0,0 +1,7 @@ +import 'package:yumi/shared/business_logic/models/res/country_res.dart'; + +class CountryMode{ + String prefix = ""; + ListprefixCountrys = []; + CountryMode(this.prefix,this.prefixCountrys); +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_activity_reward_props_type.dart b/lib/shared/data_sources/models/enum/sc_activity_reward_props_type.dart new file mode 100644 index 0000000..4c3ccd6 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_activity_reward_props_type.dart @@ -0,0 +1,5 @@ +enum SCActivityRewardATPropsType{ + PROPS, + GOLD, + GIFT, +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_auth_type.dart b/lib/shared/data_sources/models/enum/sc_auth_type.dart new file mode 100644 index 0000000..a7b5759 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_auth_type.dart @@ -0,0 +1,7 @@ +enum SCAuthType{ + MOBILE, + FACEBOOK, + GOOGLE, + HUAWEI, + APPLE +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_banner_open_type.dart b/lib/shared/data_sources/models/enum/sc_banner_open_type.dart new file mode 100644 index 0000000..ed5da21 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_banner_open_type.dart @@ -0,0 +1,3 @@ +enum SCBannerOpenType{ + ENTER_ROOM, +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_banner_type.dart b/lib/shared/data_sources/models/enum/sc_banner_type.dart new file mode 100644 index 0000000..4fabc64 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_banner_type.dart @@ -0,0 +1,6 @@ +enum SCBannerType{ + EXPLORE_PAGE, + HOME_ALERT, + GAME, + ROOM +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_currency_type.dart b/lib/shared/data_sources/models/enum/sc_currency_type.dart new file mode 100644 index 0000000..e3d26f7 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_currency_type.dart @@ -0,0 +1,5 @@ +enum SCCurrencyType{ + GOLD, + DIAMOND, + FREE, +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_date_type.dart b/lib/shared/data_sources/models/enum/sc_date_type.dart new file mode 100644 index 0000000..660a157 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_date_type.dart @@ -0,0 +1,5 @@ +enum SCDateType{ + DAY, + WEEK, + MONTH, +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_erro_code.dart b/lib/shared/data_sources/models/enum/sc_erro_code.dart new file mode 100644 index 0000000..56e1280 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_erro_code.dart @@ -0,0 +1,19 @@ +enum SCErroCode { + userNotRegistered(4000), + passwordNotRules(4061), + authUnauthorized(401), + redPacketFinished(3262), + userJoinedFamily(2518), + orderExistsCreated(5900), + unknown(-1); + + final int code; + const SCErroCode(this.code); + + static SCErroCode fromCode(int code) { + return SCErroCode.values.firstWhere( + (e) => e.code == code, + orElse: () => SCErroCode.unknown, // 避免 StateError + ); + } +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_gift_type.dart b/lib/shared/data_sources/models/enum/sc_gift_type.dart new file mode 100644 index 0000000..14a373a --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_gift_type.dart @@ -0,0 +1,8 @@ +enum SCGiftType{ + ANIMSCION, + MUSIC, + GLOBAL_GIFT, + LUCKY_GIFT, + MAGIC, + CP, +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_heartbeat_status.dart b/lib/shared/data_sources/models/enum/sc_heartbeat_status.dart new file mode 100644 index 0000000..af8f267 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_heartbeat_status.dart @@ -0,0 +1,4 @@ +enum SCHeartbeatStatus{ + VOICE_LIVE, + ONLINE +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_level_type.dart b/lib/shared/data_sources/models/enum/sc_level_type.dart new file mode 100644 index 0000000..44513fc --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_level_type.dart @@ -0,0 +1,4 @@ +enum SCLevelType{ + WEALTH, + CHARM, +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_props_type.dart b/lib/shared/data_sources/models/enum/sc_props_type.dart new file mode 100644 index 0000000..42975a2 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_props_type.dart @@ -0,0 +1,13 @@ +enum SCPropsType { + AVATAR_FRAME, + FLOAT_PICTURE, + DATA_CARD, + CHAT_BUBBLE, + RIDE, + LAYOUT, + THEME, + BADGE, + NOBLE_VIP, + FRAGMENTS, + RED_PACKET, +} diff --git a/lib/shared/data_sources/models/enum/sc_room_info_event_type.dart b/lib/shared/data_sources/models/enum/sc_room_info_event_type.dart new file mode 100644 index 0000000..0ad2f40 --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_room_info_event_type.dart @@ -0,0 +1,5 @@ +enum SCRoomInfoEventType{ + WAITING_CONFIRMED, + ID_CHANGE, + AVAILABLE +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_room_roles_type.dart b/lib/shared/data_sources/models/enum/sc_room_roles_type.dart new file mode 100644 index 0000000..b36372c --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_room_roles_type.dart @@ -0,0 +1 @@ +enum SCRoomRolesType { HOMEOWNER, ADMIN, MEMBER, TOURIST } diff --git a/lib/shared/data_sources/models/enum/sc_room_special_mike_type.dart b/lib/shared/data_sources/models/enum/sc_room_special_mike_type.dart new file mode 100644 index 0000000..2d9edbf --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_room_special_mike_type.dart @@ -0,0 +1 @@ +enum SCRoomSpecialMikeType { TYPE_VIP3, TYPE_VIP4, TYPE_VIP5, TYPE_VIP6 } diff --git a/lib/shared/data_sources/models/enum/sc_sysytem_message_type.dart b/lib/shared/data_sources/models/enum/sc_sysytem_message_type.dart new file mode 100644 index 0000000..afc29fb --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_sysytem_message_type.dart @@ -0,0 +1,18 @@ +enum SCSysytemMessageType{ + GIVE_AWAY_PROPS, + AGENT_SEND_INVITE_HOST, + BD_SEND_INVITE_AGENT, + BD_LEADER_INVITE_BD, + USER_FOLLOW, + RECEIVE_PROP_COUPON, + BD_LEADER_INVITE_BD_LEADER, + VIOLSCION_WARNING, + VIOLSCION_ADJUST, + ADMIN_INVITE_RECHARGE_AGENT, + CP_BUILD, + USER_SPECIAL_AUDIT_RESULTS, + FAMILY_APPLY_APPROVAL, + CP_LOVE_LETTER, + USER_COINS_RECEIVED, + COIN_SELLER_COINS_RECEIVED, +} \ No newline at end of file diff --git a/lib/shared/data_sources/models/enum/sc_task_code_type.dart b/lib/shared/data_sources/models/enum/sc_task_code_type.dart new file mode 100644 index 0000000..a9e159c --- /dev/null +++ b/lib/shared/data_sources/models/enum/sc_task_code_type.dart @@ -0,0 +1,19 @@ +enum SCTaskCodeType { + PERSONAL_ACTIVE_IN_ROOM, + PERSONAL_MIC_IN_ROOM, + PERSONAL_SEND_GIFT, + PERSONAL_LUCKY_GIFT_GOLD, + PERSONAL_MAGIC_GIFT_GOLD, + PERSONAL_GAME_CONSUME, + ROOM_NEW_MEMBER, + ROOM_OWNER_SEND_RED_PACKET, + ROOM_OWNER_SEND_GIFT_USER, + ROOM_OWNER_SEND_GIFT_GOLD, + ROOM_USER_SEND_GIFT_GOLD, + ROOM_MIC_USER_120MIN, + ROOM_MIC_USER_60MIN, + ROOM_MIC_USER_30MIN, + ROOM_ONLINE_USER_COUNT, + ROOM_OWNER_INVITE_MIC, + ROOM_OWNER_MIC_TIME, +} diff --git a/lib/shared/data_sources/models/message/big_broadcast_group_message.dart b/lib/shared/data_sources/models/message/big_broadcast_group_message.dart new file mode 100644 index 0000000..120d685 --- /dev/null +++ b/lib/shared/data_sources/models/message/big_broadcast_group_message.dart @@ -0,0 +1,22 @@ +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +class BigBroadcastGroupMessage { + String type = ""; + SCFloatingMessage? data; + + BigBroadcastGroupMessage(this.type, this.data); + + BigBroadcastGroupMessage.fromJson(dynamic json) { + type = json['type']; + data = json['data'] != null ? SCFloatingMessage.fromJson(json['data']) : null; + } + + Map toJson() { + final map = {}; + map['type'] = type; + if (data != null) { + map['data'] = data?.toJson(); + } + return map; + } +} diff --git a/lib/shared/data_sources/models/message/sc_floating_message.dart b/lib/shared/data_sources/models/message/sc_floating_message.dart new file mode 100644 index 0000000..63f9e4f --- /dev/null +++ b/lib/shared/data_sources/models/message/sc_floating_message.dart @@ -0,0 +1,69 @@ +class SCFloatingMessage { + String? userId; // 可选:用户ID + String? roomId; // 可选:房间ID + String? toUserId; // 可选:用户ID + String? userAvatarUrl; // 用户头像 + String? toUserAvatarUrl; // 用户头像 + String? userName; // 用户昵称 + String? toUserName; // 用户昵称 + String? giftUrl; // 礼物图标 + int? type; + int? rocketLevel; + num? coins; + num? number; + num? multiple; + int priority = 10; //排序权重 + + SCFloatingMessage({ + this.type = 0, + this.rocketLevel = 0, + this.userId = '', + this.roomId = '', + this.toUserId = '', + this.userAvatarUrl = '', + this.toUserAvatarUrl = '', + this.userName = '', + this.toUserName = '', + this.giftUrl = '', + this.number = 0, + this.coins = 0, + this.priority = 10, + this.multiple = 10, + }); + + SCFloatingMessage.fromJson(dynamic json) { + userId = json['userId']; + rocketLevel = json['rocketLevel']; + toUserId = json['toUserId']; + roomId = json['roomId']; + type = json['type']; + userAvatarUrl = json['userAvatarUrl']; + toUserAvatarUrl = json['toUserAvatarUrl']; + userName = json['userName']; + toUserName = json['toUserName']; + giftUrl = json['giftUrl']; + coins = json['coins']; + number = json['number']; + priority = json['priority']; + multiple = json['multiple']; + } + + Map toJson() { + final map = {}; + map['userId'] = userId; + map['rocketLevel'] = rocketLevel; + map['roomId'] = roomId; + map['type'] = type; + map['toUserId'] = toUserId; + map['userAvatarUrl'] = userAvatarUrl; + map['toUserAvatarUrl'] = toUserAvatarUrl; + map['userName'] = userName; + map['toUserName'] = toUserName; + map['giftUrl'] = giftUrl; + map['coins'] = coins; + map['number'] = number; + map['priority'] = priority; + map['multiple'] = multiple; + return map; + } +} diff --git a/lib/shared/data_sources/models/sc_music_mode.dart b/lib/shared/data_sources/models/sc_music_mode.dart new file mode 100644 index 0000000..cf177a0 --- /dev/null +++ b/lib/shared/data_sources/models/sc_music_mode.dart @@ -0,0 +1,37 @@ +class SCMusicMode { + SCMusicMode({String? title, String? artist,int? duration,String? localPath}) { + _title = title; + _artist = artist; + _duration = duration; + _localPath = localPath; + } + + String? _title; + String? _artist; + int? _duration; + String? _localPath; + + String? get title => _title; + + String? get artist => _artist; + + int? get duration => _duration; + + String? get localPath => _localPath; + + Map toJson() { + final map = {}; + map['title'] = _title; + map['artist'] = _artist; + map['duration'] = _duration; + map['localPath'] = _localPath; + return map; + } + + SCMusicMode.fromJson(dynamic json) { + _title = json['title']; + _artist = json['artist']; + _duration = json['duration']; + _localPath = json['localPath']; + } +} diff --git a/lib/shared/data_sources/sources/local/data_persistence.dart b/lib/shared/data_sources/sources/local/data_persistence.dart new file mode 100644 index 0000000..4d54db6 --- /dev/null +++ b/lib/shared/data_sources/sources/local/data_persistence.dart @@ -0,0 +1,248 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class DataPersistence { + static SharedPreferences? _prefs; + static bool _isInitialized = false; + static Completer? _initializationCompleter; + + // 初始化 SharedPreferences + static Future initialize() async { + if (_isInitialized) return; + + if (_initializationCompleter != null) { + return _initializationCompleter!.future; + } + + _initializationCompleter = Completer(); + + try { + _prefs = await SharedPreferences.getInstance(); + _isInitialized = true; + _initializationCompleter!.complete(); + debugPrint('SharedPreferences initialized successfully'); + } catch (e) { + _initializationCompleter!.completeError(e); + _initializationCompleter = null; + debugPrint('SharedPreferences initialization failed: $e'); + rethrow; + } + } + + // 检查是否已初始化 + static void _checkInitialized() { + if (!_isInitialized || _prefs == null) { + throw Exception( + "SharedPreferences not initialized! Call DataPersistence.initialize() first.", + ); + } + } + + // 存储字符串 + static Future setString(String key, String value) async { + _checkInitialized(); + return await _prefs!.setString(key, value); + } + + // 读取字符串 + static String getString(String key, {String defaultValue = ''}) { + if (!_isInitialized) return defaultValue; + return _prefs!.getString(key) ?? defaultValue; + } + + // 存储整数 + static Future setInt(String key, int value) async { + _checkInitialized(); + return await _prefs!.setInt(key, value); + } + + // 读取整数 + static int getInt(String key, {int defaultValue = 0}) { + if (!_isInitialized) return defaultValue; + return _prefs!.getInt(key) ?? defaultValue; + } + + // 存储布尔值 + static Future setBool(String key, bool value) async { + _checkInitialized(); + return await _prefs!.setBool(key, value); + } + + // 读取布尔值 + static bool getBool(String key, {bool defaultValue = false}) { + if (!_isInitialized) return defaultValue; + return _prefs!.getBool(key) ?? defaultValue; + } + + // 存储双精度浮点数 + static Future setDouble(String key, double value) async { + _checkInitialized(); + return await _prefs!.setDouble(key, value); + } + + // 读取双精度浮点数 + static double getDouble(String key, {double defaultValue = 0.0}) { + if (!_isInitialized) return defaultValue; + return _prefs!.getDouble(key) ?? defaultValue; + } + + // 删除指定键 + static Future remove(String key) async { + if (!_isInitialized) return false; + return await _prefs!.remove(key); + } + + // 批量删除(通过键前缀) + static Future removeByPrefix(String prefix) async { + if (!_isInitialized) return; + + final keys = _prefs!.getKeys().where((key) => key.startsWith(prefix)); + for (final key in keys) { + await _prefs!.remove(key); + } + } + + // 清空所有数据 + static Future clearAll() async { + if (!_isInitialized) return false; + return await _prefs!.clear(); + } + + // 检查键是否存在 + static bool containsKey(String key) { + if (!_isInitialized) return false; + return _prefs!.containsKey(key); + } + + // 获取所有键 + static Set getKeys() { + if (!_isInitialized) return {}; + return _prefs!.getKeys(); + } + + // 业务方法 - 与之前保持相同的API + static String getToken() { + return getString("token"); + } + + static Future setToken(String token) async { + return await setString("token", token); + } + + ///获取当前用户 + static String getCurrentUser() { + return getString("currentUser"); + } + + ///保存当前用户 + static Future setCurrentUser(String userJson) async { + return await setString("currentUser", userJson); + } + + ///保存搜索历史记录 + static Future setSearchHistroy(String userId, String userJson) async { + return await setString("${userId}-currentUser", userJson); + } + + ///获取搜索历史记录 + static String getSearchHistroy(String userId) { + return getString("${userId}-currentUser"); + } + + ///保存当前语言 + static Future setLang(String value) async { + return await setString("lang", value); + } + + ///获取当前语言 + static String getLang() { + return getString("lang"); + } + + static Future setUniqueId(String value) async { + return await setString("device_unique_id", value); + } + + static String getUniqueId() { + return getString("device_unique_id"); + } + + static Future setLastTimeRoomId(String id) async { + return await setString("last_time_room_id", id); + } + + static String getLastTimeRoomId() { + return getString("last_time_room_id"); + } + + static Future setPlayGiftMusic(bool muteMusic) async { + return await setBool("play_gift_music", muteMusic); + } + + static bool getPlayGiftMusic() { + return getBool("play_gift_music"); + } + + ///获取用户已经添加的房间音乐 + static String getUserRoomMusic() { + final currentUser = AccountStorage().getCurrentUser()?.userProfile?.account; + if (currentUser == null) return ""; + return getString("room_music$currentUser"); + } + + static Future setUserRoomMusic(String musicJson) async { + final currentUser = AccountStorage().getCurrentUser()?.userProfile?.account; + if (currentUser == null) return false; + return await setString("room_music$currentUser", musicJson); + } + + // 等待初始化完成 + static Future get initializationDone { + if (_isInitialized) return Future.value(); + return _initializationCompleter?.future ?? Future.value(); + } + + // 重置初始化状态(用于测试) + static void reset() { + _isInitialized = false; + _prefs = null; + _initializationCompleter = null; + } + + ///发送动态的草稿图片 + static List getDraftDynamicPhoto() { + String st = getString( + "draft_dynamic_photo${AccountStorage().getCurrentUser()?.userProfile?.account}", + ); + List photos = []; + if (st.isNotEmpty) { + photos = (jsonDecode(st) as List).map((e) => e as String).toList(); + } + return photos; + } + + static void setDraftDynamicPhoto(List images) { + setString( + "draft_dynamic_photo${AccountStorage().getCurrentUser()?.userProfile?.account}", + jsonEncode(images), + ); + } + + ///发送动态的草稿文本 + static String getDraftDynamicText() { + return getString( + "draft_dynamic_text${AccountStorage().getCurrentUser()?.userProfile?.account}", + ); + } + + static void setDraftDynamicText(String text) { + setString( + "draft_dynamic_text${AccountStorage().getCurrentUser()?.userProfile?.account}", + text, + ); + } +} diff --git a/lib/shared/data_sources/sources/local/file_cache_manager.dart b/lib/shared/data_sources/sources/local/file_cache_manager.dart new file mode 100644 index 0000000..b15eba1 --- /dev/null +++ b/lib/shared/data_sources/sources/local/file_cache_manager.dart @@ -0,0 +1,137 @@ +import 'dart:io'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; + +typedef FileCacheManager = MediaCache; +class MediaCache { + late CacheManager _cacheManager; + static FileCacheManager? _instance; + static String temporaryPath = ""; + static const musicPath = "/music"; + static String soundPath = ""; + final String cacheKey; + final Duration stalePeriod; + final int maxNrOfCacheObjects; + + static String get videoCachePath => "$temporaryPath"; + static String get imageCachePath => "$temporaryPath"; + + MediaCache._internal({ + required this.cacheKey, + required this.stalePeriod, + required this.maxNrOfCacheObjects, + }) { + _cacheManager = CacheManager( + Config( + cacheKey, + stalePeriod: stalePeriod, + maxNrOfCacheObjects: maxNrOfCacheObjects, + ), + ); + } + + factory MediaCache.getInstance({ + String cacheKey = 'customCache', + Duration stalePeriod = const Duration(days: 7), + int maxNrOfCacheObjects = 100, + }) { + _instance ??= FileCacheManager._internal( + cacheKey: cacheKey, + stalePeriod: stalePeriod, + maxNrOfCacheObjects: maxNrOfCacheObjects, + ); + return _instance!; + } + + Future getFilePath() async { + try { + Directory directory = await getTemporaryDirectory(); + temporaryPath = p.join(directory.path, cacheKey); + + // 使用递归创建目录,更简洁 + await Directory(temporaryPath).create(recursive: true); + await Directory("$temporaryPath/sound").create(recursive: true); + await Directory("$temporaryPath/$musicPath").create(recursive: true); + + soundPath = "$temporaryPath/sound"; + print('语音文件夹路径: $soundPath'); + return temporaryPath; + } catch (e) { + print('目录创建失败: $e'); + rethrow; // 或者返回一个备用路径 + } + } + + /// 下载或获取缓存文件 + /// [url] 文件网络地址 + /// [ignoreCache] 是否忽略缓存强制重新下载 + Future getFile({ + required String url, + bool ignoreCache = false, + }) async { + if (ignoreCache) { + // 强制重新下载 + final fileInfo = await _cacheManager.downloadFile(url); + return fileInfo.file; + } else { + // 优先从缓存获取 + final cacheFile = await _cacheManager.getSingleFile(url); + return cacheFile; + } + } + + /// 获取缓存文件路径(如果不存在则返回 null) + Future getCachedFilePath(String url) async { + final fileInfo = await _cacheManager.getFileFromCache(url); + return fileInfo?.file.path; + } + + /// 清除指定文件的缓存 + Future removeCachedFile(String url) async { + await _cacheManager.removeFile(url); + } + + /// 清空所有缓存 + Future clearAllCache() async { + await _cacheManager.emptyCache(); + } + + /// 获取缓存大小(字节) + Future getCacheSize() async { + final directory = await getTemporaryDirectory(); + final cacheDir = Directory('${directory.path}/libCachedData'); + + if (!await cacheDir.exists()) return 0; + + int totalSize = 0; + await for (var file in cacheDir.list(recursive: true)) { + if (file is File) { + totalSize += await file.length(); + } + } + return totalSize; + } + + Future putFileFromFile(String cacheKey, File localFile) async { + final cacheManager = DefaultCacheManager(); + + try { + // 1. 将本地文件添加到缓存 + await cacheManager.putFile( + cacheKey, // 缓存键(唯一标识) + localFile.readAsBytesSync(), // 文件字节数据 + fileExtension: localFile.path.split('.').last, // 文件扩展名 + ); + + // 2. 验证文件已缓存 + final cachedFile = await cacheManager.getFileFromCache(cacheKey); + if (cachedFile != null) { + print('文件已成功添加到缓存: ${cachedFile.file.path}'); + } + } catch (e) { + print('添加文件到缓存失败: $e'); + } + } + +} \ No newline at end of file diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart new file mode 100644 index 0000000..b1adbf2 --- /dev/null +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -0,0 +1,211 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/main.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/shared/tools/sc_entrance_vap_svga_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_game_screen_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +typedef FloatingScreenManager = OverlayManager; + +class OverlayManager { + final SCPriorityQueue _messageQueue = SCPriorityQueue( + (a, b) => b.priority.compareTo(a.priority), + ); + bool _isPlaying = false; + OverlayEntry? _currentOverlayEntry; + + bool _isProcessing = false; + bool _isDisposed = false; + + static final OverlayManager _instance = + OverlayManager._internal(); + + factory OverlayManager() => _instance; + + OverlayManager._internal(); + + void addMessage(SCFloatingMessage message) { + if (_isDisposed) return; + if (SCGlobalConfig.isFloatingAnimationInGlobal) { + _messageQueue.add(message); + _safeScheduleNext(); + } else { + _messageQueue.clear(); + _isPlaying = false; + _isProcessing = false; + _isDisposed = false; + } + } + + void _safeScheduleNext() { + if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; + _isProcessing = true; + + try { + _scheduleNext(); + } finally { + _isProcessing = false; + } + } + + void _scheduleNext() { + if (_isPlaying || _messageQueue.isEmpty || _isDisposed) return; + + try { + final context = navigatorKey.currentState?.context; + if (context == null || !context.mounted) return; + + // 安全地获取第一个消息 + if (_messageQueue.isEmpty) return; + final messageToProcess = _messageQueue.first; + + if (messageToProcess?.type == 1) { + final rtcProvider = Provider.of(context, listen: false); + if (rtcProvider.currenRoom == null) { + // 从队列中移除第一个元素 + _messageQueue.removeFirst(); + _safeScheduleNext(); + return; + } + } + + // 安全地移除并播放消息 + final messageToPlay = _messageQueue.removeFirst(); + _playMessage(messageToPlay); + } catch (e) { + debugPrint('播放悬浮消息出错: $e'); + _isPlaying = false; + _safeScheduleNext(); + } + } + + void _playMessage(SCFloatingMessage message) { + _isPlaying = true; + final context = navigatorKey.currentState?.context; + if (context == null || !context.mounted) { + _isPlaying = false; + _safeScheduleNext(); + return; + } + + _currentOverlayEntry = OverlayEntry( + builder: + (_) => Align( + alignment: AlignmentDirectional.topStart, + child: Transform.translate( + offset: Offset(0, 70.w), + child: _buildScreenWidget(message), + ), + ), + ); + + Overlay.of(context)?.insert(_currentOverlayEntry!); + } + + Widget _buildScreenWidget(SCFloatingMessage message) { + bool completed = false; + + void onComplete() { + if (completed) return; + completed = true; + + try { + _currentOverlayEntry?.remove(); + _currentOverlayEntry = null; + _isPlaying = false; + _safeScheduleNext(); + } catch (e) { + debugPrint('清理悬浮消息出错: $e'); + _isPlaying = false; + _safeScheduleNext(); + } + } + + switch (message.type) { + case 0: + return FloatingLuckGiftScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + case 1: + //房间礼物 + return FloatingGiftScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + case 2: + //游戏中奖 + return FloatingGameScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + case 3: + //火箭 + return FloatingRoomRocketScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + case 4: + //红包 + return FloatingRoomRedenvelopeScreenWidget( + message: message, + onAnimationCompleted: onComplete, + ); + default: + onComplete(); + return Container(); + } + } + + void activate() { + _isDisposed = false; + } + + void dispose() { + _isDisposed = true; + _currentOverlayEntry?.remove(); + _currentOverlayEntry = null; + _messageQueue.clear(); + _isPlaying = false; + _isProcessing = false; + } + + void removeRoom() { + // 正确地从 PriorityQueue 中移除特定类型的消息 + _removeMessagesByType(1); + } + + // 辅助方法:移除特定类型的消息 + void _removeMessagesByType(int type) { + // 由于 PriorityQueue 没有直接的方法来移除特定元素, + // 我们需要重建队列,排除指定类型的消息 + final newQueue = SCPriorityQueue( + (a, b) => b.priority.compareTo(a.priority), + ); + + // 遍历原始队列,只添加非指定类型的消息 + while (_messageQueue.isNotEmpty) { + final message = _messageQueue.removeFirst(); + if (message.type != type) { + newQueue.add(message); + } + } + + // 将新队列赋值给原队列 + while (newQueue.isNotEmpty) { + _messageQueue.add(newQueue.removeFirst()); + } + } + + // 辅助方法:获取队列信息 + int get queueLength => _messageQueue.length; + + bool get isPlaying => _isPlaying; +} diff --git a/lib/shared/data_sources/sources/local/user_manager.dart b/lib/shared/data_sources/sources/local/user_manager.dart new file mode 100644 index 0000000..66ac927 --- /dev/null +++ b/lib/shared/data_sources/sources/local/user_manager.dart @@ -0,0 +1,158 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_message_utils.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/main.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/modules/auth/login_route.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; + +import '../../../tools/sc_heartbeat_utils.dart'; +import '../../models/enum/sc_props_type.dart'; + +typedef UserManager = AccountStorage; + +class AccountStorage { + static AccountStorage? _instance; + + AccountStorage._internal(); + + factory AccountStorage() { + return _instance ??= AccountStorage._internal(); + } + + String token = ""; + SocialChatLoginRes? _currentUser; + + ///获取当前用户 + SocialChatLoginRes? getCurrentUser() { + if (_currentUser != null) { + return _currentUser; + } + var userJson = DataPersistence.getCurrentUser(); + if (userJson.isNotEmpty) { + _currentUser = SocialChatLoginRes.fromJson(jsonDecode(userJson)); + } + return _currentUser; + } + + ///同步优化数据 + void setCurrentUser(SocialChatLoginRes user) { + _currentUser = user; + setToken(_currentUser!.token ?? ""); + String userJson = jsonEncode(_currentUser?.toJson()); + if (userJson.isNotEmpty) { + DataPersistence.setCurrentUser(userJson); + } + } + + ///获取自己佩戴的头饰信息 + PropsResources? getHeaddress() { + PropsResources? pr; + _currentUser?.userProfile?.useProps?.forEach((value) { + if (value.propsResources?.type == SCPropsType.AVATAR_FRAME.name) { + ///判断有没有过期 + if (int.parse(value.expireTime ?? "0") > + DateTime + .now() + .millisecondsSinceEpoch) { + pr = value.propsResources; + } + } + }); + return pr; + } + + ///获取自己佩戴的气泡框 + PropsResources? getChatbox() { + PropsResources? pr; + _currentUser?.userProfile?.useProps?.forEach((value) { + if (value.propsResources?.type == SCPropsType.CHAT_BUBBLE.name) { + ///判断有没有过期 + if (int.parse(value.expireTime ?? "0") > + DateTime + .now() + .millisecondsSinceEpoch) { + pr = value.propsResources; + } + } + }); + return pr; + } + + ///获取自己佩戴的坐骑信息 + PropsResources? getMountains() { + PropsResources? pr; + _currentUser?.userProfile?.useProps?.forEach((value) { + if (value.propsResources?.type == SCPropsType.RIDE.name) { + ///判断有没有过期 + if (int.parse(value.expireTime ?? "0") > + DateTime + .now() + .millisecondsSinceEpoch) { + pr = value.propsResources; + } + } + }); + return pr; + } + + ///获取自己资料卡信息 + PropsResources? getDataCard() { + PropsResources? pr; + _currentUser?.userProfile?.useProps?.forEach((value) { + if (value.propsResources?.type == SCPropsType.DATA_CARD.name) { + ///判断有没有过期 + if (int.parse(value.expireTime ?? "0") > + DateTime.now().millisecondsSinceEpoch) { + pr = value.propsResources; + } + } + }); + return pr; + } + + String getToken() { + if (token.isNotEmpty) { + return token; + } + token = DataPersistence.getToken(); + return token; + } + + void setToken(String tk) { + token = tk; + DataPersistence.setToken(token); + } + + void _cleanUser() { + token = ""; + _currentUser = null; + DataPersistence.setToken(""); + DataPersistence.setCurrentUser(""); + } + + ///退出登录 + void logout(BuildContext context) { + _cleanUser(); + SCHeartbeatUtils.cancelTimer(); + Provider.of(context, listen: false).exitCurrentVoiceRoomSession(true); + Provider.of(context, listen: false).logout(); + SCNavigatorUtils.push(context, LoginRouter.login, clearStack: true); + SCRoomUtils.closeAllDialogs(); + SCMessageUtils.redPacketFutureCache.clear(); + SCGlobalConfig.isEntryVehicleAnimation = true; + SCGlobalConfig.isGiftSpecialEffects = true; + SCGlobalConfig.isFloatingAnimationInGlobal = true; + SCGlobalConfig.isLuckGiftSpecialEffects = true; + OverlayManager().dispose(); + } +} diff --git a/lib/shared/data_sources/sources/remote/net/api.dart b/lib/shared/data_sources/sources/remote/net/api.dart new file mode 100644 index 0000000..2973764 --- /dev/null +++ b/lib/shared/data_sources/sources/remote/net/api.dart @@ -0,0 +1,253 @@ +// api.dart +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/modules/auth/login_route.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/main.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +import '../../../models/enum/sc_erro_code.dart'; + +export 'package:dio/dio.dart'; + +_parseAndDecode(String response) => jsonDecode(response); + +parseJson(String text) => compute(_parseAndDecode, text); + +class BaseNetworkClient { + final Dio dio; + + BaseNetworkClient() : dio = Dio() { + _confD(); + init(); + } + + void _confD() { + dio.transformer = BackgroundTransformer()..jsonDecodeCallback = parseJson; + dio.options = BaseOptions( + connectTimeout: const Duration(seconds: 12), + receiveTimeout: const Duration(seconds: 15), + contentType: 'application/json; charset=UTF-8', + ); + } + + void init() {} + + // 通用 GET 请求 + Future get( + String path, { + Map? queryParams, + required T Function(dynamic) fromJson, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + return _req( + path, + method: 'GET', + queryParams: queryParams, + fromJson: fromJson, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + // 通用 put 请求 + Future put( + String path, { + dynamic data, + Map? queryParams, + required T Function(dynamic) fromJson, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + return _req( + path, + method: 'PUT', + data: data, + queryParams: queryParams, + fromJson: fromJson, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + // 通用 POST 请求 + Future post( + String path, { + dynamic data, + Map? queryParams, + required T Function(dynamic) fromJson, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + return _req( + path, + method: 'POST', + data: data, + queryParams: queryParams, + fromJson: fromJson, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + // 通用 POST 请求 + Future delete( + String path, { + dynamic data, + Map? queryParams, + required T Function(dynamic) fromJson, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + return _req( + path, + method: 'DELETE', + data: data, + queryParams: queryParams, + fromJson: fromJson, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + } + + // 核心请求方法 + Future _req( + String path, { + required String method, + dynamic data, + Map? queryParams, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + required T Function(dynamic) fromJson, + }) async { + try { + final response = await dio.request( + path, + data: data, + queryParameters: queryParams, + options: Options(method: method), + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + + // 处理基础响应 + final baseResponse = ResponseData.fromJson( + response.data as Map, + fromJsonT: fromJson, + ); + // 业务逻辑成功判断 + if (baseResponse.success) { + if (baseResponse.body != null) { + return baseResponse.body!; + } else { + if (T == bool) return false as T; + if (T == int) return 0 as T; + if (T == String) return "" as T; + SCLoadingManager.hide(); + throw Exception('Response data is null'); + } + } else { + // 业务逻辑错误 + SCLoadingManager.hide(); + throw DioException( + requestOptions: response.requestOptions, + response: response, + error: '业务错误: ${baseResponse.errorMsg}', + ); + } + } on DioException catch (e) { + // 网络错误处理 + throw _hdlErr(e); + } catch (e) { + SCLoadingManager.hide(); + throw Exception('未知错误: $e'); + } + } + + // 错误处理 + DioException _hdlErr(DioException e) { + SCLoadingManager.hide(); + switch (e.type) { + case DioExceptionType.connectionTimeout: + return DioException(requestOptions: e.requestOptions, error: '连接超时'); + case DioExceptionType.sendTimeout: + return DioException(requestOptions: e.requestOptions, error: '发送超时'); + case DioExceptionType.receiveTimeout: + return DioException(requestOptions: e.requestOptions, error: '接收超时'); + case DioExceptionType.badResponse: + var errorCode = e.response?.data["errorCode"]; + var errorMsg = e.response?.data["errorMsg"]; + if (errorCode == SCErroCode.userNotRegistered.code) { + //用户还没有注册 + SCTts.show("Please register an account first."); + BuildContext? context = navigatorKey.currentContext; + if (context != null) { + SCNavigatorUtils.push( + context, + LoginRouter.editProfile, + replace: false, + ); + } + } else if (errorCode == SCErroCode.authUnauthorized.code) { + //token过期 + BuildContext? context = navigatorKey.currentContext; + if (context != null) { + if (!inLoginPage) { + AccountStorage().logout(context); + inLoginPage = true; + } + } + } else { + if (errorMsg.toString().endsWith("balance not made")) { + BuildContext? context = navigatorKey.currentContext; + if (context != null) { + SCRoomUtils.goRecharge(context); + } + } else { + SCTts.show(errorMsg); + } + } + return DioException( + requestOptions: e.requestOptions, + response: e.response, + error: '服务器错误: ${e.response?.data["errorCode"]}', + ); + case DioExceptionType.cancel: + return DioException(requestOptions: e.requestOptions, error: 'Cancel'); + default: + return DioException( + requestOptions: e.requestOptions, + error: 'Net fail', + ); + } + } +} + +class NotSuccessException implements Exception { + final String message; + + NotSuccessException(this.message); + + factory NotSuccessException.fromRespData(ResponseData respData) { + return NotSuccessException(respData.errorMsg ?? "操作失败"); + } +} diff --git a/lib/shared/data_sources/sources/remote/net/network_client.dart b/lib/shared/data_sources/sources/remote/net/network_client.dart new file mode 100644 index 0000000..9e871a2 --- /dev/null +++ b/lib/shared/data_sources/sources/remote/net/network_client.dart @@ -0,0 +1,288 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_deviceId_utils.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/sc_logger.dart'; + +NetworkClient get http => _httpInstance; +bool inLoginPage = false; + +late final NetworkClient _httpInstance = NetworkClient(); + +final RegExp _encryptedRoutePattern = RegExp(r'^[0-9a-f]{32,}$'); + +const Map _localRouteOverrides = { + "aa35848b37e5ba74d93e5a1f719c579cb289b5940c15040473b6f57550de35df": + "/auth/account/create", + "e64f27b9ba6b37881120f4584a5444a5c684d8491b703d0af953e5cc6a5f4cec": + "/auth/account/login", + "e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44": + "/auth/account/login/channel", + "0d8319c7a696c73d58f5e0ae304dc663f574ade4154ed90ccaee524f6ba14490": + "/sys/config/banner", + "0d8319c7a696c73d58f5e0ae304dc66352cfd4d4480504cd5f589338f4f90157": + "/sys/config/banner/start-page", + "9c370c6fef50fc728152e7a3fd5b47d2841a1d9a5a2b0bb769039fdd47f01585": + "/sys/config/country", + "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687": + "/sys/version/manage/release/latest", + "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232": + "/sys/version/manage/latest/review", + "1e41384e55cbd6c2374608b129e2ed27": "/room/profile", + "3a613b7450f8fd9082b988bd21df454d": "/gift/list", + "58f9485b5b9f401c6a14bc1e6534122c7a085bb5f6a613094188ca56fe8bc03d": + "/live/user/heartbeat", + "60296415428765e04263c733bfc8428c397bb180e31856d94bf25d85eb342787": + "/gift/activity/list", + "2572c69d25a0946b0cb35b24bdfcd61c9bcef4afdaced7dfc47a641bd9584f96": + "/external/im-trtc/tmp/agora/sig", +}; + +bool _isLocalGatewayBaseUrl(String baseUrl) { + final uri = Uri.tryParse(baseUrl); + if (uri == null) return false; + return uri.host == "10.0.2.2" || + uri.host == "127.0.0.1" || + uri.host == "localhost"; +} + +String _resolveRequestPath(String baseUrl, String path) { + if (!_isLocalGatewayBaseUrl(baseUrl)) { + return path; + } + return _localRouteOverrides[path] ?? path; +} + +class NetworkClient extends BaseNetworkClient { + @override + void init() { + // 设置基础URL + dio.options.baseUrl = SCGlobalConfig.apiHost; + + // 添加拦截器 + dio.interceptors + ..add(ApiInterceptor()) + ..add(SCDioLogger()) + ..add(TimeOutInterceptor()); + + setupCacheInterceptor(dio); // 传入 dio 实例 + } + + Future setupCacheInterceptor(Dio dio) async { + // 缓存配置保持不变... + } +} + +class ApiInterceptor extends Interceptor { + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + // 确保使用最新的 baseUrl + options.baseUrl = SCGlobalConfig.apiHost; + options.path = _resolveRequestPath(options.baseUrl, options.path); + + String token = AccountStorage().getToken(); + var imei = await SCDeviceIdUtils.getDeviceId(); + Map headMap = {}; + headMap["req-lang"] = SCGlobalConfig.lang; + headMap["X-Forwarded-Proto"] = "http"; + headMap["User-Agent"] = "Dart/3.7.2(dart:io)"; + headMap["req-imei"] = imei; + + headMap["Host"] = options.uri.host; + headMap["req-app-intel"] = + "version=${SCGlobalConfig.version};build=${SCGlobalConfig.build};model=${SCGlobalConfig.model};sysVersion=${SCGlobalConfig.sysVersion};channel=${SCGlobalConfig.channel}"; + headMap["req-client"] = Platform.isAndroid ? 'Android' : 'iOS'; + headMap["req-sys-origin"] = + "origin=${SCGlobalConfig.origin};originChild=${SCGlobalConfig.originChild}"; + if (token.isNotEmpty) { + headMap["Authorization"] = "Bearer $token"; + } + + final bool usesEncryptedRoute = _encryptedRoutePattern.hasMatch( + options.path, + ); + if (usesEncryptedRoute) { + // The production gateway expects encrypted route forwarding for these paths. + headMap["Req-Likei"] = true; + } + options.headers.addAll(headMap); + + handler.next(options); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) async { + if (response.statusCode == 200) { + final ResponseData respData = ResponseData.fromJson(response.data); + if (respData.success) { + handler.next(response); + } else { + // if (respData.code == 210||respData.code == 212) { // 登录失效 + // if (!inLoginPage) { + // Fluttertoast.showToast(msg: respData.msg ?? "需要重新登录"); + // SpUtils.sp?.clear(); + // navigatorKey.currentState?.pushAndRemoveUntil( + // MaterialPageRoute(builder: (_) { + // Provider.of(_, listen: false).destoryRtm(); + // Provider.of(_, listen: false).logout(); + // Provider.of(_, listen: false).engine?.leaveChannel(); + // return const LoginPage(); + // }), + // (route) => false // 清除所有路由 + // ); + // + // inLoginPage = true; + // } + // } else { + // Fluttertoast.showToast(msg: respData.msg ?? "操作失败"); + // } + + // 使用 DioException 传递错误 + handler.reject( + DioException( + requestOptions: response.requestOptions, + error: NotSuccessException.fromRespData(respData), + response: response, + message: respData.errorMsg, + ), + ); + } + } else { + Fluttertoast.showToast(msg: 'Network error, please check the status.'); + handler.reject( + DioException( + requestOptions: response.requestOptions, + error: 'HTTP ${response.statusCode} 错误', + response: response, + ), + ); + } + } +} + +/// 响应数据结构 +class ResponseData { + final bool status; + final int? errorCode; + final String? errorCodeName; + final String? errorMsg; + final T? body; + final num? time; + + bool get success => status; + + ResponseData({ + required this.status, + this.errorCode, + this.errorCodeName, + this.errorMsg, + this.body, + this.time, + }); + + // 特殊处理基础类型 + static T? _parseData(dynamic data, T Function(dynamic) fromJsonT) { + if (data == null) return null; + + // 布尔值特殊处理 + if (T == bool) { + if (data is bool) return data as T; + if (data is int) return (data != 0) as T; + if (data is String) return (data.toLowerCase() == 'true') as T; + } + + // 其他基础类型 + if (T == int || T == double || T == String || T == bool) { + return data as T; + } + + // 复杂类型使用转换函数 + return fromJsonT(data); + } + + factory ResponseData.fromJson( + Map json, { + T Function(dynamic)? fromJsonT, + }) { + if (fromJsonT == null) { + return ResponseData( + status: json['status'], + errorCode: json['errorCode'], + errorCodeName: json['errorCodeName'], + errorMsg: json['errorMsg'], + body: json['body'], + time: json['time'], + ); + } else { + return ResponseData( + status: json['status'], + errorCode: json['errorCode'], + errorCodeName: json['errorCodeName'], + errorMsg: json['errorMsg'], + body: json['body'] != null ? _parseData(json['body'], fromJsonT) : null, + time: json['time'], + ); + } + } +} + +/// 超时拦截器 +class TimeOutInterceptor extends Interceptor { + static const int globalTimeoutSeconds = 15; + final Map _timers = {}; + + @override + void onError(DioException err, ErrorInterceptorHandler handler) async { + if (err.type == DioExceptionType.connectionTimeout || + err.type == DioExceptionType.receiveTimeout || + err.type == DioExceptionType.sendTimeout) { + print('超时错误: URL => ${err.requestOptions.baseUrl}'); + print('当前使用的Host: ${SCGlobalConfig.apiHost}'); + } + + // 请求出错,取消计时器 + final String requestKey = err.requestOptions.uri.toString(); + _timers[requestKey]?.cancel(); + _timers.remove(requestKey); + + handler.next(err); + } + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + // 为每个请求创建超时计时器 + final cancelToken = options.cancelToken ?? CancelToken(); + final String requestKey = options.uri.toString(); + + // 设置15秒后自动取消 + _timers[requestKey] = Timer( + const Duration(seconds: globalTimeoutSeconds), + () { + SCLoadingManager.hide(); + if (!cancelToken.isCancelled) { + cancelToken.cancel('请求超时(${globalTimeoutSeconds}秒)'); + // 从映射中移除计时器 + _timers.remove(requestKey); + } + }, + ); + handler.next(options); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + // 请求成功完成,取消计时器 + final String requestKey = response.requestOptions.uri.toString(); + _timers[requestKey]?.cancel(); + _timers.remove(requestKey); + handler.next(response); + } +} diff --git a/lib/shared/data_sources/sources/remote/net/sc_logger.dart b/lib/shared/data_sources/sources/remote/net/sc_logger.dart new file mode 100644 index 0000000..67a34f1 --- /dev/null +++ b/lib/shared/data_sources/sources/remote/net/sc_logger.dart @@ -0,0 +1,267 @@ +/* +* @message: 日志拦截器 +* @Author: Jack +* @Email: Jack@163.com +* @Date: 2020-06-18 19:47:32 +*/ +import 'package:dio/dio.dart'; +import 'dart:math' as math; +import 'package:flutter/foundation.dart'; +// 必须引入此库处理 Emoji 截断问题 +import 'package:characters/characters.dart'; + +/// description: Dio 日志拦截器 +class SCDioLogger extends Interceptor { + final bool request; + final bool requestHeader; + final bool requestBody; + final bool responseBody; + final bool responseHeader; + final bool error; + static const int initialTab = 1; + static const String tabStep = ' '; + final bool compact; + final int maxWidth; + + /// Log printer; defaults debugPrint to console. + void Function(Object object) logPrint; + final bool enableLog; + + SCDioLogger({ + this.enableLog = kDebugMode, + this.request = true, + this.requestHeader = true, + this.requestBody = true, + this.responseHeader = false, + this.responseBody = true, + this.error = true, + this.maxWidth = 90, + this.compact = true, + this.logPrint = print, // 修复:在 iOS 上 debugPrint 比 print 更稳健 + }); + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (!enableLog) return handler.next(options); + + if (request) _pReqHdr(options); + if (requestHeader) { + _pTable(options.queryParameters, header: 'Query Parameters'); + final requestHeaders = {}; + requestHeaders.addAll(options.headers); + requestHeaders['contentType'] = options.contentType?.toString(); + requestHeaders['responseType'] = options.responseType?.toString(); + requestHeaders['followRedirects'] = options.followRedirects; + requestHeaders['connectTimeout'] = options.connectTimeout; + requestHeaders['receiveTimeout'] = options.receiveTimeout; + _pTable(requestHeaders, header: 'Headers'); + _pTable(options.extra, header: 'Extras'); + } + if (requestBody && options.method != 'GET') { + final data = options.data; + if (data != null) { + if (data is Map) { + _pTable(data, header: 'Body'); + } else if (data is FormData) { + final formDataMap = Map() + ..addEntries(data.fields) + ..addEntries(data.files); + _pTable(formDataMap, header: 'Form data | ${data.boundary}'); + } else { + _pBlock(data.toString()); + } + } + } + handler.next(options); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + if (!enableLog) return handler.next(err); + + if (error) { + if (err.type == DioExceptionType.badResponse) { + final uri = err.response?.requestOptions.uri; + _pBox( + header: 'DioError ║ Status: ${err.response?.statusCode} ${err.response?.statusMessage}', + text: uri.toString()); + if (err.response?.data != null) { + logPrint('╔ ${err.type.toString()}'); + _pResp(err.response!); + } + _pLine('╚'); + } else { + final uri = err.requestOptions.uri; + _pBox(header: 'DioError ║ ${err.type} ║ ${uri.toString()}', text: err.message); + } + } + handler.next(err); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + if (!enableLog) return handler.next(response); + + if (responseHeader) { + final responseHeaders = {}; + response.headers.forEach((k, list) => responseHeaders[k] = list.toString()); + _pTable(responseHeaders, header: 'Headers'); + } + + if (responseBody) { + _pRespHdr(response); + logPrint('╔ Body'); + logPrint('║'); + _pResp(response); + logPrint('║'); + _pLine('╚'); + } + handler.next(response); + } + + void _pBox({String? header, String? text}) { + logPrint(''); + logPrint('╔╣ $header'); + logPrint('║ $text'); + _pLine('╚'); + } + + void _pResp(Response response) { + if (response.data != null) { + if (response.data is Map) { + _pMap(response.data); + } else if (response.data is List) { + logPrint('║${_ind()}['); + _pList(response.data); + logPrint('║${_ind()}]'); + } else { + _pBlock(response.data.toString()); + } + } + } + + void _pRespHdr(Response response) { + final uri = response.requestOptions.uri; + final method = response.requestOptions.method; + String header = 'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}'; + logPrint('╔╣ $header ${'═' * 20}'); + logPrint('║ ${uri.toString()}'); + logPrint('║ '); + } + + void _pReqHdr(RequestOptions options) { + final uri = options.uri; + final method = options.method; + _pBox(header: 'Request ║ $method ', text: uri.toString()); + } + + void _pLine([String pre = '', String suf = '╝']) => logPrint('$pre${'═' * maxWidth}'); + + void _pKV(String key, Object v) { + final pre = '╟ $key: '; + final msg = v.toString(); + + if (pre.length + msg.length > maxWidth) { + logPrint(pre); + _pBlock(msg); + } else { + logPrint('$pre$msg'); + } + } + + // 修复:使用 characters 处理截断,防止 Emoji 损坏 + void _pBlock(String msg) { + final charData = msg.characters; + int lines = (charData.length / maxWidth).ceil(); + for (int i = 0; i < lines; ++i) { + final start = i * maxWidth; + final end = math.min(start + maxWidth, charData.length); + logPrint('║ ' + charData.getRange(start, end).toString()); + } + } + + String _ind([int tabCount = initialTab]) => tabStep * tabCount; + + void _pMap(Map data, {int tabs = initialTab, bool isListItem = false, bool isLast = false}) { + final bool isRoot = tabs == initialTab; + final initialIndent = _ind(tabs); + tabs++; + + if (isRoot || isListItem) logPrint('║$initialIndent{'); + + data.keys.toList().asMap().forEach((index, key) { + final isLast = index == data.length - 1; + var value = data[key]; + if (value is String) value = '\"$value\"'; + + if (value is Map) { + if (compact && _flatMap(value)) { + logPrint('║${_ind(tabs)} $key: $value${!isLast ? ',' : ''}'); + } else { + logPrint('║${_ind(tabs)} $key: {'); + _pMap(value, tabs: tabs); + } + } else if (value is List) { + if (compact && _flatList(value)) { + logPrint('║${_ind(tabs)} $key: ${value.toString()}'); + } else { + logPrint('║${_ind(tabs)} $key: ['); + _pList(value, tabs: tabs); + logPrint('║${_ind(tabs)} ]${isLast ? '' : ','}'); + } + } else { + final msg = value.toString().replaceAll('\n', ''); + final indent = _ind(tabs); + final charMsg = msg.characters; + final linWidth = maxWidth - indent.length; + + // 修复:字符串分段时处理 Emoji + if (charMsg.length + indent.length > maxWidth) { + int lines = (charMsg.length / linWidth).ceil(); + for (int i = 0; i < lines; ++i) { + final start = i * linWidth; + final end = math.min(start + linWidth, charMsg.length); + logPrint('║${_ind(tabs)} ${charMsg.getRange(start, end).toString()}'); + } + } else { + logPrint('║${_ind(tabs)} $key: $msg${!isLast ? ',' : ''}'); + } + } + }); + + logPrint('║$initialIndent}${isListItem && !isLast ? ',' : ''}'); + } + + void _pList(List list, {int tabs = initialTab}) { + list.asMap().forEach((i, e) { + final isLast = i == list.length - 1; + if (e is Map) { + if (compact && _flatMap(e)) { + logPrint('║${_ind(tabs)} $e${!isLast ? ',' : ''}'); + } else { + _pMap(e, tabs: tabs + 1, isListItem: true, isLast: isLast); + } + } else { + logPrint('║${_ind(tabs + 2)} $e${isLast ? '' : ','}'); + } + }); + } + + bool _flatMap(Map map) { + return map.values.where((val) => val is Map || val is List).isEmpty && + map.toString().length < maxWidth; + } + + bool _flatList(List list) { + return (list.length < 10 && list.toString().length < maxWidth); + } + + void _pTable(Map? map, {String? header}) { + if (map == null || map.isEmpty) return; + logPrint('╔ $header '); + map.forEach((key, value) { + _pKV(key.toString(), (value ?? 'null').toString()); + }); + _pLine('╚'); + } +} \ No newline at end of file diff --git a/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart new file mode 100644 index 0000000..85b7613 --- /dev/null +++ b/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart @@ -0,0 +1,229 @@ +import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_version_manage_latest_res.dart'; +import 'package:yumi/shared/business_logic/models/res/country_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_level_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_start_page_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart'; +import 'package:yumi/shared/business_logic/models/res/version_manage_lates_review_res.dart'; +import 'package:yumi/shared/business_logic/repositories/config_repository.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +class SCConfigRepositoryImp implements SocialChatConfigRepository { + static SCConfigRepositoryImp? _instance; + + SCConfigRepositoryImp._internal(); + + factory SCConfigRepositoryImp() { + return _instance ??= SCConfigRepositoryImp._internal(); + } + + ///sys/config/country + @override + Future loadCountry() async { + final result = await http.get( + "9c370c6fef50fc728152e7a3fd5b47d2841a1d9a5a2b0bb769039fdd47f01585", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///sys/config/banner + @override + Future> getBanner({List?types}) async { + Map params = {}; + if (types != null) { + params["types"] = types; + } + final result = await http.get( + "0d8319c7a696c73d58f5e0ae304dc663f574ade4154ed90ccaee524f6ba14490", + queryParams: params, + fromJson: + (json) => + (json as List).map((e) => SCIndexBannerRes.fromJson(e)).toList(), + ); + return result; + } + + ///sys/config/enum/config + @override + Future getConfig() async { + final result = await http.get( + "e6fcae3c4a41806e15b4fc015126361267972b2854bbea42d0f0089c5f1c7724", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///sys/config/customer-service + @override + Future getCustomerService() async { + final result = await http.get( + "ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + + + ///ranking/top-four-with-reward + @override + Future> topFourWithReward() async { + final result = await http.post>( + "162c2587d0236c6122a197f872daf865c29cc45b9c80570e9f4432051b2040e4", + data: {}, + fromJson: + (json) => + (json as List) + .map((e) => SCTopFourWithRewardRes.fromJson(e)) + .toList(), + ); + return result; + } + + ///sys/config/sys/notice-message + @override + Future getNoticeMessage() async { + final result = await http.get( + "b45ab3738ec718525eb02dcc8b1c79165ae6295dd8ec0e521307c70a005119b2", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + + ///sys/config/banner/start-page + @override + Future> getStartPage() async { + final result = await http.get( + "0d8319c7a696c73d58f5e0ae304dc66352cfd4d4480504cd5f589338f4f90157", + fromJson: + (json) => + (json as List).map((e) => SCStartPageRes.fromJson(e)).toList(), + ); + return result; + } + + ///sys/config/room/getSudCode + @override + Future getSudCode() async { + final result = await http.get( + "690f390b1c91b6cb0b4fdbeb98215a9f2fb1663213697d032f7fe4dd9aa9e8f4", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///sys/config/country/top-six + @override + Future loadTopSix() async { + final result = await http.get( + "9c370c6fef50fc728152e7a3fd5b47d263ca03b0e63d90aeb3c57f8d946ecd78", + fromJson: (json) => CountryRes.fromJson(json), + ); + return result; + } + + ///order/product-config + @override + Future> productConfig() async { + final result = await http.get>( + "1585c72b88d0d249c7078aae852a0806cc3d8e483b8d861962a977a00523180a", + fromJson: + (json) => + (json as List).map((e) => SCProductConfigRes.fromJson(e)).toList(), + ); + return result; + } + + ///order/purchase-pay/google + @override + Future googlePay( + String product, + String signature, + String purchaseData, { + String? friendId, + }) async { + Map params = {}; + params["product"] = product; + params["signature"] = signature; + params["purchaseData"] = purchaseData; + if (friendId != null) { + params["friendId"] = friendId; + } + final result = await http.post( + "3a5ab504e90faccde8258f4ea1dd6bcdad6eec89ad8cc807b184537e0fdb6904", + data: params, + fromJson: (json) => SCGooglePayRes.fromJson(json), + ); + return result; + } + + ///order/purchase-pay/apple + @override + Future applePay( + String product, + String receipt, + String transaction, { + String? friendId, + }) async { + Map params = {}; + params["product"] = product; + params["receipt"] = receipt; + params["transaction"] = transaction; + if (friendId != null) { + params["friendId"] = friendId; + } + final result = await http.post( + "3a5ab504e90faccde8258f4ea1dd6bcd7e041553c5af750a6eff64c18e7de276", + data: params, + fromJson: (json) => SCGooglePayRes.fromJson(json), + ); + return result; + } + + ///sys/static-config/level + @override + Future configLevel() async { + final result = await http.get( + "9d964981940fe0403d53d6f44e7cf7a48c2534f1e31f6b5fa9b1fae672b836d1", + fromJson: (json) => SCLevelConfigRes.fromJson(json), + ); + return result; + } + + + ///sys/version/manage/release/latest + @override + Future versionManageLatest() async{ + final result = await http.get( + "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687", + fromJson: (json) => SCVersionManageLatestRes.fromJson(json), + ); + return result; + } + + ///sys/version/manage/latest/review + @override + Future versionManageLatestReview() async{ + final result = await http.get( + "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232", + fromJson: (json) => VersionManageLatesReviewRes.fromJson(json), + ); + return result; + } + + ///sys/config/customer-service + @override + Future customerService() async{ + final result = await http.get( + "ba316258c14cc3ebddb6d28ec314bc5704e593861ca693058e9e98ab3114cf05", + fromJson: (json) => SocialChatUserProfile.fromJson(json), + ); + return result; + } + +} diff --git a/lib/shared/data_sources/sources/repositories/sc_general_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_general_repository_imp.dart new file mode 100644 index 0000000..125bbb0 --- /dev/null +++ b/lib/shared/data_sources/sources/repositories/sc_general_repository_imp.dart @@ -0,0 +1,30 @@ +import 'dart:io'; +import 'package:yumi/shared/business_logic/repositories/general_repository.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +class SCGeneralRepositoryImp implements SocialChatGeneralRepository { + static SCGeneralRepositoryImp? _instance; + + SCGeneralRepositoryImp._internal(); + + factory SCGeneralRepositoryImp() { + return _instance ??= SCGeneralRepositoryImp._internal(); + } + + ///external/oss/upload + @override + Future upload(File image) async { + String filePath = image.path; + var name = + filePath.substring(filePath.lastIndexOf("/") + 1, filePath.length); + FormData formData = FormData.fromMap( + {"file": await MultipartFile.fromFile(filePath, filename: name)}); + final result = await http.post( + "91458261d1ab337ad7b1da3610dd496cf7f58dbb0fecacfbdfe516c4892b1694", + data: formData, + fromJson: (json) => json as String, + ); + return result; + } +} diff --git a/lib/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart new file mode 100644 index 0000000..737e68e --- /dev/null +++ b/lib/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/repositories/gift_repository.dart'; + +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +class SCGiftRepositoryImp implements SocialChatGiftRepository { + static SCGiftRepositoryImp? _instance; + + SCGiftRepositoryImp._internal(); + + factory SCGiftRepositoryImp() { + return _instance ??= SCGiftRepositoryImp._internal(); + } + + ///gift/wall + @override + Future> giftWall(String userId) async { + final result = await http.get>( + "6cda6ef50ed40e4656f0484136188abe", + queryParams: {"userId": userId}, + fromJson: + (json) => (json as List).map((e) => SocialChatGiftRes.fromJson(e)).toList(), + ); + return result; + } +} diff --git a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart new file mode 100644 index 0000000..8810bf9 --- /dev/null +++ b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart @@ -0,0 +1,825 @@ +import 'package:flutter/cupertino.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/shared/business_logic/models/req/sc_give_away_gift_room_acceptscmd.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_is_follow_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_mic_go_up_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_contribute_level_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_join_black_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_detail_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_grab_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_reward_info_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_task_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart'; +import 'package:yumi/shared/business_logic/models/res/user_count_guard_res.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_backpack_res.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_by_group_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_member_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_task_claimable_count_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart'; +import 'package:yumi/shared/business_logic/repositories/room_repository.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +class SCChatRoomRepository implements SocialChatRoomRepository { + static SCChatRoomRepository? _instance; + + SCChatRoomRepository._internal(); + + factory SCChatRoomRepository() { + return _instance ??= SCChatRoomRepository._internal(); + } + + ///room/live-voice/discovery + @override + Future> discovery({bool? allRegion}) async { + Map queryParams = {}; + if (allRegion != null) { + queryParams["allRegion"] = allRegion; + } + final result = await http.get( + "363603111e51beac2d183014dd29b81ced652239ec13178acd7001096cab7429", + queryParams: queryParams, + fromJson: + (json) => (json as List).map((e) => SocialChatRoomRes.fromJson(e)).toList(), + ); + return result; + } + + ///live/mic/list + @override + Future> micList(String roomId) async { + Map queryParams = {}; + queryParams["roomId"] = roomId; + final result = await http.get( + "b1d6a1284f5ddeaa1cbb1430f2991e8a", + queryParams: queryParams, + fromJson: + (json) => (json as List).map((e) => MicRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/profile/specific + @override + Future specific(String roomId) async { + Map queryParams = {}; + queryParams["roomId"] = roomId; + final result = await http.get( + "08c7cb961a83d24c9e0d4b11d7af3f0a8f2fe5ccf18a5ca6a0caf80ecefaeb02", + queryParams: queryParams, + fromJson: (json) => MyRoomRes.fromJson(json), + ); + return result; + } + + ///room/relation/status + @override + Future isFollowRoom(String roomId) async { + Map queryParams = {}; + queryParams["roomId"] = roomId; + final result = await http.get( + "1c9e38f372be8ae5afde3a37710c24815dee9b62b097054c3e40e333e7747681", + queryParams: queryParams, + fromJson: (json) => SCIsFollowRoomRes.fromJson(json), + ); + return result; + } + + ///room/contribution/level + @override + Future contributeLevel(String roomId) async { + Map queryParams = {}; + queryParams["roomId"] = roomId; + final result = await http.get( + "730cfe8ab28a5bb6b75adb2d29320bd85285ddc5a507f997b1c6a23980d22a70", + queryParams: queryParams, + fromJson: (json) => SCRoomContributeLevelRes.fromJson(json), + ); + return result; + } + + ///activity/room-contribution-activity + @override + Future roomContributionActivity(String roomId) async { + Map queryParams = {}; + queryParams["roomId"] = roomId; + final result = await http.get( + "fe8a114454bb0a64c51b5a2840b89b9caad8e1b76b3e1c570f21d856505cee7d1621a5cc87df186c3e223af28d3b4455", + queryParams: queryParams, + fromJson: (json) => SCRoomContributeLevelRes.fromJson(json), + ); + return result; + } + + ///live/mic/go-up + @override + Future micGoUp( + String roomId, + num mickIndex, { + String? eventType, + String? inviterId, + }) async { + Map params = {}; + params["roomId"] = roomId; + params["mickIndex"] = mickIndex; + if (eventType != null) { + params["eventType"] = eventType; + } + if (inviterId != null) { + params["inviterId"] = inviterId; + } + final result = await http.post( + "6fb96d680637b8e931b962f6e05260b0", + data: params, + fromJson: (json) => SCMicGoUpRes.fromJson(json), + ); + return result; + } + + ///live/mic/go-down + @override + Future micGoDown(String roomId, num mickIndex) async { + final result = await http.post( + "11335ecd55d99c9100f5cd070e5b73f1574aff257ce7e668d08f4caccd1c6232", + data: {"roomId": roomId, "mickIndex": mickIndex}, + fromJson: (json) => null, + ); + return result; + } + + ///live/mic/lock + @override + Future micLock(String roomId, num mickIndex, bool lock) async { + final result = await http.post( + "70ffb9681e0103463b1faf883935e55d", + data: {"roomId": roomId, "mickIndex": mickIndex, "lock": lock}, + fromJson: (json) => null, + ); + return result; + } + + ///live/mic/mute + @override + Future micMute(String roomId, num mickIndex, bool mute) async { + final result = await http.post( + "97927b83eb6b9fccda567f3bf12cbf49", + data: {"roomId": roomId, "mickIndex": mickIndex, "mute": mute}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///live/user/list + @override + Future> roomOnlineUsers(String roomId) async { + final result = await http.get( + "a01ade8fbb604c0904557a9483fcc4d9", + queryParams: {"roomId": roomId}, + fromJson: + (json) => (json as List).map((e) => SocialChatUserProfile.fromJson(e)).toList(), + ); + return result; + } + + ///user/user-profile/card + @override + Future roomUserCard(String roomId, String userId) async { + final result = await http.get( + "2fa1d4f56bb726558904ce2a50b83f967732a24a30dccae0c57cf03f27319ea4", + queryParams: {"roomId": roomId, "userId": userId}, + fromJson: (json) => RoomUserCardRes.fromJson(json), + ); + return result; + } + + ///gift/list + @override + Future> giftList() async { + final result = await http.get>( + "3a613b7450f8fd9082b988bd21df454d", + fromJson: + (json) => (json as List).map((e) => SocialChatGiftRes.fromJson(e)).toList(), + ); + return result; + } + + ///gift/group-tab + @override + Future> giftListByGroup() async { + final result = await http.get>( + "bfbb6d98524f1e8aecf695e32ed24c2f", + fromJson: + (json) => + (json as List).map((e) => GiftByGroupRes.fromJson(e)).toList(), + ); + return result; + } + + ///gift/batch + @override + Future giveGift( + List acceptUserIds, + String giftId, + num quantity, + bool checkCombo, { + String? roomId, + SCGiveAwayGiftRoomAcceptsCmd? accepts, + String? dynamicContentId, + }) async { + Map params = {}; + if (roomId != null) { + params["roomId"] = roomId; + } + params["giftId"] = giftId; + params["acceptUserIds"] = acceptUserIds; + params["quantity"] = quantity; + params["checkCombo"] = checkCombo; + if (accepts != null) { + params["accepts"] = accepts.toJson(); + } + if (dynamicContentId != null) { + params["dynamicContentId"] = dynamicContentId; + } + + final result = await http.post( + "daa4f379ff3e429e55c318b6af7291e8", + data: params, + fromJson: (json) => json as double, + ); + return result; + } + + ///live/mic/kill + @override + Future micKill(String roomId, num mickIndex) async { + final result = await http.post( + "e50ed2f3406bc9421f3271f8be367d9b", + data: {"roomId": roomId, "mickIndex": mickIndex}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///sys/reported + @override + Future reported( + String reportUserId, + String reportedUserId, + num reportType, { + String? reportedContent, + num? relatedId, + String? imageUrls, + String? videoUrls, + }) async { + Map params = {}; + params["reportUserId"] = reportUserId; + params["reportedUserId"] = reportedUserId; + params["reportType"] = reportType; + if (reportedContent != null && reportedContent.isNotEmpty) { + params["reportedContent"] = reportedContent; + } + if (relatedId != null) { + params["relatedId"] = relatedId; + } + if (imageUrls != null && imageUrls.isNotEmpty) { + params["imageUrls"] = imageUrls; + } + if (videoUrls != null && videoUrls.isNotEmpty) { + params["videoUrls"] = videoUrls; + } + final result = await http.post( + "552d752693c91a4a3dd9723634dff21f", + data: params, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/live-voice/search + @override + Future> searchRoom(String roomAccount) async { + final result = await http.get>( + "363603111e51beac2d183014dd29b81cce9dda0f9cd8c267f6e4fd3f05bda090", + queryParams: {"roomAccount": roomAccount}, + fromJson: + (json) => (json as List).map((e) => SocialChatRoomRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/blacklist/join + @override + Future joinBlacklist( + String roomId, + String userId, + num outTime, + ) async { + final result = await http.post( + "fe07c4b4a0ab1a38a5674c77444296f1a31f65259a63574dbbfbfe543f1b99a6", + data: {"roomId": roomId, "userId": userId, "outTime": outTime}, + fromJson: (json) => SCRoomJoinBlackListRes.fromJson(json), + ); + return result; + } + + ///room/blacklist/remove + @override + Future removeBlacklist(String roomId, String userId) async { + final result = await http.post( + "68900c787a89c1092b21ee1a610ef323380d4f62784dacdb4e3d4e1f60770a39", + data: {"roomId": roomId, "userId": userId}, + fromJson: (json) => null, + ); + return result; + } + + ///user/count/guard + @override + Future> userCountGuard(String userId) async { + final result = await http.get>( + "cef5832baa42d4adb30f4b10bef0887e574aff257ce7e668d08f4caccd1c6232", + queryParams: {"userId": userId}, + fromJson: + (json) => + (json as List).map((e) => UserCountGuardRes.fromJson(e)).toList(), + ); + return result; + } + + ///material/emoji/all + @override + Future> emojiAll() async { + final result = await http.get>( + "1d2546ea50639e65f04d029dd0a9655eda4aeb3a4bd182b663d60d87cb3f2938", + fromJson: + (json) => + (json as List).map((e) => SCRoomEmojiRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/member/change-role + @override + Future changeRoomRole( + String roomId, + String userType, + String changeUserId, + String operatorUserId, + String reason, + ) async { + final result = await http.put( + "39b2c4ffda0682c126435fc37480a0961be75ad669bd0bf28d4e875a45934dfe", + data: { + "roomId": roomId, + "userType": userType, + "changeUserId": changeUserId, + "operatorUserId": operatorUserId, + "reason": reason, + }, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/setting/locked + @override + Future roomLocked(String roomId, String password) async { + final result = await http.put( + "e9531da4dd5c83bd5386366519ac6d0b727c997eb7a1adb8323f850b6c541f3b", + data: {"roomId": roomId, "password": password}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/setting/unlocked + @override + Future roomUnlocked(String roomId) async { + final result = await http.post( + "d8e78b8fb4cf9f0d41716d9978235334d9382f6a419ce06c2dab0b362c318a5d", + data: {"roomId": roomId}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/setting + @override + Future updateRoomSetting( + String roomId, + String roomAcount, + BuildContext context, { + bool? touristMike, + String? takeMicRole, + bool? touristMsg, + String? joinGolds, + num? mikeSize, + bool? allowMusic, + bool? adminLockSeat, + bool? showHeartbeat, + bool? openKtvMode, + String? roomSpecialMikeType, + }) async { + Map params = {}; + params["roomId"] = roomId; + if (touristMike != null) { + params["touristMike"] = touristMike; + } + if (takeMicRole != null) { + params["takeMicRole"] = takeMicRole; + } + if (touristMsg != null) { + params["touristMsg"] = touristMsg; + } + if (joinGolds != null) { + params["joinGolds"] = joinGolds; + } + if (mikeSize != null) { + params["mikeSize"] = mikeSize; + } + if (allowMusic != null) { + params["allowMusic"] = allowMusic; + } + if (adminLockSeat != null) { + params["adminLockSeat"] = adminLockSeat; + } + if (showHeartbeat != null) { + params["showHeartbeat"] = showHeartbeat; + } + if (openKtvMode != null) { + params["openKtvMode"] = openKtvMode; + } + if (roomSpecialMikeType != null) { + params["roomSpecialMikeType"] = roomSpecialMikeType; + } + final result = await http.put( + "cd447427897a27ec5a710ae5861907c0", + data: params, + fromJson: (json) => json as bool, + ); + + Provider.of(context, listen: false).dispatchMessage( + Msg( + groupId: roomAcount, + msg: roomId, + type: SCRoomMsgType.roomSettingUpdate, + ), + addLocal: false, + ); + return result; + } + + ///room/member + @override + Future> roomMember( + String roomId, { + String? lastId, + }) async { + Map parm = {}; + parm["roomId"] = roomId; + if (lastId != null) { + parm["lastId"] = lastId; + } + final result = await http.get>( + "76393fe91160b664a81fdc0545a586ef", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => SocialChatRoomMemberRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/contribution/rank + @override + Future> roomContributionRank( + String roomId, + String dataType, { + num size = 10, + }) async { + Map parm = {}; + parm["roomId"] = roomId; + parm["dataType"] = dataType; + parm["size"] = size; + final result = await http.get>( + "730cfe8ab28a5bb6b75adb2d29320bd80ed2ff7efd67897d5b80fd620a1b0676", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => RoomGiftRankRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/setting/kick-off-microphone + @override + Future kickOffMicrophone(String roomId, String kickUserId) async { + final result = await http.post( + "a4465894e21be08d833ef701566ffe1b6a835338046b35fb94271445c996f6e3574aff257ce7e668d08f4caccd1c6232", + data: {"roomId": roomId, "kickUserId": kickUserId}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///gift/give/lucky-gift + @override + Future giveLuckyGift( + List acceptUserIds, + String giftId, + num quantity, + bool checkCombo, { + String? roomId, + SCGiveAwayGiftRoomAcceptsCmd? accepts, + }) async { + Map params = {}; + if (roomId != null) { + params["roomId"] = roomId; + } + params["giftId"] = giftId; + params["acceptUserIds"] = acceptUserIds; + params["quantity"] = quantity; + params["checkCombo"] = checkCombo; + if (accepts != null) { + params["accepts"] = accepts.toJson(); + } + + final result = await http.post( + "6ddf7ebecfa97adf23c6303877c7763ce591ff710a7abc4fe1e335d16efee21b", + data: params, + fromJson: (json) => json as double, + ); + return result; + } + + ///activity/leaderboard/appLeaderboard + @override + Future appLeaderboard() async { + final result = await http.get( + "04f600f838ff20e7f794d3b555103541adda7fb851a14e1c733d58bc5cd3a23b343860371cb9a34e6cdb9005c91c78a9", + fromJson: (json) => SCBannerLeaderboardRes.fromJson(json), + ); + return result; + } + + ///gift/backpack + @override + Future> giftBackpack() async { + final result = await http.get>( + "48ab0afa0e4305070eaa82edc3fc3996", + fromJson: + (json) => + (json as List).map((e) => SocialChatGiftBackpackRes.fromJson(e)).toList(), + ); + return result; + } + + ///api/rocket/status + @override + Future rocketStatus(String roomId) async { + Map params = {}; + params["roomId"] = roomId; + final result = await http.get( + "1b650f6fc863c8c86ec8d921340a65ccba2fafbaede21817c8d7e89774331852", + queryParams: params, + fromJson: (json) => SCRoomRocketStatusRes.fromJson(json), + ); + return result; + } + + ///api/rocket/config/enabled + @override + Future> rocketConfigEnabled() async { + final result = await http.get>( + "09620f7f773a75af633e034b80393e0ee8adb46dafdd3600f74d3c85e2c07e4f", + fromJson: + (json) => + (json as List) + .map((e) => SCRoomRocketConfigRes.fromJson(e)) + .toList(), + ); + return result; + } + + ///api/rocket/claim + @override + Future rocketClaim(String roomId) async { + final result = await http.post( + "4c083b13f2be2a26641018619bdc0347574aff257ce7e668d08f4caccd1c6232", + data: {"roomId": roomId}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room-violation/handle + @override + Future roomViolationHandle( + String roomId, + int violationType, + int operationType, + String description, { + List? imageUrls, + }) async { + Map parm = {}; + parm["roomId"] = roomId; + parm["violationType"] = violationType; + parm["operationType"] = operationType; + parm["description"] = description; + if (imageUrls != null && imageUrls.isNotEmpty) { + parm["screenshotUrls"] = imageUrls; + } + final result = await http.post( + "fc47c4e6e3ba228391cd35bf99f301206f894acdcdbac7ccb7e88e9d69ee3eff", + data: parm, + fromJson: (json) => SCViolationHandleRes.fromJson(json), + ); + return result; + } + + ///room/red-packet/list + @override + Future> roomRedPacketList( + String roomId, + int current, { + int size = 20, + }) async { + Map parm = {}; + parm["roomId"] = roomId; + parm["current"] = current; + parm["size"] = size; + final result = await http.get>( + "318de84592dca60c287ba2f54d60756abc4a9e8c4704c0f73c085e7ab50e7fdb", + queryParams: parm, + fromJson: + (json) => + (json as List) + .map((e) => SCRoomRedPacketListRes.fromJson(e)) + .toList(), + ); + return result; + } + + ///room/red-packet/create + @override + Future roomSendRedPacket( + String roomId, + String totalAmount, + String totalCount, + String expireMinutes, + ) async { + Map parm = {}; + parm["roomId"] = roomId; + parm["totalAmount"] = totalAmount; + parm["totalCount"] = totalCount; + parm["expireMinutes"] = expireMinutes; + final result = await http.post( + "318de84592dca60c287ba2f54d60756af01ca103e12cc332b59b5a8e39c778ec", + data: parm, + fromJson: (json) => SCRoomRedPacketListRes.fromJson(json), + ); + return result; + } + + ///room/red-packet/grab + @override + Future roomRedPacketGrab(String packetId) async { + Map parm = {}; + parm["packetId"] = packetId; + final result = await http.post( + "318de84592dca60c287ba2f54d60756a0b2a61d0ce4f3f4d50875dcc95b640af", + data: parm, + fromJson: (json) => SCRoomRedPacketGrabRes.fromJson(json), + ); + return result; + } + + ///room/red-packet/detail + @override + Future roomRedPacketDetail(String packetId) async { + Map parm = {}; + parm["packetId"] = packetId; + final result = await http.get( + "318de84592dca60c287ba2f54d60756a7f342deecb1b83d2d8429c59146f10f0", + queryParams: parm, + fromJson: (json) => SCRoomRedPacketDetailRes.fromJson(json), + ); + return result; + } + + ///activity/room-contribution-activity/room-reward/info + @override + Future roomRewardInfo(String roomId) async { + Map parm = {}; + parm["roomId"] = roomId; + final result = await http.get( + "fe8a114454bb0a64c51b5a2840b89b9caad8e1b76b3e1c570f21d856505cee7ddf616bd4bdc133af05422ff3f7becc575ad9f024e25ab8275b5792a94159a781", + queryParams: parm, + fromJson: (json) => SCRoomRewardInfoRes.fromJson(json), + ); + return result; + } + + ///activity/room-contribution-activity/room-reward/receive + @override + Future roomRewardReceive(String roomId) async { + Map parm = {}; + parm["roomId"] = roomId; + final result = await http.post( + "1224764475bbd027233e79b81fbdbbf8b25565bc1234221b5e7f9651e0d97576fbcc314a4dd141539dc44cd84f2f13e4c35b35e57872c47078faccd94224944e", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/red-packet/create-platform + @override + Future sendPlatformRedPack( + String roomId, + String totalAmount, + String totalCount, + String expireMinutes, + ) async { + Map parm = {}; + parm["roomId"] = roomId; + parm["totalAmount"] = totalAmount; + parm["totalCount"] = totalCount; + parm["expireMinutes"] = expireMinutes; + final result = await http.post( + "318de84592dca60c287ba2f54d60756a3d2cb7989afc93dcdbec1a68c53cac37", + data: parm, + fromJson: (json) => SCRoomRedPacketListRes.fromJson(json), + ); + return result; + } + + ///gift/activity/list + @override + Future> giftActivityList() async { + final result = await http.get>( + "60296415428765e04263c733bfc8428c397bb180e31856d94bf25d85eb342787", + fromJson: + (json) => (json as List).map((e) => SocialChatGiftRes.fromJson(e)).toList(), + ); + return result; + } + + ///room-task/list + @override + Future roomTaskList(int taskCategory) async { + Map parm = {}; + parm["taskCategory"] = taskCategory; + final result = await http.get( + "30743db31a4b5236451c71864f914359", + queryParams: parm, + fromJson: (json) => SCRoomTaskListRes.fromJson(json), + ); + return result; + } + + ///room-task/claim + @override + Future roomTaskClaim(String taskCode) async { + Map parm = {}; + parm["taskCode"] = taskCode; + final result = await http.post( + "439c60bae741f83884ada20b837a00f9", + data: parm, + fromJson: (json) => Tiers.fromJson(json), + ); + return result; + } + + ///room-task/claimable-count + @override + Future roomTaskClaimableCount() async { + final result = await http.get( + "dba51232871855314973867d38c76191ae96d33f4d1b3265974cd070f03c3830", + fromJson: (json) => SCRoomTaskClaimableCountRes.fromJson(json), + ); + return result; + } + + + + + + + +} diff --git a/lib/shared/data_sources/sources/repositories/sc_store_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_store_repository_imp.dart new file mode 100644 index 0000000..aaff4f8 --- /dev/null +++ b/lib/shared/data_sources/sources/repositories/sc_store_repository_imp.dart @@ -0,0 +1,133 @@ +import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; + +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:yumi/shared/business_logic/repositories/store_repository.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +class SCStoreRepositoryImp implements SocialChatStoreRepository { + ///material/props-store/list + @override + Future> storeList( + String currencyType, + String propsType, + ) async { + Map parm = {}; + parm["currencyType"] = currencyType; + parm["propsType"] = propsType; + final result = await http.get>( + "cfa25ffa98eedd1fe4e0c0466e0aadba85e4421b482ce9dd58a9f96d51f5a577", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => StoreListRes.fromJson(e)).toList(), + ); + return result; + } + + ///material/props-store/purchasing + @override + Future storePurchasing( + String propsId, + String type, + String currencyType, + String days, { + String? acceptUserId, + }) async { + Map parm = {}; + parm["propsId"] = propsId; + parm["type"] = type; + parm["currencyType"] = currencyType; + parm["days"] = days; + if (acceptUserId != null) { + parm["acceptUserId"] = acceptUserId; + } + final result = await http.post( + "cfa25ffa98eedd1fe4e0c0466e0aadba34acc7c71f29c8b833f6bb11b09789c9", + data: parm, + fromJson: (json) => json as double, + ); + return result; + } + + ///material/props-store/backpack + @override + Future> storeBackpack(String userId, String type) async{ + Map parm = {}; + parm["userId"] = userId; + parm["type"] = type; + final result = await http.get>( + "cfa25ffa98eedd1fe4e0c0466e0aadba7ae183d89168d8fafbd3be51ed693f3b", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => BagsListRes.fromJson(e)).toList(), + ); + return result; + } + + ///material/props-store/switch-use + @override + Future switchPropsUse(String type, String propsId, bool unload) async{ + Map parm = {}; + parm["type"] = type; + parm["propsId"] = propsId; + parm["unload"] = unload; + final result = await http.post( + "cfa25ffa98eedd1fe4e0c0466e0aadba840b6522b1ce781d9917b065f1fa7724", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/theme/customize/latest + @override + Future themeCustomizeLatest() async{ + final result = await http.get( + "dffae19fd5bf021ce728384106772e22854a7fa69c4cb0a234128de26618a9f2", + fromJson: (json) => SCRoomThemeListRes.fromJson(json), + ); + return result; + } + + ///room/theme/backpack + @override + Future> roomThemeBackpack() async{ + final result = await http.get>( + "6056121c48cba2c743ed6d2c235403a22826cd0a280582ee3ae7bfc4b1ce7391", + fromJson: + (json) => + (json as List).map((e) => SCRoomThemeListRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/theme/switch-use + @override + Future themeSwitchUse(String themeId, bool unload) async{ + Map parm = {}; + parm["themeId"] = themeId; + parm["unload"] = unload; + final result = await http.get( + "1af2a817cf106ccd65b9499d0b6987dfaa528b33bc4166e3f8db18f674507134", + queryParams: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/theme/customize + @override + Future roomThemeCustomize(String themeUrl) async{ + Map parm = {}; + parm["themeUrl"] = themeUrl; + final result = await http.post( + "dffae19fd5bf021ce728384106772e2296e9eab98ece5441112164ce0ab9521f", + data: parm, + fromJson: (json) => json as double, + ); + return result; + } + +} diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart new file mode 100644 index 0000000..62d894b --- /dev/null +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -0,0 +1,1118 @@ +import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_gold_record_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_record_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_grab_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_send_res.dart'; +import 'package:yumi/shared/business_logic/models/req/sc_mobile_auth_cmd.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_edit_room_info_res.dart'; +import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/message_friend_user_res.dart'; +import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_black_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart'; +import 'package:yumi/shared/business_logic/repositories/user_repository.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; + +class SCAccountRepository implements SocialChatUserRepository { + static SCAccountRepository? _instance; + + SCAccountRepository._internal(); + + factory SCAccountRepository() { + return _instance ??= SCAccountRepository._internal(); + } + + @override + Future getUser(String id) async { + final result = await http.get( + "", + fromJson: (json) => SocialChatLoginRes.fromJson(json), + ); + return result; + } + + ///auth/account/create + @override + Future regist( + String type, + String openId, + SCUserProfileCmd userProfileCmd, { + SCMobileAuthCmd? mobileAuthCmd, + String invitePeople = "", + }) async { + var param = {}; + param["type"] = type; + param["openId"] = openId; + param["profile"] = userProfileCmd.toJson(); + if (mobileAuthCmd != null) { + param["mobile"] = mobileAuthCmd.toJson(); + } + if (invitePeople.isNotEmpty) { + param["invitePeople"] = invitePeople; + } + final result = await http.post( + "aa35848b37e5ba74d93e5a1f719c579cb289b5940c15040473b6f57550de35df", + data: param, + fromJson: (json) => SocialChatLoginRes.fromJson(json), + ); + return result; + } + + ///auth/account/login + @override + Future loginForAccount(String account, String pwd) async { + final result = await http.post( + "e64f27b9ba6b37881120f4584a5444a5c684d8491b703d0af953e5cc6a5f4cec", + data: {"account": account, "pwd": pwd}, + fromJson: (json) => SocialChatLoginRes.fromJson(json), + ); + return result; + } + + ///auth/account/login/channel + @override + Future loginForChannel(String authType, String openId) async { + final result = await http.post( + "e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44", + data: {"authType": authType, "openId": openId}, + fromJson: (json) => SocialChatLoginRes.fromJson(json), + ); + return result; + } + + ///room/relation/follow + @override + Future> followRoomList({String? lastId}) async { + Map param = {}; + if (lastId != null) { + param["lastId"] = lastId; + } + final result = await http.get( + "59a548c91daed73d9becdf4eba7daf628ba84731fa88da5433b944ca2e6d6098", + queryParams: param, + fromJson: + (json) => + (json as List).map((e) => FollowRoomRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/relation/joined + @override + Future> joined() async { + final result = await http.get( + "06ff6a670e789a391a7af40ce5d6b15c326060e4bcd797edd258ca1d8bb139be", + fromJson: + (json) => + (json as List).map((e) => FollowRoomRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/relation/trace + @override + Future> trace({String? lastId}) async { + Map param = {}; + if (lastId != null) { + param["lastId"] = lastId; + } + final result = await http.get( + "95523e566901192267301b47f35adc8d5de33f03aaf96911b1a0c8131b938f28", + queryParams: param, + fromJson: + (json) => + (json as List).map((e) => FollowRoomRes.fromJson(e)).toList(), + ); + return result; + } + + ///room/profile + @override + Future myProfile() async { + final result = await http.get( + "1e41384e55cbd6c2374608b129e2ed27", + fromJson: (json) => MyRoomRes.fromJson(json), + ); + return result; + } + + ///room/live-voice/create + @override + Future createRoom() async { + final result = await http.post( + "363603111e51beac2d183014dd29b81cf01ca103e12cc332b59b5a8e39c778ec", + fromJson: (json) => SocialChatRoomRes.fromJson(json), + ); + return result; + } + + ///room/live-voice/entry + @override + Future entryRoom( + String roomId, { + String? pwd, + String redPackId = "", + bool needOpenRedenvelope = false, + }) async { + var param = {}; + param["roomId"] = roomId; + if (pwd != null) { + param["pwd"] = pwd; + } + //自定义字段,用来在输入密码框api拦截时获取 + param["needOpenRedenvelope"] = needOpenRedenvelope; + param["redPackId"] = redPackId; + final result = await http.post( + "363603111e51beac2d183014dd29b81c219577507bcc9c6abfaf327548fed0e0", + data: param, + fromJson: (json) => JoinRoomRes.fromJson(json), + ); + return result; + } + + ///live/user/heartbeat + @override + Future heartbeat(String status, bool upMick, {String? roomId}) async { + var parm = {}; + parm["status"] = status; + parm["upMick"] = upMick; + parm["task"] = true; + if (roomId != null) { + parm["sessionId"] = roomId; + } + final result = await http.post( + "58f9485b5b9f401c6a14bc1e6534122c7a085bb5f6a613094188ca56fe8bc03d", + data: parm, + fromJson: (json) => {}, + ); + return result; + } + + ///external/im-trtc/tmp/agora/sig + @override + Future getRtcToken(String channel, String userId) async { + final result = await http.get( + "2572c69d25a0946b0cb35b24bdfcd61c9bcef4afdaced7dfc47a641bd9584f96", + queryParams: {"channelName": channel, "userId": userId}, + fromJson: (json) => SCRtcTokenRes.fromJson(json), + ); + return result; + } + + ///live/room/quit + @override + Future quitRoom(String roomId) async { + final result = await http.get( + "3e451c31ab651a577a3b7796b32041e7", + queryParams: {"roomId": roomId}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/profile + @override + Future editRoomInfo( + String roomId, + String roomCover, + String roomName, + String roomDesc, + String event, + ) async { + final result = await http.put( + "1e41384e55cbd6c2374608b129e2ed27", + data: { + "id": roomId, + "roomCover": roomCover, + "roomName": roomName, + "roomDesc": roomDesc, + "event": event, + }, + fromJson: (json) => SCEditRoomInfoRes.fromJson(json), + ); + return result; + } + + ///user/user-profile + @override + Future loadUserInfo(String userId) async { + final result = await http.get( + "2fa1d4f56bb726558904ce2a50b83f961e1cf9b945681bdfb3d4cf7a5ecfc943", + queryParams: {"userId": userId}, + fromJson: (json) => SocialChatUserProfile.fromJson(json), + ); + return result; + } + + ///room/relation/follow/action + @override + Future followRoom(String roomId) async { + final result = await http.post( + "59a548c91daed73d9becdf4eba7daf6226c857599fe196e293c826f60146fa9f", + data: {"roomId": roomId}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/support-relation/follow + @override + Future followUser(String userId) async { + final result = await http.post( + "5ee2ca437d883bcd9a4030c44b20b059555c9f831613c06da141d92ffef9d8b7", + data: {"userId": userId}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///team/heartbeat + @override + Future anchorHeartbeat(String roomId) async { + final result = await http.get( + "d28c7d4eeb945fb765c8206c8f79d77f", + queryParams: {"roomId": roomId}, + fromJson: (json) => {}, + ); + return result; + } + + ///wallet/gold/balance + @override + Future balance() async { + final result = await http.get( + "6e864e3aa03c0c398fa2d8f909d21aef2f0b7f5a6a5c523c28407ff12d872b31", + fromJson: (json) => json as double, + ); + return result; + } + + ///user/user-profile/search + @override + Future searchUser(String account) async { + final result = await http.get( + "2fa1d4f56bb726558904ce2a50b83f9604b0c834cdef3e5a6c3f81b198ab9a4b", + queryParams: {"account": account}, + fromJson: (json) => SocialChatUserProfile.fromJson(json), + ); + return result; + } + + ///user/account/account-pwd-reset + @override + Future pwdReset(String pwd) async { + final result = await http.post( + "745ce65f1d68f106702ad3c636aca0e0ac57f82127b3ac414551924946593db7", + data: {"pwd": pwd}, + fromJson: (json) => null, + ); + return result; + } + + ///user/user-auth-type/account-is-bind + @override + Future accountIsBind() async { + final result = await http.get( + "0bc43bb5ac93dd30919c4a7c3bbd60335894d3a279a24600c6a7e5dc9536f0e10e634626c0d4977d4df50adfb36c576e", + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/user-auth-type/account-bind + @override + Future bind(String pwd) async { + final result = await http.post( + "0bc43bb5ac93dd30919c4a7c3bbd6033963c952c9eca882afb1a172894887db3574aff257ce7e668d08f4caccd1c6232", + data: {"pwd": pwd}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/user-auth-type/account-update + @override + Future updatePwd(String pwd, String oldPwd) async { + final result = await http.post( + "0bc43bb5ac93dd30919c4a7c3bbd60336a92ac11dbc1fb082f877af0c807bf9f6ad9a4bc80736146ebc083f857e42b4f", + data: {"pwd": pwd, "oldPwd": oldPwd}, + fromJson: (json) => json as bool, + ); + return result; + } + + ///room/blacklist + @override + Future> roomBlacklist( + String roomId, + String lastId, + ) async { + Map parm = {}; + parm["roomId"] = roomId; + if (lastId != "0") { + parm["lastId"] = lastId; + } + final result = await http.get>( + "9ce0dac3a29735467208f19ed4edc330", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => RoomBlackListRes.fromJson(e)).toList(), + ); + return result; + } + + ///user/user-profile/update + @override + Future updateUserInfo({ + String? userAvatar, + String? userNickname, + num? userSex, + num? age, + num? bornYear, + num? bornMonth, + num? bornDay, + String? countryId, + String? hobby, + String? autograph, + List? backgroundPhotos, + List? personalPhotos, + }) async { + Map parm = {}; + if (userAvatar != null) { + parm["userAvatar"] = userAvatar; + } + if (userNickname != null) { + parm["userNickname"] = userNickname; + } + if (userSex != null) { + parm["userSex"] = userSex; + } + + if (age != null) { + parm["age"] = age; + } + + if (bornYear != null) { + parm["bornYear"] = bornYear; + } + + if (bornMonth != null) { + parm["bornMonth"] = bornMonth; + } + if (bornDay != null) { + parm["bornDay"] = bornDay; + } + if (countryId != null) { + parm["countryId"] = countryId; + } + if (hobby != null) { + parm["hobby"] = hobby; + } + if (autograph != null) { + parm["autograph"] = autograph; + } + if (backgroundPhotos != null) { + parm["backgroundPhotos"] = backgroundPhotos; + } + if (personalPhotos != null) { + parm["personalPhotos"] = personalPhotos; + } + + final result = await http.post( + "2fa1d4f56bb726558904ce2a50b83f96365e62b6aa808acde7b56d2d5945fbe4", + data: parm, + fromJson: (json) => SocialChatUserProfile.fromJson(json), + ); + return result; + } + + ///user/support-relation/follow/my-list + @override + Future> followMyList({ + String? account, + String? lastId, + }) async { + Map parm = {}; + if (account != null) { + parm["account"] = account; + } + if (lastId != null) { + parm["lastId"] = lastId; + } + final result = await http.get>( + "5ee2ca437d883bcd9a4030c44b20b059984d86f34396dfa8045aa8afe71cb567bc4a9e8c4704c0f73c085e7ab50e7fdb", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => FollowUserRes.fromJson(e)).toList(), + ); + return result; + } + + ///user/support-relation/fans/my-list + @override + Future> fansMyList({ + String? account, + String? lastId, + }) async { + Map parm = {}; + if (account != null) { + parm["account"] = account; + } + if (lastId != null) { + parm["lastId"] = lastId; + } + final result = await http.get>( + "5ee2ca437d883bcd9a4030c44b20b05995242771a17d74da900d0d3c02debf52397bb180e31856d94bf25d85eb342787", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => FollowUserRes.fromJson(e)).toList(), + ); + return result; + } + + ///wallet/gold/this-month/asset-record + @override + Future> goldRecord({num? type, String? lastId}) async { + Map parm = {}; + if (type != null) { + parm["type"] = type; + } + if (lastId != null) { + parm["lastId"] = lastId; + } + final result = await http.get>( + "aabf121bb4c193ccceac70586aa16f3b06ad35ecd192416cde0bc922045e1dda68a9458578aa4b21fc86df1010fa2ca2", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => SCGoldRecordRes.fromJson(e)).toList(), + ); + return result; + } + + ///user/support-relation/counter + @override + Future> userCounter(String userId) async { + Map parm = {}; + parm["userId"] = userId; + final result = await http.get>( + "5ee2ca437d883bcd9a4030c44b20b05980e9d6b90837a95599c7f32e68756428", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => SCUserCounterRes.fromJson(e)).toList(), + ); + return result; + } + + ///app/h5/identity + @override + Future userIdentity({String? userId}) async { + Map parm = {}; + if (userId != null) { + parm["userId"] = userId; + } + final result = await http.get( + "d29121f09fa26c45b5322df6de0fc25b", + queryParams: parm, + fromJson: (json) => SCUserIdentityRes.fromJson(json), + ); + return result; + } + + + ///user/user-level/consumption/exp + @override + Future userLevelConsumptionExp( + String userId, + String type, + ) async { + Map parm = {}; + parm["userId"] = userId; + parm["type"] = type; + final result = await http.get( + "9863061ae38e7d452fdd7f95840a952f2751db44c8c4787063c9ccf1dcd80366", + queryParams: parm, + fromJson: (json) => SCUserLevelExpRes.fromJson(json), + ); + return result; + } + + ///task/check/in/today + @override + Future checkInToday() async { + final result = await http.get( + "b5908b3dc914a82da088fadf8be59213143ec06ec0fc08a0d7d915f0556e3821", + fromJson: (json) => json as bool, + ); + return result; + } + + ///task/check/in/list/award + @override + Future sginListAward() async { + final result = await http.get( + "acdc80e923fd539da1401826ee3e99169b3b4b9cccf8933804239f0e070c54c7", + fromJson: (json) => SCSignInRes.fromJson(json), + ); + return result; + } + + ///task/check/in/receive + @override + Future checkInReceive(String id, String resourceGroupId) async { + Map parm = {}; + parm["id"] = id; + parm["resourceGroupId"] = resourceGroupId; + final result = await http.post( + "cd4f02cf4ef018533e882d3cbb23a6aeaeb1d976c8b96774303e69619d73e630", + data: parm, + fromJson: (json) => json as int, + ); + return result; + } + + ///team/create + @override + Future teamCreate(String ownUserId) async { + Map parm = {}; + parm["ownUserId"] = ownUserId; + final result = await http.post( + "9d3a59e43ee9cf3729f57abc0c596005", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///public-message/page + @override + Future publicMessagePage( + String type, + num pageNo, + num pageSize, + ) async { + Map parm = {}; + parm["type"] = type; + parm["pageNo"] = pageNo; + parm["pageSize"] = pageSize; + final result = await http.get( + "38801b8e4843cf3f040b20e8e7a32554118a90740b43e354a7fdc9ac837e0671", + queryParams: parm, + fromJson: (json) => SCPublicMessagePageRes.fromJson(json), + ); + return result; + } + + ///user/friend-relation/list + @override + Future> friendList({String? lastId}) async { + Map parm = {}; + if (lastId != null) { + parm["lastId"] = lastId; + } + final result = await http.get>( + "4aff0909d268eb1d0a7f1ded14d28357d8c0811eb69bd36aaaf7bf3d8bf0e486", + queryParams: parm, + fromJson: + (json) => + (json as List) + .map((e) => MessageFriendUserRes.fromJson(e)) + .toList(), + ); + return result; + } + + ///user/friend-relation/search + @override + Future> friendSearch(String account) async { + Map parm = {}; + parm["account"] = account; + final result = await http.get>( + "4aff0909d268eb1d0a7f1ded14d28357879fc2cee9e8f2051bbaccb8bf07e372", + queryParams: parm, + fromJson: + (json) => + (json as List) + .map((e) => MessageFriendUserRes.fromJson(e)) + .toList(), + ); + return result; + } + + ///team/invite-message-process + @override + Future inviteHost(String id, String status) async { + Map parm = {}; + parm["id"] = id; + parm["status"] = status; + final result = await http.post( + "e5989162aa832581f74b9a383248e4b8317f6af14ecd57b0c1e01a5e47e7414d", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///team/bd/invite-message-process + @override + Future inviteAgent(String id, String status) async { + Map parm = {}; + parm["id"] = id; + parm["status"] = status; + final result = await http.post( + "5e44a26c2a5805a21846bf04cb67a75108fa66d1b5877cd04fb8d1761aa95547", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///team/bd/invite-bd-message-process + @override + Future inviteBD(String id, String status) async { + Map parm = {}; + parm["id"] = id; + parm["status"] = status; + final result = await http.post( + "bbdc8997a823680923efb54a9d31072eabf7f2c09434925bdc6cd2a847f93dcfba2fafbaede21817c8d7e89774331852", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///team/bd/invite-bdleader-message-process + @override + Future inviteBDLeader(String id, String status) async { + Map parm = {}; + parm["id"] = id; + parm["status"] = status; + final result = await http.post( + "bbdc8997a823680923efb54a9d31072eb14922cefad1be12ebcebd874a66585c7f7f07aa8fdbecc97b3d68d3389dd470", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///team/bd/invite-recharge-agent-message-process + @override + Future inviteRechargeAgent(String id, String status) async { + Map parm = {}; + parm["id"] = id; + parm["status"] = status; + final result = await http.post( + "771f90aeda269f6e1eb1421f4335c8bf02f5ada0ff7a70fca49e8b367fd079ffd991f48d7a7af790a9593ff2caf1f6fc", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/support-relation/follow/check + @override + Future followCheck(String userId) async { + Map parm = {}; + parm["userId"] = userId; + final result = await http.get( + "5ee2ca437d883bcd9a4030c44b20b059cda65c203d085b6a0dc3eb6c86372454b6de76cbe76f15fa0a33e1759219198c", + queryParams: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/support-relation/follow-new + @override + Future followNew(String userId) async { + Map parm = {}; + parm["userId"] = userId; + final result = await http.post( + "5ee2ca437d883bcd9a4030c44b20b059787556c9e7c91af11f63f466fc47a7a6574aff257ce7e668d08f4caccd1c6232", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///app/v2/task/list + @override + Future> tasks() async { + final result = await http.get>( + "6e87048da643aa0388a9e46ea391daff574aff257ce7e668d08f4caccd1c6232", + fromJson: + (json) => (json as List).map((e) => SCTaskListRes.fromJson(e)).toList(), + ); + return result; + } + + ///app/v2/task/reward + @override + Future taskReward(num taskId) async { + Map parm = {}; + parm["taskId"] = taskId; + final result = await http.get( + "0047abf8502867ed03180e3a6b505d411832ea9bebe4ec426a7036c865082475", + queryParams: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/support-relation/visitor/list + @override + Future> visitorList(num pageNumber) async { + Map parm = {}; + parm["pageNumber"] = pageNumber; + final result = await http.get>( + "5ee2ca437d883bcd9a4030c44b20b05903ab400c9bf27c42312a7e75b67b25a0397bb180e31856d94bf25d85eb342787", + queryParams: parm, + fromJson: + (json) => + (json as List).map((e) => FollowUserRes.fromJson(e)).toList(), + ); + return result; + } + + ///prop/coupon/list + @override + Future propCouponList( + num size, + num current, { + int? couponType, + }) async { + Map parm = {}; + parm["size"] = size; + parm["current"] = current; + if (couponType != null) { + parm["couponType"] = couponType; + } + final result = await http.post( + "c05bb66ab1d14464aa75f1842e222ce0574aff257ce7e668d08f4caccd1c6232", + data: parm, + fromJson: (json) => SCPropCouponListRes.fromJson(json), + ); + return result; + } + + ///prop/coupon/record/list + @override + Future couponRecordList( + num size, + num current, { + int? couponType, + int? status, + }) async { + Map parm = {}; + parm["size"] = size; + parm["current"] = current; + if (couponType != null) { + parm["couponType"] = couponType; + } + if (status != null) { + parm["status"] = status; + } + final result = await http.post( + "0348972fba6d4c510c1860e435c3f68297e6790742d0344ce6b20c597ee86fd1", + data: parm, + fromJson: (json) => SCPropCouponRecordListRes.fromJson(json), + ); + return result; + } + + ///prop/coupon/use + @override + Future couponUse(String couponNo) async { + Map parm = {}; + parm["couponNo"] = couponNo; + final result = await http.post( + "6163f60d541d3361be0d3f8ef816fc1d", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///prop/coupon/create + @override + Future couponSend(String couponNo, String receiverId) async { + Map parm = {}; + parm["couponNo"] = couponNo; + parm["receiverId"] = receiverId; + final result = await http.post( + "431750258fbc9a54a3298bf431007fd56ad9a4bc80736146ebc083f857e42b4f", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/badge/own/list + @override + Future> badgeOwnList(String type) async { + Map parm = {}; + parm["type"] = type; + final result = await http.get>( + "2d234c6012ceaca8dbd3923bb17b2adaf13e23fb57376accca26ee78f1baa860", + queryParams: parm, + fromJson: + (json) => (json as List).map((e) => WearBadge.fromJson(e)).toList(), + ); + return result; + } + + ///user/badge/toggle + @override + Future badgeToggle(String badgeId) async { + Map parm = {}; + parm["badgeId"] = badgeId; + final result = await http.post( + "84169ec3c928dde3a22cb4b7b6ae95c31e1cf9b945681bdfb3d4cf7a5ecfc943", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user-violation/handle + @override + Future userViolationHandle( + String userId, + int violationType, + int operationType, + String description, { + List? imageUrls, + }) async { + Map parm = {}; + parm["userId"] = userId; + parm["violationType"] = violationType; + parm["operationType"] = operationType; + parm["description"] = description; + if (imageUrls != null && imageUrls.isNotEmpty) { + parm["screenshotUrls"] = imageUrls; + } + final result = await http.post( + "cc84151e5fba2824710e0645046729a06f894acdcdbac7ccb7e88e9d69ee3eff", + data: parm, + fromJson: (json) => SCViolationHandleRes.fromJson(json), + ); + return result; + } + + + ///user/cp-relationship/create-apply + @override + Future cpRelationshipSendApply(String acceptApplyUserId) async { + Map parm = {}; + parm["acceptApplyUserId"] = acceptApplyUserId; + final result = await http.post( + "b06a43ca828553e84580a84da4a3af6135e967dbd0d9edde6478905c4b167fd37b53dae821c941c8c13570fbc26665ba", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/cp-relationship/process-apply + @override + Future cpRelationshipProcessApply(String applyId, bool agree) async { + Map parm = {}; + parm["applyId"] = applyId; + parm["agree"] = agree; + final result = await http.post( + "b06a43ca828553e84580a84da4a3af61cb64264b44425ec78a0115bfc976a827fd57dd4d0233d969f8991e901a14e144", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/cp-relationship/dismiss-apply + @override + Future cpRelationshipDismissApply(String cpUserId) async { + Map parm = {}; + parm["cpUserId"] = cpUserId; + final result = await http.get( + "b06a43ca828553e84580a84da4a3af6147b0e44e9311c7615dee77b6c4c425a1fd57dd4d0233d969f8991e901a14e144", + queryParams: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/friend-relation/check + @override + Future friendRelationCheck(String userId) async { + Map parm = {}; + parm["userId"] = userId; + final result = await http.get( + "4aff0909d268eb1d0a7f1ded14d28357691f1c364e338d7fc3c50cf21be0f908", + queryParams: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/red-packet/create + @override + Future userRedPacketSend( + String receiverUserId, + String totalAmount, + ) async { + Map parm = {}; + parm["receiverUserId"] = receiverUserId; + parm["totalAmount"] = totalAmount; + final result = await http.post( + "69585024cc7ee090d1e29ff7ca160a45f01ca103e12cc332b59b5a8e39c778ec", + data: parm, + fromJson: (json) => SCUserRedPacketSendRes.fromJson(json), + ); + return result; + } + + ///user/red-packet/detail + @override + Future userRedPacketDetail(String packetId) async { + Map parm = {}; + parm["packetId"] = packetId; + final result = await http.get( + "69585024cc7ee090d1e29ff7ca160a457f342deecb1b83d2d8429c59146f10f0", + queryParams: parm, + fromJson: (json) => SCUserRedPacketSendRes.fromJson(json), + ); + return result; + } + + ///user/red-packet/grab + @override + Future userRedPacketGrab(String packetId) async { + Map parm = {}; + parm["packetId"] = packetId; + final result = await http.post( + "69585024cc7ee090d1e29ff7ca160a450b2a61d0ce4f3f4d50875dcc95b640af", + data: parm, + fromJson: (json) => SCUserRedPacketGrabRes.fromJson(json), + ); + return result; + } + + ///user/blacklist/list + @override + Future> userBlacklistList({ + String? account, + String? lastId, + }) async { + Map parm = {}; + if (account != null) { + parm["account"] = account; + } + if (lastId != null) { + parm["lastId"] = lastId; + } + final result = await http.get>( + "62bd6154ca9eb9207f3e50294ed18035f13e23fb57376accca26ee78f1baa860", + queryParams: parm, + fromJson: + (json) => + (json as List) + .map((e) => MessageFriendUserRes.fromJson(e)) + .toList(), + ); + return result; + } + + ///user/blacklist/check + @override + Future blacklistCheck(String shieldUserId) async { + Map parm = {}; + parm["shieldUserId"] = shieldUserId; + final result = await http.post( + "66644b702102d48f4284444b05b1200deda65d2aec525e95d29625cf93fa9e45", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/blacklist + @override + Future addUserBlacklist(String shieldUserId) async { + Map parm = {}; + parm["shieldUserId"] = shieldUserId; + final result = await http.post( + "3f6e2326c10aaed6e637ac0d78a20a3b", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///user/blacklist/cancel + @override + Future deleteUserBlacklist(String shieldUserId) async { + Map parm = {}; + parm["shieldUserId"] = shieldUserId; + final result = await http.post( + "66644b702102d48f4284444b05b1200d13afc33ba8e2ca51c7245d9201b4569d", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } + + ///auth/logout + @override + Future logoutAccount() async { + final result = await http.delete( + "814a34263274ff501f4ca9ae76c4aae3", + fromJson: (json) => json as bool, + ); + return result; + } + + + ///user/vip/ability/update + @override + Future userVipAbilityUpdate({ + String? contactMode, + bool? kickPrevention, + bool? mysteriousInvisibility, + bool? antiBlock, + }) async { + Map parm = {}; + if (contactMode != null) { + parm["contactMode"] = contactMode; + } + if (kickPrevention != null) { + parm["kickPrevention"] = kickPrevention; + } + if (mysteriousInvisibility != null) { + parm["mysteriousInvisibility"] = mysteriousInvisibility; + } + if (antiBlock != null) { + parm["antiBlock"] = antiBlock; + } + final result = await http.post( + "9bad890ecd3a153658cbe38367fcc46f3db9e152c7701ff2e5e1d4261f763895", + data: parm, + fromJson: (json) => json as bool, + ); + return result; + } +} diff --git a/lib/shared/tools/sc_app_utils.dart b/lib/shared/tools/sc_app_utils.dart new file mode 100644 index 0000000..3ace941 --- /dev/null +++ b/lib/shared/tools/sc_app_utils.dart @@ -0,0 +1,91 @@ +import 'dart:io'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:path_provider/path_provider.dart'; + +class SCAppUtils { + + + // 递归计算文件夹大小 + static Future _gs(Directory folder) async { + try { + if (!await folder.exists()) return 0; + + double size = 0; + final files = folder.listSync(recursive: true); + + for (var file in files) { + if (file is File) { + try { + final stat = await file.stat(); + size += stat.size; + } catch (e) { + // 忽略无法访问的文件 + } + } + } + + return size; + } catch (e) { + print('计算文件夹大小出错: ${folder.path} - $e'); + return 0; + } + } + + + // 删除文件夹 + static Future _dd(Directory folder) async { + try { + if (await folder.exists()) { + await folder.delete(recursive: true); + } + } catch (e) { + print('删除文件夹失败: ${folder.path} - $e'); + } + } + + // 清理 Dio 缓存 + static Future _cd() async { + try { + final dioCacheDir = Directory( + '${(await getTemporaryDirectory()).path}/dio_cache', + ); + if (await dioCacheDir.exists()) { + await dioCacheDir.delete(recursive: true); + } + } catch (e) { + print('清理Dio缓存出错: $e'); + } + } + + // 清理图片缓存 + static Future _ci() async { + try { + // 清理 cached_network_image 缓存 + final imageCache = PaintingBinding.instance?.imageCache; + if (imageCache != null) { + imageCache.clear(); + imageCache.clearLiveImages(); + } + await clearDiskCachedImages(); + } catch (e) { + print('清理图片缓存出错: $e'); + } + } + + // 格式化缓存大小显示 + static String _fs(double size) { + if (size <= 0) return "0 B"; + + const units = ["B", "KB", "MB", "GB"]; + int unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return "${size.toStringAsFixed(2)} ${units[unitIndex]}"; + } +} diff --git a/lib/shared/tools/sc_banner_utils.dart b/lib/shared/tools/sc_banner_utils.dart new file mode 100644 index 0000000..8752ca1 --- /dev/null +++ b/lib/shared/tools/sc_banner_utils.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/tools/sc_string_utils.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; + +import '../../shared/data_sources/models/enum/sc_banner_open_type.dart'; + +class SCBannerUtils { + static void openBanner(SCIndexBannerRes item, BuildContext context) { + if (item.content == SCBannerOpenType.ENTER_ROOM.name) { + var params = item.params; + if (params != null && params.isNotEmpty) { + SCRoomUtils.goRoom(params, context); + } + } else { + var params = item.params; + if (SCStringUtils.checkIfUrl(params ?? "")) { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(params ?? "")}&showTitle=false", + replace: false, + ); + } + } + } +} diff --git a/lib/shared/tools/sc_date_utils.dart b/lib/shared/tools/sc_date_utils.dart new file mode 100644 index 0000000..9b19b62 --- /dev/null +++ b/lib/shared/tools/sc_date_utils.dart @@ -0,0 +1,103 @@ +import 'package:flutter/cupertino.dart'; +import 'package:intl/intl.dart'; +import 'package:yumi/app_localizations.dart'; + +class SCMDateUtils { + static const num ONE_HOUR = 3600000; + static const num ONE_DAY = 86400000; + static const num ONE_WEEK = 604800000; + static final String _ft1 = "yyyy-MM-dd"; + static final String _ft2 = "yyyy/MM/dd/ HH:mm:ss"; + static final String _ft3 = "yyyy-MM-dd HH:mm:ss"; + + static String formatDateTime(DateTime date) { + return DateFormat(_ft1).format(date); + } + + static String formatDateTime2(DateTime date) { + return DateFormat(_ft2).format(date); + } + + static String formatDateTime3(DateTime date) { + return DateFormat(_ft3).format(date); + } + + static String formatMessageTime(BuildContext context, DateTime date) { + var now = DateTime.now(); + num delta = now.millisecondsSinceEpoch - date.millisecondsSinceEpoch; + // LogUtils.log( + // "Delta:$delta,now.millisecondsSinceEpoch:${now.millisecondsSinceEpoch},date.millisecondsSinceEpoch:${date.millisecondsSinceEpoch}"); + if (delta < 1 * ONE_DAY) { + if (date.day < now.day) { + return SCAppLocalizations.of(context)!.yesterday( + "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}", + ); + } else { + return "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}"; + } + } else if (delta < 1 * ONE_WEEK) { + switch (date.weekday) { + case DateTime.monday: + return SCAppLocalizations.of(context)!.monday( + "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}", + ); + case DateTime.tuesday: + return SCAppLocalizations.of(context)!.tuesday( + "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}", + ); + case DateTime.wednesday: + return SCAppLocalizations.of(context)!.wednesday( + "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}", + ); + case DateTime.thursday: + return SCAppLocalizations.of(context)!.thursday( + "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}", + ); + case DateTime.friday: + return SCAppLocalizations.of(context)!.friday( + "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}", + ); + case DateTime.saturday: + return SCAppLocalizations.of(context)!.saturday( + "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}", + ); + case DateTime.sunday: + return SCAppLocalizations.of(context)!.sunday( + "${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}", + ); + } + } else { + return "${date.year}/${date.month}/${date.day} ${date.hour >= 10 ? date.hour : "0${date.hour}"}:${date.minute >= 10 ? date.minute : "0${date.minute}"}"; + } + return ""; + } + + static int calculateDaysDifference(DateTime from, DateTime to) { + // 归一化日期(忽略时间部分,确保按整天计算) + from = DateTime(from.year, from.month, from.day); + to = DateTime(to.year, to.month, to.day); + + // 计算差值并返回天数 + return to.difference(from).inDays; + } + + static String secondsToMinutes(int seconds) { + Duration duration = Duration(seconds: seconds); + return _df(duration); + } + + static String millisecondsToMinutes(int milliseconds) { + Duration duration = Duration(milliseconds: milliseconds); + return _df(duration); + } + + static String _df(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, "0"); + + if (duration.inHours > 0) { + return "${twoDigits(duration.inHours)}:${twoDigits(duration.inMinutes.remainder(60))}:${twoDigits(duration.inSeconds.remainder(60))}"; + } else { + return "${twoDigits(duration.inMinutes.remainder(60))}:${twoDigits(duration.inSeconds.remainder(60))}"; + } + } +} diff --git a/lib/shared/tools/sc_deep_link_handler.dart b/lib/shared/tools/sc_deep_link_handler.dart new file mode 100644 index 0000000..a7eb52a --- /dev/null +++ b/lib/shared/tools/sc_deep_link_handler.dart @@ -0,0 +1,61 @@ +import 'dart:async'; +import 'package:app_links/app_links.dart'; // 1. 替换导入 + +class SCDeepLinkHandler { + // 2. 创建 AppLinks 实例(通常作为单例,但也可以每次创建) + final AppLinks _ap = AppLinks(); + + StreamSubscription? _lss; // 注意流类型变为 Uri + final _lnc = StreamController.broadcast(); + + Stream get linkNotifierStream => _lnc.stream; + + Function(Uri)? _olrc; + + Future initDeepLinks({Function(Uri)? onLinkReceived}) async { + _olrc = onLinkReceived; + await _iln(); + _sl(); + } + + Future _iln() async { + try { + // 3. 使用实例方法获取初始链接,返回 Uri? 而不是 String? + Uri? initialLink = await _ap.getInitialLink(); + if (initialLink != null) { + _ph(initialLink); + } + } catch (err) { + print('获取初始链接失败: $err'); + } + } + + void _sl() { + // 4. 使用 uriLinkStream 监听链接(流类型为 Uri) + _lss = _ap.uriLinkStream.listen( + (Uri? link) { + // 注意:uriLinkStream 可能直接发出 Uri,而不是 String? + // 根据实际版本,可能是 Uri 或 String?,以下处理两者兼容 + if (link != null) { + _ph(link); + } + }, + onError: (err) { + print('监听链接流出错: $err'); + }, + ); + } + + // 5. 接收 Uri 类型参数(原来接收 String) + void _ph(Uri uri) { + print('SCDeepLinkHandler 处理链接: $uri'); + // 如果需要存储为字符串,可以使用 uri.toString() + _lnc.add(uri.toString()); + _olrc?.call(uri); + } + + void dispose() { + _lss?.cancel(); + _lnc.close(); + } +} \ No newline at end of file diff --git a/lib/shared/tools/sc_deviceId_utils.dart b/lib/shared/tools/sc_deviceId_utils.dart new file mode 100644 index 0000000..482d69d --- /dev/null +++ b/lib/shared/tools/sc_deviceId_utils.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:uuid/uuid.dart'; + +class SCDeviceIdUtils { + static void initDeviceinfo() { + PackageInfo.fromPlatform().then((info) { + SCGlobalConfig.version = info.version; + SCGlobalConfig.build = info.buildNumber; + if (Platform.isAndroid) { + SCGlobalConfig.channel = "Google"; + DeviceInfoPlugin().androidInfo.then((dev) { + SCGlobalConfig.model = dev.model; + SCGlobalConfig.sysVersion = dev.version.release; + SCGlobalConfig.sdkInt = dev.version.sdkInt; + }); + } else if (Platform.isIOS) { + SCGlobalConfig.channel = "AppStore"; + DeviceInfoPlugin().iosInfo.then((dev) { + SCGlobalConfig.model = dev.model; + SCGlobalConfig.sysVersion = dev.systemVersion; + }); + } + }); + } + + // 获取持久化的唯一设备ID + static Future getDeviceId({bool forceRefresh = false}) async { + // 如果强制刷新或配置中不存在,则重新生成 + if (forceRefresh || SCGlobalConfig.imei.isEmpty) { + final newId = await _g(); + DataPersistence.setUniqueId(newId); + SCGlobalConfig.imei = newId; + return newId; + } + + // 检查存储中的ID + String storedId = DataPersistence.getUniqueId(); + if (storedId.isNotEmpty) { + SCGlobalConfig.imei = storedId; + return storedId; + } + + // 生成新ID并存储 + final newId = await _g(); + DataPersistence.setUniqueId(newId); + SCGlobalConfig.imei = newId; + return newId; + } + + // 生成组合唯一ID + static Future _g() async { + final packageInfo = await PackageInfo.fromPlatform(); + // 生成新 UUID 并存储 + final newId = '${const Uuid().v4()}-${packageInfo.packageName}'; + DataPersistence.setUniqueId(newId); + return newId; + } +} diff --git a/lib/shared/tools/sc_dialog_queue.dart b/lib/shared/tools/sc_dialog_queue.dart new file mode 100644 index 0000000..899671a --- /dev/null +++ b/lib/shared/tools/sc_dialog_queue.dart @@ -0,0 +1,37 @@ +class SCDialogQueue { + static final SCDialogQueue _a = SCDialogQueue._(); + + factory SCDialogQueue() => _a; + + SCDialogQueue._(); + + final List _l = []; + bool _b = false; + + // 添加弹框 + void add(Function showDialogFunc) { + _l.add(showDialogFunc); + _n(); + } + + // 弹框关闭 + void dismiss() { + _b = false; + _n(); + } + + // 清空 + void clear() { + _l.clear(); + _b = false; + } + + // 尝试显示下一个 + void _n() { + if (_b || _l.isEmpty) return; + + _b = true; + final func = _l.removeAt(0); + func(); + } +} diff --git a/lib/shared/tools/sc_dialog_utils.dart b/lib/shared/tools/sc_dialog_utils.dart new file mode 100644 index 0000000..42b7be7 --- /dev/null +++ b/lib/shared/tools/sc_dialog_utils.dart @@ -0,0 +1,355 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; + +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/ui_kit/widgets/banner/index_banner_page.dart'; +import 'package:yumi/ui_kit/widgets/countdown_timer.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; + +class SCDialogUtils { + static showFirstRechargeDialog(BuildContext context) { + SmartDialog.show( + tag: "showFirstRecharge", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return Center( + child: Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + SizedBox( + height: 530.w, + width: ScreenUtil().screenWidth * 0.86, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/index/sc_icon_first_recharge_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 138.w), + GestureDetector( + child: Row( + textDirection: TextDirection.ltr, + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + "sc_images/index/sc_icon_my_rechage_title.png", + height: 25.w, + ), + Transform.translate( + offset: Offset(-3.w, 0), + child: text( + "${AccountStorage().getCurrentUser()?.userProfile?.firstRechargeAmount}", + textColor: Colors.white, + fontSize: 14.sp, + ), + ), + ], + ), + onTap: () { + SCRoomUtils.closeAllDialogs(); + SCNavigatorUtils.push( + context, + WalletRoute.recharge, + replace: false, + ); + }, + ), + SizedBox(height: 55.w), + (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.firstRechargeAmount ?? + 0) < + 0.99 + ? Container(height: 60.w) + : Stack( + alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 28.w, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + ], + ), + Image.asset( + "sc_images/index/sc_icon_claimed_text.png", + width: 70.w, + height: 35.w, + ), + ], + ), + SizedBox(height: 40.w), + (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.firstRechargeAmount ?? + 0) < + 4.99 + ? Container(height: 60.w) + : Stack( + alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 28.w, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + ], + ), + Image.asset( + "sc_images/index/sc_icon_claimed_text.png", + width: 70.w, + height: 35.w, + ), + ], + ), + SizedBox(height: 48.w), + (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.firstRechargeAmount ?? + 0) < + 9.99 + ? Container(height: 60.w) + : Stack( + alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 28.w, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(10.w), + ), + height: 60.w, + width: 60.w, + ), + ], + ), + Image.asset( + "sc_images/index/sc_icon_claimed_text.png", + width: 70.w, + height: 35.w, + ), + ], + ), + ], + ), + ), + ), + PositionedDirectional( + bottom: 7.w, + child: CountdownTimer( + expiryDate: DateTime.fromMillisecondsSinceEpoch( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.firstRechargeEndTime ?? + 0, + ), + color: Colors.white, + fontSize: 15.sp, + ), + ), + ], + ), + ); + }, + ); + } + + static void showDynamicCommentOptDialog( + BuildContext context, { + Function? reportCallback, + Function? deleteCallback, + }) { + SmartDialog.dismiss(tag: "showDynamicCommentOptDialog"); + SmartDialog.show( + tag: "showDynamicCommentOptDialog", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return SafeArea( + top: false, + child: Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.w), + topRight: Radius.circular(10.w), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 3.w, + children: [ + if (reportCallback != null) + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + child: text( + SCAppLocalizations.of(context)!.report, + textColor: Colors.black, + fontSize: 15.sp, + ), + ), + onTap: () { + reportCallback?.call(); + SmartDialog.dismiss(tag: "showDynamicCommentOptDialog"); + }, + ), + if (reportCallback != null) + Divider( + color: SocialChatTheme.dividerColor, + thickness: 0.5, + height: 10.w, + ), + if (deleteCallback != null) + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + child: text( + SCAppLocalizations.of(context)!.delete, + textColor: Colors.black, + fontSize: 15.sp, + ), + ), + onTap: () { + deleteCallback?.call(); + SmartDialog.dismiss(tag: "showDynamicCommentOptDialog"); + }, + ), + if (deleteCallback != null) + Divider( + color: SocialChatTheme.dividerColor, + thickness: 0.5, + height: 10.w, + ), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + child: text( + SCAppLocalizations.of(context)!.cancel, + textColor: Colors.black, + fontSize: 15.sp, + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showDynamicCommentOptDialog"); + }, + ), + ], + ), + ), + ); + }, + ); + } + + static void showBannerDialog( + BuildContext context, + List banners, + ) { + if (banners.isEmpty) { + return; + } + SCIndexBannerRes banner; + if (banners.length > 1) { + int index = Random().nextInt(banners.length); + banner = banners[index]; + } else { + banner = banners.first; + } + + SmartDialog.show( + tag: "showBannerDialog", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return IndexBannerPage(banner); + }, + ); + } +} diff --git a/lib/shared/tools/sc_entrance_vap_svga_manager.dart b/lib/shared/tools/sc_entrance_vap_svga_manager.dart new file mode 100644 index 0000000..3fb26de --- /dev/null +++ b/lib/shared/tools/sc_entrance_vap_svga_manager.dart @@ -0,0 +1,246 @@ +import 'package:flutter/animation.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:tancent_vap/widgets/vap_view.dart'; + +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; + +// class EntranceVapSvgaManager { +// static EntranceVapSvgaManager? _instance; +// +// EntranceVapSvgaManager._internal(); +// +// factory EntranceVapSvgaManager() => +// _instance ??= EntranceVapSvgaManager._internal(); +// +// // 使用 SCPriorityQueue 替代简单列表 +// final SCPriorityQueue _taskQueue = SCPriorityQueue( +// (a, b) => b.priority.compareTo(a.priority), +// ); +// VapController? _roomGiftvapController; +// SVGAAnimationController? _roomGiftsvgaController; +// bool _isPlaying = false; +// bool _isDisposed = false; +// +// // 绑定控制器 +// void bindEntranceVapController(VapController vapController) { +// _isDisposed = false; +// _roomGiftvapController = vapController; +// _roomGiftvapController?.setAnimListener( +// onVideoStart: () {}, +// onVideoComplete: () { +// _handleControllerState(); +// }, +// onFailed: (code, type, msg) { +// _handleControllerState(); +// }, +// ); +// } +// +// void bindEntranceSvgaController(SVGAAnimationController svgaController) { +// _isDisposed = false; +// _roomGiftsvgaController = svgaController; +// _roomGiftsvgaController?.addStatusListener((AnimationStatus status) { +// if (status.isCompleted) { +// _isPlaying = false; +// _roomGiftsvgaController?.reset(); +// _playNext(); +// } +// }); +// } +// +// // 播放任务 +// void play( +// String path, { +// int priority = 0, +// Map? customResources, +// }) { +// if (_isDisposed) return; +// final task = SCVapTask( +// path: path, +// priority: priority, +// customResources: customResources, +// ); +// +// _taskQueue.add(task); +// if (!_isPlaying) { +// _playNext(); +// } +// } +// +// // 播放下一个任务 +// Future _playNext() async { +// if (_isDisposed || _taskQueue.isEmpty || _isPlaying) return; +// +// final task = _taskQueue.removeFirst(); +// _isPlaying = true; +// try { +// final pathType = SCPathUtils.getPathType(task.path); +// if (pathType == PathType.asset) { +// await _playAsset(task); +// } else if (pathType == PathType.file) { +// await _playFile(task); +// } else if (pathType == PathType.network) { +// await _playNetwork(task); +// } +// } catch (e, s) { +// print('VAP_SVGA播放失败: $e\n$s'); +// _isPlaying = false; +// } finally { +// // 确保状态正确重置 +// if (!_isDisposed) { +// _playNext(); +// } +// } +// } +// +// // 播放资源文件 +// Future _playAsset(SCVapTask task) async { +// if (_isDisposed) return; +// if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { +// MovieEntity entity; +// if (SCGiftVapSvgaManager().videoItemCache.containsKey(task.path)) { +// entity = SCGiftVapSvgaManager().videoItemCache[task.path]!; +// } else { +// entity = await SVGAParser.shared.decodeFromAssets(task.path); +// SCGiftVapSvgaManager().videoItemCache[task.path] = entity; +// } +// _roomGiftsvgaController?.videoItem = entity; +// _roomGiftsvgaController?.reset(); +// _roomGiftsvgaController?.forward(); +// } else { +// await _roomGiftvapController?.playAsset(task.path); +// } +// } +// +// // 播放本地文件 +// Future _playFile(SCVapTask task) async { +// if (_isDisposed || _roomGiftvapController == null) return; +// await _roomGiftvapController!.playFile(task.path); +// } +// +// // 播放网络资源 +// Future _playNetwork(SCVapTask task) async { +// if (_isDisposed) return; +// if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { +// MovieEntity? entity; +// if (SCGiftVapSvgaManager().videoItemCache.containsKey(task.path)) { +// entity = SCGiftVapSvgaManager().videoItemCache[task.path]!; +// entity.autorelease = false; +// } else { +// try { +// entity = await SVGAParser.shared.decodeFromURL(task.path); +// entity.autorelease = false; +// SCGiftVapSvgaManager().videoItemCache[task.path] = entity; +// } catch (e) { +// _isPlaying = false; +// print('svga解析出错:$e'); +// } +// } +// if (entity != null) { +// _roomGiftsvgaController?.videoItem = entity; +// _roomGiftsvgaController?.reset(); +// _roomGiftsvgaController?.forward(); +// } +// } else { +// final file = await FileCacheManager.getInstance().getFile(url: task.path); +// if (file != null && !_isDisposed) { +// await _playFile( +// SCVapTask( +// path: file.path, +// priority: task.priority, +// customResources: task.customResources, +// ), +// ); +// } +// } +// } +// +// // 处理控制器状态变化 +// void _handleControllerState() { +// if (_roomGiftvapController != null && !_isDisposed) { +// _isPlaying = false; +// // 延迟一小段时间后播放下一个,避免状态冲突 +// Future.delayed(const Duration(milliseconds: 50), _playNext); +// } +// } +// +// // 释放资源 +// void dispose() { +// _isDisposed = true; +// _isPlaying = false; +// _taskQueue.clear(); +// _roomGiftvapController?.stop(); +// _roomGiftvapController?.dispose(); +// _roomGiftvapController = null; +// _roomGiftsvgaController?.stop(); +// _roomGiftsvgaController?.dispose(); +// _roomGiftsvgaController = null; +// } +// +// // 清除所有任务 +// void clearTasks() { +// _taskQueue.clear(); +// } +// +// // 获取当前队列状态 +// VapQueueStatus get queueStatus { +// return VapQueueStatus( +// isPlaying: _isPlaying, +// currentTask: _taskQueue.isNotEmpty ? _taskQueue.first : null, +// queueLength: _taskQueue.length, +// pendingTasks: List.unmodifiable(_taskQueue.unorderedElements), +// ); +// } +// } + +// 任务模型 +class SCVapTask implements Comparable { + final String path; + final int priority; // 优先级 (数值越高越优先) + final Map? customResources; // 自定义资源 + + SCVapTask({required this.path, this.priority = 0, this.customResources}); + + @override + int compareTo(SCVapTask other) { + // 先按优先级排序 + if (priority != other.priority) { + return priority.compareTo(other.priority); + } + + // 相同优先级按添加时间排序(先进先出) + return hashCode.compareTo(other.hashCode); + } +} +// 优先队列实现 +class SCPriorityQueue { + final List _els = []; + final Comparator _cmp; + + SCPriorityQueue(this._cmp); + + void add(E element) { + _els.add(element); + _els.sort(_cmp); + } + + E removeFirst() { + if (isEmpty) throw StateError("No elements"); + return _els.removeAt(0); + } + + E? get first => isEmpty ? null : _els.first; + + bool get isEmpty => _els.isEmpty; + + bool get isNotEmpty => _els.isNotEmpty; + + int get length => _els.length; + + void clear() => _els.clear(); + + List get unorderedElements => List.from(_els); +} diff --git a/lib/shared/tools/sc_file_utils.dart b/lib/shared/tools/sc_file_utils.dart new file mode 100644 index 0000000..b708eda --- /dev/null +++ b/lib/shared/tools/sc_file_utils.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +class SCFileHelper{ + // 将资源文件拷贝到应用文档目录 + static Future copyAssetToLocal(String assetPath) async { + + try { + // 获取应用文档目录 + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final String localFilePath = '${appDocDir.path}/${assetPath.split('/').last}'; + + // 检查文件是否已存在 + if (await File(localFilePath).exists()) { + print('文件已存在,直接使用: $localFilePath'); + return localFilePath; + } + + // 从Assets加载字节数据 + final ByteData data = await rootBundle.load(assetPath); + final List bytes = data.buffer.asUint8List(); + + // 将字节数据写入本地文件 + final File localFile = File(localFilePath); + await localFile.writeAsBytes(bytes); + + print('文件拷贝成功: $localFilePath'); + return localFilePath; + } catch (e) { + print('拷贝文件时发生错误: $e'); + rethrow; // 重新抛出异常以便上层处理 + } finally { + + } + } +} \ No newline at end of file diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart new file mode 100644 index 0000000..1acb053 --- /dev/null +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -0,0 +1,305 @@ +import 'package:flutter/animation.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:tancent_vap/utils/constant.dart'; +import 'package:tancent_vap/widgets/vap_view.dart'; + +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; + +class SCGiftVapSvgaManager { + Map videoItemCache = {}; + static SCGiftVapSvgaManager? _inst; + + SCGiftVapSvgaManager._internal(); + + factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal(); + + final SCPriorityQueue _tq = SCPriorityQueue( + (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 + ); + VapController? _rgc; + SVGAAnimationController? _rsc; + bool _play = false; + bool _dis = false; + + bool _pause = false; + + //是否关闭礼物特效声音 + bool _mute = false; + + void setMute(bool muteMusic) { + _mute = muteMusic; + DataPersistence.setPlayGiftMusic(_mute); + } + + bool getMute() { + return _mute; + } + + // 绑定控制器 + void bindVapCtrl(VapController vapController) { + _mute = DataPersistence.getPlayGiftMusic(); + _dis = false; + _rgc = vapController; + _rgc?.setAnimListener( + onVideoStart: () { + }, + onVideoComplete: () { + _hcs(); + }, + onFailed: (code, type, msg) { + _hcs(); + }, + ); + } + + void bindSvgaCtrl(SVGAAnimationController svgaController) { + _dis = false; + _rsc = svgaController; + _rsc?.addStatusListener((AnimationStatus status) { + if (status.isCompleted) { + _rsc?.reset(); + _play = false; + _pn(); + } + }); + } + + // 播放任务 + void play( + String path, { + int priority = 0, + Map? customResources, + int type = 0, + }) { + if (path.isEmpty) { + return; + } + if (SCGlobalConfig.sdkInt > SCGlobalConfig.maxSdkNoAnim) { + if (_dis) return; + final task = SCVapTask( + path: path, + priority: priority, + customResources: customResources, + ); + + _tq.add(task); + if (!_play) { + _pn(); + } + } + } + + // 播放下一个任务 + Future _pn() async { + if (_pause) { + return; + } + if (_dis || _tq.isEmpty || _play) return; + + final task = _tq.removeFirst(); + _play = true; + try { + final pathType = SCPathUtils.getPathType(task.path); + if (pathType == PathType.asset) { + await _pa(task); + } else if (pathType == PathType.file) { + await _pf(task); + } else if (pathType == PathType.network) { + await _pnw(task); + } + } catch (e, s) { + print('VAP_SVGA播放失败: $e\n$s'); + } finally { + // 确保状态正确重置 + // if (!_dis) { + // _pn(); + // } + } + } + + // 播放资源文件 + Future _pa(SCVapTask task) async { + if (_dis) return; + if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { + MovieEntity entity; + if (videoItemCache.containsKey(task.path)) { + entity = videoItemCache[task.path]!; + } else { + entity = await SVGAParser.shared.decodeFromAssets(task.path); + videoItemCache[task.path] = entity; + } + _rsc?.videoItem = entity; + _rsc?.reset(); + _rsc?.forward(); + } else { + if (task.customResources != null) { + task.customResources?.forEach((k, v) async { + await _rgc?.setVapTagContent(k, v); + }); + } + await _rgc?.playAsset(task.path); + } + } + + // 播放本地文件 + Future _pf(SCVapTask task) async { + if (_dis || _rgc == null) return; + await _rgc?.setMute(_mute); + if (task.customResources != null) { + task.customResources?.forEach((k, v) async { + await _rgc?.setVapTagContent(k, v); + }); + } + await _rgc!.playFile(task.path); + } + + // 播放网络资源 + Future _pnw(SCVapTask task) async { + if (_dis) return; + if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { + MovieEntity? entity; + if (videoItemCache.containsKey(task.path)) { + entity = videoItemCache[task.path]!; + entity.autorelease = false; + } else { + try { + entity = await SVGAParser.shared.decodeFromURL(task.path); + entity.autorelease = false; + videoItemCache[task.path] = entity; + } catch (e) { + _play = false; + print('svga解析出错:$e'); + } + } + if (entity != null) { + _rsc?.videoItem = entity; + _rsc?.reset(); + _rsc?.forward(); + } + } else { + final file = await FileCacheManager.getInstance().getFile(url: task.path); + if (file != null && !_dis) { + await _pf( + SCVapTask( + path: file.path, + priority: task.priority, + customResources: task.customResources, + ), + ); + } + } + } + + // 处理控制器状态变化 + void _hcs() { + if (_rgc != null && !_dis) { + _play = false; + // 延迟一小段时间后播放下一个,避免状态冲突 + Future.delayed(const Duration(milliseconds: 50), _pn); + } + } + + //暂停动画播放 + void pauseAnim() { + _pause = true; + } + + //恢复动画播放 + void resumeAnim() { + _pause = false; + _pn(); + } + + // 释放资源 + void dispose() { + _dis = true; + _play = false; + _tq.clear(); + _rgc?.stop(); + _rgc?.dispose(); + _rgc = null; + _rsc?.stop(); + _rsc?.dispose(); + _rsc = null; + } + + // 清除所有任务 + void clearTasks() { + _play = false; + _tq.clear(); + _pause = false; + } +} + +// 任务模型 +// 1. 修改 SCVapTask 类,实现 Comparable + +class SCVapTask implements Comparable { + final String path; + final int type; + final int priority; + final Map? customResources; + final int _seq; + + static int _nextSeq = 0; + + SCVapTask({ + required this.path, + this.priority = 0, + this.customResources, + this.type = 0, + }) : _seq = _nextSeq++; + + @override + int compareTo(SCVapTask other) { + // 先按优先级降序排列 + int priorityComparison = other.priority.compareTo(priority); + if (priorityComparison != 0) { + return priorityComparison; + } + // 相同优先级时,按序列号升序排列(先添加的先执行) + return _seq.compareTo(other._seq); + } + + @override + String toString() { + return 'SCVapTask{path: $path, priority: $priority, seq: $_seq}'; + } +} + +// 优先队列实现 +class SCPriorityQueue { + final List _els = []; + final Comparator _cmp; + + SCPriorityQueue(this._cmp); + + void add(E element) { + _els.add(element); + _els.sort(_cmp); + } + + E removeFirst() { + if (isEmpty) throw StateError("No elements"); + return _els.removeAt(0); + } + + E? get first => isEmpty ? null : _els.first; + + bool get isEmpty => _els.isEmpty; + + bool get isNotEmpty => _els.isNotEmpty; + + int get length => _els.length; + + void clear() => _els.clear(); + + List get unorderedElements => List.from(_els); + + // 实现 Iterable 接口 + Iterator get iterator => _els.iterator; +} diff --git a/lib/shared/tools/sc_google_auth_utils.dart b/lib/shared/tools/sc_google_auth_utils.dart new file mode 100644 index 0000000..5e23b23 --- /dev/null +++ b/lib/shared/tools/sc_google_auth_utils.dart @@ -0,0 +1,101 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +///谷歌授权登录 +class SCGoogleAuthService { + static final GoogleSignIn _g = GoogleSignIn(scopes: ['email', 'profile'],); + + // 获取当前用户 + static User? get currentUser => FirebaseAuth.instance.currentUser; + + // 检查登录状态 + static bool get isSignedIn => currentUser != null; + + // 初始化Firebase + static Future initialize() async { + await Firebase.initializeApp(); + } + + // 谷歌登录 + static Future signInWithGoogle() async { + try { + // 触发谷歌登录 + final GoogleSignInAccount? googleAccount = await _g.signIn(); + if (googleAccount == null) return null; + + // 获取认证信息 + final GoogleSignInAuthentication googleAuth = await googleAccount.authentication; + + // 创建Firebase凭证 + final OAuthCredential credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + // 使用凭证登录Firebase + final UserCredential userCredential = + await FirebaseAuth.instance.signInWithCredential(credential); + + return userCredential.user; + } catch (e) { + print('Google sign-in error: $e'); + rethrow; // 抛出异常让调用者处理 + } + } + + // 静默登录(检查现有会话) + static Future signInSilently() async { + try { + // 尝试静默登录 + final GoogleSignInAccount? googleAccount = + await _g.signInSilently(); + + if (googleAccount == null) return null; + + // 获取认证信息 + final GoogleSignInAuthentication googleAuth = + await googleAccount.authentication; + + // 创建Firebase凭证 + final OAuthCredential credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + // 使用凭证登录Firebase + final UserCredential userCredential = + await FirebaseAuth.instance.signInWithCredential(credential); + + return userCredential.user; + } catch (e) { + print('Silent sign-in error: $e'); + return null; + } + } + + // 登出 + static Future signOut() async { + try { + await _g.signOut(); + await FirebaseAuth.instance.signOut(); + } catch (e) { + print('Sign out error: $e'); + rethrow; + } + } + + // 获取用户信息 + static Map? getUserInfo() { + final user = currentUser; + if (user == null) return null; + + return { + 'uid': user.uid, + 'name': user.displayName, + 'email': user.email, + 'photoUrl': user.photoURL, + 'isEmailVerified': user.emailVerified, + }; + } +} \ No newline at end of file diff --git a/lib/shared/tools/sc_heartbeat_utils.dart b/lib/shared/tools/sc_heartbeat_utils.dart new file mode 100644 index 0000000..729bcb0 --- /dev/null +++ b/lib/shared/tools/sc_heartbeat_utils.dart @@ -0,0 +1,58 @@ +import 'dart:async'; + +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; + +import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; + +class SCHeartbeatUtils { + static Timer? _h; + static Timer? _a; + static String _c = SCHeartbeatStatus.ONLINE.name; + static String? _r; + static bool _u = false; + + ///定时发送心跳 + static void scheduleHeartbeat( + String status, + bool upMick, { + String? roomId, + }) async { + cancelAnchorTimer(); + _c = status; + _u = upMick; + _r = roomId; + SCAccountRepository().heartbeat(_c, _u, roomId: _r).whenComplete(() { + try { + _h ??= Timer.periodic(Duration(seconds: 60), (timer) { + SCAccountRepository().heartbeat(_c, _u, roomId: _r); + }); + } catch (e) {} + }); + } + + static void cancelTimer() { + _h?.cancel(); + _h = null; + _r = null; + cancelAnchorTimer(); + } + + ///上麦主播发送心跳 + static void scheduleAnchorHeartbeat(String roomId) async { + cancelAnchorTimer(); + scheduleHeartbeat(_c, true, roomId: _r); + SCAccountRepository().anchorHeartbeat(roomId).whenComplete(() { + _a ??= Timer.periodic(Duration(seconds: 60), (timer) { + try { + SCAccountRepository().anchorHeartbeat(roomId); + } catch (e) {} + }); + }); + } + + static void cancelAnchorTimer() { + _a?.cancel(); + _a = null; + _u = false; + } +} diff --git a/lib/shared/tools/sc_keybord_util.dart b/lib/shared/tools/sc_keybord_util.dart new file mode 100644 index 0000000..0a741f7 --- /dev/null +++ b/lib/shared/tools/sc_keybord_util.dart @@ -0,0 +1,64 @@ +import 'package:flutter/cupertino.dart'; + +class SCKeybordUtil { + static final List _lst = []; + static bool _kbVis = false; + static double _kbHt = 0.0; + + /// 添加全局键盘监听 + static void onChange(Function(bool visible) listener) { + _lst.add(listener); + // 立即通知当前状态 + listener(_kbVis); + } + + /// 移除监听器 + static void removeListener(Function(bool visible) listener) { + _lst.remove(listener); + } + + /// 初始化键盘监听(必须在MaterialApp外层调用) + static void init() { + WidgetsBinding.instance.addObserver(_KbObs()); + } + + /// 获取当前键盘是否可见 + static bool get isVisible => _kbVis; + + /// 获取当前键盘高度 + static double get height => _kbHt; + + /// 隐藏键盘 + static void hide(BuildContext context) { + FocusScope.of(context).unfocus(); + } + + /// 更新键盘状态(内部使用) + static void _updSt(bool visible, double height) { + if (visible != _kbVis || height != _kbHt) { + _kbVis = visible; + _kbHt = height; + + // 通知所有监听器 + for (final listener in _lst) { + listener(visible); + } + } + } +} + +// 键盘状态观察者 +class _KbObs extends WidgetsBindingObserver { + @override + void didChangeMetrics() { + final view = WidgetsBinding.instance.platformDispatcher.views.first; + + if (view.viewInsets.bottom > 0) { + // 键盘弹出 + SCKeybordUtil._updSt(true, view.viewInsets.bottom); + } else { + // 键盘收起 + SCKeybordUtil._updSt(false, 0.0); + } + } +} \ No newline at end of file diff --git a/lib/shared/tools/sc_lk_dialog_util.dart b/lib/shared/tools/sc_lk_dialog_util.dart new file mode 100644 index 0000000..9ee76e0 --- /dev/null +++ b/lib/shared/tools/sc_lk_dialog_util.dart @@ -0,0 +1,635 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/widgets/sc_lk_tap_widget.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; + +void showEnterRoomConfirm( + String roomId, + BuildContext context, { + bool needOpenRedenvelope = false, + bool fromFloting = false, + String redPackId = "", +}) { + if (roomId.isNotEmpty) { + SmartDialog.dismiss(tag: "showConfirmDialog"); + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: + fromFloting + ? SCAppLocalizations.of(context)!.enterThisVoiceChatRoom + : SCAppLocalizations.of(context)!.tips, + msg: + fromFloting + ? SCAppLocalizations.of( + context, + )!.swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt + : SCAppLocalizations.of(context)!.enterRoomConfirmTips, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + Provider.of(context, listen: false).joinVoiceRoomSession( + context, + roomId, + needOpenRedenvelope: needOpenRedenvelope, + redPackId: redPackId, + ); + }, + ); + }, + ); + } +} + +///中间对话框 半透明 +Future showCenterDialog( + BuildContext context, + Widget child, { + Color? barrierColor, +}) { + return showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: true, + transitionDuration: Duration(milliseconds: 350), + barrierColor: barrierColor ?? Colors.black.withOpacity(0.2), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Center(child: child); + }, + ); +} + +///中间对话框 半透明 +showCenterAnimationDialog(BuildContext context, Widget child) { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: true, + transitionDuration: Duration(milliseconds: 800), + barrierColor: Colors.black.withOpacity(0.2), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + print( + 'showCenterAnimationDialog:${animation.value},${secondaryAnimation.value}', + ); + CurvedAnimation curvedAnimation = CurvedAnimation( + curve: Curves.bounceOut, + reverseCurve: Curves.easeInExpo, + parent: animation, + ); + return Center( + child: ScaleTransition( + scale: Tween(begin: 0.0, end: 1.0).animate(curvedAnimation), + child: SlideTransition( + position: Tween( + begin: Offset(0.0, 0.0), + end: Offset(0.0, 3.0), + ).animate(secondaryAnimation), + child: FadeTransition( + opacity: Tween(begin: 0.0, end: 1.0).animate(curvedAnimation), + child: child, + ), + ), + ), + ); + }, + ); +} + +///中间对话框 半透明(重下面弹出) +showBottomInCenterDialog(BuildContext context, Widget child) { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: true, + transitionDuration: Duration(milliseconds: 350), + barrierColor: Colors.black.withOpacity(0.6), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return SlideTransition( + position: Tween( + begin: Offset(0.0, 2.0), + end: Offset(0.0, 0.0), + ).chain(CurveTween(curve: Curves.ease)).animate(animation), + child: Center(child: child), + ); + }, + ); +} + +///中间对话框 半透明 点外部不消失 +showCenterDialog2(BuildContext context, Widget child, {Color? mask}) { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: false, + transitionDuration: Duration(milliseconds: 350), + barrierColor: mask ?? Colors.black.withOpacity(0.6), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Center(child: child); + }, + ); +} + +///底部对话框 透明 +showBottomDialog( + BuildContext context, + Widget child, { + Color? barrierColor, + Duration? transitionDuration, +}) { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: true, + barrierColor: barrierColor ?? Colors.transparent, + transitionDuration: transitionDuration ?? Duration(milliseconds: 350), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Column( + children: [Expanded(child: SizedBox(width: width(1))), child], + ); + }, + ); +} + +///底部对话框 透明(从下面弹出) +showBottomInBottomDialog( + BuildContext context, + Widget child, { + Color? barrierColor, + bool barrierDismissible = true, +}) { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: barrierDismissible, + transitionDuration: Duration(milliseconds: 325), + barrierColor: barrierColor ?? Colors.black.withOpacity(0.3), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Column( + children: [ + Expanded(child: SizedBox(width: width(1))), + SlideTransition( + position: Tween( + begin: Offset(0.0, 2.0), + end: Offset(0.0, 0.0), + ).chain(CurveTween(curve: Curves.ease)).animate(animation), + child: child, + ), + ], + ); + }, + ); +} + +///底部对话框 半透明 +showBottomDialog2(BuildContext context, Widget child) { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: true, + transitionDuration: Duration(milliseconds: 350), + barrierColor: Colors.black.withOpacity(0.2), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Column( + children: [Expanded(child: SizedBox(width: width(1))), child], + ); + }, + ); +} + +///显示菜单 +showMenuPop(BuildContext context, Widget child) { + showGeneralDialog( + context: context, + barrierLabel: '', + barrierDismissible: true, + transitionDuration: Duration(milliseconds: 350), + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Row( + children: [ + Spacer(), + Column( + children: [SizedBox(height: height(55)), child, Spacer()], + ), + SizedBox(width: width(15)), + ], + ); + }, + ); +} + +class SCForceDialog extends StatelessWidget { + final String msg; + final Function onClick; + + const SCForceDialog({Key? key, required this.msg, required this.onClick}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: width(269), + decoration: ShapeDecoration( + color: Color(0xffffffff), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(width(10))), + ), + ), + child: _bdg(context), + ); + } + + Widget _bdg(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: width(30), + vertical: height(15), + ), + alignment: Alignment.center, + child: Text( + msg, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Color(0xff333333), + fontWeight: FontWeight.w500, + decoration: TextDecoration.none, + ), + ), + ), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + Container( + height: height(46), + alignment: Alignment.center, + child: Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Text( + "确定", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Color(0xfFFFF4FBB), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + onTap: () { + Navigator.of(context).pop(); + if (onClick != null) { + onClick(); + } + }, + ), + ), + ], + ), + ), + ], + ); + } +} + +///提示对话框 +class SCTsDialog extends StatelessWidget { + final String msg; + final Function onClick; + + const SCTsDialog({Key? key, required this.msg, required this.onClick}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: width(269), + decoration: ShapeDecoration( + color: Color(0xffffffff), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(width(10))), + ), + ), + child: _bdg(context), + ); + } + + Widget _bdg(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: width(30), + vertical: height(25), + ), + alignment: Alignment.center, + child: Text( + msg, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Color(0xff333333), + fontWeight: FontWeight.w500, + decoration: TextDecoration.none, + ), + ), + ), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + Container( + height: height(46), + alignment: Alignment.center, + child: Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Text( + "取消", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Color(0xFFAAAAAA), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ), + VerticalDivider(width: width(1), color: SocialChatTheme.dividerColor), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Text( + "确定", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + onTap: () { + Navigator.of(context).pop(); + if (onClick != null) { + onClick(); + } + }, + ), + ), + ], + ), + ), + ], + ); + } +} + +///提示对话框 +class SCTsDialog2 extends StatelessWidget { + final String msg; + final String btmText; + final Function onClick; + + const SCTsDialog2({ + Key? key, + required this.msg, + required this.onClick, + required this.btmText, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: width(269), + decoration: ShapeDecoration( + color: Color(0xffffffff), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(width(10))), + ), + ), + child: _bdg(context), + ); + } + + Widget _bdg(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: width(30), + vertical: height(15), + ), + alignment: Alignment.center, + child: Text( + msg, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Color(0xff333333), + fontWeight: FontWeight.w500, + decoration: TextDecoration.none, + ), + ), + ), + Divider(color: Color(0xffF3F3F3), endIndent: 15.w, indent: 15.w), + Container( + height: height(46), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SizedBox(width: 15.w), + Expanded( + child: SCLkTapWidget( + borderRadius: BorderRadius.circular(52), + child: Container( + width: 100.w, + height: 35.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xffF3F3F3), + borderRadius: BorderRadius.circular(52), + ), + child: Text( + "在考虑下", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Color(0xFF333333), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ), + SizedBox(width: 35.w), + Expanded( + child: SCLkTapWidget( + borderRadius: BorderRadius.circular(52), + child: Container( + width: 100.w, + height: 35.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xffBA0CF9), + borderRadius: BorderRadius.circular(52), + ), + child: Text( + "$btmText", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Colors.white, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + onTap: () { + Navigator.of(context).pop(); + if (onClick != null) { + onClick(); + } + }, + ), + ), + SizedBox(width: 15.w), + ], + ), + ), + ], + ); + } +} + +// 提示框 +class SCPromptDialog extends StatelessWidget { + final String msg; + final Function onClick; + + const SCPromptDialog({Key? key, required this.msg, required this.onClick}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: width(269), + decoration: ShapeDecoration( + color: Color(0xffffffff), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(width(10))), + ), + ), + child: _bdg(context), + ); + } + + Widget _bdg(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: width(30), + vertical: height(15), + ), + alignment: Alignment.center, + child: Text( + msg, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Color(0xff333333), + fontWeight: FontWeight.w500, + decoration: TextDecoration.none, + ), + ), + ), + Divider(height: height(1), color: SocialChatTheme.dividerColor), + Container( + height: height(46), + alignment: Alignment.center, + child: Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Text( + "我知道了", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: sp(16), + color: Color(0xffFFE400), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + onTap: () { + Navigator.of(context).pop(); + if (onClick != null) { + onClick(); + } + }, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/shared/tools/sc_lk_event_bus.dart b/lib/shared/tools/sc_lk_event_bus.dart new file mode 100644 index 0000000..4b3fadb --- /dev/null +++ b/lib/shared/tools/sc_lk_event_bus.dart @@ -0,0 +1,15 @@ +import 'package:event_bus/event_bus.dart'; + +EventBus eventBus = EventBus(); + +class SCGiveRoomLuckPageDisposeEvent {} + +class GiveRoomLuckWithOtherEvent { + String giftPic = ""; + List acceptUserIds = []; + + GiveRoomLuckWithOtherEvent(this.giftPic, this.acceptUserIds); +} + +class UpdateDynamicEvent { +} diff --git a/lib/shared/tools/sc_loading_manager.dart b/lib/shared/tools/sc_loading_manager.dart new file mode 100644 index 0000000..6bccbb7 --- /dev/null +++ b/lib/shared/tools/sc_loading_manager.dart @@ -0,0 +1,43 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +typedef SCLoadingManager = SCProgressIndicator; + +class SCProgressIndicator { + // 显示全局 Loading + static void show({String? message, BuildContext? context, bool cancelable = false}) { + SmartDialog.dismiss(status: SmartStatus.loading); + SmartDialog.showLoading( + alignment: Alignment.center, + maskColor: Colors.transparent, + animationType: SmartAnimationType.fade, + clickMaskDismiss: cancelable, // 使用新的参数 + backType: cancelable ? SmartBackType.normal : SmartBackType.block, + builder: (_) { + return Center( + child: Container( + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(8), + ), + width: 55.w, + height: 55.w, + child: CupertinoActivityIndicator(color: Colors.white24,), + ), + ); + }, + ); + } + + // static bool _backInterceptor(bool stopDefaultButtonEvent, RouteInfo info) { + // // 拦截返回键 + // return true; // 返回 true 表示已处理该事件 + // } + + // 隐藏全局 Loading + static void hide() { + SmartDialog.dismiss(status: SmartStatus.loading); + } +} diff --git a/lib/shared/tools/sc_message_notifier.dart b/lib/shared/tools/sc_message_notifier.dart new file mode 100644 index 0000000..c249976 --- /dev/null +++ b/lib/shared/tools/sc_message_notifier.dart @@ -0,0 +1,19 @@ +import 'package:audioplayers/audioplayers.dart'; +class SCMessageNotifier { + + static bool canPlay = true; + static final AudioPlayer _a = AudioPlayer(); + // 播放提示音 + static Future playNotificationSound() async { + if(!canPlay){ + return; + } + // _audioPlayer.setVolume(0.2); + try { + _a.play(AssetSource("sc_images/msg/im_noti_music.MP3")); + } catch (e) { + print('播放提示音失败: $e'); + } + } + +} \ No newline at end of file diff --git a/lib/shared/tools/sc_message_utils.dart b/lib/shared/tools/sc_message_utils.dart new file mode 100644 index 0000000..a0f46f3 --- /dev/null +++ b/lib/shared/tools/sc_message_utils.dart @@ -0,0 +1,213 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/friend_type_enum.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_online_url.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; + +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_send_res.dart'; + +class SCMessageUtils { + //私聊红包Future缓存 + static Map> redPacketFutureCache = {}; + + static Future createImageElem(File file) async { + try { + // 2. 获取文件信息 + final originalSize = file.lengthSync(); + final fileExtension = _ge(file.path).toLowerCase(); + print('压缩前文件: ${file.path}'); + print('文件类型: $fileExtension'); + print('压缩前大小: ${_fs(originalSize)}'); + + // 3. 小文件不压缩(小于 100KB) + if (originalSize < 100 * 1024) { + print('文件较小,跳过压缩'); + return file; + } + + // 4. GIF 文件特殊处理 + if (fileExtension == 'gif') { + print('GIF 文件,使用特殊处理'); + return await _gf(file); + } + + // 5. 创建临时目录和文件 + final directory = await getTemporaryDirectory(); + final timestamp = DateTime.now().millisecondsSinceEpoch; + final String fileName = + 'compressed_${timestamp}_${_fh(file)}.jpg'; + final File newFile = File("${directory.path}/$fileName"); + + // 6. 动态调整压缩质量(根据原文件大小) + final int quality = _cq(originalSize); + print('使用压缩质量: $quality%'); + + // 7. 执行压缩 + final XFile? compressedFile = + await FlutterImageCompress.compressAndGetFile( + file.absolute.path, + newFile.path, + quality: quality, + minWidth: 480, + // 设置最小宽度,保持图片可用性 + minHeight: 480, + // 设置最小高度 + rotate: 0, + format: CompressFormat.jpeg, + inSampleSize: _ss(originalSize), // 采样率,减少内存使用 + ); + + if (compressedFile == null) { + print('压缩失败,返回原文件'); + return file; + } + + // 8. 检查压缩效果 + final compressedSize = await compressedFile.length(); + final compressionRatio = + (originalSize - compressedSize) / originalSize * 100; + + print('压缩后大小: ${_fs(compressedSize)}'); + print('压缩率: ${compressionRatio.toStringAsFixed(1)}%'); + + // 9. 如果压缩后文件反而更大,返回原文件 + if (compressedSize >= originalSize) { + print('压缩后文件更大,返回原文件'); + await newFile.delete(); // 删除无用的压缩文件 + return file; + } + + // 10. 缓存管理 + File localFile = File(compressedFile.path); + final String cacheKey = _fh(localFile); + await FileCacheManager.getInstance().putFileFromFile(cacheKey, localFile); + + print('压缩成功,发送路径: ${compressedFile.path}'); + return localFile; + } catch (e) { + print('图片压缩异常: $e'); + return file; // 异常时返回原文件,保证功能可用 + } + } + + // 获取文件扩展名 + static String _ge(String path) { + return path.split('.').last; + } + + // 格式化文件大小显示 + static String _fs(int bytes) { + if (bytes <= 0) return "0 B"; + const suffixes = ["B", "KB", "MB", "GB"]; + final i = (log(bytes) / log(1024)).floor(); + return '${(bytes / pow(1024, i)).toStringAsFixed(1)} ${suffixes[i]}'; + } + + // 计算压缩质量(根据文件大小动态调整) + static int _cq(int fileSize) { + if (fileSize > 5 * 1024 * 1024) { + // > 5MB + return 50; + } else if (fileSize > 2 * 1024 * 1024) { + // 2-5MB + return 60; + } else if (fileSize > 1 * 1024 * 1024) { + // 1-2MB + return 70; + } else if (fileSize > 500 * 1024) { + // 500KB-1MB + return 80; + } else { + return 85; // < 500KB 使用较高质量 + } + } + + // 计算采样率 + static int _ss(int fileSize) { + if (fileSize > 5 * 1024 * 1024) return 4; + if (fileSize > 2 * 1024 * 1024) return 3; + if (fileSize > 1 * 1024 * 1024) return 2; + return 1; + } + + // 生成文件哈希(用于缓存键) + static String _fh(File file) { + final stats = file.statSync(); + return '${file.path}_${stats.modified.millisecondsSinceEpoch}'.hashCode + .toString(); + } + + // GIF 文件压缩处理 + static Future _gf(File file) async { + // 对于 GIF,我们可以考虑: + // 1. 使用其他库处理(如 image 包) + // 2. 调整尺寸但保持动图特性 + // 3. 或者直接返回原文件(保持动图效果) + + // 这里简单返回原文件,避免破坏 GIF 动画 + print('GIF 文件保持原样发送'); + return file; + } + + static Future generateFileThumbnail( + String videoPath, + int maxHeight, + ) async { + try { + final thumbnailPath = await VideoThumbnail.thumbnailFile( + video: videoPath, + thumbnailPath: (await getTemporaryDirectory()).path, + // 保存到临时目录 + maxHeight: maxHeight, + // 指定高度,宽度按比例缩放 + imageFormat: ImageFormat.PNG, + quality: 75, + ); + return thumbnailPath; // 返回文件路径,可用于 Image.file(File(thumbnailPath!)) + } catch (e) { + print("生成缩略图文件时出错: $e"); + return null; + } + } + + ///下载多媒体消息 + static Future downloadMessage({ + required String msgID, + required int messageType, + required int imageType, // 图片类型,仅messageType为图片消息是有效 + required bool isSnapshot, // 是否是视频封面,仅messageType为视频消息是有效 + }) async { + var result = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .downloadMessage( + msgID: msgID, + messageType: messageType, + imageType: imageType, + isSnapshot: isSnapshot, + ); + return result; + } + + Future> getMessageOnlineUrl({ + required String msgID, + }) async { + var result = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .getMessageOnlineUrl(msgID: msgID); + return result; + } + + /// 清空与该联系人的所有单聊消息记录 + static Future clearAllMessages(String userId) async { + V2TimCallback clearResult = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .clearC2CHistoryMessage(userID: userId); // 指定要清空消息的联系人ID[citation:2] + } +} diff --git a/lib/shared/tools/sc_path_utils.dart b/lib/shared/tools/sc_path_utils.dart new file mode 100644 index 0000000..7db0619 --- /dev/null +++ b/lib/shared/tools/sc_path_utils.dart @@ -0,0 +1,165 @@ +import 'package:path/path.dart' as path; + +class SCPathUtils { + /// 判断路径类型 + static PathType getPathType(String uri) { + if (uri.isEmpty) return PathType.unknown; + + // 1. 检查网络地址 + if (_nt(uri)) return PathType.network; + + // 2. 检查 Asset 路径 + if (_as(uri)) return PathType.asset; + + // 3. 检查文件路径 + if (_fl(uri)) return PathType.file; + + return PathType.unknown; + } + + // 检查是否是网络地址 + static bool _nt(String uri) { + final uriPattern = RegExp( + r'^(https?|ftp):\/\/' // 协议 + r'([\w\-]+(\.[\w\-]+)+)' // 域名 + r'(:\d+)?' // 端口 + r'(\/[^\s]*)?$', // 路径 + caseSensitive: false, + ); + + return uriPattern.hasMatch(uri) || uri.startsWith('http'); + } + + // 检查是否是 Asset 路径 + static bool _as(String uri) { + // 排除包含特殊字符的情况 + if (uri.contains('://') || uri.contains('\\') || path.isAbsolute(uri)) { + return false; + } + + // 常见的 asset 路径特征 + final isDirectAsset = + uri.startsWith('assets/') || + uri.startsWith('sc_images/') || + uri.startsWith('videos/'); + + // 检查文件扩展名 + final ext = path.extension(uri).toLowerCase(); + final isSupportedAsset = [ + '.png', + '.jpg', + '.jpeg', + '.gif', + '.mp4', + '.vap', + ].contains(ext); + + return isDirectAsset && isSupportedAsset; + } + + // 检查是否是文件路径 + static bool _fl(String uri) { + // 检查文件 URI 格式 + if (uri.startsWith('file://')) return true; + + // 检查绝对路径 + if (path.isAbsolute(uri)) return true; + + // 检查 Windows 路径 + if (uri.contains(':\\') || uri.contains(':/')) return true; + + // 检查常见文件扩展名 + final ext = path.extension(uri).toLowerCase(); + return [ + '.mp4', + '.mov', + '.avi', + '.png', + '.jpg', + '.txt', + '.pdf', + ].contains(ext); + } + + /// 获取路径的文件名(无扩展名) + static String getFileNameWithoutExtension(String filePath) { + return path.basenameWithoutExtension(filePath); + } + + /// 获取路径的文件扩展名 + static String getFileExtension(String filePath) { + return path.extension(filePath).toLowerCase(); + } + + /// 从网络地址中提取文件名 + static String getFileNameFromUrl(String url) { + try { + final uri = Uri.parse(url); + return path.basename(uri.path); + } catch (e) { + return 'unknown_file'; + } + } + + static String getFileType(String filePath) { + String extension = path.extension(filePath).toLowerCase(); + + // 图片格式 + final imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']; + // 视频格式 + final videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']; + + if (imageExtensions.contains(extension)) { + return 'image'; + } else if (videoExtensions.contains(extension)) { + return 'video_pic'; + } else { + return 'unknown'; + } + } + + static bool fileTypeIsPicAndMP4(String filePath) { + String extension = path.extension(filePath).toLowerCase(); + + // 图片格式 + final imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']; + // 视频格式 + final videoExtensions = ['.mp4']; + if (imageExtensions.contains(extension) || + videoExtensions.contains(extension)) { + return true; + } + return false; + } + + + static bool fileTypeIsPic(String filePath) { + String extension = path.extension(filePath).toLowerCase(); + + // 图片格式 + // final imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.webp']; + final imageExtensions = ['.jpg', '.jpeg', '.png']; + if (imageExtensions.contains(extension)) { + return true; + } + return false; + } + + static bool fileTypeIsPic2(String filePath) { + String extension = path.extension(filePath).toLowerCase(); + + // 图片格式 + final imageExtensions = ['.jpg', '.jpeg', '.png', '.bmp', '.webp']; + if (imageExtensions.contains(extension)) { + return true; + } + return false; + } +} + +enum PathType { + asset, // 资源文件 (assets/) + file, // 本地文件路径 + network, // 网络地址 + unknown, // 未知类型 +} diff --git a/lib/shared/tools/sc_permission_utils.dart b/lib/shared/tools/sc_permission_utils.dart new file mode 100644 index 0000000..df305c7 --- /dev/null +++ b/lib/shared/tools/sc_permission_utils.dart @@ -0,0 +1,23 @@ +import 'package:permission_handler/permission_handler.dart'; + +class SCPermissionUtils{ + static Future checkMicrophonePermission() async { + // 检查当前权限状态 + var status = await Permission.microphone.status; + if (status.isDenied) { + // 请求权限(弹出系统弹窗) + status = await Permission.microphone.request(); + } + return status.isGranted; + } + + static Future checkPhotosPermission() async { + // 检查当前权限状态 + var status = await Permission.photos.status; + if (status.isDenied) { + // 请求权限(弹出系统弹窗) + status = await Permission.photos.request(); + } + return status.isGranted; + } +} \ No newline at end of file diff --git a/lib/shared/tools/sc_pick_utils.dart b/lib/shared/tools/sc_pick_utils.dart new file mode 100644 index 0000000..4d9b8c1 --- /dev/null +++ b/lib/shared/tools/sc_pick_utils.dart @@ -0,0 +1,231 @@ +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_general_repository_imp.dart'; +import 'package:yumi/modules/user/crop/crop_image_page.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; + +class SCPickUtils { + static final ImagePicker _pkr = ImagePicker(); + + static void pickImage(BuildContext context, + OnUpLoadCallBack onUpLoadCallBack, { + double? aspectRatio, + bool? backOriginalFile = true, + bool? neeCrop = true, + }) async { + try { + final XFile? pickedFile = await _pkr.pickImage( + source: ImageSource.gallery, + imageQuality: 90, + maxWidth: 1920, + maxHeight: 1080, + ); + + if (pickedFile == null) return; + if (!SCPathUtils.fileTypeIsPic(pickedFile.path)) { + SCTts.show("Please select sc_images in .jpg, .jpeg, .png format."); + return; + } + // 检查是否为 GIF 图像 + if (await _igif(pickedFile)) { + SCTts.show("GIF sc_images are not supported"); + return; + } + + final File imageFile = File(pickedFile.path); + if (neeCrop ?? false) { + cropImage( + imageFile, + context, + aspectRatio, + backOriginalFile, + onUpLoadCallBack, + ); + } else { + if (imageFile.lengthSync() > 4000000) { + SCTts.show(SCAppLocalizations.of(context)!.theImageSizeCannotExceed); + onUpLoadCallBack?.call(false, ""); + return; + } + SCLoadingManager.show(context: context); + try { + String fileUrl = await SCGeneralRepositoryImp().upload(imageFile); + SCLoadingManager.hide(); + onUpLoadCallBack?.call(true, fileUrl); + } catch (e) { + SCTts.show("upload fail $e"); + onUpLoadCallBack?.call(false, ""); + SCLoadingManager.hide(); + } + } + } catch (e) { + onUpLoadCallBack?.call(false, ""); + print("Image selection error: $e"); + SCTts.show("Failed to select image"); + } + } + + + /// 检查图像是否为 GIF 格式 + static Future _igif(XFile file) async { + try { + // 方法1: 通过文件扩展名检查 + if (file.path.toLowerCase().endsWith('.gif')) { + return true; + } + + // 方法2: 通过读取文件头检查 (更可靠) + final bytes = await file.readAsBytes(); + if (bytes.length >= 6) { + // GIF 文件头特征: "GIF87a" 或 "GIF89a" + final header = String.fromCharCodes(bytes.sublist(0, 6)); + return header == 'GIF87a' || header == 'GIF89a'; + } + + return false; + } catch (e) { + print("Error checking GIF: $e"); + return false; + } + } + + static Future _iwp(XFile file) async { + try { + // 方法1: 通过文件扩展名检查 + if (file.path.toLowerCase().endsWith('.webp')) { + return true; + } + + // 方法2: 通过读取文件头检查 + final bytes = await file.readAsBytes(); + + if (bytes.length < 12) { + return false; + } + + // 检查基本的 WebP 文件头 + final riffHeader = String.fromCharCodes(bytes.sublist(0, 4)); + final webpHeader = String.fromCharCodes(bytes.sublist(8, 12)); + + if (riffHeader == 'RIFF' && webpHeader == 'WEBP') { + return true; + } + + // 检查扩展的 WebP 格式(如果有更多字节可用) + if (bytes.length >= 16) { + // 检查 VP8 (有损) WebP + if (bytes[12] == 0x56 && bytes[13] == 0x50 && + bytes[14] == 0x38 && bytes[15] == 0x20) { + return true; + } + + // 检查 VP8L (无损) WebP + if (bytes[12] == 0x56 && bytes[13] == 0x50 && + bytes[14] == 0x38 && bytes[15] == 0x4C) { + return true; + } + + // 检查 VP8X (扩展) WebP + if (bytes[12] == 0x56 && bytes[13] == 0x50 && + bytes[14] == 0x38 && bytes[15] == 0x58) { + return true; + } + } + + return false; + } catch (e) { + print("Error checking WebP: $e"); + return false; + } + } + + /// 拍照并过滤 GIF + static void takePhoto(BuildContext context, + OnUpLoadCallBack onUpLoadCallBack, { + double? aspectRatio, + bool? backOriginalFile = true, + }) async { + try { + final XFile? pickedFile = await _pkr.pickImage( + source: ImageSource.camera, + imageQuality: 90, + maxWidth: 1920, + maxHeight: 1080, + ); + + if (pickedFile == null) return; + + // 相机拍摄的照片不会是 GIF,但为了安全也可以检查 + if (await _igif(pickedFile)) { + SCTts.show("Unsupported image format"); + return; + } + + final File imageFile = File(pickedFile.path); + cropImage( + imageFile, + context, + aspectRatio, + backOriginalFile, + onUpLoadCallBack, + ); + } catch (e) { + print("Camera error: $e"); + SCTts.show("Camera failed"); + } + } + + /// 选择视频 (不涉及 GIF 过滤) + static Future pickVideo(BuildContext context) async { + try { + final XFile? videoFile = await _pkr.pickVideo( + source: ImageSource.gallery, + ); + + if (videoFile != null) { + return File(videoFile.path); + } + return null; + } catch (e) { + print("Video selection error: $e"); + SCTts.show("Failed to select video_pic"); + return null; + } + } + + // 原有的 cropImage 方法保持不变 + static void cropImage(File originalImage, + BuildContext context, + double? aspectRatio, + bool? backOriginalFile, + OnUpLoadCallBack onUpLoadCallBack, + { + bool needUpload = true, + }) async { + if (originalImage == null) { + return; + } + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + CropImagePage( + originalImage, + aspectRatio: aspectRatio, + backOriginalFile: backOriginalFile, + needUpload:needUpload, + onUpLoadCallBack: (success, url) { + if (url.isNotEmpty) { + onUpLoadCallBack(success, url); + } + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/shared/tools/sc_reg_exp_utils.dart b/lib/shared/tools/sc_reg_exp_utils.dart new file mode 100644 index 0000000..82f7501 --- /dev/null +++ b/lib/shared/tools/sc_reg_exp_utils.dart @@ -0,0 +1,21 @@ +class SCRegExpUtils { + static final String _ri = "]*>([\\s\\S]*?)"; + + static final _r = RegExp(_ri); + + ///检索At用户名 + static String getAtName(String input) { + if (_r.hasMatch(input)) { + return _r.firstMatch(input)!.group(1) ?? ""; + } + return ""; + } + + static String getContent(String input) { + String content = input.replaceAll( + _r.firstMatch(input)![0] ?? "", + "@${getAtName(input)}", + ); + return content; + } +} diff --git a/lib/shared/tools/sc_room_utils.dart b/lib/shared/tools/sc_room_utils.dart new file mode 100644 index 0000000..2ee806e --- /dev/null +++ b/lib/shared/tools/sc_room_utils.dart @@ -0,0 +1,302 @@ +import 'dart:math'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/modules/wallet/wallet_route.dart'; +import 'package:yumi/modules/room/voice_room_route.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/ui_kit/components/sc_float_ichart.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; + + +typedef SCRoomUtils = SCChatRoomHelper; + +class SCChatRoomHelper { + static Map roomUsersMap = {}; + + static void goRoom(String roomId, + BuildContext context, { + bool needOpenRedenvelope = false, + bool fromFloting = false, + String redPackId = "", + }) { + if (Provider + .of(context, listen: false) + .currenRoom != null) { + if (SCFloatIchart().isShow()) { + ///房间最小化了 + if (Provider + .of( + context, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id == + roomId) { + SCRoomUtils.openCurrentRoom( + context, + needOpenRedenvelope: needOpenRedenvelope, + redPackId: redPackId, + ); + } else { + showEnterRoomConfirm( + roomId, + context, + needOpenRedenvelope: needOpenRedenvelope, + fromFloting: fromFloting, + redPackId: redPackId, + ); + } + } else { + if (Provider + .of( + context, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id == + roomId) { + + } else { + showEnterRoomConfirm( + roomId, + context, + needOpenRedenvelope: needOpenRedenvelope, + fromFloting: fromFloting, + redPackId: redPackId, + ); + } + } + } else { + showEnterRoomConfirm( + roomId, + context, + needOpenRedenvelope: needOpenRedenvelope, + fromFloting: fromFloting, + redPackId: redPackId, + ); + } + } + + static void openCurrentRoom(BuildContext context, { + bool needOpenRedenvelope = false, + String? redPackId, + }) { + SCFloatIchart().remove(); + Provider.of(context, listen: false) + .loadRoomInfo( + Provider + .of( + context, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id ?? + "", + ); + Provider.of(context, listen: false) + .fetchUserProfileData(); + Provider.of(context, listen: false) + .retrieveMicrophoneList(); + Provider + .of(context, listen: false) + .closeFullGame = true; + SCNavigatorUtils.push( + context, + '${VoiceRoomRoute.voiceRoom}?id=${Provider + .of(context, listen: false) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id}', + ); + + } + + static double getCurrenProgress(int currentEnergy, + int maxEnergy, + double maxWidth,) { + var progress = (currentEnergy / maxEnergy); + var curren = progress * maxWidth; + if (curren > maxWidth) { + return maxWidth; + } else { + return curren; + } + } + + static void goRecharge(BuildContext context) { + SmartDialog.dismiss(tag: "showGoToRecharge"); + SmartDialog.show( + tag: "showGoToRecharge", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return Container( + width: ScreenUtil().screenWidth * 0.75, + height: 120.w, + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + child: Column( + children: [ + SizedBox(height: 8.w), + Row( + children: [ + Spacer(), + GestureDetector( + child: Icon( + Icons.close, + color: Colors.black, + size: 20.w, + ), + onTap: () { + SmartDialog.dismiss(tag: "showGoToRecharge"); + }, + ), + SizedBox(width: 10.w), + ], + ), + Row( + children: [ + Expanded( + child: text( + SCAppLocalizations.of( + context, + )!.insufhcientGoldsGoToRecharge, + textColor: Colors.black, + textAlign: TextAlign.center, + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + SizedBox(height: 10.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + height: 38.w, + width: 125.w, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFFFEB219), + Color(0xFFFF9326), + ], + ), + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + child: text( + SCAppLocalizations.of(context)!.goToRecharge, + textColor: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + closeAllDialogs(); + SCNavigatorUtils.push( + context, + WalletRoute.recharge, + replace: false, + ); + }, + ), + ], + ), + ); + }, + ); + } + + static int getRandomInt(int min, int max) { + final random = Random(); + return min + random.nextInt(max - min + 1); + } + + ///房间游客是否可以发消息 + static bool touristCanMsg(BuildContext context) { + ///游客可以发送文字 + if (Provider + .of( + context, + listen: false, + ) + .currenRoom + ?.roomProfile + ?.roomSetting + ?.touristMsg ?? + false) {} else { + if (Provider.of(context, listen: false) + .isTourists()) { + SCTts.show(SCAppLocalizations.of(context)!.touristsCannotSendMessages); + return false; + } + } + return true; + } + + static void roomSCGlobalConfig(String roomId) { + SCGlobalConfig.isGiftSpecialEffects = DataPersistence.getBool( + "${AccountStorage() + .getCurrentUser() + ?.userProfile + ?.account}-GiftSpecialEffects", + defaultValue: true, + ); + SCGlobalConfig.isEntryVehicleAnimation = DataPersistence.getBool( + "${AccountStorage() + .getCurrentUser() + ?.userProfile + ?.account}-EntryVehicleAnimation", + defaultValue: true, + ); + SCGlobalConfig.isFloatingAnimationInGlobal = DataPersistence.getBool( + "${AccountStorage() + .getCurrentUser() + ?.userProfile + ?.account}-FloatingAnimationInGlobal", + defaultValue: true, + ); + SCGlobalConfig.isLuckGiftSpecialEffects = DataPersistence.getBool( + "${AccountStorage() + .getCurrentUser() + ?.userProfile + ?.account}-LuckGiftSpecialEffects", + defaultValue: true, + ); + DataPersistence.setLastTimeRoomId(roomId); + } + + static void closeAllDialogs() async { + // 循环检查并关闭弹窗,直到不存在任何弹窗 + while (SmartDialog.checkExist( + dialogTypes: {SmartAllDialogType.custom, SmartAllDialogType.attach}, + )) { + SmartDialog.dismiss(); + await Future.delayed(const Duration(milliseconds: 50)); + } + } + + +} diff --git a/lib/shared/tools/sc_string_utils.dart b/lib/shared/tools/sc_string_utils.dart new file mode 100644 index 0000000..b5606d5 --- /dev/null +++ b/lib/shared/tools/sc_string_utils.dart @@ -0,0 +1,85 @@ +typedef SCStringUtils = SCStringProcessor; + +class SCStringProcessor { + static bool checkIfUrl(String str) { + if (str.startsWith("http://") || str.startsWith("https://")) { + return true; + } + return false; + } + + ///判断字符串是否为合法的Int + static bool validateInteger(String str) { + try { + int.parse(str); + return true; + } catch (e) { + return false; + } + } + + ///判断字符串是否为合法的Double + static bool validateDouble(String str) { + try { + double.parse(str); + return true; + } catch (e) { + return false; + } + } + + ///将文本转成数字 + static int convertToInteger(String? str, {int defalut = -1}) { + if (str == null || str.isEmpty) { + return defalut; + } + var value = 0; + try { + value = int.parse(str); + } catch (e) {} + return value; + } + + static String formatNumericValue(num number) { + if (number >= 1000000) { + return '${(number / 1000000).toStringAsFixed(1)}M'; + } else if (number >= 1000) { + return '${(number / 1000).toStringAsFixed(1)}K'; + } + return number.toString(); + } + + ///检测文本是否有阿语 + static bool containsArabicCharacters(String char) { + if (char.isEmpty) return false; + final code = char.runes.first; + + // 阿拉伯语主要 Unicode 范围: + // 1. 阿拉伯语基本字符 (Arabic): U+0600 - U+06FF + // 2. 阿拉伯语补充 (Arabic Supplement): U+0750 - U+077F + // 3. 阿拉伯语扩展-A (Arabic Extended-A): U+08A0 - U+08FF + // 4. 阿拉伯语扩展-B (Arabic Extended-B): U+0870 - U+089F + // 5. 阿拉伯语表示形式-A (Arabic Presentation Forms-A): U+FB50 - U+FDFF + // 6. 阿拉伯语表示形式-B (Arabic Presentation Forms-B): U+FE70 - U+FEFF + + return ( + // 阿拉伯语基本字符 + (code >= 0x0600 && code <= 0x06FF) || + // 阿拉伯语补充 + (code >= 0x0750 && code <= 0x077F) || + // 阿拉伯语扩展-A + (code >= 0x08A0 && code <= 0x08FF) || + // 阿拉伯语扩展-B + (code >= 0x0870 && code <= 0x089F) || + // 阿拉伯语表示形式-A + (code >= 0xFB50 && code <= 0xFDFF) || + // 阿拉伯语表示形式-B + (code >= 0xFE70 && code <= 0xFEFF) + ); + } + + /// 内部辅助方法,用于差异化代码结构 + static void _di() { + // 空实现,仅用于差异化代码结构 + } +} diff --git a/lib/shared/tools/sc_system_message_utils.dart b/lib/shared/tools/sc_system_message_utils.dart new file mode 100644 index 0000000..ad066fe --- /dev/null +++ b/lib/shared/tools/sc_system_message_utils.dart @@ -0,0 +1,1428 @@ +import 'dart:convert'; + +import 'package:extended_text/extended_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:marquee/marquee.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +import '../../shared/business_logic/models/res/sc_system_invit_message_res.dart'; + +class SCSystemMessageUtils { + static Widget buildAgentSendInviteHostMessage( + data, + Function(BuildContext ct, String content) showMsgItemMenu, + Function(String id) rejectOpt, + Function(String id) acceptOpt, + BuildContext context, + ) { + String expand = data["expand"] ?? ""; + if (data["content"] != null) { + var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"])); + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + showMsgItemMenu(ct, ""); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + Row( + children: [ + head(url: bean.userAvatar ?? "", width: 65.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + netImage( + url: + Provider.of( + context, + listen: false, + ) + .findCountryByName( + bean.countryName ?? "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all( + Radius.circular(3.w), + ), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + Container( + constraints: BoxConstraints(maxWidth: 160.w), + child: text( + bean.userNickname ?? "", + fontWeight: FontWeight.bold, + textColor: Colors.white, + fontSize: 16.sp, + ), + ), + ], + ), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + (bean.account != bean.actualAccount) + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png", + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + bean.actualAccount ?? "", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: bean.actualAccount ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + SizedBox(height: 3.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xffFF6464), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.reject, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + rejectOpt(expand); + }, + ), + Spacer(), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xff7B64FF), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.accept, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + acceptOpt(expand); + }, + ), + SizedBox(width: 10.w), + ], + ), + Divider(color: Colors.black12, thickness: 0.5, height: 28.w), + ExtendedText( + SCAppLocalizations.of(context)!.inviteYouToBecomeHost, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: 14.sp, color: Colors.black54), + ), + SizedBox(height: 5.w), + ], + ), + ), + ); + }, + ); + } + return Container(); + } + + static Widget buildBdSendInviteAgentMessage( + data, + Function(BuildContext ct, String content) showMsgItemMenu, + Function(String id) rejectOpt, + Function(String id) acceptOpt, + BuildContext context, + ) { + String expand = data["expand"] ?? ""; + if (data["content"] != null) { + var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"])); + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + showMsgItemMenu(ct, ""); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + Row( + children: [ + head(url: bean.userAvatar ?? "", width: 65.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + netImage( + url: + Provider.of( + context, + listen: false, + ) + .findCountryByName( + bean.countryName ?? "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all( + Radius.circular(3.w), + ), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + Container( + constraints: BoxConstraints(maxWidth: 160.w), + child: text( + bean.userNickname ?? "", + fontWeight: FontWeight.bold, + textColor: Colors.black, + fontSize: 16.sp, + ), + ), + ], + ), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + (bean.account != bean.actualAccount) + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png", + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + bean.actualAccount ?? "", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: bean.actualAccount ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + SizedBox(height: 3.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xffFF6464), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.reject, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + rejectOpt(expand); + }, + ), + Spacer(), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xff7B64FF), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.accept, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + acceptOpt(expand); + }, + ), + SizedBox(width: 10.w), + ], + ), + Divider(color: Colors.black12, thickness: 0.5, height: 28.w), + ExtendedText( + SCAppLocalizations.of(context)!.inviteYouToBecomeAgent, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: 14.sp, color: Colors.black54), + ), + SizedBox(height: 5.w), + ], + ), + ), + ); + }, + ); + } + return Container(); + } + + static Widget buildBdLeaderInviteBdMessage( + data, + Function(BuildContext ct, String content) showMsgItemMenu, + Function(String id) rejectOpt, + Function(String id) acceptOpt, + BuildContext context, + ) { + String expand = data["expand"] ?? ""; + if (data["content"] != null) { + var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"])); + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + showMsgItemMenu(ct, ""); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + Row( + children: [ + head(url: bean.userAvatar ?? "", width: 65.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + netImage( + url: + Provider.of( + context, + listen: false, + ) + .findCountryByName( + bean.countryName ?? "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all( + Radius.circular(3.w), + ), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + Container( + constraints: BoxConstraints(maxWidth: 160.w), + child: text( + bean.userNickname ?? "", + fontWeight: FontWeight.bold, + textColor: Colors.black, + fontSize: 16.sp, + ), + ), + ], + ), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + (bean.account != bean.actualAccount) + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png", + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + bean.actualAccount ?? "", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: bean.actualAccount ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + SizedBox(height: 3.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xffFF6464), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.reject, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + rejectOpt(expand); + }, + ), + Spacer(), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xff7B64FF), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.accept, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + acceptOpt(expand); + }, + ), + SizedBox(width: 10.w), + ], + ), + Divider(color: Colors.black12, thickness: 0.5, height: 28.w), + ExtendedText( + SCAppLocalizations.of(context)!.inviteYouToBecomeBD, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: 14.sp, color: Colors.black54), + ), + SizedBox(height: 5.w), + ], + ), + ), + ); + }, + ); + } + return Container(); + } + + static Widget buildBdLeaderInviteBdLeaderMessage( + data, + Function(BuildContext ct, String content) showMsgItemMenu, + Function(String id) rejectOpt, + Function(String id) acceptOpt, + BuildContext context, + ) { + String expand = data["expand"] ?? ""; + if (data["content"] != null) { + var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"])); + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + showMsgItemMenu(ct, ""); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + Row( + children: [ + head(url: bean.userAvatar ?? "", width: 65.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + netImage( + url: + Provider.of( + context, + listen: false, + ) + .findCountryByName( + bean.countryName ?? "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all( + Radius.circular(3.w), + ), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + Container( + constraints: BoxConstraints(maxWidth: 160.w), + child: text( + bean.userNickname ?? "", + fontWeight: FontWeight.bold, + textColor: Colors.black, + fontSize: 16.sp, + ), + ), + ], + ), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + (bean.account != bean.actualAccount) + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png", + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + bean.actualAccount ?? "", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: bean.actualAccount ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + SizedBox(height: 3.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xffFF6464), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.reject, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + rejectOpt(expand); + }, + ), + Spacer(), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xff7B64FF), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.accept, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + acceptOpt(expand); + }, + ), + SizedBox(width: 10.w), + ], + ), + Divider(color: Colors.black12, thickness: 0.5, height: 28.w), + ExtendedText( + SCAppLocalizations.of( + context, + )!.bDLeaderInviteYouToBecomeBDLeader, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: 14.sp, color: Colors.black54), + ), + SizedBox(height: 5.w), + ], + ), + ), + ); + }, + ); + } + return Container(); + } + + static Widget buildAdminInviteRechargeAgentMessage( + data, + Function(BuildContext ct, String content) showMsgItemMenu, + Function(String id) rejectOpt, + Function(String id) acceptOpt, + BuildContext context, + ) { + String expand = data["expand"] ?? ""; + if (data["content"] != null) { + var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"])); + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + showMsgItemMenu(ct, ""); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + Row( + children: [ + head(url: bean.userAvatar ?? "", width: 65.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + netImage( + url: + Provider.of( + context, + listen: false, + ) + .findCountryByName( + bean.countryName ?? "", + ) + ?.nationalFlag ?? + "", + borderRadius: BorderRadius.all( + Radius.circular(3.w), + ), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 3.w), + Container( + constraints: BoxConstraints(maxWidth: 160.w), + child: text( + bean.userNickname ?? "", + fontWeight: FontWeight.bold, + textColor: Colors.black, + fontSize: 16.sp, + ), + ), + ], + ), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + (bean.account != bean.actualAccount) + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png", + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + bean.actualAccount ?? "", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: bean.actualAccount ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + SizedBox(height: 3.w), + Row( + children: [ + SizedBox(width: 10.w), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xffFF6464), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.reject, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + rejectOpt(expand); + }, + ), + Spacer(), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + height: 25.w, + width: 65.w, + decoration: BoxDecoration( + color: Color(0xff7B64FF), + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: text( + SCAppLocalizations.of(context)!.accept, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ), + onTap: () { + acceptOpt(expand); + }, + ), + SizedBox(width: 10.w), + ], + ), + Divider(color: Colors.black12, thickness: 0.5, height: 28.w), + ExtendedText( + SCAppLocalizations.of(context)!.adminInviteRechargeAgent, + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: 14.sp, color: Colors.black54), + ), + SizedBox(height: 5.w), + ], + ), + ), + ); + }, + ); + } + return Container(); + } + + static Widget buildCPBuildMessage( + data, + Function(BuildContext ct, String content) showMsgItemMenu, + Function(String id) rejectOpt, + Function(String id) acceptOpt, + BuildContext context, + ) { + String expand = data["expand"] ?? ""; + if (data["content"] != null) { + var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"])); + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + showMsgItemMenu(ct, ""); + }, + child: Container( + height: 490.w, + width: ScreenUtil().screenWidth * 0.8, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_dialog_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 58.w), + Container( + child: text( + bean.applyType == "APPLY" + ? SCAppLocalizations.of(context)!.cpRequest + : SCAppLocalizations.of(context)!.reconcileInvitation, + fontSize: 21.sp, + textColor: Color(0xffDB5872), + fontWeight: FontWeight.bold, + ), + ), + Transform.translate( + offset: Offset(0, -23), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + top: 22.w, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + head(url: bean.userAvatar ?? "", width: 80.w), + SizedBox(width: 12.w), + head( + url: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userAvatar ?? + "", + width: 80.w, + ), + ], + ), + ), + Image.asset( + "sc_images/person/sc_icon_send_cp_requst_dialog_head.png", + height: 100.w, + fit: BoxFit.fill, + ), + ], + ), + ), + Transform.translate( + offset: Offset(0, -23), + child: Container( + height: 25.w, + width: ScreenUtil().screenWidth * 0.6, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_username_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + Container( + alignment: Alignment.center, + width: 100.w, + height: 15.w, + child: + (bean.userNickname?.length ?? 0) > 8 + ? Marquee( + text: bean.userNickname ?? "", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + bean.userNickname ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + SizedBox(width: 20.w), + Container( + alignment: Alignment.center, + width: 100.w, + height: 15.w, + child: + (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname + ?.length ?? + 0) > + 8 + ? Marquee( + text: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname ?? + "", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname ?? + "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ), + ), + Container( + padding: EdgeInsets.only(bottom: 12.w), + margin: EdgeInsets.symmetric(horizontal: 18.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_dialog_content.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + Container( + margin: EdgeInsets.symmetric( + horizontal: 35.w, + ).copyWith(top: 40.w), + child: text( + bean.applyType == "APPLY" + ? SCAppLocalizations.of( + context, + )!.areYouSureYouWantToSpend5( + bean.userNickname ?? "", + ) + : SCAppLocalizations.of( + context, + )!.areYouSureYouWantToSpend6( + bean.userNickname ?? "", + ), + fontWeight: FontWeight.w500, + textColor: Color(0xffFF79A1), + maxLines: 7, + fontSize: 14.sp, + ), + ), + ], + ), + ), + SizedBox(height: 10.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.only(top: 7.w), + alignment: AlignmentDirectional.center, + width: 120.w, + height: 45.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_cancel_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: text( + SCAppLocalizations.of(context)!.reject, + fontSize: 17.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ), + ), + onTap: () { + rejectOpt(expand); + }, + ), + SizedBox(width: 25.w), + GestureDetector( + child: Container( + padding: EdgeInsets.only(top: 7.w), + alignment: AlignmentDirectional.center, + width: 120.w, + height: 45.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_ok_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: text( + SCAppLocalizations.of(context)!.accept, + fontSize: 17.sp, + fontWeight: FontWeight.bold, + textColor: Color(0xffDB5872), + ), + ), + onTap: () { + acceptOpt(expand); + }, + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + return Container(); + } + + static Widget buildCPLoveLetterMessage( + data, + Function(BuildContext ct, String content) showMsgItemMenu, + BuildContext context, + ) { + if (data["content"] != null) { + var bean = SCSystemInvitMessageRes.fromJson(jsonDecode(data["content"])); + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + showMsgItemMenu(ct, ""); + }, + child: Container( + height: 400.w, + width: ScreenUtil().screenWidth * 0.8, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/msg/sc_icon_cp_lover_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 35.w), + Image.asset( + SCGlobalConfig.lang == "ar" + ? "sc_images/msg/sc_icon_cp_lover_text_ar.png" + : "sc_images/msg/sc_icon_cp_lover_text_en.png", + width: 110.w, + fit: BoxFit.fill, + ), + SizedBox(height: 45.w), + Row( + children: [ + SizedBox(width: 55.w), + netImage( + url: bean.userAvatar ?? "", + width: 45.w, + height: 45.w, + shape: BoxShape.circle, + ), + Column( + children: [ + Row( + children: [ + Container( + alignment: Alignment.center, + width: 100.w, + height: 15.w, + child: + (bean.userNickname?.length ?? 0) > 8 + ? Marquee( + text: bean.userNickname ?? "", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xffDB5872), + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + bean.userNickname ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + color: Color(0xffDB5872), + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(vertical: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + (bean.account != bean.actualAccount) + ? "sc_images/general/sc_icon_special_id_bg.png" + : "sc_images/general/sc_icon_id_bg.png", + ), + fit: BoxFit.fitWidth, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + SizedBox(width: 38.w), + text( + bean.actualAccount ?? "", + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + SizedBox(width: 5.w), + Image.asset( + "sc_images/room/sc_icon_user_card_copy_id.png", + width: 12.w, + height: 12.w, + ), + SizedBox(width: 8.w), + ], + ), + ), + onTap: () { + Clipboard.setData( + ClipboardData(text: bean.actualAccount ?? ""), + ); + SCTts.show( + SCAppLocalizations.of(context)!.copiedToClipboard, + ); + }, + ), + ], + ), + ], + ), + SizedBox(height: 5.w), + Container( + height: 160.w, + margin: EdgeInsets.symmetric(horizontal: 53.w), + child: SingleChildScrollView( + child: ExtendedText( + bean.content ?? "", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xffDB5872), + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + return Container(); + } + + static Widget buildUserCoinsReceivedMessage( + data, + Function(BuildContext ct, String content) showMsgItemMenu, + BuildContext context, + ) { + if (data["content"] != null) { + return Builder( + builder: (ct) { + return GestureDetector( + onLongPress: () { + showMsgItemMenu(ct, ""); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(blurRadius: 2.w, color: Colors.black26)], + borderRadius: BorderRadius.only( + topLeft: Radius.circular(0), + topRight: Radius.circular(8.w), + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8), + ), + ), + padding: EdgeInsets.all(8.0.w), + child: Column( + children: [ + text( + SCAppLocalizations.of(context)!.coinsReceived, + fontSize: 16.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + ExtendedText( + data["content"], + // specialTextSpanBuilder: MySpecialTextSpanBuilder(), + style: TextStyle(fontSize: 14.sp, color: Colors.black54), + ) + ], + ), + ), + ); + }, + ); + } + return Container(); + } +} diff --git a/lib/shared/tools/sc_text_utils.dart b/lib/shared/tools/sc_text_utils.dart new file mode 100644 index 0000000..65ebb63 --- /dev/null +++ b/lib/shared/tools/sc_text_utils.dart @@ -0,0 +1,94 @@ +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +richText({required String txt, required String key, Color highlight = Colors.red, Color defaultColor = Colors.black}) { + assert(txt != null); + assert(key != null); + if (txt.contains(key)) { + // 采用正则表达式环视保留分割的文本 + // 目前只考虑命中一次的情况简单分割 + // List sps = txt.trim().split('$key'); //new RegExp('[?=$key]') + + // 使用最新的算法来计算 + List sps = splitStr(txt, key); + if (sps.length > 0) { + List textSpanList = []; + for (String sp in sps) { + print(sp); + textSpanList.add(TextSpan(text: sp, style: TextStyle(color: sp == key ? highlight : defaultColor))); + } + return Text.rich(TextSpan(children: textSpanList)); + } + } + return Text(txt, style: TextStyle(color: defaultColor),); +} + +List splitStr(String txt, String key) { + int keyLength = key.length; + int index = txt.indexOf(key); + // 因为要replace,所以使用备份的字符串 + String bakTxt = txt; + if (index > -1) { + List indexes = []; + indexes.add(index); + int count = 0; + while (index > -1) { + bakTxt = bakTxt.replaceFirst('$key', ''); + int localIndex = bakTxt.indexOf(key); + if (localIndex > -1) { + count += 1; + indexes.add(localIndex + keyLength * count); + index = localIndex + keyLength * count; + } else { + index = localIndex; + } + } + print('所有的索引:${indexes.toString()}'); + + List results = []; + + // 对上一步算出的所有索引进行迭代,导出分割后的数组 + int indexArrayIndex = 0; + while (indexArrayIndex < indexes.length) { + // 从左往后迭代。 + if (indexArrayIndex == 0) { + if (indexes[indexArrayIndex] == 0) { + results.add(txt.substring(0, keyLength)); + } else { + results.add(txt.substring(0, indexes[indexArrayIndex])); + results.add(txt.substring(indexes[indexArrayIndex], indexes[indexArrayIndex] + keyLength)); + } + } else { + results.add(txt.substring(indexes[indexArrayIndex], indexes[indexArrayIndex] + keyLength)); + } + + int endIndex = indexes[indexArrayIndex] + keyLength; + + // 如果还有索引,这一次只能算到下一个索引前 + if (indexArrayIndex < indexes.length - 1) { + if (endIndex < indexes[indexArrayIndex + 1]) { + results.add(txt.substring(endIndex, indexes[indexArrayIndex + 1])); + } else { + // 刚好是下一个自己,等下一次循环再说 + } + } else { + //只有一个索引的情况 + if (endIndex < txt.length) { + results.add(txt.substring(endIndex, txt.length)); + } else { + //结束了 + } + } + + indexArrayIndex += 1; + } + + print('迭代的结果:$results'); + return results; + } + + return []; +} + diff --git a/lib/shared/tools/sc_user_utils.dart b/lib/shared/tools/sc_user_utils.dart new file mode 100644 index 0000000..8f6246c --- /dev/null +++ b/lib/shared/tools/sc_user_utils.dart @@ -0,0 +1,100 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_message_utils.dart'; + +class SCAccountHelper { + static void optBlacklist(String userId,bool isBlacklist,BuildContext context,Function(bool succ) callback) { + SmartDialog.dismiss( + tag: + "showUserInfoOptMenu", + ); + SmartDialog.show( + tag: "showConfirmDialog", + alignment: + Alignment.center, + debounce: true, + animationType: + SmartAnimationType + .fade, + builder: (_) { + return MsgDialog( + title: + SCAppLocalizations.of( + context!, + )!.tips, + msg: + isBlacklist + ? SCAppLocalizations.of( + context, + )!.areYouSureToCancelBlacklist + : SCAppLocalizations.of( + context, + )!.areYouSureYouWantToBlockThisUser, + btnText: + SCAppLocalizations.of( + context, + )!.confirm, + onEnsure: () { + SCLoadingManager.show(); + if (isBlacklist) { + SCAccountRepository() + .deleteUserBlacklist( + userId, + ) + .then(( + result, + ) { + SCTts.show( + SCAppLocalizations.of( + context, + )!.successfullyRemovedFromTheBlacklist, + ); + SCLoadingManager.hide(); + isBlacklist = + !isBlacklist; + callback.call(isBlacklist); + }) + .catchError(( + e, + ) { + SCLoadingManager.hide(); + }); + } else { + SCAccountRepository() + .addUserBlacklist( + userId, + ) + .then(( + result, + ) { + SCTts.show( + SCAppLocalizations.of( + context, + )!.successfullyAddedToTheBlacklist, + ); + SCMessageUtils.clearAllMessages( + userId, + ); + SCLoadingManager.hide(); + isBlacklist = + !isBlacklist; + callback.call(isBlacklist); + }) + .catchError(( + e, + ) { + SCLoadingManager.hide(); + }); + } + }, + ); + }, + ); + + } +} diff --git a/lib/shared/tools/sc_version_utils.dart b/lib/shared/tools/sc_version_utils.dart new file mode 100644 index 0000000..7da88f1 --- /dev/null +++ b/lib/shared/tools/sc_version_utils.dart @@ -0,0 +1,14 @@ +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; + +class SCVersionUtils { + static Future checkReview() async { + try { + var versionInfo = await SCConfigRepositoryImp().versionManageLatestReview(); + if (versionInfo.review != null) { + SCGlobalConfig.isReview = + "${versionInfo.review?.version}" == SCGlobalConfig.version; + } + } catch (e) {} + } +} diff --git a/lib/ui_kit/components/anim/sc_scaled_button.dart b/lib/ui_kit/components/anim/sc_scaled_button.dart new file mode 100644 index 0000000..afa5a08 --- /dev/null +++ b/lib/ui_kit/components/anim/sc_scaled_button.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +/// 按钮按下缩小,离开还原 +class SCScaledButton extends StatefulWidget { + + final Widget child; + final VoidCallback? onClick; + + const SCScaledButton({Key? key, required this.child, this.onClick}) : super(key: key); + + + @override + _ScaledButtonState createState() => _ScaledButtonState(); +} + +class _ScaledButtonState extends State with TickerProviderStateMixin{ + + late AnimationController controller; + late Animation tween; + @override + void initState() { + super.initState(); + controller = AnimationController(duration: const Duration(milliseconds: 100), vsync: this); + tween = Tween(begin:1.0, end:1.5).animate(controller); + } + + @override + void dispose() { + super.dispose(); + controller?.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: ScaleTransition( + scale: tween, + alignment: Alignment.center, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.onClick, + + onTapDown: (details){ + controller.forward(); + }, + onTapUp: (details){ + controller.reset(); + }, + onTapCancel: (){ + controller.reset(); + }, + child: widget.child, + ), + ), + ); + } +} diff --git a/lib/ui_kit/components/appbar/socialchat_appbar.dart b/lib/ui_kit/components/appbar/socialchat_appbar.dart new file mode 100644 index 0000000..2cc8ecb --- /dev/null +++ b/lib/ui_kit/components/appbar/socialchat_appbar.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; + +class SocialChatAppBar extends StatefulWidget implements PreferredSizeWidget { + final Widget child; + final double height; + final backgroundColor; + + //LinearGradient gradient; + + @override + State createState() { + return SocialChatAppBarState(); + } + + SocialChatAppBar({ + required this.child, + this.height = kToolbarHeight, + this.backgroundColor = Colors.white, + //this.gradient = const LinearGradient(colors: [Colors.white, Colors.white] , begin: Alignment.topLeft, end: Alignment.bottomRight) + }); + + @override + Size get preferredSize => Size.fromHeight(height); +} + +class SocialChatAppBarState extends State { + @override + Widget build(BuildContext context) { + // SystemUiOverlayStyle style = SystemUiOverlayStyle( + // statusBarColor: Colors.white, + // ///这是设置状态栏的图标和字体的颜色 + // ///Brightness.light 一般都是显示为白色 + // ///Brightness.dark 一般都是显示为黑色 + // statusBarIconBrightness: Brightness.dark + // ); + // SystemChrome.setSystemUIOverlayStyle(style); + return Container( + color: widget.backgroundColor, + //decoration: BoxDecoration(gradient: widget.gradient), + child: SafeArea( + top: true, + child: Container(height: widget.height, child: widget.child), + ), + ); + } +} + +class SocialChatStandardAppBar extends StatelessWidget + implements PreferredSizeWidget { + final String title; + final Widget? titleWidget; + final Widget? leading; + final List actions; + final Color backgroundColor; + final Color? backButtonColor; + final Gradient gradient; + final Function? onTag; + final bool showBackButton; + + SocialChatStandardAppBar({ + this.title = "", + required this.actions, + this.backgroundColor = Colors.white, + this.backButtonColor, + this.titleWidget, + this.showBackButton = true, + this.leading, + this.onTag, + this.gradient = const LinearGradient( + colors: [Colors.transparent, Colors.transparent], + ), + }); + + @override + Widget build(BuildContext context) { + var leadingWidget = Container( + width: titleWidget == null ? 38 : null, + alignment: AlignmentDirectional.centerStart, + child: + !showBackButton + ? Container() + : leading ?? + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (onTag != null) { + onTag!(); + } else { + Navigator.pop(context); + } + }, + child: Container( + width: width(50), + height: height(30), + alignment: AlignmentDirectional.centerStart, + padding: EdgeInsetsDirectional.only(start: width(15)), + child: Icon( + SCGlobalConfig.lang == "ar" + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + size: 28.w, + color: backButtonColor ?? Color(0xffffffff), + ), + ), + ), + ); + var titleBody = + titleWidget ?? + Align( + alignment: Alignment.center, + child: Text( + title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: ScreenUtil().setSp(18), + color: backButtonColor ?? Color(0xffffffff), + fontWeight: FontWeight.w500, + ), + ), + ); + var body; + if (titleWidget != null) { + body = Container( + margin: EdgeInsets.only(top: ScreenUtil().statusBarHeight), + child: Row( + children: [ + leadingWidget, + Expanded(child: titleBody), + if (actions != null) + Row(mainAxisAlignment: MainAxisAlignment.end, children: actions), + ], + ), + ); + } + return Container( + decoration: BoxDecoration( + gradient: + gradient ?? + LinearGradient(colors: [Color(0xffffffff), Color(0xffffffff)]), + ), + child: + body ?? + Container( + margin: EdgeInsets.only(top: ScreenUtil().statusBarHeight), + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + leadingWidget, + titleBody, + if (actions != null) + Align( + alignment: AlignmentDirectional.centerEnd, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: actions, + ), + ), + ], + ), + ), + ); + } + + @override + // TODO: implement preferredSize + Size get preferredSize => Size.fromHeight(kToolbarHeight); +} diff --git a/lib/ui_kit/components/custom_cached_image.dart b/lib/ui_kit/components/custom_cached_image.dart new file mode 100644 index 0000000..d68308f --- /dev/null +++ b/lib/ui_kit/components/custom_cached_image.dart @@ -0,0 +1,53 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CustomCachedImage extends StatelessWidget { + final String imageUrl; + final double? width; + final double? height; + final BoxFit fit; + final double borderRadius; + final Widget? placeholder; + final Widget? errorWidget; + + const CustomCachedImage({ + super.key, + required this.imageUrl, + this.width, + this.height, + this.fit = BoxFit.cover, + this.borderRadius = 0, + this.placeholder, + this.errorWidget, + }); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), + child: CachedNetworkImage( + imageUrl: imageUrl, + width: width, + height: height, + fit: fit, + placeholder: (context, url) => + placeholder ?? _defaultPlaceholder(), + errorWidget: (context, url, error) => + errorWidget ?? _defaultErrorWidget(), + ), + ); + } + + Widget _defaultPlaceholder() { + return Center( + child: CupertinoActivityIndicator(), + ); + } + + Widget _defaultErrorWidget() { + return Center( + child: Image.asset("sc_images/general/sc_icon_loading.png"), + ); + } +} \ No newline at end of file diff --git a/lib/ui_kit/components/dialog/dialog.dart b/lib/ui_kit/components/dialog/dialog.dart new file mode 100644 index 0000000..6b510ea --- /dev/null +++ b/lib/ui_kit/components/dialog/dialog.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:loading_indicator_view_plus/loading_indicator_view_plus.dart'; + +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; + +typedef OnFutureFunc = Future Function(); +typedef OnSuccess = Function(dynamic v); + +class STLoadingDialog extends Dialog { + String text; + Widget? centerContent; + final GestureTapCallback? verifyCallback; + final double? height; + final double? width; + + STLoadingDialog({ + Key? key, + required this.text, + this.height, + this.width, + this.centerContent, + this.verifyCallback, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + //创建透明层 + type: MaterialType.transparency, //透明类型 + child: new Center( + //保证控件居中效果 + child: new Container( + constraints: BoxConstraints(maxWidth: ScreenUtil().screenWidth * 0.4), + decoration: ShapeDecoration( + color: Color(0xffffffff), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + ), + child: AspectRatio( + aspectRatio: 1, + child: new Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + BallClipRotateMultipleIndicator( + color: Color(0xffFF50BC), + ), + Text( + "${text ?? "loading..."}", + style: TextStyle(fontSize: 14.sp, color: Color(0xff888888)), + ) + ], + ), + ), + ), + ), + ); + } +} + +class FutureLoadingDialog extends StatefulWidget { + String text; + Widget? centerContent; + final GestureTapCallback? verifyCallback; + final double? height; + final double? width; + final OnFutureFunc future; + final OnSuccess? onSuccess; + final OnSuccess? onError; + + FutureLoadingDialog({ + Key? key, + this.text = "", + this.height, + this.width, + this.centerContent, + this.verifyCallback, + required this.future, + this.onSuccess, + this.onError, + }) : assert(future != null), + super(key: key); + + @override + _FutureLoadingDialogState createState() => _FutureLoadingDialogState(); +} + +class _FutureLoadingDialogState extends State { + @override + void initState() { + // TODO: implement initState + super.initState(); + widget.future.call().then((value) { + SCNavigatorUtils.goBack(context); + widget.onSuccess?.call(value); + }).catchError((e) { + SCNavigatorUtils.goBack(context); + widget.onError?.call(e); + }); + } + + @override + Widget build(BuildContext context) { + return Material( + //创建透明层 + type: MaterialType.transparency, //透明类型 + child: new Center( + //保证控件居中效果 + child: new Container( + constraints: BoxConstraints(maxWidth: ScreenUtil().screenWidth * 0.4), + decoration: ShapeDecoration( + color: Color(0xffffffff), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + ), + child: AspectRatio( + aspectRatio: 1, + child: new Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + BallClipRotateMultipleIndicator( + color: Color(0xffFF50BC), + ), + Text( + "${widget.text ?? "loading..."}", + style: TextStyle(fontSize: 14.sp, color: Color(0xff888888)), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/components/dialog/dialog_base.dart b/lib/ui_kit/components/dialog/dialog_base.dart new file mode 100644 index 0000000..e751a4c --- /dev/null +++ b/lib/ui_kit/components/dialog/dialog_base.dart @@ -0,0 +1,603 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; + +// 基础对话框 +class MsgDialog extends Dialog { + final String msg; + String title = ""; + String? btnText; + String? cancelText; + late BuildContext context; + final VoidCallback onEnsure; + final VoidCallback? onCancel; + bool isDark = false; + bool isBtnBgFlip = false; + bool leftConfirm = false; + + MsgDialog({ + Key? key, + this.title = "", + required this.msg, + required this.onEnsure, + this.btnText, + this.cancelText, + this.isDark = false, + this.isBtnBgFlip = false, + this.leftConfirm = false, + this.onCancel, + }); + + @override + Widget build(BuildContext context) { + this.context = context; + + return Material( + //创建透明层 + type: MaterialType.transparency, //透明类型 + child: Stack( + children: [ + GestureDetector( + onTap: () { + //空白区域点击对话框消失 + SmartDialog.dismiss(tag: "showConfirmDialog"); + }, + ), + _buildContentView(context), + ], + ), + ); + } + + Widget _buildContentView(BuildContext context) { + return Center( + //保证控件居中效果 + child: Container( + constraints: BoxConstraints(maxHeight: 420.w), + margin: EdgeInsets.symmetric(horizontal: 18.w), + decoration: ShapeDecoration( + color: isDark ? Color(0xff161616) : Color(0xff09372E), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(width(10))), + ), + ), + child: _buildDialog(context), + ), + ); + } + + Widget _buildDialog(BuildContext context) { + return Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 15.w), + title.isNotEmpty + ? Container( + alignment: Alignment.center, + child: text( + title, + fontSize: 16.sp, + lineHeight: 1.1, + letterSpacing: 0.1, + textColor: isDark ? Colors.black : Colors.white, + fontWeight: FontWeight.bold, + ), + ) + : Container(), + SizedBox(height: 15.w), + Container( + margin: EdgeInsets.symmetric(horizontal: 8.w), + alignment: Alignment.center, + child: text( + msg, + maxLines: 20, + fontSize: 14.sp, + lineHeight: 1.1, + letterSpacing: 0.1, + textColor: isDark ? Colors.black : Colors.white, + textAlign: TextAlign.center, + ), + ), + SizedBox(height: 10.w), + Container( + margin: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), + height: height(38), + alignment: Alignment.center, + child: Row( + textDirection: + leftConfirm ? TextDirection.rtl : TextDirection.ltr, + children: [ + Expanded( + child: GestureDetector( + child: Container( + decoration: + isBtnBgFlip + ? BoxDecoration( + borderRadius: BorderRadius.circular(33.w), + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + isDark + ? Color(0xff260054) + : Color(0xff18F2B1), + isDark + ? Color(0xff6105B7) + : Color(0xff18F2B1), + ], + ), + ) + : BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: + isDark + ? Colors.transparent + : Colors.transparent, + border: Border.all( + color: Colors.white, + width: 1.w, + ), + ), + alignment: Alignment.center, + child: text( + cancelText ?? SCAppLocalizations.of(context)!.cancel, + fontSize: 14.sp, + textColor: + isDark + ? Colors.grey + : (isBtnBgFlip ? Colors.black : Colors.white), + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + onCancel?.call(); + SmartDialog.dismiss(tag: "showConfirmDialog"); + }, + ), + ), + SizedBox(width: 15.w), + Expanded( + child: GestureDetector( + child: Container( + decoration: + isBtnBgFlip + ? BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: + isDark + ? Colors.transparent + : Colors.white, + border: Border.all( + color: Color(0xffE6E6E6), + width: 1.w, + ), + ) + : BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + gradient: LinearGradient( + begin: Alignment.bottomLeft, + end: Alignment.topRight, + colors: [ + isDark + ? Color(0xff260054) + : Color(0xff18F2B1), + isDark + ? Color(0xff6105B7) + : Color(0xff18F2B1), + ], + ), + ), + alignment: Alignment.center, + child: text( + btnText ?? SCAppLocalizations.of(context)!.yes, + fontSize: 14.sp, + textColor: isBtnBgFlip ? Colors.grey : Colors.black, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + onEnsureClick(); + }, + ), + ), + ], + ), + ), + SizedBox(height: 15.w), + ], + ), + PositionedDirectional( + end: 10.w, + top: 10.w, + child: SCDebounceWidget( + child: Image.asset( + "sc_images/general/sc_icon_msg_tips_close.png", + height: 20.w, + ), + onTap: (){ + SmartDialog.dismiss(tag: "showConfirmDialog"); + }, + ), + ), + ], + ); + } + + /*确定按钮点击事件*/ + onEnsureClick() { + onEnsure(); + SmartDialog.dismiss(tag: "showConfirmDialog"); + } +} + +// ignore: must_be_immutable +class OpenRoomLoadingDialog extends Dialog { + String text; + Widget? centerContent; + final GestureTapCallback? verifyCallback; + final double? height; + final double? width; + + OpenRoomLoadingDialog({ + Key? key, + this.text = "", + this.height, + this.width, + this.centerContent, + this.verifyCallback, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + //创建透明层 + type: MaterialType.transparency, //透明类型 + child: Center( + //保证控件居中效果 + child: Container( + constraints: BoxConstraints(maxWidth: ScreenUtil().screenWidth * 0.4), + decoration: ShapeDecoration( + color: Colors.black.withOpacity(0.78), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + ), + padding: EdgeInsets.all(15.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset("sc_images/raw/loading.webp", width: 80.w), + SizedBox(height: 12.w), + Text( + "正在进入聊天室......", + style: TextStyle(fontSize: 12.sp, color: Colors.white), + ), + ], + ), + ), + ), + ); + } +} + +class DialogLine extends StatefulWidget { + @override + _DialogLineState createState() => _DialogLineState(); +} + +class _DialogLineState extends State with TickerProviderStateMixin { + late AnimationController animationController1; + late AnimationController animationController2; + late AnimationController animationController3; + + @override + void initState() { + // TODO: implement initState + animationController1 = AnimationController( + vsync: this, + duration: Duration(milliseconds: 350), + ); + animationController3 = AnimationController( + vsync: this, + duration: Duration(milliseconds: 350), + ); + animationController2 = AnimationController( + vsync: this, + duration: Duration(milliseconds: 350), + ); + animationController1.forward(); + animationController1.addStatusListener((status) { + if (status == AnimationStatus.completed) { + animationController1.reverse(); + animationController2.forward(); + } + }); + animationController2.addStatusListener((status) { + if (status == AnimationStatus.completed) { + animationController2.reverse(); + animationController3.forward(); + } + }); + animationController3.addStatusListener((status) { + if (status == AnimationStatus.completed) { + animationController3.reverse(); + animationController1.forward(); + } + }); + super.initState(); + } + + @override + void dispose() { + // TODO: implement dispose + animationController1.dispose(); + animationController2.dispose(); + animationController3.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + ScaleTransition( + scale: Tween(begin: 0.5, end: 1.0).animate(animationController1), + child: _container(), + ), + SizedBox(width: 6), + ScaleTransition( + scale: Tween(begin: 0.5, end: 1.0).animate(animationController2), + child: _container(), + ), + SizedBox(width: 6), + ScaleTransition( + scale: Tween(begin: 0.5, end: 1.0).animate(animationController3), + child: _container(), + ), + ], + ); + } + + Container _container() { + return Container( + height: 28, + width: 28, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.deepPurpleAccent, + Colors.deepPurpleAccent.withOpacity(0.5), + ], + ), + ), + ); + } +} + +class KickedOutOfRoomDialog extends StatefulWidget { + final String roomId; + final String userId; + + const KickedOutOfRoomDialog({ + super.key, + required this.roomId, + required this.userId, + }); + + @override + _KickedOutOfRoomDialogState createState() => _KickedOutOfRoomDialogState(); +} + +class _KickedOutOfRoomDialogState extends State { + int selectType = -1; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: 260.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 22.w), + text( + SCAppLocalizations.of(context)!.kickedOutOfRoom, + fontSize: 16.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + SizedBox(height: 14.w), + GestureDetector( + child: _buildItem("5 minutes", selectType == 0), + onTap: () { + setState(() { + selectType = 0; + }); + }, + ), + SizedBox(height: 14.w), + GestureDetector( + child: _buildItem("24 hours", selectType == 1), + onTap: () { + setState(() { + selectType = 1; + }); + }, + ), + SizedBox(height: 14.w), + GestureDetector( + child: _buildItem("1 month", selectType == 2), + onTap: () { + setState(() { + selectType = 2; + }); + }, + ), + SizedBox(height: 14.w), + GestureDetector( + child: _buildItem( + SCAppLocalizations.of(context)!.permanent, + selectType == 3, + ), + onTap: () { + setState(() { + selectType = 3; + }); + }, + ), + SizedBox(height: 13.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: 20.w), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 5.w, + ), + child: text( + SCAppLocalizations.of(context)!.cancel, + fontSize: 14.sp, + textColor: Colors.grey, + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showKickedOutOfRoomDialog"); + }, + ), + Spacer(), + SCDebounceWidget( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 5.w, + ), + child: text( + SCAppLocalizations.of(context)!.confirm, + fontSize: 14.sp, + textColor: Colors.black, + ), + ), + onTap: () async { + if (selectType > -1) { + String roomId = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + ""; + if (selectType == 0) { + await SCChatRoomRepository().joinBlacklist( + roomId, + widget.userId, + 5, + ); + SCTts.show( + SCAppLocalizations.of(context)!.operationSuccessful, + ); + SmartDialog.dismiss(tag: "showKickedOutOfRoomDialog"); + } else if (selectType == 1) { + await SCChatRoomRepository().joinBlacklist( + roomId, + widget.userId, + 1440, + ); + SCTts.show( + SCAppLocalizations.of(context)!.operationSuccessful, + ); + SmartDialog.dismiss(tag: "showKickedOutOfRoomDialog"); + } else if (selectType == 2) { + await SCChatRoomRepository().joinBlacklist( + roomId, + widget.userId, + 43200, + ); + SCTts.show( + SCAppLocalizations.of(context)!.operationSuccessful, + ); + SmartDialog.dismiss(tag: "showKickedOutOfRoomDialog"); + } else if (selectType == 3) { + await SCChatRoomRepository().joinBlacklist( + roomId, + widget.userId, + 5184000, + ); + SCTts.show( + SCAppLocalizations.of(context)!.operationSuccessful, + ); + SmartDialog.dismiss(tag: "showKickedOutOfRoomDialog"); + } + } + }, + ), + SizedBox(width: 20.w), + ], + ), + SizedBox(height: 20.w), + ], + ), + ); + } + + _buildItem(String time, bool isSelect) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 6.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.transparent, + border: Border.all(color: SocialChatTheme.textSecondary, width: 1.w), + ), + width: 155.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 3.w), + Image.asset( + isSelect + ? "sc_images/login/sc_icon_login_ser_select.png" + : "sc_images/login/sc_icon_login_ser_select_un.png", + width: 16.w, + height: 16.w, + ), + SizedBox(width: 10.w), + text(time, fontSize: 14.sp, textColor: Colors.black), + ], + ), + ); + } +} diff --git a/lib/ui_kit/components/sc_compontent.dart b/lib/ui_kit/components/sc_compontent.dart new file mode 100644 index 0000000..a8a38dd --- /dev/null +++ b/lib/ui_kit/components/sc_compontent.dart @@ -0,0 +1,624 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:tancent_vap/utils/constant.dart'; +import 'package:tancent_vap/widgets/vap_view.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/ui_kit/widgets/headdress/headdress_widget.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; + +import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; + +Widget head({ + required String url, + required double width, + double? height, + Border? border, + String? headdress, + BoxShape shape = BoxShape.circle, + BoxFit fit = BoxFit.cover, + BorderRadius? borderRadius, + bool showDefault = true, + bool isRoom = false, +}) { + return RepaintBoundary( + child: Stack( + alignment: Alignment.center, + children: [ + Container( + padding: EdgeInsets.all(width * 0.12), + width: width, + height: height ?? width, + child: ExtendedImage.network( + url, + width: width, + height: height ?? width, + fit: fit, + cache: true, + shape: shape, + border: border, + clearMemoryCacheWhenDispose: false, + clearMemoryCacheIfFailed: true, + borderRadius: borderRadius, + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == LoadState.completed) { + return ExtendedRawImage( + image: state.extendedImageInfo?.image, + fit: fit, + ); + } else if (state.extendedImageLoadState == LoadState.loading) { + if (showDefault) { + return ExtendedImage.asset( + shape: shape, + "sc_images/general/sc_icon_loading.png", + fit: BoxFit.cover, + ); + } + return Container(); + } else { + return ExtendedImage.asset( + shape: shape, + "sc_images/general/sc_icon_avar_defalt.png", + fit: BoxFit.cover, + ); + } + }, + ), + ), + headdress != null && SCPathUtils.fileTypeIsPic2(headdress) + ? netImage(url: headdress, width: width, height: width) + : Container(), + headdress != null && + SCPathUtils.getFileExtension(headdress).toLowerCase() == + ".svga" && + SCGlobalConfig.sdkInt > SCGlobalConfig.maxSdkNoAnim + ? IgnorePointer( + child: SVGAHeadwearWidget( + width: width, + height: width, + resource: headdress, + ), + ) + : Container(), + headdress != null && + SCPathUtils.getFileExtension(headdress).toLowerCase() == ".mp4" && + SCGlobalConfig.sdkInt > SCGlobalConfig.maxSdkNoAnim + ? SizedBox( + width: width, + height: width, + child: IgnorePointer( + child: VapView( + scaleType: ScaleType.centerCrop, + repeat: 90000000, + onViewCreated: (controller) { + FileCacheManager.getInstance().getFile(url: headdress).then( + (file) { + controller.playFile(file.path); + }, + ); + }, + ), + ), + ) + : Container(), + ], + ), + ); +} + +Widget netImage({ + required String url, + String? defaultImg, + bool noDefaultImg = false, + double? width, + double? height, + BorderRadius? borderRadius, + BoxFit? fit, + BoxShape? shape, + Border? border, +}) { + // print('${SCGlobalConfig.imgHost}$image?imageslim'); + return ExtendedImage.network( + url, + width: width, + height: height ?? width, + fit: fit ?? BoxFit.cover, + cache: true, + shape: shape ?? BoxShape.rectangle, + borderRadius: borderRadius, + clearMemoryCacheWhenDispose: false, + clearMemoryCacheIfFailed: true, + border: border, + gaplessPlayback: true, + loadStateChanged: (ExtendedImageState state) { + if (state.extendedImageLoadState == LoadState.completed) { + return ExtendedRawImage( + image: state.extendedImageInfo?.image, + fit: fit ?? BoxFit.cover, + ); + } else if (state.extendedImageLoadState == LoadState.failed) { + return noDefaultImg + ? Container() + : Image.asset( + defaultImg ?? "sc_images/general/sc_icon_loading.png", + fit: BoxFit.cover, + ); + } else { + return noDefaultImg + ? Container() + : Image.asset( + defaultImg ?? "sc_images/general/sc_icon_loading.png", + fit: BoxFit.cover, + ); + } + }, + ); +} + +/// 空页面 +///默认空页面 +mainEmpty({ + String msg = "No data", + Widget? image, + bool center = true, + Color textColor = const Color(0xffffffff), +}) { + List list = []; + list.add(SizedBox(height: height(center ? 40.w : 0.w))); + list.add( + image ?? + Image.asset( + 'sc_images/general/sc_icon_loading.png', + width: 121.w, + fit: BoxFit.fitWidth, + ), + ); + list.add(SizedBox(height: height(10.w))); + list.add( + Text( + msg, + style: TextStyle( + fontSize: sp(12), + color: textColor, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: + center ? MainAxisAlignment.center : MainAxisAlignment.start, + children: list, + ), + ], + ); +} + +msgRoleTag(String role, {double? width, double? height}) { + String image = ""; + if (SCRoomRolesType.HOMEOWNER.name == role) { + image = 'fz'; + } else if (SCRoomRolesType.ADMIN.name == role) { + image = "gly"; + } else if (SCRoomRolesType.MEMBER.name == role) { + image = "hy"; + } else if (SCRoomRolesType.TOURIST.name == role) { + image = "guest"; + } + if (image.isEmpty) return SizedBox.shrink(); + return Container( + alignment: Alignment.center, + width: width ?? 22.w, + height: height ?? 22.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_room_$image.png"), + ), + ), + ); +} + +///性别 0女 1男 +xb(num? sex, {double? height, Color? color = Colors.white}) { + // print('sex:$sex'); + // if (sex == null) return Container(); + String image = "sc_images/login/sc_icon_sex_man.png"; + if (sex == 0) { + image = "sc_images/login/sc_icon_sex_woman.png"; + } + return Image.asset( + image, + height: height ?? 14.w, + fit: BoxFit.fill, + color: color, + ); +} + +xb2(num? sex, {double? width, double? height}) { + // print('sex:$sex'); + if (sex == null) return Container(); + String image = "sc_images/login/sc_icon_sex_man_bg.png"; + if (sex == 0) { + image = "sc_images/login/sc_icon_sex_woman_bg.png"; + } + return Image.asset( + image, + width: width ?? 28.w, + height: height ?? 14.w, + fit: BoxFit.fill, + ); +} + +///搜索框 +searchWidget({ + Function? onChange, + Function? onTap, + String? hint, + double? hintSize, + Color? borderColor, + EdgeInsetsGeometry? padding, + TextEditingController? controller, + Color? textColor, + Color? serachIconColor, + FocusNode? focusNode, +}) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (onTap != null) { + onTap(); + } + }, + child: Container( + margin: + padding ?? + EdgeInsetsDirectional.only(start: width(15), end: width(0)), + alignment: AlignmentDirectional.centerStart, + height: width(36), + //width: width(345), + decoration: BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.all(Radius.circular(height(20))), + border: Border.all(color: borderColor ?? Colors.black12, width: 1.w), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: width(10)), + Image.asset( + "sc_images/index/sc_icon_serach2.png", + width: 20.w, + color: serachIconColor ?? Color(0xff18F2B1), + height: 20.w + ), + SizedBox(width: width(6)), + _buildPhoneInput( + onChange, + onTap == null, + hint ?? 'Search by ID', + controller, + textColor, + hintSize, + focusNode, + ), + ], + ), + ), + ); +} + +_buildPhoneInput( + Function? onChange, + bool enabled, + String tint, + TextEditingController? controller, + Color? textColor, + double? hintSize, + FocusNode? focusNode, +) { + return Expanded( + child: TextField( + controller: controller, + onChanged: (text) { + if (onChange != null) { + onChange(text); + } + }, + focusNode: focusNode, + autofocus: false, + enabled: enabled, + maxLength: 11, + cursorColor: SocialChatTheme.primaryColor, + decoration: InputDecoration( + hintText: tint, + hintStyle: TextStyle(color: SocialChatTheme.textSecondary, fontSize: hintSize ?? sp(14)), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + contentPadding: EdgeInsets.only(bottom: 0), + ), + style: TextStyle( + textBaseline: TextBaseline.alphabetic, + fontSize: hintSize ?? sp(14), + color: textColor ?? Colors.black, + ), + ), + ); +} + +///vip标识 +getVIPBadge(String? type, {double? width, double? height}) { + return Container(); +} + +///用户等级 +getUserLevel(num level, {double? width, double? height, double? fontSize}) { + String icon = ""; + if (level > 0 && level < 11) { + icon = "sc_images/level/sc_icon_user_level_1_10.png"; + } else if (level > 10 && level < 21) { + icon = "sc_images/level/sc_icon_user_level_10_20.png"; + } else if (level > 20 && level < 31) { + icon = "sc_images/level/sc_icon_user_level_20_30.png"; + } else if (level > 30 && level < 41) { + icon = "sc_images/level/sc_icon_user_level_30_40.png"; + } else if (level > 40 && level < 51) { + icon = "sc_images/level/sc_icon_user_level_40_50.png"; + } + return icon.isNotEmpty + ? Directionality( + textDirection: TextDirection.ltr, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + Image.asset(icon, width: width, height: height, fit: BoxFit.fill), + PositionedDirectional( + end: level > 9 ? 10.w : 15.w, + child: text( + "$level", + fontSize: fontSize ?? 12.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ) + : Container(); +} + +///用户魅力等级 +getWealthLevel(num level, {double? width, double? height, double? fontSize}) { + String icon = ""; + if (level > 0 && level < 11) { + icon = "sc_images/level/sc_icon_wealth_level_1_10.png"; + } else if (level > 10 && level < 21) { + icon = "sc_images/level/sc_icon_wealth_level_10_20.png"; + } else if (level > 20 && level < 31) { + icon = "sc_images/level/sc_icon_wealth_level_20_30.png"; + } else if (level > 30 && level < 41) { + icon = "sc_images/level/sc_icon_wealth_level_30_40.png"; + } else if (level > 40 && level < 51) { + icon = "sc_images/level/sc_icon_wealth_level_40_50.png"; + } + return icon.isNotEmpty + ? Directionality( + textDirection: TextDirection.ltr, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + Image.asset(icon, width: width, height: height, fit: BoxFit.fill), + PositionedDirectional( + end: level > 9 ? 10.w : 15.w, + child: text( + "$level", + fontSize: fontSize ?? 12.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ) + : Container(); +} + +String getGiftObtCoinsBg(num obtCoins) { + String icon = ""; + if (obtCoins > -1 && obtCoins < 5000) { + icon = "sc_images/room/sc_icon_luck_gift_obt_coins_bg_0.png"; + } else if (obtCoins > 4999 && obtCoins < 50000) { + icon = "sc_images/room/sc_icon_luck_gift_obt_coins_bg_1.png"; + } else if (obtCoins > 49999 && obtCoins < 100000) { + icon = "sc_images/room/sc_icon_luck_gift_obt_coins_bg_2.png"; + } else if (obtCoins > 99999 && obtCoins < 1000000) { + icon = "sc_images/room/sc_icon_luck_gift_obt_coins_bg_3.png"; + } else if (obtCoins > 999999 && obtCoins < 10000000) { + icon = "sc_images/room/sc_icon_luck_gift_obt_coins_bg_4.png"; + } else if (obtCoins > 9999999) { + icon = "sc_images/room/sc_icon_luck_gift_obt_coins_bg_5.png"; + } + return icon; +} + +///数字图片 +buildNum(String num, {double? size = 18}) { + if (num.isNotEmpty) { + List eList = []; + int index = 0; + num.split("").forEach((item) { + if (item == "k") { + eList.add( + Container( + margin: EdgeInsetsDirectional.only( + start: (index * (size ?? 18) * 0.9).w, + ), + child: Image.asset( + "sc_images/room/sc_icon_k.png", + width: size, + height: size, + ), + ), + ); + } else if (item == "m") { + eList.add( + Container( + margin: EdgeInsetsDirectional.only( + start: (index * (size ?? 18) * 0.9).w, + ), + child: Image.asset( + "sc_images/room/sc_icon_m.png", + width: size, + height: size, + ), + ), + ); + } else { + eList.add( + Container( + margin: EdgeInsetsDirectional.only( + start: (index * (size ?? 18) * 0.8).w, + ), + child: Image.asset( + "sc_images/room/sc_icon_number_$item.png", + width: size, + height: size, + ), + ), + ); + } + index = index + 1; + }); + return Directionality( + textDirection: TextDirection.ltr, + child: Stack(children: eList), + ); + } else { + return Container(); + } +} + +///游戏飘屏数字图片 +buildNumForGame(String num, {double? size = 18}) { + if (num.isNotEmpty) { + List eList = []; + int index = 0; + num.split("").forEach((item) { + if (item == "k") { + eList.add( + Container( + margin: EdgeInsetsDirectional.only( + start: (index * (size ?? 18) * 0.65).w, + ), + child: Image.asset( + "sc_images/general/sc_icon_game_numk.png", + width: size, + height: size, + ), + ), + ); + } else if (item == "m") { + eList.add( + Container( + margin: EdgeInsetsDirectional.only( + start: (index * (size ?? 18) * 0.65).w, + ), + child: Image.asset( + "sc_images/general/sc_icon_game_numm.png", + width: size, + height: size, + ), + ), + ); + } else { + eList.add( + Container( + margin: EdgeInsetsDirectional.only( + start: (index * (size ?? 18) * 0.6).w, + ), + child: Image.asset( + "sc_images/general/sc_icon_game_num$item.png", + width: size, + height: size, + ), + ), + ); + } + index = index + 1; + }); + return Directionality( + textDirection: TextDirection.ltr, + child: Stack(children: eList), + ); + } else { + return Container(); + } +} + +///房间幸运数字 +buildNumForRoomLuckNum(String num, {double? size = 18}) { + if (num.isNotEmpty) { + List eList = []; + int index = 0; + num.split("").forEach((item) { + eList.add( + Container( + margin: EdgeInsetsDirectional.only( + start: (index * (size ?? 18) * 0.7).w, + ), + child: Image.asset( + "sc_images/room/sc_icon_lucknumber_$item.png", + width: size, + height: size, + ), + ), + ); + index = index + 1; + }); + return Directionality( + textDirection: TextDirection.ltr, + child: Stack(children: eList), + ); + } else { + return Container(); + } +} + +buildFamilyProgressLinearGradient(int level) { + if (level == 0) { + return LinearGradient(colors: [Color(0xff2E4957), Color(0xffC9E7E8)]); + } else if (level == 1) { + return LinearGradient(colors: [Color(0xff5A3F31), Color(0xffF8CD85)]); + } else if (level == 2) { + return LinearGradient(colors: [Color(0xff0D5ACE), Color(0xffCBD9E8)]); + } else if (level == 3) { + return LinearGradient(colors: [Color(0xffC57E1C), Color(0xffCBD9E8)]); + } else if (level == 10) { + return LinearGradient(colors: [Color(0xff3C7FBE), Color(0xffADE1F1)]); + } else if (level == 11) { + return LinearGradient(colors: [Color(0xff300A67), Color(0xffA53EFF)]); + } + return LinearGradient(colors: [Color(0xff2E4957), Color(0xffC9E7E8)]); +} + +buildVipNameColor(String type) { + return null; +} diff --git a/lib/ui_kit/components/sc_debounce_widget.dart b/lib/ui_kit/components/sc_debounce_widget.dart new file mode 100644 index 0000000..7e2aa32 --- /dev/null +++ b/lib/ui_kit/components/sc_debounce_widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/cupertino.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; + +///防抖封装 +class SCDebounceWidget extends StatefulWidget { + final Widget child; + final VoidCallback onTap; + final Duration debounceTime; + final String? tips; + + const SCDebounceWidget({ + Key? key, + required this.child, + required this.onTap, + this.tips, + this.debounceTime = const Duration(milliseconds: 550), + }) : super(key: key); + + @override + _SCDebounceWidgetState createState() => _SCDebounceWidgetState(); +} + +class _SCDebounceWidgetState extends State { + DateTime? _lastClickTime; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, // 确保透明区域也能点击 + child: widget.child, + onTap: () { + DateTime now = DateTime.now(); + if (_lastClickTime == null || + now.difference(_lastClickTime!) > widget.debounceTime) { + _lastClickTime = now; + widget.onTap(); + }else { + if (widget.tips != null) { + SCTts.show(widget.tips ?? ""); + } + } + }, + ); + } +} diff --git a/lib/ui_kit/components/sc_float_ichart.dart b/lib/ui_kit/components/sc_float_ichart.dart new file mode 100644 index 0000000..3322e2c --- /dev/null +++ b/lib/ui_kit/components/sc_float_ichart.dart @@ -0,0 +1,223 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +// 默认位置 +Offset kDefaultFloatOffset = Offset( + ScreenUtil().screenWidth - width(90), + ScreenUtil().screenHeight - height(120), +); + +class SCFloatIchart { + /// 单例模式 + static final SCFloatIchart _SCFloatIchart = SCFloatIchart._internal(); //1 + factory SCFloatIchart() { + return _SCFloatIchart; + } + + SCFloatIchart._internal(); + + bool _inserted = false; + OverlayEntry? overlayEntry; + BuildContext? context; + Offset offset = kDefaultFloatOffset; + + show() { + if (overlayEntry != null) { + remove(); // 先移除已有的 + } + var overlayState = Overlay.of(context!); + overlayEntry = OverlayEntry( + builder: (context) { + if (inLoginPage) { + remove(); + return Container(); + } else { + return buildToastLayout(); + } + }, + ); + overlayState.insert(overlayEntry!); + _inserted = true; // 标记已插入 + } + + bool isShow() { + return _inserted; + } + + remove() { + if (overlayEntry != null && overlayEntry!.mounted) { + if (_inserted) { + overlayEntry?.remove(); + _inserted = false; + } + } + } + + LayoutBuilder buildToastLayout() { + Timer? timer; + return LayoutBuilder( + builder: (context, constraints) { + return Stack( + children: [ + Positioned( + left: offset.dx, + top: offset.dy, + child: GestureDetector( + //更新child的位置 + onPanUpdate: (details) { + var localPosition = details.delta; + var dx = (offset.dx + localPosition.dx).clamp( + 0.0, + ScreenUtil().screenWidth - width(90), + ); + var dy = (offset.dy + localPosition.dy).clamp( + 0.0, + ScreenUtil().screenHeight - height(55) - kToolbarHeight, + ); + offset = Offset(dx, dy); + overlayEntry!.markNeedsBuild(); + }, + //拖动结束,处理child贴边悬浮 + onPanEnd: (details) { + var oldPosition = offset.dx; + var targets = + offset.dx + width(80) > (ScreenUtil().screenWidth / 2) + ? ScreenUtil().screenWidth - width(90) + : 0; + timer = Timer.periodic(Duration(milliseconds: 1), (t) { + if (targets > 0) { + oldPosition++; + } else { + oldPosition--; + } + offset = Offset(oldPosition.toDouble(), offset.dy); + overlayEntry!.markNeedsBuild(); + if (oldPosition < 0 || + oldPosition > ScreenUtil().screenWidth - width(90)) { + timer?.cancel(); + } + }); + }, + child: Container( + child: float(), + margin: EdgeInsets.only(bottom: height(kToolbarHeight)), + ), + ), + ), + ], + ); + }, + ); + } + + Widget float() { + JoinRoomRes? room = + Provider.of( + context!, + listen: false, + ).currenRoom; + // User user = Provider.of(context, listen: false).maiMap[0]; + SocialChatLoginRes? user = AccountStorage().getCurrentUser(); + return FloatRoomWindow(room: room, user: user, remove: remove); + } + + void init(BuildContext context) { + this.context = context; + } +} + +class FloatRoomWindow extends StatefulWidget { + final JoinRoomRes? room; + final SocialChatLoginRes? user; + final Function remove; + + const FloatRoomWindow({Key? key, this.room, this.user, required this.remove}) + : super(key: key); + + @override + _FloatRoomWindowState createState() => _FloatRoomWindowState(); +} + +class _FloatRoomWindowState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SCDebounceWidget( + onTap: () { + SCRoomUtils.openCurrentRoom(context); + }, + child: Container( + height: 55.w, + width: 90.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + color: SocialChatTheme.primaryLight, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: 8.w), + Stack( + alignment: Alignment.center, + children: [ + netImage( + url: widget.room?.roomProfile?.roomProfile?.roomCover ?? "", + width: 40.w, + height: 40.w, + borderRadius: BorderRadius.circular(8.w), + ), + Image.asset( + "sc_images/general/sc_icon_online_user.png", + width: 15.w, + height: 15.w, + ), + ], + ), + SizedBox(width: width(5)), + SCDebounceWidget( + onTap: () { + Provider.of( + context, + listen: false, + ).exitCurrentVoiceRoomSession(false); + Timer(Duration(milliseconds: 550), () { + widget.remove.call(); + }); + }, + child: Padding( + padding: EdgeInsets.all(5.w), + child: Image.asset( + "sc_images/index/sc_icon_room_flot_close.png", + width: 20.w, + height: 20.w, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui_kit/components/sc_lk_fading_edge_scrollview.dart b/lib/ui_kit/components/sc_lk_fading_edge_scrollview.dart new file mode 100644 index 0000000..98b6117 --- /dev/null +++ b/lib/ui_kit/components/sc_lk_fading_edge_scrollview.dart @@ -0,0 +1,298 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Flutter widget for displaying fading edge at start/end of scroll views +class SCFadingEdgeScrollView extends StatefulWidget { + /// child widget + final Widget child; + + /// scroll controller of child widget + /// + /// Look for more documentation at [ScrollView.scrollController] + final ScrollController scrollController; + + /// Whether the scroll view scrolls in the reading direction. + /// + /// Look for more documentation at [ScrollView.reverse] + final bool reverse; + + /// The axis along which child view scrolls + /// + /// Look for more documentation at [ScrollView.scrollDirection] + final Axis scrollDirection; + + /// what part of screen on start half should be covered by fading edge gradient + /// [gradientFractionOnStart] must be 0 <= [gradientFractionOnStart] <= 1 + /// 0 means no gradient, + /// 1 means gradients on start half of widget fully covers it + final double gradientFractionOnStart; + + /// what part of screen on end half should be covered by fading edge gradient + /// [gradientFractionOnEnd] must be 0 <= [gradientFractionOnEnd] <= 1 + /// 0 means no gradient, + /// 1 means gradients on start half of widget fully covers it + final double gradientFractionOnEnd; + + /// set to true if you want scrollController passed to widget to be disposed when widget's state is disposed + final bool shouldDisposeScrollController; + + const SCFadingEdgeScrollView._internal({ + Key? key, + required this.child, + required this.scrollController, + required this.reverse, + required this.scrollDirection, + required this.gradientFractionOnStart, + required this.gradientFractionOnEnd, + required this.shouldDisposeScrollController, + }) : assert(child != null), + assert(scrollController != null), + assert(reverse != null), + assert(scrollDirection != null), + assert(gradientFractionOnStart >= 0 && gradientFractionOnStart <= 1), + assert(gradientFractionOnEnd >= 0 && gradientFractionOnEnd <= 1), + super(key: key); + + /// Constructor for creating [SCFadingEdgeScrollView] with [ScrollView] as child + /// child must have [ScrollView.controller] set + factory SCFadingEdgeScrollView.fromScrollView({ + Key? key, + required ScrollView child, + double gradientFractionOnStart = 0.1, + double gradientFractionOnEnd = 0.1, + bool shouldDisposeScrollController = false, + }) { + assert(child.controller != null, "Child must have controller set"); + + return SCFadingEdgeScrollView._internal( + key: key, + child: child, + scrollController: child.controller!, + scrollDirection: child.scrollDirection, + reverse: child.reverse, + gradientFractionOnStart: gradientFractionOnStart, + gradientFractionOnEnd: gradientFractionOnEnd, + shouldDisposeScrollController: shouldDisposeScrollController, + ); + } + + /// Constructor for creating [SCFadingEdgeScrollView] with [SingleChildScrollView] as child + /// child must have [SingleChildScrollView.controller] set + factory SCFadingEdgeScrollView.fromSingleChildScrollView({ + Key? key, + required SingleChildScrollView child, + double gradientFractionOnStart = 0.1, + double gradientFractionOnEnd = 0.1, + bool shouldDisposeScrollController = false, + }) { + assert(child.controller != null, "Child must have controller set"); + + return SCFadingEdgeScrollView._internal( + key: key, + child: child, + scrollController: child.controller!, + scrollDirection: child.scrollDirection, + reverse: child.reverse, + gradientFractionOnStart: gradientFractionOnStart, + gradientFractionOnEnd: gradientFractionOnEnd, + shouldDisposeScrollController: shouldDisposeScrollController, + ); + } + + /// Constructor for creating [SCFadingEdgeScrollView] with [PageView] as child + /// child must have [PageView.controller] set + factory SCFadingEdgeScrollView.fromPageView({ + Key? key, + required PageView child, + double gradientFractionOnStart = 0.1, + double gradientFractionOnEnd = 0.1, + bool shouldDisposeScrollController = false, + }) { + assert(child.controller != null, "Child must have controller set"); + + return SCFadingEdgeScrollView._internal( + key: key, + child: child, + scrollController: child.controller!, + scrollDirection: child.scrollDirection, + reverse: child.reverse, + gradientFractionOnStart: gradientFractionOnStart, + gradientFractionOnEnd: gradientFractionOnEnd, + shouldDisposeScrollController: shouldDisposeScrollController, + ); + } + + /// Constructor for creating [SCFadingEdgeScrollView] with [AnimatedList] as child + /// child must have [AnimatedList.controller] set + factory SCFadingEdgeScrollView.fromAnimatedList({ + Key? key, + required AnimatedList child, + double gradientFractionOnStart = 0.1, + double gradientFractionOnEnd = 0.1, + bool shouldDisposeScrollController = false, + }) { + assert(child.controller != null, "Child must have controller set"); + + return SCFadingEdgeScrollView._internal( + key: key, + child: child, + scrollController: child.controller!, + scrollDirection: child.scrollDirection, + reverse: child.reverse, + gradientFractionOnStart: gradientFractionOnStart, + gradientFractionOnEnd: gradientFractionOnEnd, + shouldDisposeScrollController: shouldDisposeScrollController, + ); + } + + @override + _FadingEdgeScrollViewState createState() => _FadingEdgeScrollViewState(); +} + +class _FadingEdgeScrollViewState extends State + with WidgetsBindingObserver { + late ScrollController _controller; + bool _isScrolledToStart = false; + bool _isScrolledToEnd = false; + + @override + void initState() { + super.initState(); + + _controller = widget.scrollController; + _isScrolledToStart = _controller.initialScrollOffset == 0; + _controller.addListener(_onScroll); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_controller == null) { + return; + } + + if (_isScrolledToEnd == null && + _controller.position.maxScrollExtent == 0) { + setState(() { + _isScrolledToEnd = true; + }); + } + }); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + _controller.removeListener(_onScroll); + if (widget.shouldDisposeScrollController) { + _controller.dispose(); + } + } + + void _onScroll() { + final offset = _controller.offset; + final minOffset = _controller.position.minScrollExtent; + final maxOffset = _controller.position.maxScrollExtent; + + final isScrolledToEnd = offset >= maxOffset; + final isScrolledToStart = offset <= minOffset; + + if (isScrolledToEnd != _isScrolledToEnd || + isScrolledToStart != _isScrolledToStart) { + setState(() { + _isScrolledToEnd = isScrolledToEnd; + _isScrolledToStart = isScrolledToStart; + }); + } + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + setState(() { + // Add the shading or remove it when the screen resize (web/desktop) or mobile is rotated + if (_controller.hasClients) { + final offset = _controller.offset; + final maxOffset = _controller.position.maxScrollExtent; + if (maxOffset == 0 && offset == 0) { + // Not scrollable + _isScrolledToStart = true; + _isScrolledToEnd = true; + } else if (maxOffset == offset) { + // Scrollable but at end + _isScrolledToStart = false; + _isScrolledToEnd = true; + } else if (maxOffset > 0 && offset == 0) { + // Scrollable but at start + _isScrolledToStart = true; + _isScrolledToEnd = false; + } else { + // Scroll in progress/not are either end + _isScrolledToStart = false; + _isScrolledToEnd = false; + } + } + }); + } + + @override + Widget build(BuildContext context) { + if (_isScrolledToStart == null && _controller.hasClients) { + final offset = _controller.offset; + final minOffset = _controller.position.minScrollExtent; + final maxOffset = _controller.position.maxScrollExtent; + + _isScrolledToEnd = offset >= maxOffset; + _isScrolledToStart = offset <= minOffset; + } + + return ShaderMask( + shaderCallback: (bounds) => LinearGradient( + begin: _gradientStart, + end: _gradientEnd, + stops: [ + 0, + widget.gradientFractionOnStart * 0.5, + 1 - widget.gradientFractionOnEnd * 0.5, + 1, + ], + colors: _getColors( + widget.gradientFractionOnStart > 0 && !(_isScrolledToStart ?? true), + widget.gradientFractionOnEnd > 0 && !(_isScrolledToEnd ?? false)), + ).createShader( + bounds.shift(Offset(-bounds.left, -bounds.top)), + textDirection: Directionality.of(context), + ), + child: widget.child, + blendMode: BlendMode.dstIn, + ); + } + + AlignmentGeometry get _gradientStart => + widget.scrollDirection == Axis.vertical + ? _verticalStart + : _horizontalStart; + + AlignmentGeometry get _gradientEnd => + widget.scrollDirection == Axis.vertical ? _verticalEnd : _horizontalEnd; + + Alignment get _verticalStart => + widget.reverse ? Alignment.bottomCenter : Alignment.topCenter; + + Alignment get _verticalEnd => + widget.reverse ? Alignment.topCenter : Alignment.bottomCenter; + + AlignmentDirectional get _horizontalStart => widget.reverse + ? AlignmentDirectional.centerEnd + : AlignmentDirectional.centerStart; + + AlignmentDirectional get _horizontalEnd => widget.reverse + ? AlignmentDirectional.centerStart + : AlignmentDirectional.centerEnd; + + List _getColors(bool isStartEnabled, bool isEndEnabled) => [ + (isStartEnabled ? Colors.transparent : Colors.white), + Colors.white, + Colors.white, + (isEndEnabled ? Colors.transparent : Colors.white) + ]; +} diff --git a/lib/ui_kit/components/sc_page_list.dart b/lib/ui_kit/components/sc_page_list.dart new file mode 100644 index 0000000..73b98ff --- /dev/null +++ b/lib/ui_kit/components/sc_page_list.dart @@ -0,0 +1,213 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; + +///分页列表 +class SCPageList extends StatefulWidget { + const SCPageList({Key? key}) : super(key: key); + + @override + SCPageListState createState() => SCPageListState(); +} + +class SCPageListState extends State { + final RefreshController _refreshController = RefreshController( + initialRefresh: false, + ); + + ///当前第几页 + int currentPage = 0; + List items = []; + Color backgroundColor = Colors.white; + + ///是否开启分页 + bool enablePullUp = true; + bool enablePullDown = true; + bool isLoading = false; + bool needLoading = true; + int pageCount = 20; + EdgeInsetsGeometry? padding; + + bool isShowDivider = true; + bool isGridView = false; + bool isShowBottomMsg = true; + bool isShowFooter = true; + bool isCanClickEmpty = true; + int gridViewCount = 3; + + SliverGridDelegateWithFixedCrossAxisCount? gridDelegate; + + void loadComplete() { + isLoading = false; + _refreshController.refreshCompleted(); + _refreshController.loadComplete(); + } + + ///刷新列表 + void loadData(int page) { + setState(() { + isLoading = true; + }); + loadPage( + page: page, + onSuccess: (list) { + if (page == 1) { + items.clear(); + } + items.addAll(list); + if (mounted) setState(() {}); + currentPage = page; + _refreshController.refreshCompleted(); + _refreshController.loadComplete(); + if (list.length < pageCount) { + _refreshController.loadNoData(); + } + isLoading = false; + if (mounted) setState(() {}); + }, + onErr: () { + _refreshController.loadNoData(); + _refreshController.refreshCompleted(); + isLoading = false; + if (mounted) setState(() {}); + }, + ); + } + + Widget buildList(BuildContext context) { + return Container( + color: backgroundColor, + child: SmartRefresher( + enablePullDown: enablePullDown, + enablePullUp: enablePullUp, + controller: _refreshController, + onRefresh: () { + loadData(1); + }, + onLoading: () { + loadData(currentPage + 1); + }, + footer: + isShowFooter + ? null + : CustomFooter( + height: 1, + builder: (context, mode) { + return SizedBox( + height: 1, + width: 1, + child: SizedBox(height: 1, width: 1), + ); + }, + ), + child: + items.isEmpty + ? (isCanClickEmpty + ? GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + loadData(1); + }, + child: + needLoading && isLoading + ? Center(child: CupertinoActivityIndicator(color: Colors.white24,)) + : empty(), + ) + : needLoading && isLoading + ? Center(child: CupertinoActivityIndicator(color: Colors.white24)) + : empty()) + : _buildList(), + ), + ); + } + + Widget _buildList() { + if (isGridView) { + return GridView.builder( + gridDelegate: + gridDelegate ?? + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridViewCount, + ), + itemBuilder: (c, i) { + return buildItemOne(items[i], i); + }, + padding: padding ?? EdgeInsets.zero, + itemCount: items.length, + ); + } + return ListView.separated( + itemBuilder: (context, i) => buildItemOne(items[i], i), + itemCount: items.length, + padding: padding ?? EdgeInsets.zero, + separatorBuilder: (context, i) => builderDivider(), + ); + } + + ///默认空页面 + empty() { + // List list = []; + // list.add(SizedBox( + // height: height(80), + // )); + // list.add(Image.asset('sc_images/main/empty.png')); + // list.add( + // SizedBox( + // height: height(15), + // ), + // ); + // list.add( + // Text("暂无数据", + // style: TextStyle( + // fontSize: sp(14), + // color: Color(0xff999999), + // fontWeight: FontWeight.w400, + // decoration: TextDecoration.none, + // height: 1, + // )), + // ); + + //return Column(mainAxisSize: MainAxisSize.min, children: list); + return mainEmpty(msg: SCAppLocalizations.of(context)!.noData); + } + + ///默认列表项 + Widget buildItemOne(M item, int index) { + return buildItem(item); + } + + ///默认列表项 + Widget buildItem(M item) { + return Card(child: Center(child: Text(item.toString()))); + } + + ///默认加载数据 + void loadPage({ + required int page, + required Function(List) onSuccess, + Function? onErr, + }) { + onSuccess([]); + } + + @override + Widget build(BuildContext context) { + return Container(); + } + + builderDivider() { + return isShowDivider + ? Divider( + height: height(1), + color: SocialChatTheme.dividerColor, + indent: width(15), + endIndent: width(15), + ) + : Container(); + } +} diff --git a/lib/ui_kit/components/sc_tts.dart b/lib/ui_kit/components/sc_tts.dart new file mode 100644 index 0000000..329fb9d --- /dev/null +++ b/lib/ui_kit/components/sc_tts.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class SCTts{ + static show(String msg, {gravity = ToastGravity.BOTTOM}) { + Fluttertoast.showToast( + msg: msg, + toastLength: Toast.LENGTH_SHORT, + gravity: gravity, //位置 + timeInSecForIosWeb: 2, //时间 + backgroundColor: Colors.black45, + textColor: Colors.white); + } +} \ No newline at end of file diff --git a/lib/ui_kit/components/socialchat_gradient_button.dart b/lib/ui_kit/components/socialchat_gradient_button.dart new file mode 100644 index 0000000..fc072c8 --- /dev/null +++ b/lib/ui_kit/components/socialchat_gradient_button.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +/// 背景带渐变色的button +socialchatGradientButton({ + double radius = 25, + LinearGradient? gradient, + required Function onPress, + required String text, + Color textColor = Colors.white, + double textSize = 14, +}) { + return GestureDetector( + child: Container( + height: 28.w, + alignment: AlignmentDirectional.center, + margin: EdgeInsetsDirectional.fromSTEB(5, 5, 0, 5), + padding: EdgeInsets.symmetric(horizontal: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radius), + gradient: + gradient ?? LinearGradient(colors: [Colors.white, Colors.white]), + ), + child: Text(text, style: TextStyle(color: textColor, fontSize: textSize)), + ), + onTap: () { + onPress(); + }, + ); +} diff --git a/lib/ui_kit/components/socialchat_tap_widget.dart b/lib/ui_kit/components/socialchat_tap_widget.dart new file mode 100644 index 0000000..19809e5 --- /dev/null +++ b/lib/ui_kit/components/socialchat_tap_widget.dart @@ -0,0 +1,106 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +// ignore: must_be_immutable +class SocialChatTapWidget extends StatefulWidget { + final Widget? child; + final Function? onTap; + Color highlightColor; + final Duration? duration; + final BorderRadius? borderRadius; + + SocialChatTapWidget( + {Key? key, + this.child, + this.onTap, + this.highlightColor = Colors.black12, + this.duration, this.borderRadius}) + : super(key: key); + + @override + TapWidgetState createState() { + return TapWidgetState(); + } +} + +class TapWidgetState extends State + with SingleTickerProviderStateMixin { + late AnimationController _ctl; + late Animation _colorAnimation; + Duration? _duration; + bool get onTapEnable => widget.onTap != null; + + @override + void initState() { + super.initState(); + _duration = widget.duration; + _duration ??= Duration(milliseconds: 1); + _ctl = AnimationController(vsync: this, duration: _duration); + widget.highlightColor??=Colors.black12; + + _colorAnimation = Tween( + begin: widget.highlightColor.withOpacity(0.0), + end: widget.highlightColor, + ).animate(_ctl); + } + + @override + void dispose() { + _ctl.stop(); + _ctl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: AnimatedBuilder( + animation: _ctl, + builder: (BuildContext context, Widget? child) { + return Container( + foregroundDecoration: BoxDecoration( + color: _colorAnimation.value, + borderRadius: widget.borderRadius + ), + child: widget.child, + ); + }, + ), + onTap: (){ + if(widget.onTap!=null){ + widget.onTap!(); + } + }, + onTapDown: (d) { + if (onTapEnable) _ctl.forward(); + }, + onTapUp: (d) { + if (onTapEnable) prepareToIdle(); + }, + onTapCancel: (){ + if (onTapEnable) prepareToIdle(); + }, + ); + } + + void prepareToIdle() { + late AnimationStatusListener listener; + listener = (AnimationStatus statue) { + if (statue == AnimationStatus.completed) { + _ctl.removeStatusListener(listener); + toStart(); + } + }; + _ctl.addStatusListener(listener); + if (!_ctl.isAnimating) { + _ctl.removeStatusListener(listener); + toStart(); + } + } + + void toStart() { + _ctl.stop(); + _ctl.reverse(); + } +} \ No newline at end of file diff --git a/lib/ui_kit/components/text/marquee_gradient_text.dart b/lib/ui_kit/components/text/marquee_gradient_text.dart new file mode 100644 index 0000000..1c3d8ed --- /dev/null +++ b/lib/ui_kit/components/text/marquee_gradient_text.dart @@ -0,0 +1,233 @@ +import 'package:flutter/material.dart'; + +class MarqueeGradientText extends StatefulWidget { + final String text; + final double speed; // 滚动速度(像素/秒) + final double sweepSpeed; // 扫光速度(单位/秒) + final double gapWidth; // 滚动时文字间的间距(像素) + final List gradientColors; + final double fontSize; + final bool needScroll; // 外部控制是否滚动 + final bool needGradient; // 外部控制是否扫光 + final Color textColor; + final FontWeight? fontWeight; + + const MarqueeGradientText({ + super.key, + required this.text, + this.speed = 30, + this.sweepSpeed = 0.5, + this.fontSize = 12, + this.needScroll = false, + this.needGradient = false, + this.fontWeight, + this.textColor = Colors.white, + this.gapWidth = 80, + this.gradientColors = const [Colors.red, Colors.yellow, Colors.red], + }); + + @override + State createState() => _MarqueeGradientTextState(); +} + +class _MarqueeGradientTextState extends State + with TickerProviderStateMixin { + late ScrollController _scrollController; + AnimationController? _scrollAnimationController; + late AnimationController _sweepAnimationController; + Animation? _scrollAnimation; + late Animation _sweepAnimation; + + // 不再使用 _fullText 变量,改为计算属性 + String get _fullText { + if (widget.needScroll) { + double spaceWidth = widget.fontSize * 0.6; // 估算空格宽度 + int gapSpaces = (widget.gapWidth / spaceWidth).ceil(); + String gap = List.filled(gapSpaces, ' ').join(); + return widget.text + gap + widget.text; + } else { + return widget.text; + } + } + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _initSweepAnimation(); // 初始化扫光动画(仅一次) + + // 首次启动滚动动画(如果需要) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && widget.needScroll && _scrollController.hasClients) { + _startScrollAnimation(); + } + }); + } + + void _initSweepAnimation() { + if (widget.needGradient) { + double cycleSeconds = 4.0 / widget.sweepSpeed; + _sweepAnimationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: (cycleSeconds * 1000).round()), + ); + _sweepAnimation = Tween(begin: -2.0, end: 2.0).animate( + CurvedAnimation(parent: _sweepAnimationController, curve: Curves.easeOut), + ); + _sweepAnimationController.repeat(); + } else { + _sweepAnimationController = AnimationController(vsync: this, duration: Duration.zero); + _sweepAnimation = AlwaysStoppedAnimation(-2.0); + } + } + + void _startScrollAnimation() { + // 如果已有动画,先停止并销毁 + _scrollAnimationController?.stop(); + _scrollAnimationController?.dispose(); + _scrollAnimationController = null; + + // 计算滚动距离(拼接文本长度的一半) + double totalScrollDistance = _scrollController.position.maxScrollExtent / 2; + if (totalScrollDistance <= 0) return; + + double durationSeconds = totalScrollDistance / widget.speed; + _scrollAnimationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: (durationSeconds * 1000).round()), + ); + + _scrollAnimation = Tween(begin: 0, end: totalScrollDistance).animate( + CurvedAnimation(parent: _scrollAnimationController!, curve: Curves.easeOut), + ); + + void scrollListener() { + if (!mounted || !_scrollController.hasClients) return; + _scrollController.jumpTo(_scrollAnimation!.value); + } + + _scrollAnimation!.addListener(scrollListener); + _scrollAnimationController!.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _scrollAnimationController!.reset(); + _scrollAnimationController!.forward(); + } + }); + + _scrollAnimationController!.forward(); + } + + void _resetScrollAnimation() { + // 当 needScroll 或文本长度变化时,重置滚动动画 + if (widget.needScroll && _scrollController.hasClients) { + _startScrollAnimation(); + } else { + // 如果不再需要滚动,停止动画并回到起始位置 + _scrollAnimationController?.stop(); + _scrollAnimationController?.dispose(); + _scrollAnimationController = null; + _scrollController.jumpTo(0); + } + } + + void _resetSweepAnimation() { + // 当 needGradient 变化时,重新初始化扫光动画 + _sweepAnimationController.dispose(); + _initSweepAnimation(); + } + + @override + void didUpdateWidget(covariant MarqueeGradientText oldWidget) { + super.didUpdateWidget(oldWidget); + + // 检查哪些属性变化了,并执行对应更新 + if (widget.text != oldWidget.text || + widget.needScroll != oldWidget.needScroll || + widget.speed != oldWidget.speed || + widget.gapWidth != oldWidget.gapWidth || + widget.fontSize != oldWidget.fontSize) { + // 文本长度或滚动参数变化 -> 需要重新启动滚动动画 + // 但必须等布局完成后才能获取 maxScrollExtent + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _resetScrollAnimation(); + } + }); + } + + if (widget.needGradient != oldWidget.needGradient || + widget.sweepSpeed != oldWidget.sweepSpeed || + widget.gradientColors != oldWidget.gradientColors) { + // 扫光参数变化 -> 重新创建扫光动画 + _resetSweepAnimation(); + } + } + + @override + void dispose() { + _scrollAnimationController?.dispose(); + _sweepAnimationController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // 1. 根据最新 widget 属性生成显示文本(动态计算) + final String displayText = _fullText; + + // 2. 构建基础文本组件 + Widget textWidget = Text( + displayText, + style: TextStyle( + fontSize: widget.fontSize, + fontWeight: widget.fontWeight, + color: widget.textColor, + ), + overflow: TextOverflow.visible, + maxLines: 1, + ); + + // 3. 如果需要扫光,包裹 ShaderMask + if (widget.needGradient) { + textWidget = AnimatedBuilder( + animation: _sweepAnimation, + builder: (context, child) { + double beginVal = _sweepAnimation.value; + double endVal = beginVal + 0.3; + return ShaderMask( + shaderCallback: (Rect rect) { + return LinearGradient( + begin: Alignment(beginVal, 0), + end: Alignment(endVal, 0), + colors: widget.gradientColors, + ).createShader(rect); + }, + blendMode: BlendMode.srcIn, + child: child, + ); + }, + child: textWidget, + ); + } + + // 4. 如果需要滚动,包裹 SingleChildScrollView + if (widget.needScroll) { + textWidget = SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + controller: _scrollController, + child: textWidget, + ); + } + + return ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: widget.gradientColors, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ).createShader(bounds), + child: textWidget, + ); + } +} \ No newline at end of file diff --git a/lib/ui_kit/components/text/sc_button.dart b/lib/ui_kit/components/text/sc_button.dart new file mode 100644 index 0000000..041e214 --- /dev/null +++ b/lib/ui_kit/components/text/sc_button.dart @@ -0,0 +1,51 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +Widget btn({GestureTapCallback? onTap, Widget? child}) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: child, + ); +} + +Widget btnWithBorder({ + GestureTapCallback? onTap, + Widget? child, + double paddingVertical = 0, + double paddingHorizontal = 0, + double marginHorizontal = 0, + double marginVertical = 0, + EdgeInsetsGeometry? margin, + EdgeInsetsGeometry? padding, + Color borderColor = Colors.transparent, + double borderWidth = 0, + Color color = Colors.transparent, + double radius = 0, + double? width, + double? height, + AlignmentGeometry? alignment, + Gradient? gradient, + BorderRadiusGeometry? borderRadius, + ImageProvider? bgImage, +}) { + return btn( + onTap: onTap, + child: Container( + width: width, + height: height, + alignment: alignment, + padding: padding ?? EdgeInsets.symmetric(vertical: paddingVertical.w, horizontal: paddingHorizontal.w), + margin: margin ?? EdgeInsets.symmetric(horizontal: marginHorizontal, vertical: marginVertical), + decoration: BoxDecoration( + border: Border.all(color: borderColor, width: borderWidth), + borderRadius: borderRadius ?? BorderRadius.all(Radius.circular(radius)), + color: color, + gradient: gradient, + image: bgImage != null ? DecorationImage(image: bgImage, fit: BoxFit.fill) : null, + ), + child: child, + ), + ); +} diff --git a/lib/ui_kit/components/text/sc_text.dart b/lib/ui_kit/components/text/sc_text.dart new file mode 100644 index 0000000..ce0292c --- /dev/null +++ b/lib/ui_kit/components/text/sc_text.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/text/marquee_gradient_text.dart'; + + +Widget text( + String content, { + double fontSize = 12, + Color textColor = Colors.white, + int maxLines = 1, + TextDecoration decoration = TextDecoration.none, + FontWeight fontWeight = FontWeight.w400, + TextOverflow overflow = TextOverflow.ellipsis, + TextAlign? textAlign, + double? maxWidth, + double? letterSpacing, + double? lineHeight, + FontStyle? fontStyle, + List? shadows, + TextDirection? textDirection, +}) { + return Container( + constraints: BoxConstraints(maxWidth: maxWidth??double.infinity), + child: Text( + content, + overflow: overflow, + maxLines: maxLines, + textAlign: textAlign, + style: TextStyle( + fontSize: fontSize.sp, + color: textColor, + fontWeight: fontWeight, + decoration: decoration, + letterSpacing: letterSpacing, + height: lineHeight, + fontStyle: fontStyle, + shadows: shadows, + decorationThickness: 2, + decorationColor: textColor, + ), + textDirection: textDirection, + ), + ); +} + +Widget socialchatNickNameText( + String content, { + double fontSize = 12, + int maxLines = 1, + Color textColor = Colors.white, + double? maxWidth, + String? type, + bool needScroll = false, + FontWeight fontWeight = FontWeight.w400, + double? speed, + double? sweepSpeed, + double? gapWidth, +}) { + if (content.isEmpty) { + return Container(); + } + return text(content,fontSize: fontSize, maxLines: maxLines,maxWidth: maxWidth, textColor: textColor, fontWeight: fontWeight); +} diff --git a/lib/ui_kit/theme/socialchat_theme.dart b/lib/ui_kit/theme/socialchat_theme.dart new file mode 100644 index 0000000..b5c605f --- /dev/null +++ b/lib/ui_kit/theme/socialchat_theme.dart @@ -0,0 +1,436 @@ +import 'package:flutter/material.dart'; + +/// SocialChat马甲包独立主题系统 +/// 定义完全独立的视觉设计语言,与原始应用区分 + +class SocialChatTheme { + // 主色调色板 (橙色主题) + static const Color primaryColor = Color(0xff18F2B1); // 主色 - 橙色 + static const Color primaryLight = Color(0xff18F2B1); // 浅橙色 + static const Color primaryDark = Color(0xffC41C00); // 深橙色 + + // 次要颜色 + static const Color secondaryColor = Color(0xff2196F3); // 蓝色作为强调色 + static const Color secondaryLight = Color(0xff6EC6FF); + static const Color secondaryDark = Color(0xff0069C0); + + // 中性色 + static const Color backgroundColor = Color(0xffF8F9FA); // 背景色 + static const Color surfaceColor = Color(0xffffffff); // 表面色 + static const Color errorColor = Color(0xffF44336); // 错误色 + + // 文本颜色 + static const Color textPrimary = Color(0xffffffff); + static const Color textSecondary = Color(0xff8c8b8b); + static const Color textDisabled = Color(0xffBDBDBD); + static const Color textOnPrimary = Color(0xffffffff); // 主色上的文本 + static const Color textOnSecondary = Color(0xffffffff); // 次色上的文本 + + // 边框和分隔线 + static const Color dividerColor = Color(0xffEEEEEE); + static const Color borderColor = Colors.transparent; + + // 功能颜色 + static const Color successColor = Color(0xff4CAF50); + static const Color warningColor = Color(0xffFF9800); + static const Color infoColor = Color(0xff2196F3); + + // 透明度 + static const Color transparent = Colors.transparent; + + // 阴影 + static List get cardShadow => [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ]; + + static List get buttonShadow => [ + BoxShadow( + color: primaryColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ]; + + // 间距系统 (基于8px网格) + static const double spacingXS = 4.0; + static const double spacingS = 8.0; + static const double spacingM = 16.0; + static const double spacingL = 24.0; + static const double spacingXL = 32.0; + static const double spacingXXL = 48.0; + + // 圆角系统 + static const double borderRadiusXS = 4.0; + static const double borderRadiusS = 8.0; + static const double borderRadiusM = 12.0; + static const double borderRadiusL = 16.0; + static const double borderRadiusXL = 24.0; + static const double borderRadiusCircle = 50.0; + + // 字体 + static const String fontFamily = 'Roboto'; // 使用Roboto字体,与原始应用区分 + static const String fontFamilyFallback = 'sans-serif'; + + // 文字样式 + static TextStyle get displayLarge => const TextStyle( + fontFamily: fontFamily, + fontSize: 32, + fontWeight: FontWeight.w700, + color: textPrimary, + ); + + static TextStyle get displayMedium => const TextStyle( + fontFamily: fontFamily, + fontSize: 28, + fontWeight: FontWeight.w600, + color: textPrimary, + ); + + static TextStyle get displaySmall => const TextStyle( + fontFamily: fontFamily, + fontSize: 24, + fontWeight: FontWeight.w600, + color: textPrimary, + ); + + static TextStyle get headlineMedium => const TextStyle( + fontFamily: fontFamily, + fontSize: 20, + fontWeight: FontWeight.w600, + color: textPrimary, + ); + + static TextStyle get headlineSmall => const TextStyle( + fontFamily: fontFamily, + fontSize: 18, + fontWeight: FontWeight.w600, + color: textPrimary, + ); + + static TextStyle get titleLarge => const TextStyle( + fontFamily: fontFamily, + fontSize: 16, + fontWeight: FontWeight.w600, + color: textPrimary, + ); + + static TextStyle get titleMedium => const TextStyle( + fontFamily: fontFamily, + fontSize: 14, + fontWeight: FontWeight.w500, + color: textPrimary, + ); + + static TextStyle get titleSmall => const TextStyle( + fontFamily: fontFamily, + fontSize: 12, + fontWeight: FontWeight.w500, + color: textSecondary, + ); + + static TextStyle get bodyLarge => const TextStyle( + fontFamily: fontFamily, + fontSize: 16, + fontWeight: FontWeight.w400, + color: textPrimary, + ); + + static TextStyle get bodyMedium => const TextStyle( + fontFamily: fontFamily, + fontSize: 14, + fontWeight: FontWeight.w400, + color: textPrimary, + ); + + static TextStyle get bodySmall => const TextStyle( + fontFamily: fontFamily, + fontSize: 12, + fontWeight: FontWeight.w400, + color: textSecondary, + ); + + static TextStyle get labelLarge => const TextStyle( + fontFamily: fontFamily, + fontSize: 14, + fontWeight: FontWeight.w500, + color: textOnPrimary, + ); + + static TextStyle get labelMedium => const TextStyle( + fontFamily: fontFamily, + fontSize: 12, + fontWeight: FontWeight.w500, + color: textOnPrimary, + ); + + static TextStyle get labelSmall => const TextStyle( + fontFamily: fontFamily, + fontSize: 10, + fontWeight: FontWeight.w500, + color: textSecondary, + ); + + // 暗色主题颜色 + static const Color darkBackgroundColor = Color(0xff121212); + static const Color darkSurfaceColor = Color(0xff1E1E1E); + static const Color darkTextPrimary = Color(0xffE0E0E0); + static const Color darkTextSecondary = Color(0xff9E9E9E); + static const Color darkDividerColor = Color(0xff373737); + static const Color darkBorderColor = Color(0xff424242); + + // 主题数据 + static ThemeData get lightTheme { + return ThemeData( + // 颜色 + primaryColor: primaryColor, + primaryColorLight: primaryLight, + primaryColorDark: primaryDark, + colorScheme: const ColorScheme.light( + primary: primaryColor, + secondary: secondaryColor, + background: backgroundColor, + surface: surfaceColor, + error: errorColor, + onPrimary: textOnPrimary, + onSecondary: textOnSecondary, + ), + + // 字体 + fontFamily: fontFamily, + + // 文字主题 + textTheme: TextTheme( + displayLarge: displayLarge, + displayMedium: displayMedium, + displaySmall: displaySmall, + headlineMedium: headlineMedium, + headlineSmall: headlineSmall, + titleLarge: titleLarge, + titleMedium: titleMedium, + titleSmall: titleSmall, + bodyLarge: bodyLarge, + bodyMedium: bodyMedium, + bodySmall: bodySmall, + labelLarge: labelLarge, + labelMedium: labelMedium, + labelSmall: labelSmall, + ), + + // 组件主题 + appBarTheme: const AppBarTheme( + backgroundColor: surfaceColor, + foregroundColor: textPrimary, + elevation: 0, + centerTitle: true, + ), + + cardTheme: CardThemeData( + color: surfaceColor, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + ), + margin: const EdgeInsets.all(spacingS), + ), + + buttonTheme: ButtonThemeData( + buttonColor: primaryColor, + textTheme: ButtonTextTheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + ), + ), + + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: textOnPrimary, + padding: const EdgeInsets.symmetric( + horizontal: spacingL, + vertical: spacingM, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + ), + textStyle: labelLarge, + ), + ), + + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + side: const BorderSide(color: primaryColor), + padding: const EdgeInsets.symmetric( + horizontal: spacingL, + vertical: spacingM, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + ), + textStyle: labelLarge.copyWith(color: primaryColor), + ), + ), + + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + borderSide: const BorderSide(color: borderColor), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + borderSide: const BorderSide(color: borderColor), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + borderSide: const BorderSide(color: primaryColor), + ), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.all(spacingM), + ), + + // 其他 + useMaterial3: true, + visualDensity: VisualDensity.adaptivePlatformDensity, + ); + } + + // 暗色主题 + static ThemeData get darkTheme { + return ThemeData( + // 颜色 + primaryColor: primaryColor, + primaryColorLight: primaryLight, + primaryColorDark: primaryDark, + colorScheme: const ColorScheme.dark( + primary: primaryColor, + secondary: secondaryColor, + background: darkBackgroundColor, + surface: darkSurfaceColor, + error: errorColor, + onPrimary: textOnPrimary, + onSecondary: textOnSecondary, + ), + + // 字体 + fontFamily: fontFamily, + + // 文字主题 + textTheme: TextTheme( + displayLarge: displayLarge.copyWith(color: darkTextPrimary), + displayMedium: displayMedium.copyWith(color: darkTextPrimary), + displaySmall: displaySmall.copyWith(color: darkTextPrimary), + headlineMedium: headlineMedium.copyWith(color: darkTextPrimary), + headlineSmall: headlineSmall.copyWith(color: darkTextPrimary), + titleLarge: titleLarge.copyWith(color: darkTextPrimary), + titleMedium: titleMedium.copyWith(color: darkTextPrimary), + titleSmall: titleSmall.copyWith(color: darkTextSecondary), + bodyLarge: bodyLarge.copyWith(color: darkTextPrimary), + bodyMedium: bodyMedium.copyWith(color: darkTextPrimary), + bodySmall: bodySmall.copyWith(color: darkTextSecondary), + labelLarge: labelLarge, + labelMedium: labelMedium, + labelSmall: labelSmall.copyWith(color: darkTextSecondary), + ), + + // 组件主题 + appBarTheme: const AppBarTheme( + backgroundColor: darkSurfaceColor, + foregroundColor: darkTextPrimary, + elevation: 0, + centerTitle: true, + ), + + cardTheme: CardThemeData( + color: darkSurfaceColor, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + ), + margin: const EdgeInsets.all(spacingS), + ), + + buttonTheme: ButtonThemeData( + buttonColor: primaryColor, + textTheme: ButtonTextTheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + ), + ), + + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: textOnPrimary, + padding: const EdgeInsets.symmetric( + horizontal: spacingL, + vertical: spacingM, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + ), + textStyle: labelLarge, + ), + ), + + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + side: const BorderSide(color: primaryColor), + padding: const EdgeInsets.symmetric( + horizontal: spacingL, + vertical: spacingM, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + ), + textStyle: labelLarge.copyWith(color: primaryColor), + ), + ), + + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + borderSide: const BorderSide(color: darkBorderColor), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + borderSide: const BorderSide(color: darkBorderColor), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadiusM), + borderSide: const BorderSide(color: primaryColor), + ), + filled: true, + fillColor: darkSurfaceColor, + contentPadding: const EdgeInsets.all(spacingM), + ), + + // 其他 + useMaterial3: true, + visualDensity: VisualDensity.adaptivePlatformDensity, + brightness: Brightness.dark, + ); + } + + // 渐变 + static LinearGradient get primaryGradient => LinearGradient( + colors: [primaryColor, primaryLight], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ); + + static LinearGradient get secondaryGradient => LinearGradient( + colors: [secondaryColor, secondaryLight], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ); + + static LinearGradient get successGradient => LinearGradient( + colors: [successColor, Color(0xff66BB6A)], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ); +} \ No newline at end of file diff --git a/lib/ui_kit/widgets/bag/props_bag_chatbox_detail_dialog.dart b/lib/ui_kit/widgets/bag/props_bag_chatbox_detail_dialog.dart new file mode 100644 index 0000000..1d44c88 --- /dev/null +++ b/lib/ui_kit/widgets/bag/props_bag_chatbox_detail_dialog.dart @@ -0,0 +1,165 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/modules/store/store_route.dart'; + +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; + +class PropsBagChatboxDetailDialog extends StatefulWidget { + BagsListRes res; + + PropsBagChatboxDetailDialog(this.res); + + @override + _PropsBagChatboxDetailDialogState createState() => + _PropsBagChatboxDetailDialogState(); +} + +class _PropsBagChatboxDetailDialogState + extends State { + final debouncer = Debouncer(); + + ///自己当前佩戴的气泡框 + PropsResources? myChatbox; + + @override + void initState() { + super.initState(); + myChatbox = AccountStorage().getChatbox(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 280.w, + width: 230.w, + decoration: BoxDecoration( + color: Color(0xff03523a), + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 20.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.viewFrame, + fontSize: 16.w, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + ], + ), + SizedBox(height: 25.w), + netImage(url: widget.res.propsResources?.cover??"",width: 80.w,height: 25.w), + SizedBox(height: 30.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12), + ), + child: text( + (widget.res.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch + ? SCAppLocalizations.of(context)!.renewal + : myChatbox?.id == widget.res.propsResources?.id + ? SCAppLocalizations.of(context)!.unUse + : SCAppLocalizations.of(context)!.use, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if ((widget.res.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch) { + ///过期 续费操作 + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + return; + } + if (myChatbox?.id != widget.res.propsResources?.id) { + ///佩戴 + _use(widget.res, false); + } else { + ///卸下 + _use(widget.res, true); + } + }, + ); + }, + ), + ], + ), + Positioned( + right: 20.w, + top: 18.w, + child: GestureDetector( + child: Icon(Icons.close, size: 20.w), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + }, + ), + ), + ], + ), + ); + } + + void _use(BagsListRes res, bool unload) { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .switchPropsUse( + SCPropsType.CHAT_BUBBLE.name, + res.propsResources?.id ?? "", + unload, + ) + .then((value) async { + await Provider.of( + context, + listen: false, + ).fetchUserProfileData(loadGuardCount: false); + Future.delayed(Duration(milliseconds: 400), () { + SCLoadingManager.hide(); + SmartDialog.dismiss(tag: "showPropsDetail"); + }); + if (!unload) { + SCTts.show(SCAppLocalizations.of(context)!.successfulWear); + } else { + SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded); + } + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} diff --git a/lib/ui_kit/widgets/bag/props_bag_headdress_detail_dialog.dart b/lib/ui_kit/widgets/bag/props_bag_headdress_detail_dialog.dart new file mode 100644 index 0000000..0ca79a5 --- /dev/null +++ b/lib/ui_kit/widgets/bag/props_bag_headdress_detail_dialog.dart @@ -0,0 +1,176 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/ui_kit/widgets/headdress/headdress_widget.dart'; + +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; + +class PropsBagHeaddressDetailDialog extends StatefulWidget { + BagsListRes res; + + PropsBagHeaddressDetailDialog(this.res); + + @override + _PropsBagHeaddressDetailDialogState createState() => + _PropsBagHeaddressDetailDialogState(); +} + +class _PropsBagHeaddressDetailDialogState + extends State { + final debouncer = Debouncer(); + + ///自己当前佩戴的头饰 + PropsResources? myHeaddress; + + @override + void initState() { + super.initState(); + myHeaddress = AccountStorage().getHeaddress(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 300.w, + width: 230.w, + decoration: BoxDecoration( + color: Color(0xff03523a), + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 20.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.viewFrame, + fontSize: 16.w, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + ], + ), + SizedBox(height: 25.w), + head( + url: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userAvatar ?? + "", + width: 95.w, + height: 95.w, + headdress: widget.res.propsResources?.sourceUrl + ), + SizedBox(height: 30.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12), + ), + child: text( + (widget.res.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch + ? SCAppLocalizations.of(context)!.renewal + : myHeaddress?.id == widget.res.propsResources?.id + ? SCAppLocalizations.of(context)!.unUse + : SCAppLocalizations.of(context)!.use, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if ((widget.res.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch) { + ///过期 续费操作 + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + return; + } + if (myHeaddress?.id != widget.res.propsResources?.id) { + ///佩戴 + _use(widget.res, false); + } else { + ///卸下 + _use(widget.res, true); + } + }, + ); + }, + ), + ], + ), + Positioned( + right: 20.w, + top: 18.w, + child: GestureDetector( + child: Image.asset( + "sc_images/general/sc_icon_msg_tips_close.png", + height: 20.w, + color: Colors.white, + ), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + }, + ), + ), + ], + ), + ); + } + + void _use(BagsListRes res, bool unload) { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .switchPropsUse( + SCPropsType.AVATAR_FRAME.name, + res.propsResources?.id ?? "", + unload, + ) + .then((value) async { + await Provider.of(context, listen: false).fetchUserProfileData(loadGuardCount: false); + Future.delayed(Duration(milliseconds: 400),(){ + SCLoadingManager.hide(); + SmartDialog.dismiss(tag: "showPropsDetail"); + }); + if (!unload) { + SCTts.show(SCAppLocalizations.of(context)!.successfulWear); + } else { + SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded); + } + }).catchError((e){ + SCLoadingManager.hide(); + }); + } +} diff --git a/lib/ui_kit/widgets/bag/props_bag_mountains_detail_dialog.dart b/lib/ui_kit/widgets/bag/props_bag_mountains_detail_dialog.dart new file mode 100644 index 0000000..e64d488 --- /dev/null +++ b/lib/ui_kit/widgets/bag/props_bag_mountains_detail_dialog.dart @@ -0,0 +1,163 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/bags_list_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/modules/store/store_route.dart'; + +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; + +class PropsBagMountainsDetailDialog extends StatefulWidget { + BagsListRes res; + + PropsBagMountainsDetailDialog(this.res); + + @override + _PropsBagMountainsDetailDialogState createState() => + _PropsBagMountainsDetailDialogState(); +} + +class _PropsBagMountainsDetailDialogState + extends State { + final debouncer = Debouncer(); + ///自己当前佩戴的坐骑 + PropsResources? myMountains; + @override + void initState() { + super.initState(); + myMountains = AccountStorage().getMountains(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 300.w, + width: 230.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 20.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.viewFrame, + fontSize: 16.w, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + ], + ), + SizedBox(height: 20.w), + netImage( + url: widget.res.propsResources?.cover ?? "", + width: 130.w, + height: 130.w, + ), + SizedBox(height: 20.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12), + ), + child: text( + (widget.res.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch + ? SCAppLocalizations.of(context)!.renewal + : myMountains?.id == widget.res.propsResources?.id + ? SCAppLocalizations.of(context)!.unUse + : SCAppLocalizations.of(context)!.use, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if ((widget.res.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch) { + ///过期 续费操作 + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + return; + } + if (myMountains?.id != widget.res.propsResources?.id) { + ///佩戴 + _use(widget.res, false); + } else { + ///卸下 + _use(widget.res, true); + } + }, + ); + }, + ), + ], + ), + Positioned( + right: 20.w, + top: 18.w, + child: GestureDetector( + child: Icon(Icons.close, size: 20.w), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + }, + ), + ), + ], + ), + ); + } + + void _use(BagsListRes res, bool unload) { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .switchPropsUse( + SCPropsType.RIDE.name, + res.propsResources?.id ?? "", + unload, + ) + .then((value) async { + await Provider.of(context, listen: false).fetchUserProfileData(loadGuardCount: false); + Future.delayed(Duration(milliseconds: 400),(){ + SCLoadingManager.hide(); + SmartDialog.dismiss(tag: "showPropsDetail"); + }); + if (!unload) { + SCTts.show(SCAppLocalizations.of(context)!.successfulWear); + } else { + SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded); + } + }).catchError((e){ + SCLoadingManager.hide(); + }); + } + +} diff --git a/lib/ui_kit/widgets/bag/props_bag_theme_detail_dialog.dart b/lib/ui_kit/widgets/bag/props_bag_theme_detail_dialog.dart new file mode 100644 index 0000000..383b084 --- /dev/null +++ b/lib/ui_kit/widgets/bag/props_bag_theme_detail_dialog.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/modules/store/store_route.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; + +import '../../../shared/business_logic/models/res/sc_room_theme_list_res.dart'; + +class PropsBagThemeDetailDialog extends StatefulWidget { + SCRoomThemeListRes res; + Function() refresh; + + PropsBagThemeDetailDialog(this.res, this.refresh); + + @override + _PropsBagThemeDetailDialogState createState() => + _PropsBagThemeDetailDialogState(); +} + +class _PropsBagThemeDetailDialogState extends State { + final debouncer = Debouncer(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 280.w, + width: 230.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Stack( + children: [ + Column( + children: [ + SizedBox(height: 20.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + SCAppLocalizations.of(context)!.viewFrame, + fontSize: 16.w, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + ], + ), + SizedBox(height: 20.w), + netImage( + url: widget.res?.themeBack ?? "", + width: 110.w, + height: 110.w, + borderRadius: BorderRadius.all(Radius.circular(8.w)), + ), + SizedBox(height: 20.w), + GestureDetector( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12), + ), + child: text( + (widget.res.expireTime ?? 0) > 0 + ? ((widget.res.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch + ? SCAppLocalizations.of(context)!.renewal + : widget.res.useTheme ?? false + ? SCAppLocalizations.of(context)!.unUse + : SCAppLocalizations.of(context)!.use) + : ((widget.res.useTheme ?? false) + ? SCAppLocalizations.of(context)!.unUse + : SCAppLocalizations.of(context)!.use), + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if ((widget.res.expireTime ?? 0) <= 0) { + } else { + if ((widget.res.expireTime ?? 0) < + DateTime.now().millisecondsSinceEpoch) { + ///过期 续费操作 + SCNavigatorUtils.push( + context, + StoreRoute.list, + replace: false, + ); + return; + } + } + + if (widget.res.useTheme ?? false) { + ///卸下 + _use(widget.res, true); + } else { + ///佩戴 + _use(widget.res, false); + } + }, + ); + }, + ), + ], + ), + Positioned( + right: 20.w, + top: 18.w, + child: GestureDetector( + child: Icon(Icons.close, size: 20.w), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + }, + ), + ), + ], + ), + ); + } + + void _use(SCRoomThemeListRes res, bool unload) { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .themeSwitchUse(res.id ?? "", unload) + .then((value) async { + SCLoadingManager.hide(); + SmartDialog.dismiss(tag: "showPropsDetail"); + if (!unload) { + SCTts.show(SCAppLocalizations.of(context)!.successfulWear); + Provider.of(context!, listen: false).updateRoomBG(res); + } else { + Provider.of( + context!, + listen: false, + ).updateRoomBG(SCRoomThemeListRes()); + SCTts.show(SCAppLocalizations.of(context)!.successfullyUnloaded); + } + + ///发送一条消息更新远端房间信息 + String? groupId = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.roomAccount; + if (groupId != null) { + Msg msg = Msg( + groupId: groupId, + msg: unload ? "" : jsonEncode(res.toJson()), + type: SCRoomMsgType.roomBGUpdate, + ); + Provider.of( + context!, + listen: false, + ).dispatchMessage(msg, addLocal: false); + } + widget.refresh(); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } +} diff --git a/lib/ui_kit/widgets/banner/index_banner_page.dart b/lib/ui_kit/widgets/banner/index_banner_page.dart new file mode 100644 index 0000000..a2fc5af --- /dev/null +++ b/lib/ui_kit/widgets/banner/index_banner_page.dart @@ -0,0 +1,116 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/main.dart'; + +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_banner_utils.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; + +class IndexBannerPage extends StatefulWidget { + SCIndexBannerRes res; + + IndexBannerPage(this.res); + + @override + _IndexBannerPageState createState() => _IndexBannerPageState(); +} + +class _IndexBannerPageState extends State { + bool isSelect = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8.w)), + constraints: BoxConstraints(maxHeight: ScreenUtil().screenHeight * 0.7), + width: ScreenUtil().screenWidth * 0.9, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + SCDebounceWidget( + child: netImage( + url: widget.res.cover ?? "", + borderRadius: BorderRadius.circular(8.w), + ), + onTap: () { + print('ads:${widget.res.toJson()}'); + SmartDialog.dismiss(tag: "showBannerDialog"); + SCBannerUtils.openBanner(widget.res, navigatorKey.currentState!.context); + }, + ), + PositionedDirectional( + end: 12.w, + top: 12.w, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Icon(Icons.close, size: 18.w, color: Colors.white), + onTap: () { + SCRoomUtils.closeAllDialogs(); + }, + ), + ), + + PositionedDirectional( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(8.w), + bottomRight: Radius.circular(8.w), + ), + ), + width: ScreenUtil().screenWidth * 0.9, + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Row( + children: [ + SizedBox(width: 8.w), + Image.asset( + !isSelect + ? "sc_images/general/sc_icon_unselect.png" + : "sc_images/general/sc_icon_select.png", + width: 16.w, + height: 16.w, + ), + SizedBox(width: 3.w), + text( + SCAppLocalizations.of(context)!.noPromptsToday, + fontSize: 14.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ], + ), + ), + onTap: () { + isSelect = !isSelect; + setState(() {}); + DataPersistence.setBool( + "IndexBannerTodayShow${SCMDateUtils.formatDateTime(DateTime.now())}", + isSelect, + ); + }, + ), + start: 0.w, + bottom: 0.w, + ), + ], + ), + ); + } +} diff --git a/lib/ui_kit/widgets/countdown_timer.dart b/lib/ui_kit/widgets/countdown_timer.dart new file mode 100644 index 0000000..47f3b14 --- /dev/null +++ b/lib/ui_kit/widgets/countdown_timer.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'dart:async'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; + +class CountdownTimer extends StatefulWidget { + final DateTime expiryDate; + final Color? color; + final double? fontSize; + final FontWeight? fontWeight; + + const CountdownTimer({ + super.key, + required this.expiryDate, + this.color, + this.fontSize, + this.fontWeight, + }); + + @override + State createState() => _CountdownTimerState(); +} + +class _CountdownTimerState extends State { + Timer? _timer; + Duration _remainingTime = Duration.zero; + + @override + void initState() { + super.initState(); + _calculateRemainingTime(); + _startTimer(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void _calculateRemainingTime() { + final now = DateTime.now(); + if (widget.expiryDate.isAfter(now)) { + setState(() { + _remainingTime = widget.expiryDate.difference(now); + }); + } else { + setState(() { + _remainingTime = Duration.zero; + }); + } + } + + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + _calculateRemainingTime(); + }); + } + + String _formatDuration(Duration duration) { + if (duration.inSeconds <= 0) { + return SCAppLocalizations.of(context)!.expired; + } + + String twoDigits(int n) => n.toString().padLeft(2, '0'); + + final days = duration.inDays; + final hours = twoDigits(duration.inHours.remainder(24)); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + + if (days > 0) { + return '${days}D $hours:$minutes:$seconds'; + } + return '$hours:$minutes:$seconds'; + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + _formatDuration(_remainingTime), + fontSize: widget.fontSize ?? 14.sp, + fontWeight: widget.fontWeight ?? FontWeight.bold, + textColor: + _remainingTime.inSeconds <= 0 + ? Colors.grey + : (widget.color ?? Colors.black), + ), + ], + ); + } +} diff --git a/lib/ui_kit/widgets/headdress/headdress_widget.dart b/lib/ui_kit/widgets/headdress/headdress_widget.dart new file mode 100644 index 0000000..b9691c8 --- /dev/null +++ b/lib/ui_kit/widgets/headdress/headdress_widget.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; + +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; + +class SVGAHeadwearWidget extends StatefulWidget { + // 资源路径(网络URL或本地assets路径) + final String resource; + + // 尺寸 + final double? width; + final double? height; + + // 动画控制参数 + final bool autoPlay; + final int loops; // 循环次数(0表示无限循环) + final bool clearsAfterStop; + + // 缓存控制 + final bool useCache; + + // 回调函数 + final VoidCallback? onCompleted; + final Function(String)? onError; + final VoidCallback? onStartLoading; + final VoidCallback? onFinishLoading; + + const SVGAHeadwearWidget({ + Key? key, + required this.resource, + this.width, + this.height, + this.autoPlay = true, + this.loops = 0, + this.clearsAfterStop = true, + this.useCache = true, + this.onCompleted, + this.onError, + this.onStartLoading, + this.onFinishLoading, + }) : super(key: key); + + @override + _SVGAHeadwearWidgetState createState() => _SVGAHeadwearWidgetState(); +} + +class _SVGAHeadwearWidgetState extends State + with SingleTickerProviderStateMixin { + SVGAAnimationController? _animationController; + bool _isLoading = true; + bool _isNetworkResource = false; + bool _hasError = false; + + @override + void initState() { + super.initState(); + _isNetworkResource = widget.resource.startsWith('http'); + _animationController = SVGAAnimationController(vsync: this); + + if (widget.autoPlay) { + _loadAnimation(); + } + + // 添加动画状态监听 + _animationController?.addStatusListener((status) { + if (status == AnimationStatus.completed && widget.onCompleted != null) { + widget.onCompleted!(); + } + }); + } + + // 加载动画(带缓存) + Future _loadAnimation() async { + if (widget.onStartLoading != null) { + widget.onStartLoading!(); + } + + setState(() { + _isLoading = true; + _hasError = false; + }); + + try { + // 检查缓存 + MovieEntity? videoItem; + if (widget.useCache) { + videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource]; + } + + // 如果没有缓存,则进行解析 + if (videoItem == null) { + videoItem = + _isNetworkResource + ? await SVGAParser.shared.decodeFromURL(widget.resource) + : await SVGAParser.shared.decodeFromAssets(widget.resource); + videoItem.autorelease =false; + // 存入缓存 + if (widget.useCache) { + SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem; + } + } + + if (mounted) { + setState(() { + _animationController?.videoItem = videoItem; + _isLoading = false; + }); + + // 根据循环次数设置播放方式 + if (widget.loops == 0) { + _animationController?.repeat(); + } else { + _animationController?.repeat(count: widget.loops); + } + + if (widget.onFinishLoading != null) { + widget.onFinishLoading!(); + } + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + _hasError = true; + }); + } + + if (widget.onError != null) { + SCTts.show("fail-${e.toString()}"); + widget.onError!(e.toString()); + } + } + } + + // 播放动画 + void play() { + if (_animationController?.videoItem != null) { + _animationController?.reset(); + if (widget.loops == 0) { + _animationController?.repeat(); + } else { + _animationController?.repeat(count: widget.loops); + } + } else { + _loadAnimation(); + } + } + + // 暂停动画 + void pause() { + _animationController?.stop(); + } + + // 停止动画 + void stop() { + _animationController?.stop(); + if (widget.clearsAfterStop) { + _animationController?.videoItem = null; + } + } + + // 重新加载动画 + void reload() { + if (widget.useCache) { + // 清除缓存项 + SCGiftVapSvgaManager().videoItemCache.remove(widget.resource); + } + _loadAnimation(); + } + + @override + void dispose() { + stop(); + _animationController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (_isLoading && !widget.autoPlay) { + return Container( + width: widget.width, + height: widget.height, + child: Center(child: CircularProgressIndicator()), + ); + } + + if (_hasError) { + return Container(); + } + + return Container( + width: widget.width, + height: widget.height, + child: + _animationController != null && + _animationController!.videoItem != null + ? SVGAImage( + _animationController!, + fit: BoxFit.fill, + clearsAfterStop: widget.clearsAfterStop, + ) + : Container(), + ); + } +} diff --git a/lib/ui_kit/widgets/msg/bottom_message_conversation_list_page.dart b/lib/ui_kit/widgets/msg/bottom_message_conversation_list_page.dart new file mode 100644 index 0000000..3bd0f57 --- /dev/null +++ b/lib/ui_kit/widgets/msg/bottom_message_conversation_list_page.dart @@ -0,0 +1,426 @@ +import 'dart:convert'; +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/widgets/msg/message_conversation_list_page.dart'; + +import '../../../modules/chat/chat_route.dart'; + +class BottomMessageConversationListPage extends StatefulWidget { + @override + _BottomMessageConversationListPageState createState() => + _BottomMessageConversationListPageState(); +} + +class _BottomMessageConversationListPageState + extends State { + bool showSystemAnnouncementTips = true; + + @override + void initState() { + super.initState(); + showSystemAnnouncementTips = DataPersistence.getBool( + "${AccountStorage().getCurrentUser()?.userProfile?.account}-ShowSystemAnnouncementTips", + defaultValue: true, + ); + _checkLogin(); + } + + @override + void dispose() { + super.dispose(); + } + + void _checkLogin() async { + V2TimValueCallback getLoginStatusRes = + await TencentImSDKPlugin.v2TIMManager.getLoginStatus(); + if (getLoginStatusRes.code == 0) { + var status = getLoginStatusRes.data; // getLoginStatusRes.data为用户登录状态值 + if (status == 1) { + // 已登录 + ///处理有时候拉不到最后一条消息 + Provider.of( + context, + listen: false, + ).initConversation().then((value) { + Future.delayed(Duration(milliseconds: 550), () { + Provider.of( + context, + listen: false, + ).initConversation().then((value) { + setState(() {}); + }); + }); + }); + } else if (status == 2) { + // 登录中 + } else if (status == 3) { + // 未登录 + Provider.of( + context, + listen: false, + ).loginTencetRtm(context).then((value) { + _checkLogin(); + }); + } + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: ClipRect( + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + height: 490.w, + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + children: [ + SizedBox(width: 30.w), + Spacer(), + text( + SCAppLocalizations.of(context)!.message, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + Spacer(), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.all(3.w), + child: Icon( + Icons.clear, + size: 22.w, + color: Colors.white, + ), + ), + onTap: () { + SCNavigatorUtils.goBack(context); + }, + ), + SizedBox(width: 5.w), + ], + ), + _headMenu(context), + showSystemAnnouncementTips + ? Container( + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(10.w), + ), + padding: EdgeInsets.all(6.w), + margin: EdgeInsets.all(10.w), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 5.w), + Image.asset( + "sc_images/msg/sc_icon_message_activity.png", + height: 20.w, + ), + SizedBox(width: 5.w), + Expanded( + child: text( + SCAppLocalizations.of( + context, + )!.systemAnnouncement, + textColor: Colors.white, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + ), + ), + SCDebounceWidget( + child: Icon( + Icons.close, + size: 18.w, + color: Colors.white, + ), + onTap: () { + setState(() { + showSystemAnnouncementTips = false; + }); + DataPersistence.setBool( + "${AccountStorage().getCurrentUser()?.userProfile?.account}-ShowSystemAnnouncementTips", + showSystemAnnouncementTips, + ); + }, + ), + SizedBox(width: 5.w), + ], + ), + SizedBox(height: 3.w), + Container( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: + SCAppLocalizations.of( + context, + )!.systemAnnouncementTips1, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + SCAppLocalizations.of( + context, + )!.systemAnnouncementTips, + style: TextStyle( + fontSize: 11.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + TextSpan( + text: + "【${SCAppLocalizations.of(context)!.contactUs}】", + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + ) + : Container(), + Expanded(child: MessageConversationListPage(true)), + ], + ), + ), + ), + ), + ); + } + + Widget _headMenu(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + return Container( + margin: EdgeInsets.only( + top: 15.w, + bottom: 10.w, + left: 10.w, + right: 10.w, + ), + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SCDebounceWidget( + debounceTime: Duration(milliseconds: 3000), + child: + provider.activityUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${provider.activityUnReadCount > 99 ? "99+" : provider.activityUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Column( + children: [ + Image.asset( + "sc_images/msg/sc_icon_message_activity.png", + width: 55.w, + height: 55.w, + ), + text( + SCAppLocalizations.of(context)!.activity, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + ) + : Column( + children: [ + Image.asset( + "sc_images/msg/sc_icon_message_activity.png", + width: 55.w, + height: 55.w, + ), + text( + SCAppLocalizations.of(context)!.activity, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + onTap: () { + provider.updateActivityCount(0); + SCNavigatorUtils.push( + context, + SCChatRouter.activity, + replace: false, + ); + }, + ), + SCDebounceWidget( + debounceTime: Duration(milliseconds: 3000), + child: + provider.systemUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${provider.systemUnReadCount > 99 ? "99+" : provider.systemUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Column( + children: [ + Image.asset( + "sc_images/msg/sc_icon_message_system.png", + width: 55.w, + height: 55.w, + ), + text( + SCAppLocalizations.of(context)!.system, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + ) + : Column( + children: [ + Image.asset( + "sc_images/msg/sc_icon_message_system.png", + width: 55.w, + height: 55.w, + ), + text( + SCAppLocalizations.of(context)!.system, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + onTap: () async { + var conversation = V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: "atyou-admin", + conversationID: SCGlobalConfig.imAdmin, + ); + provider.updateSystemCount(0); + var bool = await provider.startConversation(conversation); + if (!bool) return; + var json = jsonEncode(conversation.toJson()); + SCNavigatorUtils.push( + context, + "${SCChatRouter.systemChat}?conversation=${Uri.encodeComponent(json)}", + ); + }, + ), + SCDebounceWidget( + debounceTime: Duration(milliseconds: 3000), + child: + provider.notifcationUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${provider.notifcationUnReadCount > 99 ? "99+" : provider.notifcationUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Column( + children: [ + Image.asset( + "sc_images/msg/sc_icon_message_noti.png", + width: 55.w, + height: 55.w, + ), + text( + SCAppLocalizations.of(context)!.notifcation, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + ) + : Column( + children: [ + Image.asset( + "sc_images/msg/sc_icon_message_noti.png", + width: 55.w, + height: 55.w, + ), + text( + SCAppLocalizations.of(context)!.notifcation, + textColor: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + onTap: () { + provider.updateNotificationCount(0); + SCNavigatorUtils.push( + context, + SCChatRouter.notifcation, + replace: false, + ); + }, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui_kit/widgets/msg/message_conversation_list_page.dart b/lib/ui_kit/widgets/msg/message_conversation_list_page.dart new file mode 100644 index 0000000..01bdc26 --- /dev/null +++ b/lib/ui_kit/widgets/msg/message_conversation_list_page.dart @@ -0,0 +1,534 @@ +import 'dart:convert'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_text_elem.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_date_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; + +import '../../../modules/chat/chat_route.dart'; + +class MessageConversationListPage extends StatefulWidget { + bool isRoom; + + MessageConversationListPage(this.isRoom, {super.key}); + + @override + _MessageConversationListPageState createState() => + _MessageConversationListPageState(); +} + +class _MessageConversationListPageState + extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (_, provider, __) { + return Column( + children: [ + Expanded( + child: CustomScrollView( + shrinkWrap: true, + slivers: [ + SliverToBoxAdapter( + child: + provider.customerInfo != null && + provider.customerInfo?.id != + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id + ? Stack( + children: [ + Container( + margin: EdgeInsets.symmetric( + horizontal: 15.w, + ), + child: Row( + children: [ + Image.asset( + "sc_images/general/sc_icon_logo.png", + height: 48.w, + ), + SizedBox(width: 10.w), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + text( + SCAppLocalizations.of( + context, + )!.contactUs, + fontWeight: FontWeight.w600, + textColor: + widget.isRoom + ? Colors.white + : Colors.black, + fontSize: 15.sp, + ), + Row( + children: [ + Expanded( + child: Text( + "[${SCAppLocalizations.of(context)!.newMessage}]", + maxLines: 1, + overflow: + TextOverflow.ellipsis, + style: TextStyle( + fontSize: ScreenUtil() + .setSp(13), + color: + widget.isRoom + ? Colors.white54 + : Color( + 0xFF666666, + ), + ), + ), + ), + SizedBox(width: 5.w), + ], + ), + ], + ), + ), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 88.w, + height: 26.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 18.w, + ), + gradient: LinearGradient( + colors: [ + Color(0xff963EFB), + Color(0xffDCAEF6), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: text( + SCAppLocalizations.of( + context, + )!.contactUs, + fontWeight: FontWeight.w600, + textColor: Colors.white, + fontSize: 13.sp, + ), + ), + onTap: () async { + var conversation = V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: provider.customerInfo?.id, + conversationID: '', + ); + provider.updateCustomerCount(0); + var bool = + await Provider.of( + context, + listen: false, + ).startConversation(conversation); + if (!bool) return; + var json = jsonEncode( + conversation.toJson(), + ); + SCNavigatorUtils.push( + context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + }, + ), + ], + ), + ), + // 未读数量 + PositionedDirectional( + bottom: 0, + end: 10.w, + child: Visibility( + visible: provider.customerUnReadCount > 0, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 6.w, + ), + alignment: Alignment.center, + height: 16.w, + decoration: BoxDecoration( + //gradient: LinearGradient(colors: [Color(0xffA447FF), Color(0xff623CE9)]), + color: Colors.red, + shape: BoxShape.circle, + //borderRadius: BorderRadius.all(Radius.circular(7.w)), + ), + child: Text( + "${provider.customerUnReadCount > 99 ? '99+' : provider.customerUnReadCount}", + style: TextStyle( + fontSize: 10.sp, + color: Color(0xffffffff), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + ), + ), + ], + ) + : Container(), + ), + SliverToBoxAdapter( + child: _msgListView( + provider.conversationList, + isRoom: widget.isRoom, + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + + Widget _msgListView( + List chatList, { + EdgeInsetsGeometry? margin, + bool isTop = false, + bool isRoom = false, + }) { + if (chatList.isEmpty) { + if (isTop) return Container(); + return mainEmpty(); + } + return Container( + margin: margin ?? EdgeInsets.only(bottom: 56.w), + child: ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.only( + bottom: isTop ? 0 : ScreenUtil().bottomBarHeight, + ), + itemBuilder: + (_, index) => _item( + chatList[index], + isTop: isTop, + index: index, + isRoom: isRoom, + ), + separatorBuilder: + (_, index) => Divider( + color: Colors.black12, + indent: 65.w, + endIndent: 15.w, + height: 0.5.w, + thickness: 0.5.w, + ), + itemCount: chatList.length, + ), + ); + } + + Widget _item( + V2TimConversation conversation, { + bool isTop = false, + bool isRoom = false, + required int index, + }) { + return ConversationItem( + conversation: conversation, + index: index, + isRoom: isRoom, + key: Key(conversation.conversationID), + ); + } +} + +class ConversationItem extends StatefulWidget { + final V2TimConversation conversation; + final int index; + final bool isRoom; + + const ConversationItem({ + Key? key, + required this.conversation, + required this.index, + required this.isRoom, + }) : super(key: key); + + @override + _ConversationItemState createState() => _ConversationItemState(); +} + +class _ConversationItemState extends State { + V2TimConversation get conversation => widget.conversation; + SocialChatUserProfile? user; + + @override + void initState() { + loadUserInfo(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + String content = ""; + if (conversation.lastMessage == null) return Container(); + // if (conversation.lastMessage.elemList.length == 0) return Container(); + // if (conversation.lastMessage != null && conversation.type != FTIMConversationType.System) { + if (conversation.lastMessage != null) { + if (conversation.lastMessage?.status == + MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED) { + content = "[${SCAppLocalizations.of(context)!.messageHasBeenRecalled}]"; + } else { + if (conversation.lastMessage!.elemType == + MessageElemType.V2TIM_ELEM_TYPE_TEXT) { + V2TimTextElem textElem = conversation.lastMessage!.textElem!; + content = textElem.text!; + } else if (conversation.lastMessage!.elemType == + MessageElemType.V2TIM_ELEM_TYPE_IMAGE) { + content = SCAppLocalizations.of(context)!.image; + } else if (conversation.lastMessage!.elemType == + MessageElemType.V2TIM_ELEM_TYPE_VIDEO) { + content = SCAppLocalizations.of(context)!.video; + } else if (conversation.lastMessage!.elemType == + MessageElemType.V2TIM_ELEM_TYPE_SOUND) { + content = SCAppLocalizations.of(context)!.sound; + } else if (conversation.lastMessage!.elemType == + MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) { + V2TimCustomElem customElem = conversation.lastMessage!.customElem!; + content = customElem.desc ?? ""; + /*if (customElem.extension == 'sendGift') { + content = "[礼物]"; + } else { + content = "[收到一条消息]"; + }*/ + content = SCAppLocalizations.of(context)!.receivedAMessage; + } + } + } + /*if (conversation.type == ConversationType.V2TIM_C2C) { + userProfile = UserProfileCacheManager.getData(conversation.id); + if (userProfile == null) { + UserProfileCacheManager.checkCacheAndAdd([conversation.id]).then((value) { + setState(() {}); + }); + return Container(); + } + }*/ + // print('time :${Platform.isAndroid ? (conversation.lastMsg?.timestamp ?? 0) * 1000 : conversation.lastMsg?.timestamp}'); + return GestureDetector( + onTap: () async { + if (conversation != null) { + conversation.unreadCount = 0; + var bool = await Provider.of( + context, + listen: false, + ).startConversation(conversation); + if (!bool) return; + var json = jsonEncode(widget.conversation.toJson()); + SCNavigatorUtils.push( + context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + } + }, + onLongPress: () { + showDeleteConfirm(); + }, + child: Container( + decoration: BoxDecoration( + //color: Colors.white, + ), + child: Column( + children: [ + Container( + width: ScreenUtil().screenWidth, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w), + decoration: BoxDecoration( + //color: Colors.white, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + head(url: user?.userAvatar ?? "", width: 55.w), + SizedBox(width: 6.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: Row( + children: [ + socialchatNickNameText( + maxWidth: 152.w, + user?.userNickname ?? "", + fontSize: 14.sp, + fontWeight: FontWeight.w500, + textColor: + widget.isRoom + ? Colors.black + : Colors.white, + type: user?.getVIP()?.name ?? "", + needScroll: + (user + ?.userNickname + ?.characters + .length ?? + 0) > + 12, + ), + SizedBox(width: 5.w), + xb( + user?.userSex ?? 0, + color: + widget.isRoom + ? Colors.white54 + : Colors.black26, + ), + ], + ), + ), + SizedBox(width: 6.w), + Text( + SCMDateUtils.formatMessageTime( + context, + DateTime.fromMillisecondsSinceEpoch( + (conversation.lastMessage!.timestamp ?? 0) * + 1000, + ), + ), + style: TextStyle( + fontSize: 13.sp, + color: + widget.isRoom + ? Colors.white54 + : Color(0xff8d8d8d), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + height: 1, + ), + ), + ], + ), + SizedBox(height: 3.w), + Row( + children: [ + Expanded( + child: Text( + content, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: ScreenUtil().setSp(13), + color: + widget.isRoom + ? Colors.white54 + : Color(0xFF666666), + ), + ), + ), + SizedBox(width: 5.w), + // 未读数量 + Visibility( + visible: conversation.unreadCount! > 0, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 6.w), + alignment: Alignment.center, + height: 16.w, + decoration: BoxDecoration( + //gradient: LinearGradient(colors: [Color(0xffA447FF), Color(0xff623CE9)]), + color: Colors.red, + shape: BoxShape.circle, + //borderRadius: BorderRadius.all(Radius.circular(7.w)), + ), + child: Text( + "${conversation.unreadCount! > 99 ? '99+' : conversation.unreadCount}", + style: TextStyle( + fontSize: 10.sp, + color: Color(0xffffffff), + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + void showDeleteConfirm() { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.deleteConversationTips, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () async { + Provider.of( + context, + listen: false, + ).clearC2CHistoryMessage(conversation.conversationID ?? "", true); + }, + ); + }, + ); + } + + void loadUserInfo() { + if (conversation.conversationID != "administrator" && + conversation.conversationID != "customer" && + conversation.conversationID != "article") { + SCAccountRepository().loadUserInfo("${conversation.userID}").then((value) { + user = value; + setState(() {}); + }); + } + } +} diff --git a/lib/ui_kit/widgets/pop/pop_route.dart b/lib/ui_kit/widgets/pop/pop_route.dart new file mode 100644 index 0000000..8d5c863 --- /dev/null +++ b/lib/ui_kit/widgets/pop/pop_route.dart @@ -0,0 +1,46 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class PopRoute extends PopupRoute { + final Duration _duration = const Duration(milliseconds: 350); + final Widget child; + + PopRoute({required this.child}); + + @override + Color? get barrierColor => null; + + @override + bool get barrierDismissible => false; // 改为 false + + @override + String? get barrierLabel => null; + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + return child; + } + + // 关键:添加这个方法来支持 iOS 手势 + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + // 在 iOS 上使用 Cupertino 页面过渡 + if (Platform.isIOS) { + return CupertinoPageTransition( + primaryRouteAnimation: animation, + secondaryRouteAnimation: secondaryAnimation, + child: child, + linearTransition: false, + ); + } + // 保持原有过渡(没有过渡) + return child; + } + + @override + Duration get transitionDuration => _duration; +} \ No newline at end of file diff --git a/lib/ui_kit/widgets/pop/sc_pop_widget.dart b/lib/ui_kit/widgets/pop/sc_pop_widget.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart b/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart new file mode 100644 index 0000000..80209c3 --- /dev/null +++ b/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart @@ -0,0 +1,776 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/main.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/services/gift/gift_animation_manager.dart'; + + +///房间礼物滚屏动画 +class LGiftAnimalPage extends StatefulWidget { + @override + State createState() { + return _GiftAnimalPageState(); + } +} + +class _GiftAnimalPageState extends State + with TickerProviderStateMixin { + @override + Widget build(BuildContext context) { + return Container( + height: 305.w, + child: Stack( + children: [ + Consumer( + builder: (context, ref, child) { + return ref.giftMap[0] != null + ? AnimatedBuilder( + animation: ref.animationControllerList[0].controller, + builder: (context, child) { + return Container( + margin: ref.animationControllerList[0].verticalAnimation.value, + width: ScreenUtil().screenWidth * 0.75, + child: Stack( + alignment: AlignmentDirectional.centerStart, + clipBehavior: Clip.none, + children: [ + Container( + padding: EdgeInsetsDirectional.only( + top: 5.w, + ), + width: ScreenUtil().screenWidth * 0.65, + height: + 35.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + getBg(""), + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 3.w), + netImage( + url: ref.giftMap[0]!.sendUserPic, + shape: BoxShape.circle, + width: 25.w, + height: 25.w, + ), + SizedBox(width: 4.w), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + socialchatNickNameText( + maxWidth: 82.w, + ref.giftMap[0]!.sendUserName ?? "", + fontSize: 12.sp, + fontWeight: FontWeight.w500, + type: "", + needScroll: + (ref + .giftMap[0]! + .sendUserName + .characters + .length ?? + 0) > + 8, + ), + //SizedBox(width: 2.w,), + Row( + children: [ + Text( + "${SCAppLocalizations.of(context)!.sendTo} ", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + ), + ), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 62.w, + ), + child: Text( + "${ref.giftMap[0]?.sendToUserName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color:Color(0xFFFFD400), + ), + ), + ), + ], + ), + ], + ), + SizedBox(width: 4.w), + ], + ), + ), + Positioned( + left: 155.w, + child: Row( + children: [ + netImage( + url: ref.giftMap[0]!.giftPic, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(4.w), + width: 32.w, + height: 32.w, + ), + SizedBox(width: 12.w), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "x", + style: TextStyle( + fontSize: + ref + .animationControllerList[0] + .sizeAnimation + .value + .w, + fontStyle: FontStyle.italic, + color: Color(0xFFFFD400), + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + "${ref.giftMap[0]!.giftCount}", + style: TextStyle( + fontSize: + ref + .animationControllerList[0] + .sizeAnimation + .value, + fontStyle: FontStyle.italic, + color: Color(0xFFFFD400), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ) + : Container(); + }, + ), + Consumer( + builder: (context, ref, child) { + return ref.giftMap[1] != null + ? AnimatedBuilder( + animation: ref.animationControllerList[1].controller, + builder: (context, child) { + return Container( + margin: ref.animationControllerList[1].verticalAnimation.value, + width: ScreenUtil().screenWidth * 0.75, + child: Stack( + alignment: AlignmentDirectional.centerStart, + clipBehavior: Clip.none, + children: [ + Container( + padding: EdgeInsetsDirectional.only( + top: + 6.w, + ), + width: ScreenUtil().screenWidth * 0.65, + height: + 35.w, + decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(52), + // gradient: LinearGradient( + // colors: [ + // Color(0xffFFE400), + // Color(0xffFFE400).withOpacity(0.7), + // Color(0xffFFE400).withOpacity(0), + // ] + // ), + image: DecorationImage( + image: AssetImage( + getBg(""), + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 3.w), + netImage( + url: ref.giftMap[1]!.sendUserPic, + shape: BoxShape.circle, + width: 25.w, + height: 25.w, + ), + SizedBox(width: 4.w), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + socialchatNickNameText( + maxWidth: 82.w, + ref.giftMap[1]!.sendUserName ?? "", + fontSize: 12.sp, + fontWeight: FontWeight.w500, + type: "", + needScroll: + (ref + .giftMap[1]! + .sendUserName + .characters + .length ?? + 0) > + 8, + ), + //SizedBox(width: 2.w,), + Row( + children: [ + Text( + "${SCAppLocalizations.of(context)!.sendTo} ", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + ), + ), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 62.w, + ), + child: Text( + "${ref.giftMap[1]?.sendToUserName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color:Color(0xFFFFD400), + ), + ), + ), + ], + ), + ], + ), + SizedBox(width: 4.w), + ], + ), + ), + Positioned( + left: 155.w, + child: Row( + children: [ + netImage( + url: ref.giftMap[1]!.giftPic, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(4.w), + width: 32.w, + height: 32.w, + ), + SizedBox(width: 12.w), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "x", + style: TextStyle( + fontSize: + ref + .animationControllerList[1] + .sizeAnimation + .value + .w, + fontStyle: FontStyle.italic, + color: Color(0xFFFFD400), + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + "${ref.giftMap[1]!.giftCount}", + style: TextStyle( + fontSize: + ref + .animationControllerList[1] + .sizeAnimation + .value, + fontStyle: FontStyle.italic, + color: Color(0xFFFFD400), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ) + : Container(); + }, + ), + Consumer( + builder: (context, ref, child) { + return ref.giftMap[2] != null + ? AnimatedBuilder( + animation: ref.animationControllerList[2].controller, + builder: (context, child) { + return Container( + margin: ref.animationControllerList[2].verticalAnimation.value, + width: ScreenUtil().screenWidth * 0.75, + child: Stack( + alignment: AlignmentDirectional.centerStart, + clipBehavior: Clip.none, + children: [ + Container( + padding: EdgeInsetsDirectional.only( + top: + 6.w, + ), + width: ScreenUtil().screenWidth * 0.65, + height: + 35.w, + decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(52), + // gradient: LinearGradient( + // colors: [ + // Color(0xffFFE400), + // Color(0xffFFE400).withOpacity(0.7), + // Color(0xffFFE400).withOpacity(0), + // ] + // ), + image: DecorationImage( + image: AssetImage( + getBg(""), + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 3.w), + netImage( + url: ref.giftMap[2]!.sendUserPic, + shape: BoxShape.circle, + width: 25.w, + height: 25.w, + ), + SizedBox(width: 4.w), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + socialchatNickNameText( + maxWidth: 82.w, + ref.giftMap[2]!.sendUserName ?? "", + fontSize: 12.sp, + fontWeight: FontWeight.w500, + type: "", + needScroll: + (ref + .giftMap[2]! + .sendUserName + .characters + .length ?? + 0) > + 8, + ), + //SizedBox(width: 2.w,), + Row( + children: [ + Text( + "${SCAppLocalizations.of(context)!.sendTo} ", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + ), + ), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 62.w, + ), + child: Text( + "${ref.giftMap[2]?.sendToUserName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color:Color(0xFFFFD400), + ), + ), + ), + ], + ), + ], + ), + SizedBox(width: 4.w), + ], + ), + ), + Positioned( + left: 155.w, + child: Row( + children: [ + netImage( + url: ref.giftMap[2]!.giftPic, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(4.w), + width: 32.w, + height: 32.w, + ), + SizedBox(width: 12.w), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "x", + style: TextStyle( + fontSize: + ref + .animationControllerList[2] + .sizeAnimation + .value + .w, + fontStyle: FontStyle.italic, + color: Color(0xFFFFD400), + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + "${ref.giftMap[2]!.giftCount}", + style: TextStyle( + fontSize: + ref + .animationControllerList[2] + .sizeAnimation + .value, + fontStyle: FontStyle.italic, + color: Color(0xFFFFD400), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ) + : Container(); + }, + ), + Consumer( + builder: (context, ref, child) { + return ref.giftMap[3] != null + ? AnimatedBuilder( + animation: ref.animationControllerList[3].controller, + builder: (context, child) { + return Container( + margin: ref.animationControllerList[3].verticalAnimation.value, + width: ScreenUtil().screenWidth * 0.75, + child: Stack( + alignment: AlignmentDirectional.centerStart, + clipBehavior: Clip.none, + children: [ + Container( + padding: EdgeInsetsDirectional.only( + top: + 6.w, + ), + width: ScreenUtil().screenWidth * 0.65, + height: + 35.w, + decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(52), + // gradient: LinearGradient( + // colors: [ + // Color(0xffFFE400), + // Color(0xffFFE400).withOpacity(0.7), + // Color(0xffFFE400).withOpacity(0), + // ] + // ), + image: DecorationImage( + image: AssetImage( + getBg(""), + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 3.w), + netImage( + url: ref.giftMap[3]!.sendUserPic, + shape: BoxShape.circle, + width: 25.w, + height: 25.w, + ), + SizedBox(width: 4.w), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + socialchatNickNameText( + maxWidth: 82.w, + ref.giftMap[3]!.sendUserName ?? "", + fontSize: 12.sp, + fontWeight: FontWeight.w500, + type: "", + needScroll: + (ref + .giftMap[3]! + .sendUserName + .characters + .length ?? + 0) > + 8, + ), + //SizedBox(width: 2.w,), + Row( + children: [ + Text( + "${SCAppLocalizations.of(context)!.sendTo} ", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + ), + ), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 62.w, + ), + child: Text( + "${ref.giftMap[3]?.sendToUserName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color:Color(0xFFFFD400), + ), + ), + ), + ], + ), + ], + ), + SizedBox(width: 4.w), + ], + ), + ), + Positioned( + left: 155.w, + child: Row( + children: [ + netImage( + url: ref.giftMap[3]!.giftPic, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(4.w), + width: 32.w, + height: 32.w, + ), + SizedBox(width: 12.w), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "x", + style: TextStyle( + fontSize: + ref + .animationControllerList[3] + .sizeAnimation + .value + .w, + fontStyle: FontStyle.italic, + color: Color(0xFFFFD400), + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: + "${ref.giftMap[3]!.giftCount}", + style: TextStyle( + fontSize: + ref + .animationControllerList[3] + .sizeAnimation + .value, + fontStyle: FontStyle.italic, + color: Color(0xFFFFD400), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ) + : Container(); + }, + ), + ], + ), + ); + } + + @override + void dispose() { + Provider.of( + navigatorKey.currentState!.context, + listen: false, + ).cleanupAnimationResources(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + initAnimal(); + } + + void initAnimal() { + List beans = []; + double top = 60; + for (int i = 0; i < 4; i++) { + var bean = LGiftScrollingScreenAnimsBean(); + var controller = AnimationController( + value: 0, + duration: const Duration(milliseconds: 5000), + vsync: this, + ); + bean.controller = controller; + // bean.transverseAnimation = Tween( + // begin: Offset(ScreenUtil().screenWidth, 0), + // end: Offset(0, 0), + // ).animate( + // CurvedAnimation( + // parent: controller, + // curve: Interval(0.0, 0.45, curve: Curves.ease), + // ), + // ); + bean.verticalAnimation = EdgeInsetsTween( + begin: EdgeInsets.only(top: top), + end: EdgeInsets.only(top: 0), + ).animate( + CurvedAnimation( + parent: controller, + curve: Interval(0.2, 1, curve: Curves.easeOut), + ), + ); + bean.sizeAnimation = Tween(begin: 0, end: 22).animate( + CurvedAnimation( + parent: controller, + curve: Interval(0.45, 0.55, curve: Curves.ease), + ), + ); + beans.add(bean); + top = top + 70; + } + beans[0].controller.addStatusListener((state) { + if (state == AnimationStatus.completed) { + //动画完成监听 + Provider.of( + context, + listen: false, + ).markAnimationAsFinished(0); + } + }); + beans[1].controller.addStatusListener((state) { + if (state == AnimationStatus.completed) { + //动画完成监听 + Provider.of( + context, + listen: false, + ).markAnimationAsFinished(1); + } + }); + beans[2].controller.addStatusListener((state) { + if (state == AnimationStatus.completed) { + //动画完成监听 + Provider.of( + context, + listen: false, + ).markAnimationAsFinished(2); + } + }); + beans[3].controller.addStatusListener((state) { + if (state == AnimationStatus.completed) { + //动画完成监听 + Provider.of( + context, + listen: false, + ).markAnimationAsFinished(3); + } + }); + Provider.of( + context, + listen: false, + ).attachAnimationControllers(beans); + } + + String getBg(String type) { + return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png"; + } +} + +class LGiftScrollingScreenAnimsBean { + // late Animation transverseAnimation; + late Animation verticalAnimation; + late AnimationController controller; + late Animation sizeAnimation; +} + +class LGiftModel { + //发送者的名字 + String sendUserName = ""; + + //发送给谁 + String sendToUserName = ""; + + //发送者的头像 + String sendUserPic = ""; + + //礼物的图片 + String giftPic = ""; + + //礼物的名字 + String giftName = ""; + + //一次发送礼物的数量 + num giftCount = 0; + + //id + String labelId = ""; + +} diff --git a/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart b/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart new file mode 100644 index 0000000..67e0fa3 --- /dev/null +++ b/lib/ui_kit/widgets/room/anim/room_entrance_screen.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; + + +class RoomAnimationQueueScreen extends StatefulWidget { + const RoomAnimationQueueScreen({super.key}); + + @override + State createState() => + _RoomAnimationQueueScreenState(); +} + +class _RoomAnimationQueueScreenState extends State { + final List _animationQueue = []; + bool _isQueueProcessing = false; + final Map> _animationKeys = {}; + + @override + void initState() { + super.initState(); + Provider.of(context, listen: false).msgUserJoinListener = + _msgUserJoinListener; + } + + _msgUserJoinListener(Msg msg) { + Future.delayed(Duration(milliseconds: 550), () { + _addToQueue(msg); + }); + } + + void _addToQueue(Msg msg) { + setState(() { + final taskId = DateTime.now().millisecondsSinceEpoch; + final animationKey = GlobalKey<_RoomEntranceAnimationState>(); + + _animationKeys[taskId] = animationKey; + + final task = AnimationTask( + id: taskId, + msg: msg, + onComplete: () { + if (_animationQueue.isNotEmpty && + _animationQueue.first.id == taskId) { + setState(() { + _animationQueue.removeAt(0); + _animationKeys.remove(taskId); + }); + + if (_animationQueue.isNotEmpty) { + _startNextAnimation(); + } else { + setState(() => _isQueueProcessing = false); + } + } + }, + ); + + _animationQueue.add(task); + }); + + if (!_isQueueProcessing && _animationQueue.isNotEmpty) { + setState(() => _isQueueProcessing = true); + _startNextAnimation(); + } + } + + void _startNextAnimation({int retryCount = 0}) { + if (_animationQueue.isEmpty) return; + + SchedulerBinding.instance.addPostFrameCallback((_) { + if (_animationQueue.isNotEmpty) { + final task = _animationQueue.first; + final key = _animationKeys[task.id]; + + if (key?.currentState != null) { + key!.currentState!._startAnimation(); + } else if (retryCount < 3) { + // 有限次重试,避免无限循环 + Future.delayed(const Duration(milliseconds: 50), () { + _startNextAnimation(retryCount: retryCount + 1); + }); + } else { + // 重试多次失败后,跳过当前动画 + print("动画启动失败,跳过当前任务"); + task.onComplete(); + } + } + }); + } + + void _clearQueue() { + setState(() { + _animationQueue.clear(); + _animationKeys.clear(); + _isQueueProcessing = false; + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + if (_animationQueue.isNotEmpty) + RoomEntranceAnimation( + key: _animationKeys[_animationQueue.first.id], + msg: _animationQueue.first.msg, + onComplete: _animationQueue.first.onComplete, + ), + ], + ), + ], + ), + ); + } +} + +class AnimationTask { + final int id; + final Msg msg; + final VoidCallback onComplete; + + AnimationTask({ + required this.id, + required this.msg, + required this.onComplete, + }); +} + +class RoomEntranceAnimation extends StatefulWidget { + final VoidCallback onComplete; + final Msg msg; + + const RoomEntranceAnimation({ + super.key, + required this.onComplete, + required this.msg, + }); + + @override + State createState() => _RoomEntranceAnimationState(); +} + +class _RoomEntranceAnimationState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _offsetAnimation; + bool _isAnimating = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 4), + vsync: this, + ); + + _offsetAnimation = TweenSequence([ + TweenSequenceItem( + tween: Tween( + begin: const Offset(1.0, 0.0), + end: const Offset(0.0, 0.0), + ).chain(CurveTween(curve: Curves.easeOut)), + weight: 40.0, + ), + TweenSequenceItem( + tween: ConstantTween(const Offset(0.0, 0.0)), + weight: 20.0, + ), + TweenSequenceItem( + tween: Tween( + begin: const Offset(0.0, 0.0), + end: const Offset(-1.0, 0.0), + ).chain(CurveTween(curve: Curves.easeIn)), + weight: 40.0, + ), + ]).animate(_controller); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() => _isAnimating = false); + widget.onComplete(); + } + }); + } + + void _startAnimation() { + if (_isAnimating) return; + + setState(() => _isAnimating = true); + _controller.reset(); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SlideTransition( + position: _offsetAnimation, + child: Container( + width: ScreenUtil().screenWidth * 0.8, + height: 37.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + getEntranceBg(""), + ), + fit: BoxFit.fill, + ), + ), + child: Row( + textDirection: TextDirection.ltr, + children: [ + Padding( + padding: EdgeInsets.all(2.w), + child: netImage( + url: widget.msg.user?.userAvatar ?? "", + width: 28.w, + height: 28.w, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 5.w), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 2.w), + socialchatNickNameText( + maxWidth: 150.w, + widget.msg.user?.userNickname ?? "", + fontSize: 12.sp, + type: "", + needScroll: + (widget.msg.user?.userNickname?.characters.length ?? 0) > + 15, + ), + text( + SCAppLocalizations.of(context)!.enterTheRoom, + fontSize: 12.sp, + lineHeight: 0.9, + textColor: Colors.white, + ), + ], + ), + ], + ), + ), + ); + } + + String getEntranceBg(String type) { + return "sc_images/room/entrance/sc_icon_room_entrance_no_vip_bg.png"; + } +} diff --git a/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart b/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart new file mode 100644 index 0000000..a47859b --- /dev/null +++ b/lib/ui_kit/widgets/room/anim/room_entrance_widget.dart @@ -0,0 +1,734 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'dart:async'; +import 'dart:typed_data'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; + +// 动画队列项 +class AnimationQueueItem { + final String resource; + final Map? dynamicData; // 动态数据(文本、图片等) + final VoidCallback? onCompleted; + final Function(String)? onError; + Completer? completer; + + AnimationQueueItem({ + required this.resource, + this.dynamicData, + this.onCompleted, + this.onError, + }); +} + +// 动态数据类型 +class DynamicData { + final Map? textReplacements; // 文本替换 + final Map? imageReplacements; // 图片替换(二进制数据) + final Map? imageAssetPaths; // 图片资源路径 + + DynamicData({ + this.textReplacements, + this.imageReplacements, + this.imageAssetPaths, + }); +} + +// 动画队列管理器 +class RoomEntranceQueueManager { + static final RoomEntranceQueueManager _instance = + RoomEntranceQueueManager._internal(); + + factory RoomEntranceQueueManager() => _instance; + + RoomEntranceQueueManager._internal(); + + // 队列列表 + final List _queue = []; + + // 当前是否正在播放 + bool _isPlaying = false; + + // 当前播放器实例 + _RoomEntranceWidgetState? _currentPlayer; + + // 队列回调 + void Function()? onQueueUpdated; + + // 最大队列长度限制 + int maxQueueLength = 20; + + // 自动播放延迟时间(毫秒) + int autoPlayDelay = 500; + + // 添加动画到队列 + Future addToQueue(AnimationQueueItem item) { + final completer = Completer(); + item.completer = completer; + + // 检查队列长度限制 + if (_queue.length >= maxQueueLength) { + // 移除最早的动画 + final removedItem = _queue.removeAt(0); + removedItem.completer?.completeError('Queue overflow, item removed'); + } + + _queue.add(item); + + // 通知队列更新 + if (onQueueUpdated != null) { + onQueueUpdated!(); + } + + // 如果没有正在播放,则开始播放队列 + if (!_isPlaying) { + _startNextAnimation(); + } + + return completer.future; + } + + // 获取当前队列长度 + int get queueLength => _queue.length; + + // 获取等待中的队列项 + List get waitingItems => + _queue.isNotEmpty && _isPlaying + ? _queue.sublist(1) + : List.from(_queue); + + // 设置当前播放器 + void setCurrentPlayer(_RoomEntranceWidgetState player) { + _currentPlayer = player; + } + + // 清除当前播放器 + void clearCurrentPlayer() { + _currentPlayer = null; + } + + // 开始播放下一个动画 + void _startNextAnimation() { + if (_queue.isEmpty) { + _isPlaying = false; + if (onQueueUpdated != null) { + onQueueUpdated!(); + } + return; + } + + _isPlaying = true; + final nextItem = _queue.first; + + // 如果有延迟,则延迟播放 + Future.delayed(Duration(milliseconds: autoPlayDelay), () { + if (_currentPlayer != null && _currentPlayer!.mounted) { + // 播放队列中的动画 + _currentPlayer!.playFromQueue( + resource: nextItem.resource, + dynamicData: + nextItem.dynamicData != null + ? DynamicData( + textReplacements: nextItem.dynamicData?['textReplacements'], + imageReplacements: + nextItem.dynamicData?['imageReplacements'], + imageAssetPaths: nextItem.dynamicData?['imageAssetPaths'], + ) + : null, + onCompleted: () { + // 动画完成回调 + if (nextItem.onCompleted != null) { + nextItem.onCompleted!(); + } + + // 完成Completer + nextItem.completer?.complete(); + + // 从队列中移除当前项 + _queue.removeAt(0); + + // 通知队列更新 + if (onQueueUpdated != null) { + onQueueUpdated!(); + } + + // 播放下一个 + _startNextAnimation(); + }, + onError: (error) { + // 错误处理 + if (nextItem.onError != null) { + nextItem.onError!(error); + } + + // 完成Completer(带错误) + nextItem.completer?.completeError(error); + + // 从队列中移除当前项 + _queue.removeAt(0); + + // 通知队列更新 + if (onQueueUpdated != null) { + onQueueUpdated!(); + } + + // 播放下一个 + _startNextAnimation(); + }, + ); + } else { + // 如果播放器不可用,跳过当前项 + _queue.removeAt(0); + _startNextAnimation(); + } + }); + } + + // 清空队列 + void clearQueue() { + for (final item in _queue) { + item.completer?.completeError('Queue cleared'); + } + _queue.clear(); + _isPlaying = false; + + if (onQueueUpdated != null) { + onQueueUpdated!(); + } + } + + // 移除指定位置的队列项 + void removeAtIndex(int index) { + if (index >= 0 && index < _queue.length) { + final removedItem = _queue.removeAt(index); + removedItem.completer?.completeError('Item removed from queue'); + + if (onQueueUpdated != null) { + onQueueUpdated!(); + } + } + } + + // 获取队列状态 + Map get queueStatus { + return { + 'isPlaying': _isPlaying, + 'queueLength': _queue.length, + 'currentResource': _queue.isNotEmpty ? _queue.first.resource : null, + 'waitingCount': + _queue.length > (_isPlaying ? 1 : 0) + ? _queue.length - (_isPlaying ? 1 : 0) + : 0, + }; + } +} + +class RoomEntranceWidget extends StatefulWidget { + // 尺寸 + final double? width; + final double? height; + + // 动画控制参数 + final int loops; // 循环次数(0表示无限循环) + final bool clearsAfterStop; + + // 缓存控制 + final bool useCache; + + // 队列相关参数 + final bool useQueue; // 是否使用队列 + final int queueDelay; // 队列播放延迟(毫秒) + + // 回调函数(不再需要onCompleted和onError,因为通过队列管理) + final VoidCallback? onStartLoading; + final VoidCallback? onFinishLoading; + + // 是否在初始化时自动开始监听队列 + final bool autoStartQueue; + + const RoomEntranceWidget({ + Key? key, + this.width, + this.height, + this.loops = 0, + this.clearsAfterStop = true, + this.useCache = true, + this.useQueue = true, + this.queueDelay = 500, + this.onStartLoading, + this.onFinishLoading, + this.autoStartQueue = true, + }) : super(key: key); + + @override + _RoomEntranceWidgetState createState() => _RoomEntranceWidgetState(); +} + +class _RoomEntranceWidgetState extends State + with SingleTickerProviderStateMixin { + SVGAAnimationController? _animationController; + bool _isLoading = false; + bool _hasError = false; + String? _currentResource; + + // 用于存储SVGAImage组件的key,以便刷新占位符 + GlobalKey _svgaKey = GlobalKey(); + + // 队列管理器实例 + final RoomEntranceQueueManager _queueManager = RoomEntranceQueueManager(); + + // 队列播放相关 + VoidCallback? _queueCompletedCallback; + Function(String)? _queueErrorCallback; + + // 动态数据 + DynamicData? _currentDynamicData; + + @override + void initState() { + super.initState(); + _animationController = SVGAAnimationController(vsync: this); + + // 设置队列延迟 + _queueManager.autoPlayDelay = widget.queueDelay; + + // 设置当前播放器 + _queueManager.setCurrentPlayer(this); + + // 添加动画状态监听 + _animationController?.addStatusListener((status) { + if (status == AnimationStatus.completed) { + // 如果是在队列播放中,调用队列完成回调 + if (_queueCompletedCallback != null) { + _queueCompletedCallback!(); + _queueCompletedCallback = null; + _queueErrorCallback = null; + } + } + }); + + // 如果需要自动开始队列,则检查队列 + if (widget.autoStartQueue && widget.useQueue) { + // 延迟一小段时间,确保组件已经挂载 + Future.delayed(Duration(milliseconds: 100), () { + _queueManager._startNextAnimation(); + }); + } + } + + // 从队列播放(内部使用) + void playFromQueue({ + required String resource, + DynamicData? dynamicData, + VoidCallback? onCompleted, + Function(String)? onError, + }) { + _queueCompletedCallback = onCompleted; + _queueErrorCallback = onError; + _currentDynamicData = dynamicData; + _currentResource = resource; + + // 加载动画 + _loadAnimation( + resource: resource, + dynamicData: dynamicData, + isFromQueue: true, + ); + } + + // 加载动画 + Future _loadAnimation({ + required String resource, + DynamicData? dynamicData, + bool isFromQueue = false, + }) async { + if (widget.onStartLoading != null) { + widget.onStartLoading!(); + } + + setState(() { + _isLoading = true; + _hasError = false; + }); + + try { + final isNetworkResource = resource.startsWith('http'); + MovieEntity? videoItem; + + if (widget.useCache) { + videoItem = SCGiftVapSvgaManager().videoItemCache[resource]; + } + + if (videoItem == null) { + videoItem = + isNetworkResource + ? await SVGAParser.shared.decodeFromURL(resource) + : await SVGAParser.shared.decodeFromAssets(resource); + videoItem.autorelease = false; + + if (widget.useCache) { + SCGiftVapSvgaManager().videoItemCache[resource] = videoItem; + } + } + + if (mounted) { + // 应用动态数据(文本、图片替换) + await _applyDynamicData(videoItem, dynamicData); + + setState(() { + _animationController?.videoItem = videoItem; + _isLoading = false; + }); + + // 开始播放动画 + _startAnimation(); + + if (widget.onFinishLoading != null) { + widget.onFinishLoading!(); + } + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + _hasError = true; + }); + } + + // 队列错误回调 + if (isFromQueue && _queueErrorCallback != null) { + _queueErrorCallback!(e.toString()); + _queueErrorCallback = null; + _queueCompletedCallback = null; + } + + // 显示错误信息 + SCTts.show("SVGA加载失败: ${e.toString()}"); + } + } + + // 应用动态数据(文本、图片替换) + Future _applyDynamicData( + MovieEntity videoItem, + DynamicData? dynamicData, + ) async { + if (dynamicData == null) return; + + // 1. 应用文本替换 + if (dynamicData.textReplacements != null) { + await _applyTextReplacements(videoItem, dynamicData.textReplacements!); + } + + // 2. 应用图片替换(二进制数据) + if (dynamicData.imageReplacements != null) { + await _applyImageReplacements(videoItem, dynamicData.imageReplacements!); + } + + // 3. 应用图片资源路径替换 + if (dynamicData.imageAssetPaths != null) { + await _applyImageAssetReplacements( + videoItem, + dynamicData.imageAssetPaths!, + ); + } + } + + // 应用文本替换 + Future _applyTextReplacements( + MovieEntity videoItem, + Map textReplacements, + ) async { + // 这里需要根据具体的SVGA插件API来替换文本 + // 示例代码,具体实现取决于插件支持 + if (videoItem.dynamicItem != null) { + textReplacements.forEach((key, value) { + try { + // 根据插件API设置文本 + // 注意:flutter_svga插件的API可能会有所不同 + videoItem.dynamicItem!.setText( + TextPainter( + text: TextSpan( + text: "Hello, World!", + style: TextStyle( + fontSize: 28, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + key, + ); + } catch (e) { + print('设置文本占位符 $key 失败: $e'); + } + }); + } + } + + // 应用图片替换(二进制数据) + Future _applyImageReplacements( + MovieEntity videoItem, + Map imageReplacements, + ) async { + // 检查是否支持图片替换 + if (videoItem.images != null) { + imageReplacements.forEach((key, imageData) { + try { + // 替换图片数据 + // 注意:flutter_svga插件的API可能会有所不同 + if (videoItem.images!.containsKey(key)) { + videoItem.images![key] = imageData; + } + } catch (e) { + print('设置图片占位符 $key 失败: $e'); + } + }); + } + } + + // 应用图片资源路径替换 + Future _applyImageAssetReplacements( + MovieEntity videoItem, + Map imageAssetPaths, + ) async { + // 如果需要从assets加载图片,可以实现这里 + // 这通常需要将assets图片转换为Uint8List + } + + // 开始播放动画 + void _startAnimation() { + _animationController?.reset(); + + // 根据循环次数设置播放方式 + if (widget.loops == 0) { + _animationController?.repeat(); + } else { + _animationController?.repeat(count: widget.loops); + } + } + + // 播放指定资源(外部调用) + Future playResource({ + required String resource, + DynamicData? dynamicData, + }) async { + if (widget.useQueue) { + // 使用队列,添加到队列 + final queueItem = AnimationQueueItem( + resource: resource, + dynamicData: + dynamicData != null + ? { + 'textReplacements': dynamicData.textReplacements, + 'imageReplacements': dynamicData.imageReplacements, + 'imageAssetPaths': dynamicData.imageAssetPaths, + } + : null, + ); + + return _queueManager.addToQueue(queueItem); + } else { + // 不使用队列,直接播放 + return _loadAnimation( + resource: resource, + dynamicData: dynamicData, + isFromQueue: false, + ); + } + } + + // 暂停动画 + void pause() { + _animationController?.stop(); + } + + // 停止动画 + void stop() { + _animationController?.stop(); + if (widget.clearsAfterStop) { + _animationController?.videoItem = null; + setState(() { + _currentResource = null; + }); + } + } + + // 重新加载当前动画 + void reload() { + if (_currentResource != null) { + if (widget.useCache) { + SCGiftVapSvgaManager().videoItemCache.remove(_currentResource); + } + + _loadAnimation( + resource: _currentResource!, + dynamicData: _currentDynamicData, + isFromQueue: false, + ); + } + } + + @override + void dispose() { + stop(); + _animationController?.dispose(); + // 从队列管理器中移除当前播放器 + _queueManager.clearCurrentPlayer(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // 显示加载状态 + if (_isLoading) { + return Container( + width: widget.width, + height: widget.height, + child: Center(child: CircularProgressIndicator()), + ); + } + + // 显示错误状态 + if (_hasError) { + return Container( + width: widget.width, + height: widget.height, + child: Center(child: Icon(Icons.error, color: Colors.red)), + ); + } + + // 显示空状态(没有动画时) + if (_animationController?.videoItem == null) { + return Container( + width: widget.width, + height: widget.height, + child: Center( + child: Text('等待动画...', style: TextStyle(color: Colors.grey)), + ), + ); + } + + // 显示SVGA动画 + return Container( + width: widget.width, + height: widget.height, + child: SVGAImage( + key: _svgaKey, + _animationController!, + fit: BoxFit.contain, + clearsAfterStop: widget.clearsAfterStop, + ), + ); + } +} + +// 便捷使用的全局方法 +class RoomEntranceHelper { + // 添加入场动画到队列 + static Future addEntranceAnimation({ + required String resource, + Map? textReplacements, + Map? imageReplacements, + Map? imageAssetPaths, + VoidCallback? onCompleted, + Function(String)? onError, + }) async { + final dynamicData = DynamicData( + textReplacements: textReplacements, + imageReplacements: imageReplacements, + imageAssetPaths: imageAssetPaths, + ); + + final queueItem = AnimationQueueItem( + resource: resource, + dynamicData: { + 'textReplacements': dynamicData.textReplacements, + 'imageReplacements': dynamicData.imageReplacements, + 'imageAssetPaths': dynamicData.imageAssetPaths, + }, + onCompleted: onCompleted, + onError: onError, + ); + + return RoomEntranceQueueManager().addToQueue(queueItem); + } + + // 获取队列状态 + static Map getQueueStatus() { + return RoomEntranceQueueManager().queueStatus; + } + + // 清空队列 + static void clearQueue() { + RoomEntranceQueueManager().clearQueue(); + } + + // 设置队列更新回调 + static void setQueueUpdateCallback(void Function() callback) { + RoomEntranceQueueManager().onQueueUpdated = callback; + } + + // 设置队列最大长度 + static void setMaxQueueLength(int length) { + RoomEntranceQueueManager().maxQueueLength = length; + } + + // 设置自动播放延迟 + static void setAutoPlayDelay(int delayMs) { + RoomEntranceQueueManager().autoPlayDelay = delayMs; + } +} + +// 使用示例 +class RoomEntranceExample extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + // 创建播放器(不需要初始资源) + RoomEntranceWidget( + width: 200, + height: 200, + useQueue: true, + autoStartQueue: true, + ), + + SizedBox(height: 20), + + // 添加动画到队列的按钮 + ElevatedButton( + onPressed: () { + RoomEntranceHelper.addEntranceAnimation( + resource: 'assets/animations/entrance.svga', + textReplacements: {'username': '张三', 'level': 'VIP 10'}, + onCompleted: () { + print('入场动画播放完成'); + }, + ); + }, + child: Text('播放入场动画'), + ), + + // 添加另一个动画 + ElevatedButton( + onPressed: () { + RoomEntranceHelper.addEntranceAnimation( + resource: 'https://example.com/animations/vip_entrance.svga', + textReplacements: {'username': '李四', 'gift_name': '超级火箭'}, + ); + }, + child: Text('播放VIP入场动画'), + ), + + // 查看队列状态 + ElevatedButton( + onPressed: () { + final status = RoomEntranceHelper.getQueueStatus(); + print('队列状态: $status'); + }, + child: Text('查看队列状态'), + ), + ], + ); + } +} diff --git a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart new file mode 100644 index 0000000..f68ae32 --- /dev/null +++ b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; + +class LuckGiftNomorAnimWidget extends StatefulWidget { + @override + _LuckGiftNomorAnimWidgetState createState() => + _LuckGiftNomorAnimWidgetState(); +} + +class _LuckGiftNomorAnimWidgetState extends State { + @override + void initState() { + super.initState(); + Provider.of(context, listen: false).showLuckGiftBigHead = true; + } + + dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: Consumer( + builder: (context, provider, child) { + return provider.currentPlayingLuckGift != null + ? Container( + height: 380.w, + margin: EdgeInsets.only(top: 10.w), + child: Stack( + children: [ + Image.asset( + "sc_images/room/sc_icon_luck_gift_nomore.webp", + fit: BoxFit.fitWidth, + ), + provider.showLuckGiftBigHead?Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 125.w), + netImage( + url: + provider + .currentPlayingLuckGift + ?.data + ?.userAvatar ?? + "", + shape: BoxShape.circle, + height: 105.w, + width: 105.w, + ), + SizedBox(height: 16.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/general/sc_icon_jb.png", + height: 35.w, + ), + SizedBox(width: 3.w), + Image.asset( + "sc_images/general/sc_icon_game_numxx.png", + height: 20.w, + ), + Transform.translate( + offset: Offset(-5, 0), + child: buildNumForGame( + (provider + .currentPlayingLuckGift + ?.data + ?.awardAmount ?? + 0) > + 9999 + ? "${((provider.currentPlayingLuckGift?.data?.awardAmount ?? 0) / 1000).toStringAsFixed(0)}k" + : "${(provider.currentPlayingLuckGift?.data?.awardAmount ?? 0)}", + size: 22.w, + ), + ), + ], + ), + SizedBox(height: 12.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(width: 15.w), + buildNumForGame( + "${provider.currentPlayingLuckGift?.data?.multiple ?? 0}", + size: 22.w, + ), + Transform.translate( + offset: Offset(-3, 3), + child: Image.asset( + SCGlobalConfig.lang=="ar"? "sc_images/room/sc_icon_times_text_ar.png":"sc_images/room/sc_icon_times_text_en.png", + height: 12.w, + ), + ), + ], + ), + ], + ):Container(), + ], + ), + ) + : Container(); + }, + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart b/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart new file mode 100644 index 0000000..b40a733 --- /dev/null +++ b/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svga/flutter_svga.dart'; +import 'package:tancent_vap/utils/constant.dart'; +import 'package:tancent_vap/widgets/vap_view.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; + +class VapPlusSvgaPlayer extends StatefulWidget { + final String tag; + + const VapPlusSvgaPlayer({super.key, required this.tag}); + + @override + State createState() => _VapPlusSvgaPlayerState(); +} + +class _VapPlusSvgaPlayerState extends State + with TickerProviderStateMixin { + late SVGAAnimationController _svgaController; + + @override + void initState() { + super.initState(); + _svgaController = SVGAAnimationController(vsync: this); + if (widget.tag == "room_gift") { + SCGiftVapSvgaManager().bindSvgaCtrl(_svgaController); + } + } + + @override + void dispose() { + SCGiftVapSvgaManager().dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned.fill( + child: IgnorePointer( + // VapView可以通过外层包Container(),设置宽高来限制弹出视频的宽高 + // VapView can set the width and height through the outer package Container() to limit the width and height of the pop-up video + child: VapView( + scaleType: ScaleType.centerCrop, + onViewCreated: (controller) { + if (widget.tag == "room_gift") { + SCGiftVapSvgaManager().bindVapCtrl(controller); + // VapManager().play("https://res.cloudinary.com/dkmchpua1/video/upload/v1737624783/vcg9co6yyfqsadgety1n.mp4"); + } + }, + ), + ), + ), + Positioned.fill(child: SVGAImage(_svgaController, fit: BoxFit.fill)), + ], + ); + } +} diff --git a/lib/ui_kit/widgets/room/empty_mai_select.dart b/lib/ui_kit/widgets/room/empty_mai_select.dart new file mode 100644 index 0000000..dd77216 --- /dev/null +++ b/lib/ui_kit/widgets/room/empty_mai_select.dart @@ -0,0 +1,328 @@ +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; + +import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; + +class EmptyMaiSelect extends StatelessWidget { + final num index; + final SocialChatUserProfile? clickUser; + + const EmptyMaiSelect({Key? key, required this.index, this.clickUser}) + : super(key: key); + + @override + Widget build(BuildContext context) { + RtcProvider provider = Provider.of(context, listen: false); + List list = []; + list.add(SizedBox(height: 10.w)); + if (index == -1) { + if (provider.isFz() || provider.isGL()) { + if (clickUser?.id != + AccountStorage().getCurrentUser()?.userProfile?.id) { + list.add( + _item( + SCAppLocalizations.of(context)!.inviteToTheMicrophone, + "sc_images/room/sc_icon_mic_go_up.png", + context, + () { + Navigator.of(context).pop(); + Provider.of( + context, + listen: false, + ).dispatchMessage( + Msg( + groupId: + provider + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomAccount ?? + "", + msg: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname, + role: AccountStorage().getCurrentUser()?.userProfile?.id, + type: SCRoomMsgType.bsm, + toUser: clickUser, + ), + addLocal: false, + ); + }, + ), + ); + } + } + } else { + MicRes? userWheat = provider.roomWheatMap[index]; + if (userWheat?.user?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + if (provider.isFz()) { + ///点击的是自己 + if (userWheat?.micMute ?? false) { + list.add( + _item( + SCAppLocalizations.of(context)!.openTheMic, + "sc_images/room/sc_icon_mic_open.png", + context, + () { + Navigator.of(context).pop(); + provider.jieJinMai(index); + }, + ), + ); + } else { + list.add( + _item( + SCAppLocalizations.of(context)!.muteTheMic, + "sc_images/room/sc_icon_mic_mute.png", + context, + () { + Navigator.of(context).pop(); + provider.jinMai(index); + }, + ), + ); + } + } + + list.add( + _item( + SCAppLocalizations.of(context)!.leavelTheMic, + "sc_images/room/sc_icon_mic_leavel.png", + context, + () { + Navigator.of(context).pop(); + provider.xiaMai(index); + }, + ), + ); + } else { + if (provider.isFz() || provider.isGL()) { + ///操作的人是房主,并且座位没人 + if (clickUser == null) { + if (!(userWheat?.micLock ?? false)) { + list.add( + _item( + SCAppLocalizations.of(context)!.takeTheMic, + "sc_images/room/sc_icon_mic_go_up.png", + context, + () { + Navigator.of(context).pop(); + provider.shangMai(index); + }, + ), + ); + } + if ((userWheat?.micLock ?? false)) { + list.add( + _item( + SCAppLocalizations.of(context)!.unlockTheMic, + "sc_images/room/sc_icon_mic_unlock.png", + context, + () { + Navigator.of(context).pop(); + provider.jieFeng(index); + }, + ), + ); + } else { + list.add( + _item( + SCAppLocalizations.of(context)!.lockTheMic, + "sc_images/room/sc_icon_mic_lock.png", + context, + () { + Navigator.of(context).pop(); + provider.fengMai(index); + }, + ), + ); + } + } + + if (clickUser?.roles == SCRoomRolesType.HOMEOWNER.name || + clickUser?.roles == SCRoomRolesType.ADMIN.name) { + if (provider.isFz()) { + if (userWheat?.micMute ?? false) { + list.add( + _item( + SCAppLocalizations.of(context)!.openTheMic, + "sc_images/room/sc_icon_mic_open.png", + context, + () { + Navigator.of(context).pop(); + provider.jieJinMai(index); + }, + ), + ); + } else { + list.add( + _item( + SCAppLocalizations.of(context)!.muteTheMic, + "sc_images/room/sc_icon_mic_mute.png", + context, + () { + Navigator.of(context).pop(); + provider.jinMai(index); + }, + ), + ); + } + } + } else { + if (userWheat?.micMute ?? false) { + list.add( + _item( + SCAppLocalizations.of(context)!.openTheMic, + "sc_images/room/sc_icon_mic_open.png", + context, + () { + Navigator.of(context).pop(); + provider.jieJinMai(index); + }, + ), + ); + } else { + list.add( + _item( + SCAppLocalizations.of(context)!.muteTheMic, + "sc_images/room/sc_icon_mic_mute.png", + context, + () { + Navigator.of(context).pop(); + provider.jinMai(index); + }, + ), + ); + } + } + } else { + if (userWheat?.user == null) { + if (!(userWheat?.micLock ?? false)) { + list.add( + _item( + SCAppLocalizations.of(context)!.takeTheMic, + "sc_images/room/sc_icon_mic_go_up.png", + context, + () { + Navigator.of(context).pop(); + provider.shangMai(index); + }, + ), + ); + } else { + // list.add( + // _item('Leavel the mic ', "sc_images/room/sc_icon_mic_leavel.png", () { + // Navigator.of(context).pop(); + // provider.xiaMai(index); + // }), + // ); + } + } + } + } + } + if (clickUser != null) { + list.add( + _item( + SCAppLocalizations.of(context)!.openUserProfleCard, + "sc_images/room/sc_icon_open_card.png", + context, + () { + Navigator.of(context).pop(); + showBottomInCenterDialog( + context!, + RoomUserInfoCard(userId: clickUser?.id), + ); + }, + ), + ); + } + list.add( + _item(SCAppLocalizations.of(context)!.cancel, null, context, () { + Navigator.of(context).pop(); + }), + ); + list.add(SizedBox(height: 10.w)); + return SafeArea( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: ScreenUtil().screenWidth, + color: Color(0xff09372E).withOpacity(0.5), + child: Column(children: list), + ), + ), + ), + ); + } + + Widget _item( + String msg, + String? icon, + BuildContext context, + Function onClick, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + onClick(); + }, + child: Column( + children: [ + Container( + decoration: + msg == SCAppLocalizations.of(context)!.cancel + ? BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.all(Radius.circular(32.w)), + ) + : null, + width: ScreenUtil().screenWidth * 0.7, + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.center, + child: Text( + msg, + style: TextStyle( + fontSize: sp(16), + color: Colors.white, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/exit_min_room_page.dart b/lib/ui_kit/widgets/room/exit_min_room_page.dart new file mode 100644 index 0000000..97974fc --- /dev/null +++ b/lib/ui_kit/widgets/room/exit_min_room_page.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart'; +import 'package:yumi/services/gift/gift_animation_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +import '../../components/sc_float_ichart.dart'; + +class ExitMinRoomPage extends StatefulWidget { + bool isFz; + String roomId; + + ExitMinRoomPage(this.isFz, this.roomId); + + @override + _ExitMinRoomPageState createState() => _ExitMinRoomPageState(); +} + +class _ExitMinRoomPageState extends State { + SCUserIdentityRes? userIdentity; + bool isLoading = true; + + @override + void initState() { + super.initState(); + SCAccountRepository() + .userIdentity( + userId: + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.userId, + ) + .then((v) { + userIdentity = v; + isLoading = false; + setState(() {}); + }) + .catchError((e) { + isLoading = false; + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + SCDebounceWidget( + child: Container(color: Colors.transparent), + onTap: () { + Navigator.of(context).pop(); + }, + ), + Column( + spacing: 55.w, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SCDebounceWidget( + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_min_room.png", + width: 48.w, + height: 48.w, + ), + SizedBox(height: 8.w), + text( + SCAppLocalizations.of(context)!.mInimize, + fontSize: 15.sp, + textColor: Colors.white, + fontWeight: FontWeight.w500, + ), + ], + ), + onTap: () { + SCFloatIchart().show(); + SCNavigatorUtils.popUntil( + context, + ModalRoute.withName(SCRoutes.home), + ); + Provider.of( + context, + listen: false, + ).cleanupAnimationResources(); + }, + ), + SCDebounceWidget( + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_exit_room.png", + width: 48.w, + height: 48.w, + ), + SizedBox(height: 8.w), + text( + SCAppLocalizations.of(context)!.exit, + fontSize: 15.sp, + textColor: Colors.white, + fontWeight: FontWeight.w500, + ), + ], + ), + onTap: () { + SCNavigatorUtils.goBack(context); + Provider.of( + context, + listen: false, + ).exitCurrentVoiceRoomSession(false); + }, + ), + ], + ), + ], + ); + } +} diff --git a/lib/ui_kit/widgets/room/explore_banner_view.dart b/lib/ui_kit/widgets/room/explore_banner_view.dart new file mode 100644 index 0000000..9842305 --- /dev/null +++ b/lib/ui_kit/widgets/room/explore_banner_view.dart @@ -0,0 +1,94 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/shared/tools/sc_banner_utils.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; + +class ExploreBannerView extends StatefulWidget { + @override + _ExploreBannerViewState createState() => _ExploreBannerViewState(); +} + +class _ExploreBannerViewState extends State { + int _currentIndex = 0; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return _banner(ref); + }, + ); + } + + _banner(SCAppGeneralManager ref) { + if (ref.exploreBanners.isEmpty) { + return Container(); + } + return Container( + child: Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + CarouselSlider( + options: CarouselOptions( + height: 118.w, + autoPlay: true, + // 启用自动播放 + enlargeCenterPage: false, + // 居中放大当前页面 + enableInfiniteScroll: true, + // 启用无限循环 + autoPlayAnimationDuration: Duration(milliseconds: 800), + // 自动播放动画时长 + viewportFraction: 1, + // 视口分数 + onPageChanged: (index, reason) { + _currentIndex = index; + setState(() {}); + }, + ), + items: + ref.exploreBanners.map((item) { + return GestureDetector( + child: netImage( + url: item.cover ?? "", + height: 118.w, + fit: BoxFit.fill, + borderRadius: BorderRadius.circular(12.w), + ), + onTap: () { + print('ads:${item.toJson()}'); + SCBannerUtils.openBanner(item,context); + }, + ); + }).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: + ref.exploreBanners.asMap().entries.map((entry) { + return Container( + width: 6.0, + height: 6.0, + margin: EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 4.0, + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + _currentIndex == entry.key + ? Colors.blue + : Colors.white, + ), + ); + }).toList(), + ), + ], + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/floating/floating_game_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_game_screen_widget.dart new file mode 100644 index 0000000..54f717e --- /dev/null +++ b/lib/ui_kit/widgets/room/floating/floating_game_screen_widget.dart @@ -0,0 +1,412 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/main.dart'; +import 'package:marquee/marquee.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +class FloatingGameScreenWidget extends StatefulWidget { + final SCFloatingMessage message; + final VoidCallback onAnimationCompleted; // 动画完成回调 + + const FloatingGameScreenWidget({ + Key? key, + required this.message, + required this.onAnimationCompleted, + }) : super(key: key); + + @override + _FloatingGameScreenWidgetState createState() => + _FloatingGameScreenWidgetState(); +} + +class _FloatingGameScreenWidgetState extends State + with TickerProviderStateMixin { + Debouncer debouncer = Debouncer(); + late AnimationController _controller; + late Animation _firstAnimation; // 第一段:从右到中 + late Animation _secondAnimation; // 第二段:从中到左 + late AnimationController _swipeController; // 新增:滑动动画控制器 + late Animation _swipeAnimation; // 新增:滑动动画 + + bool _firstPhaseCompleted = false; + bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 + + ///游戏赢的金币按数量分等级,不同等级特效背景也不一样 + int level = 1; + + @override + void initState() { + super.initState(); + if ((widget.message.coins ?? 0) > 14999 && + (widget.message.coins ?? 0) < 50000) { + level = 1; + } else if ((widget.message.coins ?? 0) > 49999 && + (widget.message.coins ?? 0) < 100000) { + level = 2; + } else if ((widget.message.coins ?? 0) > 99999 && + (widget.message.coins ?? 0) < 300000) { + level = 3; + } else if ((widget.message.coins ?? 0) > 299999 && + (widget.message.coins ?? 0) < 500000) { + level = 4; + } else if ((widget.message.coins ?? 0) > 499999) { + level = 5; + } + + // 主动画控制器 + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), // 每段动画1.5秒 + vsync: this, + ); + + // 滑动动画控制器 + _swipeController = AnimationController( + duration: const Duration(milliseconds: 550), // 滑动动画500ms + vsync: this, + ); + + // 监听滑动动画完成 + _swipeController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onAnimationCompleted(); + } + }); + + // 第一段动画:从右侧飘到中间 + _firstAnimation = Tween( + begin: const Offset(1.5, 0.0), // 从屏幕右侧外开始 + end: Offset.zero, // 到屏幕中心 + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + // 第二段动画:从中间飘到左侧 + _secondAnimation = Tween( + begin: Offset.zero, // 从屏幕中心开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + // 滑动动画:从当前位置滑动到左侧屏幕外 + _swipeAnimation = Tween( + begin: Offset.zero, // 从当前位置开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation( + parent: _swipeController, + curve: Curves.easeIn, + )); + + // 延迟启动动画 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _startAnimationSequence(); + } + }); + } + + void _startAnimationSequence() async { + // 第一阶段:从右到中 + _controller.reset(); + await _controller.forward(); + + // 停顿2秒(你代码中是3秒,这里保持原样) + await Future.delayed(const Duration(seconds: 3)); + + if (!mounted) return; + + // 第二阶段:从中到左 + setState(() { + _firstPhaseCompleted = true; + }); + _controller.reset(); + await _controller.forward(); + + if (mounted && !_isSwipeAnimating) { + widget.onAnimationCompleted(); + } + } + + // 处理向左滑动 + void _handleSwipeLeft() { + if (_isSwipeAnimating) return; + + setState(() { + _isSwipeAnimating = true; + }); + + // 停止主动画 + _controller.stop(); + + // 启动滑动动画 + _swipeController.reset(); + _swipeController.forward(); + } + + String _coinsVaFormat(num coins) { + int value = coins.toInt(); + if (value > 99999) { + return "${(value / 1000).toStringAsFixed(0)}k"; + } + return "$value"; + } + + @override + void dispose() { + _controller.dispose(); + _swipeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _buildGameAnimation(); + } + + Widget _buildGameAnimation() { + // 如果正在执行滑动动画,使用滑动动画 + if (_isSwipeAnimating) { + return SlideTransition( + position: _swipeAnimation, + child: _buildContent(), + ); + } + + // 否则使用原来的动画 + return SlideTransition( + position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation, + child: _buildContent(), + ); + } + + Widget _buildContent() { + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true, + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl + ? -velocity + : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + ///向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: Align( + alignment: Alignment.topCenter, + child: Directionality( + textDirection: TextDirection.ltr, + child: Container( + alignment: Alignment.topCenter, + height: 120.w, + width: 325.w, + margin: EdgeInsets.only(top: 10.w), + child: Stack( + alignment: + level == 5 + ? AlignmentDirectional.topCenter + : AlignmentDirectional.center, + children: [ + if (widget.message.userAvatarUrl?.isNotEmpty ?? false) + _buildHead(), + Image.asset("sc_images/index/sc_icon_gamebroad_lv${level}_bg.webp"), + Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + SizedBox(width: level < 2 ? 82.w : 95.w), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: + level < 2 + ? 22.w + : (level == 5 ? 31.w : 25.w), + ), + Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: 55.w, + maxHeight: 18.w, + ), + child: + (widget.message.userName?.length ?? + 0) > + 8 + ? Marquee( + text: + widget.message.userName ?? + "", + style: TextStyle( + fontSize: 12.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: + TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration( + seconds: 1, + ), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: + Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: + Curves.easeOut, + ) + : Text( + widget.message.userName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: + TextDecoration.none, + ), + ), + ), + text( + "ID:${widget.message.userId} ", + textColor: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12.sp, + ), + ], + ), + Transform.translate( + offset: Offset( + 0, + level < 2 ? -5 : (level == 5 ? -5 : 0), + ), + child: Row( + children: [ + text( + SCAppLocalizations.of(context)!.get, + textColor: Colors.orange, + fontWeight: FontWeight.w900, + fontSize: 16.sp, + fontStyle: FontStyle.italic + ), + buildNumForGame( + _coinsVaFormat( + widget.message.coins ?? 0, + ), + size: + (widget.message.coins ?? 0) > 99999 + ? 26.w + : 20.w, + ), + ], + ), + ), + ], + ), + ], + ), + ), + Container( + margin: EdgeInsets.only(top: level < 2 ? 10.w : 21.w), + child: netImage( + url: widget.message.giftUrl ?? "", + width: 28.w, + borderRadius: BorderRadius.all(Radius.circular(3.0)), + ), + ), + SizedBox(width: level < 2 ? 63.w : 65.w), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildHead() { + if (level == 1) { + return PositionedDirectional( + start: 43.w, + child: Container( + margin: EdgeInsets.only(top: 12.w), + child: head(url: widget.message.userAvatarUrl ?? "", width: 40.w), + ), + ); + } else if (level == 2) { + return PositionedDirectional( + start: 10.w, + child: Container( + margin: EdgeInsets.only(top: 25.w), + child: head(url: widget.message.userAvatarUrl ?? "", width: 70.w), + ), + ); + } else if (level == 3) { + return PositionedDirectional( + start: 10.w, + child: Container( + margin: EdgeInsets.only(top: 25.w), + child: head(url: widget.message.userAvatarUrl ?? "", width: 70.w), + ), + ); + } else if (level == 4) { + return PositionedDirectional( + start: 10.w, + child: Container( + margin: EdgeInsets.only(top: 25.w), + child: head(url: widget.message.userAvatarUrl ?? "", width: 70.w), + ), + ); + } else if (level == 5) { + return PositionedDirectional( + start: 22.w, + top: 20.w, + child: Container( + child: head(url: widget.message.userAvatarUrl ?? "", width: 48.w), + ), + ); + } + return Container(); + } +} \ No newline at end of file diff --git a/lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart new file mode 100644 index 0000000..5caf91a --- /dev/null +++ b/lib/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart @@ -0,0 +1,297 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/main.dart'; +import 'package:marquee/marquee.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +class FloatingGiftScreenWidget extends StatefulWidget { + final SCFloatingMessage message; + final VoidCallback onAnimationCompleted; // 动画完成回调 + + const FloatingGiftScreenWidget({ + Key? key, + required this.message, + required this.onAnimationCompleted, + }) : super(key: key); + + @override + _FloatingGiftScreenWidgetState createState() => + _FloatingGiftScreenWidgetState(); +} + +class _FloatingGiftScreenWidgetState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _offsetAnimation; + late AnimationController _swipeController; // 新增:滑动动画控制器 + late Animation _swipeAnimation; // 新增:滑动动画 + Debouncer debouncer = Debouncer(); + + bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 + + @override + void initState() { + super.initState(); + + // 主动画控制器 + _controller = AnimationController( + duration: const Duration(seconds: 5), + vsync: this, + ); + + // 滑动动画控制器 + _swipeController = AnimationController( + duration: const Duration(milliseconds: 550), // 滑动动画500ms + vsync: this, + ); + + // 监听滑动动画完成 + _swipeController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onAnimationCompleted(); + } + }); + + // 主动画:从右向左移动 + _offsetAnimation = Tween( + begin: const Offset(1.0, 0.0), + end: const Offset(-1.0, 0.0), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + + // 滑动动画:从当前位置快速向左滑出 + _swipeAnimation = Tween( + begin: Offset.zero, // 从当前位置开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn)); + + // 监听主动画完成 + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed && !_isSwipeAnimating) { + widget.onAnimationCompleted(); + } + }); + + // 延迟启动动画,确保组件已经完全构建 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.forward(); + } + }); + } + + // 处理向左滑动 + void _handleSwipeLeft() { + if (_isSwipeAnimating) return; + + setState(() { + _isSwipeAnimating = true; + }); + + // 停止主动画 + _controller.stop(); + + // 启动滑动动画 + _swipeController.reset(); + _swipeController.forward(); + } + + @override + void dispose() { + _controller.dispose(); + _swipeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _buildGiftAnimation(); + } + + ///礼物总价钱达到10000 + Widget _buildGiftAnimation() { + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl ? -velocity : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + // 向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: SlideTransition( + position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, + child: Container( + alignment: Alignment.center, + height: 54.w, + width: 320.w, + margin: EdgeInsets.only(top: 20.w), + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_gift_flosc_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + // 可以根据消息模型丰富内容,例如显示头像 + if (widget.message.userAvatarUrl?.isNotEmpty ?? false) + head(url: widget.message.userAvatarUrl ?? "", width: 42.w), + SizedBox(width: 4.w), + Expanded( + child: Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: 48.w, + maxHeight: 21.w, + ), + child: + (widget.message.userName?.length ?? 0) > 6 + ? Marquee( + text: widget.message.userName ?? "", + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + widget.message.userName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + text( + SCAppLocalizations.of(context)!.sendTo, + textColor: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.bold, + ), + Container( + constraints: BoxConstraints( + maxWidth: 48.w, + maxHeight: 21.w, + ), + child: + (widget.message.toUserName?.length ?? 0) > 6 + ? Marquee( + text: widget.message.toUserName ?? "", + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + widget.message.toUserName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ), + netImage(url: widget.message.giftUrl ?? "", width: 32.w), + SizedBox(width: 3.w), + Image.asset( + "sc_images/room/sc_icon_x.png", + width: (widget.message.number ?? 0) > 99 ? 15.w : 18.w, + ), + buildNum( + "${widget.message.number}", + size: (widget.message.number ?? 0) > 99 ? 15.w : 18.w, + ), + ], + ), + ), + ), + ); + } + + String _getLuckGiftFloatBg() { + if ((widget.message.coins ?? 0) > 4999 && + (widget.message.coins ?? 0) < 10000) { + return "sc_images/room/sc_icon_luck_gift_flosc_bg1.png"; + } else if ((widget.message.coins ?? 0) > 9999 && + (widget.message.coins ?? 0) < 75000) { + return "sc_images/room/sc_icon_luck_gift_flosc_bg2.png"; + } else if ((widget.message.coins ?? 0) > 74999 && + (widget.message.coins ?? 0) < 150000) { + return "sc_images/room/sc_icon_luck_gift_flosc_bg3.png"; + } else if ((widget.message.coins ?? 0) > 149999 && + (widget.message.coins ?? 0) < 1500000) { + return "sc_images/room/sc_icon_luck_gift_flosc_bg4.png"; + } else if ((widget.message.coins ?? 0) > 1499999) { + return "sc_images/room/sc_icon_luck_gift_flosc_bg5.png"; + } + + return ""; + } +} diff --git a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart new file mode 100644 index 0000000..e01a2bb --- /dev/null +++ b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart @@ -0,0 +1,468 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/main.dart'; +import 'package:marquee/marquee.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +class FloatingLuckGiftScreenWidget extends StatefulWidget { + final SCFloatingMessage message; + final VoidCallback onAnimationCompleted; // 动画完成回调 + + const FloatingLuckGiftScreenWidget({ + Key? key, + required this.message, + required this.onAnimationCompleted, + }) : super(key: key); + + @override + _FloatingLuckGiftScreenWidgetState createState() => + _FloatingLuckGiftScreenWidgetState(); +} + +class _FloatingLuckGiftScreenWidgetState + extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _offsetAnimation; + late AnimationController _swipeController; // 新增:滑动动画控制器 + late Animation _swipeAnimation; // 新增:滑动动画 + Debouncer debouncer = Debouncer(); + + bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 + + @override + void initState() { + super.initState(); + + // 主动画控制器 + _controller = AnimationController( + duration: const Duration(seconds: 5), + vsync: this, + ); + + // 滑动动画控制器 + _swipeController = AnimationController( + duration: const Duration(milliseconds: 550), // 滑动动画500ms + vsync: this, + ); + + // 监听滑动动画完成 + _swipeController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onAnimationCompleted(); + } + }); + + // 主动画:从右向左移动 + _offsetAnimation = Tween( + begin: const Offset(1.0, 0.0), + end: const Offset(-1.0, 0.0), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + + // 滑动动画:从当前位置快速向左滑出 + _swipeAnimation = Tween( + begin: Offset.zero, // 从当前位置开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn)); + + // 监听主动画完成 + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed && !_isSwipeAnimating) { + widget.onAnimationCompleted(); + } + }); + + // 延迟启动动画,确保组件已经完全构建 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.forward(); + } + }); + } + + // 处理向左滑动 + void _handleSwipeLeft() { + if (_isSwipeAnimating) return; + + setState(() { + _isSwipeAnimating = true; + }); + + // 停止主动画 + _controller.stop(); + + // 启动滑动动画 + _swipeController.reset(); + _swipeController.forward(); + } + + @override + void dispose() { + _controller.dispose(); + _swipeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _buildLuckGiftAnimation(); + } + + ///幸运礼物飘屏 + Widget _buildLuckGiftAnimation() { + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl ? -velocity : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + // 向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: SlideTransition( + position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, + child: Container( + alignment: Alignment.center, + height: 83.w, + width: 350.w, + child: Stack( + children: [ + Transform.flip( + flipX: SCGlobalConfig.lang == "ar" ? true : false, // 水平翻转 + flipY: false, // 垂直翻转设为 false + child: Image.asset( + "sc_images/room/sc_icon_luck_gift_flosc_n_bg.png", + fit: BoxFit.fill, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + Container( + margin: EdgeInsetsDirectional.only(top: 14.w, start: 1.w), + child: netImage( + url: widget.message.userAvatarUrl ?? "", + width: 52.w, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.w), + child: Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: 85.w, + maxHeight: 20.w, + ), + child: + (widget.message.userName?.length ?? 0) > 6 + ? Marquee( + text: "${widget.message.userName} ", + style: TextStyle( + fontSize: 13.sp, + color: Color(0xffFEF129), + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + letterSpacing: 0.1, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + "${widget.message.userName} ", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Color(0xffFEF129), + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: SCAppLocalizations.of(context)!.get, + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.1, + ), + ), + TextSpan( + text: + " ${widget.message.coins} ${SCAppLocalizations.of(context)!.coins3} ", + style: TextStyle( + fontSize: 12.sp, + color: Color(0xffFEF129), + fontWeight: FontWeight.bold, + letterSpacing: 0.1, + ), + ), + TextSpan( + text: + SCAppLocalizations.of( + context, + )!.fromLuckyGifts, + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.1, + ), + ), + ], + ), + textAlign: TextAlign.start, + strutStyle: StrutStyle( + height: 1.1, // 行高倍数 + fontWeight: FontWeight.bold, + forceStrutHeight: true, // 强制应用行高 + ), + ), + ), + SizedBox(width: 6.w), + Container( + width: 80.w, + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 2.w), + buildNumForGame( + "${widget.message.multiple ?? 0}", + size: 25.w, + ), + SizedBox(height: 3.w), + SCGlobalConfig.lang == "ar" + ? Image.asset( + "sc_images/room/sc_icon_times_text_ar.png", + height: 12.w, + ) + : Image.asset( + "sc_images/room/sc_icon_times_text_en.png", + height: 12.w, + ), + ], + ), + ), + SizedBox(width: 6.w), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + ///礼物总价钱达到10000 + Widget _buildGiftAnimation() { + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl ? -velocity : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + // 向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: SlideTransition( + position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, + child: Container( + alignment: Alignment.center, + height: 50.w, + width: 290.w, + margin: EdgeInsets.only(top: 20.w), + padding: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_gift_flosc_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + // 可以根据消息模型丰富内容,例如显示头像 + if (widget.message.userAvatarUrl?.isNotEmpty ?? false) + head(url: widget.message.userAvatarUrl ?? "", width: 42.w), + SizedBox(width: 4.w), + Expanded( + child: Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: 48.w, + maxHeight: 21.w, + ), + child: + (widget.message.userName?.length ?? 0) > 6 + ? Marquee( + text: widget.message.userName ?? "", + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + widget.message.userName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + text( + SCAppLocalizations.of(context)!.sendTo, + textColor: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.bold, + ), + Container( + constraints: BoxConstraints( + maxWidth: 48.w, + maxHeight: 21.w, + ), + child: + (widget.message.toUserName?.length ?? 0) > 6 + ? Marquee( + text: widget.message.toUserName ?? "", + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + widget.message.toUserName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ), + netImage(url: widget.message.giftUrl ?? "", width: 32.w), + SizedBox(width: 3.w), + Image.asset("sc_images/room/sc_icon_x.png", width: 18.w), + buildNum("${widget.message.number}", size: 18.w), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart new file mode 100644 index 0000000..17ca305 --- /dev/null +++ b/lib/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/main.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +class FloatingRoomRedenvelopeScreenWidget extends StatefulWidget { + final SCFloatingMessage message; + final VoidCallback onAnimationCompleted; // 动画完成回调 + + const FloatingRoomRedenvelopeScreenWidget({ + Key? key, + required this.message, + required this.onAnimationCompleted, + }) : super(key: key); + + @override + _FloatingRoomRedenvelopeScreenWidgetState createState() => + _FloatingRoomRedenvelopeScreenWidgetState(); +} + +class _FloatingRoomRedenvelopeScreenWidgetState + extends State + with TickerProviderStateMixin { + Debouncer debouncer = Debouncer(); + late AnimationController _controller; + late Animation _firstAnimation; // 第一段:从右到中 + late Animation _secondAnimation; // 第二段:从中到左 + late AnimationController _swipeController; // 新增:滑动动画控制器 + late Animation _swipeAnimation; // 新增:滑动动画 + + bool _firstPhaseCompleted = false; + bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 + + @override + void initState() { + super.initState(); + // 主动画控制器 + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), // 每段动画1.5秒 + vsync: this, + ); + + // 滑动动画控制器 + _swipeController = AnimationController( + duration: const Duration(milliseconds: 550), // 滑动动画500ms + vsync: this, + ); + + // 监听滑动动画完成 + _swipeController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onAnimationCompleted(); + } + }); + + // 第一段动画:从右侧飘到中间 + _firstAnimation = Tween( + begin: const Offset(1.5, 0.0), // 从屏幕右侧外开始 + end: Offset.zero, // 到屏幕中心 + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + // 第二段动画:从中间飘到左侧 + _secondAnimation = Tween( + begin: Offset.zero, // 从屏幕中心开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + // 滑动动画:从当前位置滑动到左侧屏幕外 + _swipeAnimation = Tween( + begin: Offset.zero, // 从当前位置开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation( + parent: _swipeController, + curve: Curves.easeIn, + )); + + // 延迟启动动画 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _startAnimationSequence(); + } + }); + } + void _startAnimationSequence() async { + // 第一阶段:从右到中 + _controller.reset(); + await _controller.forward(); + + // 停顿2秒(你代码中是3秒,这里保持原样) + await Future.delayed(const Duration(seconds: 3)); + + if (!mounted) return; + + // 第二阶段:从中到左 + setState(() { + _firstPhaseCompleted = true; + }); + _controller.reset(); + await _controller.forward(); + + if (mounted && !_isSwipeAnimating) { + widget.onAnimationCompleted(); + } + } + + + @override + void dispose() { + _controller.dispose(); + _swipeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // 如果正在执行滑动动画,使用滑动动画 + if (_isSwipeAnimating) { + return SlideTransition( + position: _swipeAnimation, + child: _buildRedenvelopeAnimation(), + ); + } + + // 否则使用原来的动画 + return SlideTransition( + position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation, + child: _buildRedenvelopeAnimation(), + ); + } + + Widget _buildRedenvelopeAnimation() { + // 使用Positioned.fill确保飘屏覆盖全屏宽度,并定位在特定高度(如顶部100像素) + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + needOpenRedenvelope: true, + redPackId: widget.message.toUserId ?? "", + fromFloting: true + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl + ? -velocity + : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + ///向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: Align( + alignment: Alignment.topCenter, + child: Directionality( + textDirection: TextDirection.ltr, + child: Container( + alignment: Alignment.topCenter, + height: 120.w, + width: 325.w, + margin: EdgeInsets.only(top: 10.w), + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + PositionedDirectional( + start: 5.w, + top: 31.w, + child: head( + url: widget.message.userAvatarUrl ?? "", + width: 63.w, + ), + ), + Image.asset("sc_images/room/sc_icon_redenvelope_broad_bg.webp"), + PositionedDirectional( + start: 70.w, + top: 52.w, + child: text( + SCAppLocalizations.of(context)!.openTheTreasureChest, + textColor: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14.sp, + ), + ), + ], + ), + ), + ), + ), + ); + } + + // 处理向左滑动 + void _handleSwipeLeft() { + if (_isSwipeAnimating) return; + + setState(() { + _isSwipeAnimating = true; + }); + + // 停止主动画 + _controller.stop(); + + // 启动滑动动画 + _swipeController.reset(); + _swipeController.forward(); + } +} diff --git a/lib/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart new file mode 100644 index 0000000..9a5624c --- /dev/null +++ b/lib/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart @@ -0,0 +1,288 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/main.dart'; +import 'package:marquee/marquee.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +class FloatingRoomRocketScreenWidget extends StatefulWidget { + final SCFloatingMessage message; + final VoidCallback onAnimationCompleted; // 动画完成回调 + + const FloatingRoomRocketScreenWidget({ + Key? key, + required this.message, + required this.onAnimationCompleted, + }) : super(key: key); + + @override + _FloatingRoomRocketScreenWidgetState createState() => + _FloatingRoomRocketScreenWidgetState(); +} + +class _FloatingRoomRocketScreenWidgetState + extends State + with TickerProviderStateMixin { + Debouncer debouncer = Debouncer(); + late AnimationController _controller; + late Animation _firstAnimation; // 第一段:从右到中 + late Animation _secondAnimation; // 第二段:从中到左 + late AnimationController _swipeController; // 新增:滑动动画控制器 + late Animation _swipeAnimation; // 新增:滑动动画 + + bool _firstPhaseCompleted = false; + bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 + + @override + void initState() { + super.initState(); + // 主动画控制器 + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), // 每段动画1.5秒 + vsync: this, + ); + + // 滑动动画控制器 + _swipeController = AnimationController( + duration: const Duration(milliseconds: 550), // 滑动动画500ms + vsync: this, + ); + + // 监听滑动动画完成 + _swipeController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onAnimationCompleted(); + } + }); + + // 第一段动画:从右侧飘到中间 + _firstAnimation = Tween( + begin: const Offset(1.5, 0.0), // 从屏幕右侧外开始 + end: Offset.zero, // 到屏幕中心 + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + // 第二段动画:从中间飘到左侧 + _secondAnimation = Tween( + begin: Offset.zero, // 从屏幕中心开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + // 滑动动画:从当前位置滑动到左侧屏幕外 + _swipeAnimation = Tween( + begin: Offset.zero, // 从当前位置开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation( + parent: _swipeController, + curve: Curves.easeIn, + )); + + // 延迟启动动画 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _startAnimationSequence(); + } + }); + } + void _startAnimationSequence() async { + // 第一阶段:从右到中 + _controller.reset(); + await _controller.forward(); + + // 停顿2秒(你代码中是3秒,这里保持原样) + await Future.delayed(const Duration(seconds: 3)); + + if (!mounted) return; + + // 第二阶段:从中到左 + setState(() { + _firstPhaseCompleted = true; + }); + _controller.reset(); + await _controller.forward(); + + if (mounted && !_isSwipeAnimating) { + widget.onAnimationCompleted(); + } + } + + + @override + void dispose() { + _controller.dispose(); + _swipeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // 如果正在执行滑动动画,使用滑动动画 + if (_isSwipeAnimating) { + return SlideTransition( + position: _swipeAnimation, + child: _buildRocketAnimation(), + ); + } + + // 否则使用原来的动画 + return SlideTransition( + position: _firstPhaseCompleted ? _secondAnimation : _firstAnimation, + child: _buildRocketAnimation(), + ); + } + + Widget _buildRocketAnimation() { + // 使用Positioned.fill确保飘屏覆盖全屏宽度,并定位在特定高度(如顶部100像素) + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl + ? -velocity + : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + ///向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: Align( + alignment: Alignment.topCenter, + child: Directionality( + textDirection: TextDirection.ltr, + child: Container( + alignment: Alignment.topCenter, + height: 128.w, + width: 290.w, + margin: EdgeInsets.only(top: 10.w), + padding: EdgeInsetsDirectional.only(start: 3.w, end: 25.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/index/sc_icon_rocket_broad_bg_lv${widget.message.rocketLevel}.webp", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + SizedBox(width: 5.w), + Container( + child: head( + url: widget.message.userAvatarUrl ?? "", + width: 66.w, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox(height: 6.w), + Row( + children: [ + SizedBox( + width: 60.w, + height: 22.w, + child: Marquee( + text: "ID:${widget.message.userId} ", + style: TextStyle( + fontSize: 15.sp, + color: Colors.yellow, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w900, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ), + ), + text( + SCAppLocalizations.of(context)!.launchedARocket, + textColor: Colors.white, + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic, + fontSize: 13.sp, + ), + ], + ), + Transform.translate( + offset: Offset(0, -5), + child: text( + SCAppLocalizations.of(context)!.letGoToWatch, + textColor: Colors.white, + fontWeight: FontWeight.w900, + fontSize: 17.sp, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + // 处理向左滑动 + void _handleSwipeLeft() { + if (_isSwipeAnimating) return; + + setState(() { + _isSwipeAnimating = true; + }); + + // 停止主动画 + _controller.stop(); + + // 启动滑动动画 + _swipeController.reset(); + _swipeController.forward(); + } +} diff --git a/lib/ui_kit/widgets/room/invite/invite_room_dialog.dart b/lib/ui_kit/widgets/room/invite/invite_room_dialog.dart new file mode 100644 index 0000000..9d6aeb6 --- /dev/null +++ b/lib/ui_kit/widgets/room/invite/invite_room_dialog.dart @@ -0,0 +1,138 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; +import 'package:yumi/main.dart'; + +class InviteRoomDialog extends StatefulWidget { + SCFloatingMessage msg; + + InviteRoomDialog(this.msg); + + @override + _InviteRoomDialogState createState() => _InviteRoomDialogState(); +} + +class _InviteRoomDialogState extends State { + Timer? _timer; + int countdown = 10; + + @override + void initState() { + super.initState(); + _startTimer(); + } + + /// 启动倒计时 + void _startTimer() { + // 计时器(`Timer`)组件的定期(`periodic`)构造函数,创建一个新的重复计时器。 + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + if (countdown == 0) { + _cancelTimer(); + SmartDialog.dismiss(tag: "showInviteRoom"); + return; + } + countdown--; + setState(() {}); + }); + } + + /// 取消倒计时的计时器。 + void _cancelTimer() { + // 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。 + _timer?.cancel(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + width: ScreenUtil().screenWidth * 0.8, + padding: EdgeInsets.all(15.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 3.w), + Row( + children: [ + Spacer(), + text("$countdown", textColor: Colors.black87, fontSize: 14.sp), + SizedBox(width: 3.w), + GestureDetector( + child: Image.asset( + "sc_images/general/sc_icon_clear_c.png", + width: 18.w, + ), + onTap: () { + SmartDialog.dismiss(tag: "showInviteRoom"); + }, + ), + ], + ), + netImage( + url: widget.msg.userAvatarUrl ?? "", + width: 80.w, + height: 80.w, + shape: BoxShape.circle, + ), + SizedBox(height: 10.w), + Row( + children: [ + Expanded( + child: text( + SCAppLocalizations.of(context)!.inviteGoRoomTips, + textColor: Colors.black87, + fontSize: 15.sp, + maxLines: 5, + ), + ), + ], + ), + SizedBox(height: 10.w), + SCDebounceWidget( + child: Container( + height: 45.w, + margin: EdgeInsets.symmetric(horizontal: 35.w), + alignment: Alignment.center, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_inv_go_btn.png"), + fit: BoxFit.fill, + ), + ), + child: text( + SCAppLocalizations.of(context)!.enterTheRoom, + textColor: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showInviteRoom"); + SCRoomUtils.goRoom(widget.msg.roomId ?? "", navigatorKey.currentState!.context); + }, + ), + ], + ), + ); + } +} + +class _timer {} diff --git a/lib/ui_kit/widgets/room/recommend_banner_view.dart b/lib/ui_kit/widgets/room/recommend_banner_view.dart new file mode 100644 index 0000000..6a172bf --- /dev/null +++ b/lib/ui_kit/widgets/room/recommend_banner_view.dart @@ -0,0 +1,95 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/shared/tools/sc_banner_utils.dart'; +import 'package:yumi/shared/tools/sc_string_utils.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/ui_kit/widgets/pop/pop_route.dart'; + +class RecommendBannerView extends StatefulWidget { + @override + _RecommendBannerViewState createState() => _RecommendBannerViewState(); +} + +class _RecommendBannerViewState extends State { + int _currentIndex = 0; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return _banner(ref); + }, + ); + } + + _banner(SCAppGeneralManager ref) { + if (ref.homeBanners.isEmpty) { + return Container( + height: 118.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Color(0xfff1f1f1), + ), + ); + } + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + CarouselSlider( + options: CarouselOptions( + height: 118.w, + autoPlay: true, + // 启用自动播放 + enlargeCenterPage: false, + // 居中放大当前页面 + enableInfiniteScroll: true, + // 启用无限循环 + autoPlayAnimationDuration: Duration(milliseconds: 800), + // 自动播放动画时长 + viewportFraction: 1, + // 视口分数 + onPageChanged: (index, reason) { + _currentIndex = index; + setState(() {}); + }, + ), + items: + ref.homeBanners.map((item) { + return GestureDetector( + child: netImage( + url: item.cover ?? "", + height: 118.w, + fit: BoxFit.fill, + borderRadius: BorderRadius.circular(12.w), + ), + onTap: () { + print('ads:${item.toJson()}'); + SCBannerUtils.openBanner(item,context); + }, + ); + }).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: + ref.homeBanners.asMap().entries.map((entry) { + return Container( + width: 6.0, + height: 6.0, + margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + _currentIndex == entry.key ? Colors.blue : Colors.white, + ), + ); + }).toList(), + ), + ], + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_banner_view.dart b/lib/ui_kit/widgets/room/room_banner_view.dart new file mode 100644 index 0000000..d40850f --- /dev/null +++ b/lib/ui_kit/widgets/room/room_banner_view.dart @@ -0,0 +1,90 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/shared/tools/sc_banner_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; + +class RoomBannerView extends StatefulWidget { + @override + _RoomBannerViewState createState() => _RoomBannerViewState(); +} + +class _RoomBannerViewState extends State { + int _currentIndex = 0; + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return _banner(ref); + }, + ); + } + + _banner(SCAppGeneralManager ref) { + return ref.roomBanners.isEmpty + ? Container() + : SizedBox( + width: 46.w, + child: Column( + children: [ + CarouselSlider( + options: CarouselOptions( + height: 46.w, + autoPlay: true, + // 启用自动播放 + enlargeCenterPage: false, + // 居中放大当前页面 + aspectRatio: 1 / 1, + // 宽高比 + enableInfiniteScroll: true, + // 启用无限循环 + autoPlayAnimationDuration: Duration(milliseconds: 800), + // 自动播放动画时长 + viewportFraction: 1, + // 视口分数 + onPageChanged: (index, reason) { + _currentIndex = index; + setState(() {}); + }, + ), + items: + ref.roomBanners.map((item) { + return GestureDetector( + child: netImage(url: item.smallCover ?? ""), + onTap: () { + print('ads:${item.toJson()}'); + SCBannerUtils.openBanner(item, context); + }, + ); + }).toList(), + ), + SingleChildScrollView( + scrollDirection:Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: + ref.roomBanners.asMap().entries.map((entry) { + return Container( + width: 4.0, + height: 4.0, + margin: EdgeInsetsDirectional.fromSTEB(2, 3, 2, 0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + _currentIndex == entry.key + ? Colors.blue + : Colors.white, + ), + ); + }).toList(), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_bottom_widget.dart b/lib/ui_kit/widgets/room/room_bottom_widget.dart new file mode 100644 index 0000000..0566066 --- /dev/null +++ b/lib/ui_kit/widgets/room/room_bottom_widget.dart @@ -0,0 +1,233 @@ +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/widgets/room/room_menu_dialog.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/modules/gift/gift_page.dart'; + +import '../../../app/routes/sc_fluro_navigator.dart'; +import '../../../modules/index/main_route.dart'; +import '../../../services/audio/rtm_manager.dart'; +import '../../components/sc_debounce_widget.dart'; + +class RoomBottomWidget extends StatefulWidget { + @override + _RoomBottomWidgetState createState() => _RoomBottomWidgetState(); +} + +class _RoomBottomWidgetState extends State { + int roomMenuStime1 = 0; + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + Container( + height: 55.w, + margin: EdgeInsetsDirectional.only(start: 25.w), + child: Row( + children: [ + SCDebounceWidget( + onTap: () { + if (SCRoomUtils.touristCanMsg(context)) { + Navigator.push( + context, + PopRoute(child: RoomMsgInput()), + ); + } + }, child: Image.asset( + "sc_images/room/icon_room_input_t.png", + width: 30.w, + height: 30.w, + ), + ), + Spacer(), + Consumer( + builder: (context, ref, child) { + return _mic1(ref); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Image.asset( + "sc_images/room/sc_icon_botton_menu.png", + width: 30.w, + height: 30.w, + fit: BoxFit.contain, + ), + onTap: () { + SmartDialog.show( + tag: "showRoomMenuDialog", + alignment: Alignment.bottomCenter, + debounce: true, + animationType: SmartAnimationType.fade, + maskColor: Colors.transparent, + clickMaskDismiss: true, + builder: (_) { + return RoomMenuDialog(roomMenuStime1, (eTime) { + roomMenuStime1 = eTime; + }); + }, + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Selector( + selector: (c, p) => p.allUnReadCount, + shouldRebuild: (prev, next) => prev != next, + builder: (_, allUnReadCount, __) { + return allUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${allUnReadCount > 99 ? "99+" : allUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Image.asset( + "sc_images/room/sc_icon_botton_message.png", + width: 30.w, + height: 30.w, + fit: BoxFit.contain, + ), + ) + : Image.asset( + "sc_images/room/sc_icon_botton_message.png", + width: 30.w, + height: 30.w, + fit: BoxFit.contain, + ); + }, + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.message}?isFromRoom=true", + ); + }, + ), + SizedBox(width: 15.w), + ], + ), + ), + PositionedDirectional( + bottom: 8.w, + child: SCDebounceWidget( + onTap: () { + SmartDialog.show( + tag: "showGiftControl", + alignment: Alignment.bottomCenter, + maskColor: Colors.transparent, + animationType: SmartAnimationType.fade, + clickMaskDismiss: true, + builder: (_) { + return GiftPage(); + }, + ); + }, + child: Image.asset( + "sc_images/room/sc_icon_botton_gift.png", + width: 45.w, + height: 45.w, + fit: BoxFit.contain, + ), + ), + ), + ], + ); + } + + _mic1(RtcProvider provider) { + ///默认不显示 + bool show = false; + + ///在麦上 + provider.roomWheatMap.forEach((k, v) { + if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { + show = true; + } + }); + + return Visibility( + visible: show, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + provider.isMic = !provider.isMic; + + ///没被禁麦才显示 + provider.roomWheatMap.forEach((k, v) { + if (v.user?.id == + AccountStorage().getCurrentUser()?.userProfile?.id && + !provider.roomWheatMap[k]!.micMute!) { + if (!provider.isMic) { + Provider.of( + context, + listen: false, + ).engine?.adjustRecordingSignalVolume(100); + Provider.of( + context, + listen: false, + ).engine?.setClientRole( + role: ClientRoleType.clientRoleBroadcaster, + ); + } else { + if (Provider.of( + context, + listen: false, + ).isMusicPlaying) { + Provider.of( + context, + listen: false, + ).engine?.adjustRecordingSignalVolume(0); + } else { + Provider.of( + context, + listen: false, + ).engine?.setClientRole( + role: ClientRoleType.clientRoleAudience, + ); + Provider.of( + context, + listen: false, + ).engine?.muteLocalAudioStream(provider.isMic); + } + } + } + }); + }); + }, + child: Padding( + padding: EdgeInsets.only(right: 12.w), + child: Container( + width: 30.w, + height: 30.w, + alignment: Alignment.center, + // decoration: BoxDecoration( + // color: Colors.white.withOpacity(0.2), shape: BoxShape.circle + // ), + child: Image.asset( + "sc_images/room/${provider.isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png", + width: 30.w, + fit: BoxFit.fill, + gaplessPlayback: true, + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_charm_page.dart b/lib/ui_kit/widgets/room/room_charm_page.dart new file mode 100644 index 0000000..735e16f --- /dev/null +++ b/lib/ui_kit/widgets/room/room_charm_page.dart @@ -0,0 +1,177 @@ +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; + +class RoomCharmPage extends StatefulWidget { + @override + _RoomCharmPageState createState() => _RoomCharmPageState(); +} + +class _RoomCharmPageState extends State { + JoinRoomRes? room; + + @override + void initState() { + super.initState(); + room = Provider.of(context!, listen: false).currenRoom; + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.w), + topRight: Radius.circular(12.w), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + height: 260.w, + decoration: BoxDecoration( + color: Colors.black54, + ), + child: Column( + children: [ + SizedBox(height: 6.w), + Row( + children: [ + SizedBox(width: 27.w), + Spacer(), + text( + SCAppLocalizations.of(context)!.charm, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + Spacer(), + GestureDetector( + behavior: HitTestBehavior.opaque, + child: Container( + color: Colors.transparent, + padding: EdgeInsets.all(3.w), + child: Icon( + Icons.close, + size: 22.w, + color: Colors.white, + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showRoomCharm"); + }, + ), + SizedBox(width: 5.w), + ], + ), + SizedBox(height: 10.w), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 15.w), + text( + SCAppLocalizations.of(context)!.gameRules, + fontSize: 15.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + ], + ), + SizedBox(height: 10.w), + Container( + margin: EdgeInsets.symmetric(horizontal: 15.w), + alignment: AlignmentDirectional.centerStart, + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 10.w, + ), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(12), + ), + child: text( + SCAppLocalizations.of(context)!.charmGameRulesTips, + textColor: Colors.white54, + maxLines: 5, + fontWeight: FontWeight.w600, + fontSize: 14.sp, + ), + ), + SizedBox(height: 20.w), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + width: 130.w, + height: 45.w, + decoration: BoxDecoration( + color: + (room?.roomProfile?.roomSetting?.showHeartbeat ?? + false) + ? Colors.white38 + : SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(30.w), + ), + child: text( + (room?.roomProfile?.roomSetting?.showHeartbeat ?? false) + ? SCAppLocalizations.of(context)!.stop + : SCAppLocalizations.of(context)!.start, + fontWeight: FontWeight.bold, + fontSize: 18.sp, + ), + ), + onTap: () { + SCChatRoomRepository() + .updateRoomSetting( + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + "", + Provider.of(context, listen: false) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomAccount ?? + "", + context, + showHeartbeat: + !((room + ?.roomProfile + ?.roomSetting + ?.showHeartbeat) ?? + false), + ) + .whenComplete(() { + Provider.of( + context, + listen: false, + ).loadRoomInfo( + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + "", + ); + SmartDialog.dismiss(tag: "showRoomCharm"); + }); + }, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_head_widget.dart b/lib/ui_kit/widgets/room/room_head_widget.dart new file mode 100644 index 0000000..cf9c747 --- /dev/null +++ b/lib/ui_kit/widgets/room/room_head_widget.dart @@ -0,0 +1,245 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:marquee/marquee.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/modules/room/detail/room_detail_page.dart'; +import 'package:yumi/modules/room/voice_room_route.dart'; +import 'package:yumi/ui_kit/widgets/room/exit_min_room_page.dart'; +import 'package:yumi/ui_kit/widgets/room/sc_edit_room_announcement_page.dart'; + +class RoomHeadWidget extends StatefulWidget { + @override + _RoomHeadWidgetState createState() => _RoomHeadWidgetState(); +} + +class _RoomHeadWidgetState extends State { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + return Row( + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + showBottomInBottomDialog( + context!, + RoomDetailPage(provider.isFz()), + ); + }, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 6.w, + horizontal: 12.w, + ), + child: Row( + children: [ + Stack( + alignment: Alignment.center, + children: [ + netImage( + url: + provider + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomCover ?? + "", + width: 28.w, + height: 28.w, + borderRadius: BorderRadius.all( + Radius.circular(8.w), + ), + ), + Selector( + selector: (c, p) => p.roomIsMute, + shouldRebuild: (prev, next) => prev != next, + builder: (_, isMute, __) { + return isMute + ? ClipOval( + child: Container( + color: Colors.black54, + padding: EdgeInsets.all(2.w), + child: Image.asset( + "sc_images/room/sc_icon_mic_mute.png", + height: 18.w, + width: 18.w, + ), + ), + ) + : Container(); + }, + ), + ], + ), + SizedBox(width: 6.w), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: 10.w), + Container( + constraints: BoxConstraints( + maxWidth: 106.w, + maxHeight: 17.w, + ), + child: + (provider + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomName + ?.length ?? + 0) > + 10 + ? Marquee( + text: + provider + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomName ?? + "", + style: TextStyle( + fontSize: 14.sp, + color: Color(0xffffffff), + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 40.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + provider + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomName ?? + '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14.sp, + color: Color(0xffffffff), + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + + text( + "ID:${provider.currenRoom?.roomProfile?.userProfile?.getID()}", + fontSize: 13.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white70, + ), + ], + ), + SizedBox(width: 6.w), + provider.currenRoom?.roomProfile?.roomProfile?.userId != + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id + ? Selector( + selector: + (c, p) => + p.isFollowRoomRes?.followRoom ?? false, + shouldRebuild: (prev, next) => prev != next, + builder: (_, follow, __) { + return !follow + ? GestureDetector( + child: Image.asset( + "sc_images/room/sc_icon_room_follow_no.png", + width: 38.w, + height: 22.w, + ), + onTap: () { + provider.followCurrentVoiceRoom(); + }, + ) + : Container(); + }, + ) + : Container(), + ], + ), + ), + ), + ], + ), + Spacer(), + GestureDetector( + child: Image.asset( + "sc_images/room/sc_icon_room_edit_noti.png", + width: 32.w, + height: 32.w, + ), + onTap: () { + if (provider.isFz()) { + SCNavigatorUtils.push( + context, + "${VoiceRoomRoute.roomEdit}?need=false", + replace: false, + ); + } else { + SmartDialog.show( + tag: "showEditNotiPage", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + maskColor: Colors.black54, + builder: (_) { + return SCEditRoomAnnouncementPage(); + }, + ); + } + // provider.extRoom(); + }, + ), + SizedBox(width: 6.w), + GestureDetector( + child: Image.asset( + "sc_images/room/sc_icon_room_ext_min.png", + width: 32.w, + height: 32.w, + ), + onTap: () { + showCenterDialog( + context, + ExitMinRoomPage( + provider.isFz(), + provider.currenRoom?.roomProfile?.roomProfile?.id ?? "", + ), + barrierColor: Colors.black54, + ); + // provider.extRoom(); + }, + ), + SizedBox(width: 12.w), + ], + ); + }, + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_menu_dialog.dart b/lib/ui_kit/widgets/room/room_menu_dialog.dart new file mode 100644 index 0000000..8d45e26 --- /dev/null +++ b/lib/ui_kit/widgets/room/room_menu_dialog.dart @@ -0,0 +1,270 @@ +import 'dart:ui' as ui; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart'; +import 'package:yumi/main.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/shared/tools/sc_pick_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import '../../../app/routes/sc_fluro_navigator.dart'; +import '../../../modules/index/main_route.dart'; +import '../../../modules/room/voice_room_route.dart'; +import '../../../modules/store/store_route.dart'; +import '../../components/sc_debounce_widget.dart'; +import '../../components/sc_tts.dart'; +import '../../components/text/sc_text.dart'; + +class RoomMenuDialog extends StatefulWidget { + int roomMenuStime = 0; + Function(int eTime) callBack; + + RoomMenuDialog(this.roomMenuStime, this.callBack); + + @override + _RoomMenuDialogState createState() => _RoomMenuDialogState(); +} + +class _RoomMenuDialogState extends State { + List items1 = []; + List items2 = []; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + // items1.clear(); + // items1.add( + // RoomMenu( + // 0, + // SCAppLocalizations.of(context)!.music, + // "sc_icon_room_music_tag.png", + // ), + // ); + // items1.add( + // RoomMenu( + // 1, + // SCAppLocalizations.of(context)!.redEnvelope, + // "sc_icon_redpackg_tag.png", + // ), + // ); + // items1.add( + // RoomMenu(3, SCAppLocalizations.of(context)!.rps, "sc_icon_rps_tag.png"), + // ); + // items1.add( + // RoomMenu(2, SCAppLocalizations.of(context)!.dice, "sc_icon_dice_tag.png"), + // ); + // items1.add( + // RoomMenu( + // 4, + // SCAppLocalizations.of(context)!.luckNumber, + // "sc_icon_luck_number_tag.png", + // ), + // ); + items2.clear(); + if (Provider.of(context, listen: false).isFz()) { + items2.add( + RoomMenu( + 0, + SCAppLocalizations.of(context)!.micManagement, + "sc_icon_menu_mic_model_change.png", + ), + ); + } + + items2.add( + RoomMenu( + 1, + SCAppLocalizations.of(context)!.clearMessage, + "sc_icon_room_msg_clear.png", + ), + ); + items2.add( + RoomMenu( + 2, + SCAppLocalizations.of(context)!.picture, + "sc_icon_room_msg_pic.png", + ), + ); + if (Provider.of(context, listen: false).isFz()) { + items2.add( + RoomMenu( + 3, + SCAppLocalizations.of(context)!.settings, + "sc_icon_room_menu_settins.png", + ), + ); + } + items2.add( + RoomMenu( + 4, + SCAppLocalizations.of(context)!.sound2, + Provider.of(context, listen: false).roomIsMute + ? "sc_icon_mic_mute.png" + : "sc_icon_mic_open.png", + ), + ); + if (!Provider.of(context, listen: false).isFz()) { + items2.add( + RoomMenu( + 5, + SCAppLocalizations.of(context)!.report, + "sc_icon_room_report.png", + ), + ); + } + return SafeArea( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: ScreenUtil().screenWidth, + height: 200.w, + color: Color(0xff09372E).withOpacity(0.5), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 15.w), + GridView.builder( + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.only(bottom: 2.w), + shrinkWrap: true, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, // 每行3个项目 + crossAxisSpacing: 2, + mainAxisSpacing: 2, + childAspectRatio: 1, + ), + itemBuilder: (c, i) { + return _buildItem2(items2[i], i); + }, + itemCount: items2.length, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildItem2(RoomMenu item, int i) { + return SCDebounceWidget( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset("sc_images/room/${item.icon}", width: 45.w, height: 45.w), + SizedBox(height: 10.w), + text( + item.title, + fontSize: 12.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + lineHeight: 1.1, + letterSpacing: 0.1, + maxLines: 2, + textAlign: TextAlign.center, + ), + ], + ), + onTap: () async { + if (item.id == 0) { + SmartDialog.dismiss(tag: "showRoomMenuDialog"); + SmartDialog.show( + tag: "showRoomMicSwitch", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + debounce: true, + builder: (_) { + return RoomMicSwitchPage(); + }, + ); + } else if (item.id == 1) { + Provider.of(context, listen: false).clearMessage(); + SmartDialog.dismiss(tag: "showRoomMenuDialog"); + } else if (item.id == 2) { + if (SCRoomUtils.touristCanMsg(context)) { + SCPickUtils.pickImage(context, (bool success, String url) { + if (success) { + Provider.of( + context, + listen: false, + ).dispatchMessage( + Msg( + groupId: + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.roomAccount, + msg: url, + type: SCRoomMsgType.image, + role: + Provider.of( + context, + listen: false, + ).currenRoom?.entrants?.roles ?? + "", + user: AccountStorage().getCurrentUser()?.userProfile, + ), + addLocal: true, + ); + } + SmartDialog.dismiss(tag: "showRoomMenuDialog"); + }, neeCrop: false); + } + } else if (item.id == 3) { + SmartDialog.dismiss(tag: "showRoomMenuDialog"); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + "${VoiceRoomRoute.roomEdit}?need=false", + replace: false, + ); + } else if (item.id == 4) { + SmartDialog.dismiss(tag: "showRoomMenuDialog"); + Provider.of( + context, + listen: false, + ).toggleRemoteAudioMuteForAllUsers(); + } else if (item.id == 5) { + SmartDialog.dismiss(tag: "showRoomMenuDialog"); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + "${SCMainRoute.report}?type=room&tageId=${Provider.of(context, listen: false).currenRoom?.roomProfile?.roomProfile?.id}", + replace: false, + ); + } + }, + ); + } +} + +class RoomMenu { + int id = 0; + + String title = ""; + String icon = ""; + + RoomMenu(this.id, this.title, this.icon); +} diff --git a/lib/ui_kit/widgets/room/room_msg_input.dart b/lib/ui_kit/widgets/room/room_msg_input.dart new file mode 100644 index 0000000..f9f205f --- /dev/null +++ b/lib/ui_kit/widgets/room/room_msg_input.dart @@ -0,0 +1,212 @@ +import 'dart:ui' as ui; + +import 'package:extended_text_field/extended_text_field.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; + +///聊天输入框 +class RoomMsgInput extends StatefulWidget { + String? atTextContent; + + RoomMsgInput({this.atTextContent}); + + @override + _RoomMsgInputState createState() => _RoomMsgInputState(); +} + +class _RoomMsgInputState extends State { + bool showSend = false; + + FocusNode msgNode = FocusNode(); + + TextEditingController controller = TextEditingController(); + + @override + void initState() { + super.initState(); + if (widget.atTextContent != null) { + controller.value = TextEditingValue(text: widget.atTextContent ?? ""); + } + } + + @override + Widget build(BuildContext context) { + // FocusScope.of(context).requestFocus(msgNode); + return Scaffold( + backgroundColor: Colors.transparent, + body: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + ), + ), + float(context), + ], + ), + ); + } + + Widget float(BuildContext context) { + return Container( + decoration: BoxDecoration(color: Colors.black), + padding: EdgeInsets.symmetric(horizontal: 25.w, vertical: 12.w), + height: 56.0, + child: Container( + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(height(5)), + ), + child: Row( + children: [ + Expanded( + child: ExtendedTextField( + controller: controller, + textDirection: + widget.atTextContent != null ? TextDirection.ltr : null, + specialTextSpanBuilder: AtTextSpanBuilder(), + focusNode: msgNode, + autofocus: true, + onSubmitted: (s) { + Navigator.pop(context); + if (controller.text.isNotEmpty) { + var currenRoom = + Provider.of( + context, + listen: false, + ).currenRoom; + if (currenRoom != null) { + Provider.of(context, listen: false).dispatchMessage( + Msg( + groupId: + currenRoom + .roomProfile + ?.roomProfile + ?.roomAccount ?? + "", + role: + Provider.of( + context, + listen: false, + ).currenRoom?.entrants?.roles ?? + "", + msg: controller.text, + type: SCRoomMsgType.text, + user: AccountStorage().getCurrentUser()?.userProfile, + ), + ); + } + } + }, + onChanged: (s) { + setState(() { + showSend = controller.text.isNotEmpty; + }); + }, + decoration: InputDecoration( + hintText: SCAppLocalizations.of(context)!.pleaseChatFfriendly, + fillColor: Colors.transparent, + hintStyle: TextStyle(color: Colors.white60, fontSize: sp(14)), + contentPadding: const EdgeInsets.symmetric(horizontal: 15), + counterText: '', + isDense: true, + filled: false, + focusColor: Colors.transparent, + hoverColor: Colors.transparent, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + ), + style: TextStyle( + fontSize: ScreenUtil().setSp(14), + color: Colors.white, + ), + ), + ), + GestureDetector( + onTap: () { + Navigator.pop(context); + if (controller.text.isNotEmpty) { + var currenRoom = + Provider.of( + context, + listen: false, + ).currenRoom; + if (currenRoom != null) { + Provider.of(context, listen: false).dispatchMessage( + Msg( + groupId: + currenRoom.roomProfile?.roomProfile?.roomAccount ?? + "", + role: + Provider.of( + context, + listen: false, + ).currenRoom?.entrants?.roles ?? + "", + msg: controller.text, + type: SCRoomMsgType.text, + user: AccountStorage().getCurrentUser()?.userProfile, + ), + addLocal: true, + ); + } + } + }, + child: Container( + padding: EdgeInsets.all(4.w), + width: 30.w, + height: 30.w, + child: Image.asset("sc_images/room/sc_icon_room_message_send.png"), + ), + ), + SizedBox(width: 3.w), + ], + ), + ), + ); + } +} + +class PopRoute extends PopupRoute { + final Duration _duration = Duration(milliseconds: 350); + Widget child; + + PopRoute({required this.child}); + + @override + Color? get barrierColor => null; + + @override + bool get barrierDismissible => true; + + @override + String? get barrierLabel => null; + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return child; + } + + @override + Duration get transitionDuration => _duration; +} diff --git a/lib/ui_kit/widgets/room/room_msg_item.dart b/lib/ui_kit/widgets/room/room_msg_item.dart new file mode 100644 index 0000000..58e3d52 --- /dev/null +++ b/lib/ui_kit/widgets/room/room_msg_item.dart @@ -0,0 +1,1384 @@ +import 'dart:convert'; +import 'package:extended_image/extended_image.dart' + show ExtendedNetworkImageProvider; +import 'package:extended_text/extended_text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; +import 'package:yumi/modules/index/main_route.dart'; + +import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart'; +import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +import '../../components/sc_compontent.dart'; +import '../../components/sc_tts.dart'; +import '../../components/text/sc_text.dart'; + +///消息item +class MsgItem extends StatefulWidget { + final Function(SocialChatUserProfile? user) onClick; + Msg msg; + + MsgItem({Key? key, required this.msg, required this.onClick}) + : super(key: key); + + @override + _MsgItemState createState() => _MsgItemState(); +} + +class _MsgItemState extends State { + @override + Widget build(BuildContext context) { + //gift + // if (msg.type == Roomtype.gift.code) { + // return _buildGiftMsg(context); + // } + + ///系统提示 + if (widget.msg.type == SCRoomMsgType.systemTips) { + return _systemTipsRoomItem(context); + } + + ///进入房间 + if (widget.msg.type == SCRoomMsgType.joinRoom) { + return _enterRoomItem(context); + } + + ///礼物 + if (widget.msg.type == SCRoomMsgType.gift) { + return _buildGiftMsg(context); + } + + ///火箭用户中奖 + if (widget.msg.type == SCRoomMsgType.rocketRewardUser) { + return _buildRocketRewardMsg(context); + } + + ///幸运礼物 + if (widget.msg.type == SCRoomMsgType.gameLuckyGift) { + return _buildGameLuckyGiftMsg(context); + } + + ///幸运礼物 + if (widget.msg.type == SCRoomMsgType.gameLuckyGift_5) { + return _buildGameLuckyGiftMsg_5(context); + } + + ///房间身份变动 + if (widget.msg.type == SCRoomMsgType.roomRoleChange) { + return _buildRoleChangeMsg(context); + } + + ///踢出房间 + if (widget.msg.type == SCRoomMsgType.qcfj) { + widget.msg.msg = SCAppLocalizations.of(context)!.kickRoomTips; + } + + ///图片消息 + if (widget.msg.type == SCRoomMsgType.image) { + return _buildImageMsg(context); + } + + ///掷骰子 + if (widget.msg.type == SCRoomMsgType.roomDice) { + return _buildDiceMsg(context); + } + + ///石头剪刀布 + if (widget.msg.type == SCRoomMsgType.roomRPS) { + return _buildRPSMsg(context); + } + + ///幸运数字 + if (widget.msg.type == SCRoomMsgType.roomLuckNumber) { + return _buildLuckNumberMsg(context); + } + if (widget.msg.user != null) { + PropsResources? chatBox = widget.msg.user?.getChatBox(); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child:Container( + constraints: BoxConstraints( + maxWidth: + ScreenUtil().screenWidth * 0.7, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: ExtendedText( + widget.msg?.msg ?? '', + style: TextStyle( + fontSize: sp(13), + color: Color( + _msgColor(widget.msg.type ?? ""), + ), + fontWeight: FontWeight.w500, + ), + softWrap: true, // 自动换行 + specialTextSpanBuilder: AtTextSpanBuilder( + onTapCall: (userId) {}, + ), + ), + ), + onLongPress: () { + if (!(widget.msg?.msg ?? "").startsWith(AtText.flag)) { + Clipboard.setData( + ClipboardData(text: widget.msg?.msg ?? ''), + ); + SCTts.show("Copied"); + } + }, + ), + ), + ], + ), + ], + ), + ), + ); + } else { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(8.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: Text( + widget.msg?.msg ?? '', + style: TextStyle( + fontSize: sp(13), + color: Color(_msgColor(widget.msg.type ?? "")), + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.left, + ), + ), + ); + } + } + + ///系统提示 + _systemTipsRoomItem(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(8.w), + vertical: width(8), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 8.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: Text( + widget.msg?.msg ?? '', + style: TextStyle( + fontSize: sp(13), + height: 1.5.w, + color: Color(_msgColor(widget.msg.type ?? "")), + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.left, + ), + ), + ); + } + + ///火箭用户中奖 + _buildRocketRewardMsg(BuildContext context) { + List spans = []; + + spans.add( + TextSpan( + text: "${widget.msg.user?.userNickname} ", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.inRocket, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset( + "sc_images/room/sc_icon_room_rocket_lv${widget.msg.number}.png", + width: 18.w, + ), + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.obtain, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: + widget.msg.role == SCActivityRewardATPropsType.GOLD.name + ? Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w) + : netImage(url: widget.msg.msg ?? "", width: 18.w), + ), + ); + Widget text = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + strutStyle: StrutStyle( + height: 1.8, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), + margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: text, + ), + ); + } + + ///幸运礼物 + _buildGameLuckyGiftMsg(BuildContext context) { + List spans = []; + + spans.add( + TextSpan( + text: "${widget.msg.user?.userNickname} ", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + widget.onClick(widget.msg.user); + }, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.sendTo, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: " ${widget.msg.toUser?.userNickname} ", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + widget.onClick(widget.msg.toUser); + }, + ), + ); + SCGlobalConfig.lang == "ar" + ? spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w), + ), + ) + : spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: netImage(url: widget.msg.gift?.giftPhoto ?? "", width: 18.w), + ), + ); + spans.add( + TextSpan( + text: " ${SCAppLocalizations.of(context)!.obtain}", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: " ${widget.msg.awardAmount}", + style: TextStyle( + fontSize: sp(12), + color: SocialChatTheme.primaryColor, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + SCGlobalConfig.lang == "ar" + ? spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: netImage(url: widget.msg.gift?.giftPhoto ?? "", width: 18.w), + ), + ) + : spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.asset("sc_images/general/sc_icon_jb.png", width: 18.w), + ), + ); + Widget text = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + strutStyle: StrutStyle( + height: 1.3, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), + margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: text, + ), + ); + } + + ///幸运礼物中奖5倍以及以上 + _buildGameLuckyGiftMsg_5(BuildContext context) { + List spans = []; + spans.add( + TextSpan( + text: "${widget.msg.user?.userNickname} ", + style: TextStyle( + fontSize: sp(12), + color: Color(0xffFEF129), + fontWeight: FontWeight.w500, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + widget.onClick(widget.msg.user); + }, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.get, + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: " ${widget.msg.awardAmount} ", + style: TextStyle( + fontSize: sp(12), + color: Color(0xffFEF129), + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.coins3, + style: TextStyle( + fontSize: sp(12), + color: Color(0xffFEF129), + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: " ${SCAppLocalizations.of(context)!.receivedFromALuckyGift}", + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + Widget text = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + strutStyle: StrutStyle( + height: 1.2, // 行高倍数 + fontWeight: FontWeight.w500, + forceStrutHeight: true, // 强制应用行高 + ), + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + constraints: BoxConstraints(minHeight: 68.w), + padding: EdgeInsets.symmetric(vertical: 5.w), + margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_luck_gift_msg_n_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + SizedBox(width: 10.w), + Expanded(child: text), + Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/room/sc_icon_luck_gift_msg_n_ball.png", + ), + fit: BoxFit.fill, + ), + ), + alignment: AlignmentDirectional.center, + width: 55.w, + height: 60.w, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 10.w), + buildNumForGame(widget.msg.msg ?? "0", size: 18.w), + SizedBox(height: 3.w), + SCGlobalConfig.lang == "ar" + ? Image.asset( + "sc_images/room/sc_icon_times_text_ar.png", + height: 12.w, + ) + : Image.asset( + "sc_images/room/sc_icon_times_text_en.png", + height: 10.w, + ), + ], + ), + ), + SizedBox(width: 15.w), + ], + ), + ), + ); + } + + Widget _buildGiftMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + padding: EdgeInsets.symmetric(horizontal: width(5.w), vertical: width(4)), + margin: EdgeInsets.symmetric(horizontal: width(10)).copyWith(bottom: 8.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child:Container( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: SCAppLocalizations.of(context)!.sendTo, + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + ), + ), + TextSpan(text: " "), + TextSpan( + text: widget.msg.toUser?.userNickname ?? "", + style: TextStyle( + fontSize: 13.sp, + color: SocialChatTheme.primaryColor, + ), + recognizer: + TapGestureRecognizer()..onTap = () {}, + ), + TextSpan(text: " "), + WidgetSpan( + child: netImage( + url: widget.msg.gift?.giftPhoto ?? "", + width: 22.w, + height: 22.w, + ), + ), + TextSpan(text: " "), + TextSpan( + text: "x${widget.msg.number}", + style: TextStyle( + fontSize: 13.sp, + color: Colors.white, + ), + recognizer: + TapGestureRecognizer()..onTap = () {}, + ), + ], + ), + textAlign: TextAlign.left, + ), + ), + ), + ], + ), + ], + ), + ); + } + + ///进入房间 + _enterRoomItem(BuildContext context) { + List spans = []; + if (widget.msg.role != null && widget.msg.role != 0) { + spans.add( + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: EdgeInsets.only(right: 3.w), + child: msgRoleTag(widget.msg.role ?? "", width: 18.w), + ), + ), + ); + } + spans.add( + TextSpan( + text: "${widget.msg?.user?.userNickname ?? ''} ", + style: TextStyle( + fontSize: sp(13), + color: _roleNameColor(widget.msg.role ?? ""), + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + spans.add( + TextSpan( + text: SCAppLocalizations.of(context)!.joinRoomTips, + style: TextStyle( + fontSize: sp(13), + color: Color(_msgColor(widget.msg.type ?? "")), + fontWeight: FontWeight.w500, + ), + ), + ); + Widget text = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(8.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 8.w), + child: text, + ), + ); + } + + Widget _buildMedals(List wearBadge) { + return wearBadge.isNotEmpty + ? Expanded( + child: SingleChildScrollView( + child: SizedBox( + height: 25.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: wearBadge.length ?? 0, + itemBuilder: (context, index) { + return netImage( + width: 25.w, + height: 25.w, + url: wearBadge[index].selectUrl ?? "", + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 5.w); + }, + ), + ), + ), + ) + : Container(); + } + + int _msgColor(String type) { + switch (type) { + case SCRoomMsgType.joinRoom: + case SCRoomMsgType.systemTips: + case SCRoomMsgType.welcome: + return 0xFFFFFFFF; + case SCRoomMsgType.gift: + return 0xFFFFFFFF; + case SCRoomMsgType.shangMai: + case SCRoomMsgType.xiaMai: + case SCRoomMsgType.qcfj: + case SCRoomMsgType.killXiaMai: + return 0xFFFFFFFF; + break; + // case type.text: + // return _mapTextColor(); + // break; + default: + return 0xFFFFFFFF; + } + } + + _roleNameColor(String role) { + if (SCRoomRolesType.HOMEOWNER.name == role) { + return Color(0xff2AC0F2); + } else if (SCRoomRolesType.ADMIN.name == role) { + return Color(0xffF9A024); + } else if (SCRoomRolesType.MEMBER.name == role) { + return Color(0xff28ED77); + } else { + return Colors.white; + } + } + + Widget _buildRoleChangeMsg(BuildContext context) { + var text = ""; + if (SCRoomRolesType.ADMIN.name == widget.msg.msg) { + text = SCAppLocalizations.of(context)!.adminByHomeowner; + } else if (SCRoomRolesType.MEMBER.name == widget.msg.msg) { + text = SCAppLocalizations.of(context)!.memberByHomeowner; + } else { + text = SCAppLocalizations.of(context)!.touristByHomeowner; + } + List spans = []; + if (widget.msg.toUser != null) { + spans.add( + TextSpan( + text: "${widget.msg.toUser?.userNickname ?? ''} ", + style: TextStyle( + fontSize: sp(13), + color: Color(0xff2AC0F2), + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ); + } + spans.add( + TextSpan( + text: text, + style: TextStyle( + fontSize: sp(13), + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ); + Widget textView = Text.rich( + TextSpan(children: spans), + textAlign: TextAlign.left, + ); + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(8.w), + vertical: width(8), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 8.w), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.all(Radius.circular(15.w)), + ), + child: textView, + ), + ); + } + + Widget _buildImageMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child:Container( + constraints: BoxConstraints( + maxWidth: + ScreenUtil().screenWidth * 0.5, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: netImage(url: widget.msg.msg ?? ""), + ), + onTap: () { + String encodedUrls = Uri.encodeComponent( + jsonEncode([widget.msg.msg]), + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", + ); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildDiceMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child:Container( + constraints: BoxConstraints( + maxWidth: + ScreenUtil().screenWidth * 0.5, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: + (widget.msg.hasPlayedAnimation ?? false) + ? Image.asset( + "sc_images/room/sc_icon_dice_${widget.msg.msg}.png", + height: 35.w, + ) + : FutureBuilder( + future: Future.delayed( + Duration(milliseconds: 2000), + ), + // 传入Future + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + // 根据snapshot的状态来构建UI + if (snapshot.connectionState == + ConnectionState.done) { + widget.msg.hasPlayedAnimation = + true; + return Image.asset( + "sc_images/room/sc_icon_dice_${widget.msg.msg}.png", + height: 35.w, + ); + } else { + return Image.asset( + "sc_images/room/sc_icon_dice_animl.webp", + height: 35.w, + ); + } + }, + ), + ), + onTap: () {}, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildRPSMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child:Container( + constraints: BoxConstraints( + maxWidth: + ScreenUtil().screenWidth * 0.5, // 最大宽度约束 + ), + decoration: BoxDecoration( + color: Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: + (widget.msg.hasPlayedAnimation ?? false) + ? Image.asset( + "sc_images/room/sc_icon_rps_${widget.msg.msg}.png", + height: 35.w, + ) + : FutureBuilder( + future: Future.delayed( + Duration(milliseconds: 2000), + ), + // 传入Future + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + // 根据snapshot的状态来构建UI + if (snapshot.connectionState == + ConnectionState.done) { + widget.msg.hasPlayedAnimation = + true; + return Image.asset( + "sc_images/room/sc_icon_rps_${widget.msg.msg}.png", + height: 35.w, + ); + } else { + return Image.asset( + "sc_images/room/sc_icon_rps_animal.webp", + height: 35.w, + ); + } + }, + ), + ), + onTap: () {}, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildLuckNumberMsg(BuildContext context) { + return Container( + alignment: AlignmentDirectional.topStart, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: width(5.w), + vertical: width(4), + ), + margin: EdgeInsets.symmetric( + horizontal: width(10), + ).copyWith(bottom: 6.w), + child: Column( + children: [ + _buildUserInfoLine(context), + SizedBox(height: 6.w), + Row( + children: [ + Flexible( + // 使用Flexible替代Expanded + child: GestureDetector( + child:Container( + constraints: BoxConstraints( + maxWidth: + ScreenUtil().screenWidth * 0.7, // 最大宽度约束 + ), + decoration: BoxDecoration( + color:Colors.black38, + borderRadius: BorderRadius.only( + topRight: Radius.circular(15.w), + bottomLeft: Radius.circular(15.w), + bottomRight: Radius.circular(15.w), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 4.w, + ), + margin: EdgeInsetsDirectional.only( + start: 54.w, + ).copyWith(bottom: 8.w), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SCGlobalConfig.lang == "ar" + ? Image.asset( + "sc_images/room/sc_icon_luck_num_text_ar.png", + height: 20.w, + ) + : Image.asset( + "sc_images/room/sc_icon_luck_num_text_en.png", + height: 20.w, + ), + buildNumForRoomLuckNum( + widget.msg.msg ?? "", + size: 20.w, + ), + ], + ), + ), + onTap: () {}, + ), + ), + ], + ), + ], + ), + ), + ); + } + + _buildUserInfoLine(BuildContext context) { + if ((widget.msg.needUpDataUserInfo) ?? false) { + SCRoomUtils.roomUsersMap[widget.msg.user?.id ?? ""] == null; + } + return SCRoomUtils.roomUsersMap[widget.msg.user?.id ?? ""] == null + ? FutureBuilder( + future: SCAccountRepository().loadUserInfo(widget.msg.user?.id ?? ""), + builder: (ct, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasData) { + SocialChatUserProfile user = snapshot.data!; + SCRoomUtils.roomUsersMap[user.id ?? ""] = user; + return _buildNewUserInfoLine(context, user); + } + } + return Row( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onClick(widget.msg.user); + }, + child: Stack( + alignment: AlignmentDirectional.bottomEnd, + children: [ + head( + url: widget.msg.user?.userAvatar ?? "", + width: 48.w, + // headdress: msg.user?.getHeaddress()?.sourceUrl, + ), + Positioned( + child: msgRoleTag(widget.msg.role ?? "", width: 14.w), + bottom: 2.w, + right: 2.w, + ), + ], + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + getWealthLevel( + widget.msg.user?.wealthLevel ?? 0, + width: 48.w, + height: 23.w, + fontSize: 10.sp, + ), + getUserLevel( + widget.msg.user?.charmLevel ?? 0, + width: 48.w, + height: 23.w, + fontSize: 10.sp, + ), + SizedBox(width: 3.w), + socialchatNickNameText( + maxWidth: 120.w, + fontWeight: FontWeight.w500, + widget.msg.user?.userNickname ?? "", + fontSize: 13.sp, + type: widget.msg.user?.getVIP()?.name ?? "", + needScroll: + (widget + .msg + .user + ?.userNickname + ?.characters + .length ?? + 0) > + 10, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + widget.msg.user?.getVIP() != null + ? netImage( + url: widget.msg.user?.getVIP()?.cover ?? "", + width: 25.w, + height: 25.w, + ) + : Container(), + _buildMedals( + (widget.msg.user?.wearBadge?.where((item) { + return item.use ?? false; + }).toList() ?? + []), + ), + ], + ), + ], + ), + ), + ], + ); + }, + ) + : _buildNewUserInfoLine( + context, + SCRoomUtils.roomUsersMap[widget.msg.user?.id ?? ""]!, + ); + } + + _buildNewUserInfoLine(BuildContext context, SocialChatUserProfile user) { + return Row( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onClick(user); + }, + child: Stack( + alignment: AlignmentDirectional.bottomEnd, + children: [ + head( + url: user.userAvatar ?? "", + width: 48.w, + // headdress: msg.user?.getHeaddress()?.sourceUrl, + ), + Positioned( + child: msgRoleTag(widget.msg.role ?? "", width: 14.w), + bottom: 2.w, + right: 2.w, + ), + ], + ), + ), + SizedBox(width: 3.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + getWealthLevel( + user.wealthLevel ?? 0, + width: 48.w, + height: 23.w, + fontSize: 10.sp, + ), + getUserLevel( + user.charmLevel ?? 0, + width: 48.w, + height: 23.w, + fontSize: 10.sp, + ), + SizedBox(width: 3.w), + socialchatNickNameText( + maxWidth: 120.w, + fontWeight: FontWeight.w500, + user.userNickname ?? "", + fontSize: 13.sp, + type: user.getVIP()?.name ?? "", + needScroll: + (user.userNickname?.characters.length ?? 0) > 10, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + user.getVIP() != null + ? netImage( + url: user.getVIP()?.cover ?? "", + width: 25.w, + height: 25.w, + ) + : Container(), + _buildMedals( + user.wearBadge?.where((item) { + return item.use ?? false; + }).toList() ?? + [], + ), + ], + ), + ], + ), + ), + ], + ); + } +} + +///消息 +class Msg { + String? groupId; + String? msg; + MicRes? userWheat; + String? type; + SocialChatUserProfile? user; + String? role = ""; + SocialChatGiftRes? gift; + + //礼物个数 + num? number; + + num? awardAmount; + + // 送礼给谁 + SocialChatUserProfile? toUser; + int? time = 0; + + //动画是否已经播放(骰子、石头剪刀布、随机数字) + bool? hasPlayedAnimation = false; + + bool? needUpDataUserInfo = false; + + Msg({ + required this.groupId, + required this.msg, + required this.type, + this.user, + this.toUser, + this.userWheat, + this.role = "", + this.gift, + this.number, + this.awardAmount, + this.hasPlayedAnimation, + this.needUpDataUserInfo, + }) { + time = DateTime.now().millisecondsSinceEpoch; + } + + Msg.fromJson(dynamic json) { + groupId = json['groupId']; + msg = json['msg']; + type = json['type']; + role = json['role']; + time = json['time']; + number = json['number']; + awardAmount = json['awardAmount']; + hasPlayedAnimation = json['hasPlayedAnimation']; + needUpDataUserInfo = json['needUpDataUserInfo']; + userWheat = + json['userWheat'] != null ? MicRes.fromJson(json['userWheat']) : null; + user = json['user'] != null ? SocialChatUserProfile.fromJson(json['user']) : null; + toUser = + json['toUser'] != null ? SocialChatUserProfile.fromJson(json['toUser']) : null; + + gift = json['gift'] != null ? SocialChatGiftRes.fromJson(json['gift']) : null; + } + + Map toJson() { + final map = {}; + map['groupId'] = groupId; + map['msg'] = msg; + map['type'] = type; + map['role'] = role; + map['time'] = time; + map['awardAmount'] = awardAmount; + map['number'] = number; + map['hasPlayedAnimation'] = hasPlayedAnimation; + map['needUpDataUserInfo'] = needUpDataUserInfo; + if (userWheat != null) { + map['userWheat'] = userWheat?.toJson(); + } + if (user != null) { + map['user'] = user?.toJson(); + } + if (toUser != null) { + map['toUser'] = toUser?.toJson(); + } + if (gift != null) { + map['gift'] = gift?.toJson(); + } + return map; + } +} diff --git a/lib/ui_kit/widgets/room/room_online_user_widget.dart b/lib/ui_kit/widgets/room/room_online_user_widget.dart new file mode 100644 index 0000000..d7e05af --- /dev/null +++ b/lib/ui_kit/widgets/room/room_online_user_widget.dart @@ -0,0 +1,173 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/services/room/rc_room_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/modules/room/online/room_online_page.dart'; +import 'package:yumi/modules/room/rank/room_gift_rank_page.dart'; + +class RoomOnlineUserWidget extends StatefulWidget { + @override + _RoomOnlineUserWidgetState createState() => _RoomOnlineUserWidgetState(); +} + +class _RoomOnlineUserWidgetState extends State { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return Row( + children: [ + _buildExperience(), + SizedBox(width: 15.w), + ref.onlineUsers.isNotEmpty + ? Expanded( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Spacer(), + SizedBox( + height: 25.w, + child: Stack( + clipBehavior: Clip.none, + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + ref.onlineUsers.length, + (index) { + return Transform.translate( + offset: Offset(-3.w * index, 0), + child: Padding( + padding: EdgeInsets.only(right: 0.w), + child: netImage( + url: + ref + .onlineUsers[index] + .userAvatar ?? + "", + width: 23.w, + height: 23.w, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 1.w, + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ), + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + vertical: 3.w, + ), + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_online_peple.png", + width: 12.w, + height: 12.sp, + ), + text( + "${ref.onlineUsers.length}", + fontSize: 9.sp, + ), + ], + ), + ), + onTap: () { + showBottomInBottomDialog( + context, + RoomOnlinePage( + roomId: + ref + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id, + ), + ); + }, + ), + SizedBox(width: 5.w,) + ], + ), + ) + : Container(), + ], + ); + }, + ); + } + + ///房间榜单入口 + _buildExperience() { + return Consumer( + builder: (context, ref, child) { + return GestureDetector( + child: Container( + width: 90.w, + height: 27.w, + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadiusDirectional.only( + topEnd: Radius.circular(20.w), + bottomEnd: Radius.circular(20.w), + ), + ), + alignment: AlignmentDirectional.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/room/sc_icon_room_contribute.png", + width: 18.w, + height: 18.w, + ), + SizedBox(width: 5.w), + text( + "${ref.roomContributeLevelRes?.thisWeekIntegral ?? 0}", + fontSize: 13.sp, + textColor: Colors.orangeAccent, + fontWeight: FontWeight.w600, + ), + Icon( + Icons.chevron_right, + size: 13.w, + color: Colors.orangeAccent, + ), + ], + ), + ), + onTap: () { + SmartDialog.show( + tag: "showRoomGiftRankPage", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + builder: (_) { + return RoomGiftRankPage(); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_play_widget.dart b/lib/ui_kit/widgets/room/room_play_widget.dart new file mode 100644 index 0000000..3f74341 --- /dev/null +++ b/lib/ui_kit/widgets/room/room_play_widget.dart @@ -0,0 +1,60 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_banner_view.dart'; +import '../../../modules/index/main_route.dart'; +import '../../../services/audio/rtm_manager.dart'; + +class RoomPlayWidget extends StatefulWidget { + @override + _RoomPlayWidgetState createState() => _RoomPlayWidgetState(); +} + +class _RoomPlayWidgetState extends State { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + alignment: Alignment.bottomCenter, + child: SingleChildScrollView( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + Container( + height: 56.w, + margin: EdgeInsets.symmetric(vertical: 5.w), + child: RoomBannerView(), + ), + SizedBox(height: 10.w), + ], + ), + SizedBox(width: 10.w), + ], + ), + ), + ), + ], + ), + ), + ], + ); + ; + }, + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_user_card_setting.dart b/lib/ui_kit/widgets/room/room_user_card_setting.dart new file mode 100644 index 0000000..3b18a53 --- /dev/null +++ b/lib/ui_kit/widgets/room/room_user_card_setting.dart @@ -0,0 +1,172 @@ +import 'dart:ui' as ui; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/widgets/room/set_room_role_page.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; + +import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; + +class RoomUserCardSetting extends StatefulWidget { + String? roomId; + RoomUserCardRes? userCardInfo; + + RoomUserCardSetting({super.key, this.roomId, this.userCardInfo}); + + @override + _RoomUserCardSettingState createState() => _RoomUserCardSettingState(); +} + +class _RoomUserCardSettingState extends State { + bool isOnMai = false; + bool micMute = false; + + @override + void initState() { + super.initState(); + isOnMai = + Provider.of( + context, + listen: false, + ).userOnMaiInIndex(widget.userCardInfo?.userProfile?.id ?? "") != + -1; + } + + @override + Widget build(BuildContext context) { + List list = []; + list.add(SizedBox(height: 10.w)); + if (isOnMai) { + if (Provider.of(context, listen: false).isFz()) { + ///操作的人是房主 + list.add( + _item(SCAppLocalizations.of(context)!.removeTheMic, null, () { + Navigator.of(context).pop(); + Provider.of( + context, + listen: false, + ).killXiaMai(widget.userCardInfo?.userProfile?.id ?? ""); + }), + ); + } else { + if (widget.userCardInfo?.roomRole != SCRoomRolesType.ADMIN.name && + widget.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name) { + list.add( + _item(SCAppLocalizations.of(context)!.removeTheMic, null, () { + Navigator.of(context).pop(); + Provider.of( + context, + listen: false, + ).killXiaMai(widget.userCardInfo?.userProfile?.id ?? ""); + }), + ); + } + } + } + if (Provider.of(context, listen: false).isFz()) { + list.add( + _item(SCAppLocalizations.of(context)!.setUpAnIdentity, null, () { + Navigator.of(context).pop(); + showCenterDialog(context, SetRoomRolePage(widget.userCardInfo)); + }), + ); + } + if (widget.userCardInfo?.roomRole != SCRoomRolesType.ADMIN.name && + widget.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name) { + list.add( + _item(SCAppLocalizations.of(context)!.kickedOutOfRoom, null, () { + Navigator.of(context).pop(); + SmartDialog.show( + tag: "showKickedOutOfRoomDialog", + maskColor: Colors.black26, + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + clickMaskDismiss: true, + builder: (_) { + return KickedOutOfRoomDialog( + roomId: widget.roomId ?? "", + userId: widget.userCardInfo?.userProfile?.id ?? "", + ); + }, + ); + }), + ); + } + list.add( + _item(SCAppLocalizations.of(context)!.cancel, null, () { + Navigator.of(context).pop(); + }), + ); + list.add(SizedBox(height: 10.w)); + return SafeArea( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: ScreenUtil().screenWidth, + color: Color(0xff09372E).withOpacity(0.5), + child: Column(children: list), + ), + ), + ), + ); + } + + Widget _item(String msg, String? icon, Function onClick) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + onClick(); + }, + child: Column( + children: [ + Container( + decoration: + msg == SCAppLocalizations.of(context)!.cancel + ? BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.all(Radius.circular(32.w)), + ) + : null, + width: ScreenUtil().screenWidth * 0.7, + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + icon != null + ? Image.asset(icon, height: 18.w, width: 18.w) + : Container(), + SizedBox(width: 5.w), + Container( + alignment: Alignment.center, + child: Text( + msg, + style: TextStyle( + fontSize: sp(16), + color:Colors.white, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_user_info_card.dart b/lib/ui_kit/widgets/room/room_user_info_card.dart new file mode 100644 index 0000000..4541317 --- /dev/null +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -0,0 +1,957 @@ +import 'dart:convert'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/main.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart'; +import 'package:yumi/ui_kit/widgets/room/room_user_card_setting.dart'; +import 'package:marquee/marquee.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/user_count_guard_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/modules/gift/gift_page.dart'; + +import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; +import '../../../modules/chat/chat_route.dart'; + +class RoomUserInfoCard extends StatefulWidget { + String? userId; + + RoomUserInfoCard({super.key, this.userId}); + + @override + _RoomUserInfoCardState createState() => _RoomUserInfoCardState(); +} + +class _RoomUserInfoCardState extends State { + SocialChatUserProfileManager? userProvider; + String roomId = ""; + SCUserIdentityRes? userIdentity; + + @override + void initState() { + super.initState(); + userProvider = Provider.of( + context, + listen: false, + ); + roomId = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + ""; + userProvider?.roomUserCard(roomId, widget.userId ?? ""); + SCAccountRepository().userIdentity(userId: widget.userId).then((v) { + userIdentity = v; + setState(() {}); + }); + } + + @override + void dispose() { + super.dispose(); + userProvider?.userCardInfo = null; + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Consumer( + builder: (context, ref, child) { + if (ref.userCardInfo == null) { + return SizedBox( + height: ScreenUtil().screenHeight * 0.7, + child: Center( + child: Container( + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(8), + ), + width: 55.w, + height: 55.w, + child: CupertinoActivityIndicator(color: Colors.white24), + ), + ), + ); + } + return Container( + constraints: BoxConstraints( + maxHeight: ScreenUtil().screenHeight * 0.7, + ), + margin: EdgeInsets.symmetric(horizontal: 15.w), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: Alignment.center, + children: [ + Container( + margin: EdgeInsets.only( + top: + ref.userCardInfo?.userProfile + ?.getDataCard() + ?.sourceUrl != + null + ? 45.w + : 55.w, + ), + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: + ref.userCardInfo?.userProfile + ?.getDataCard() + ?.sourceUrl != + null + ? [ + Colors.transparent, + Colors.transparent, + ] + : [ + Color(0xff18F2B1), + Color(0xffE4FFF6), + Color(0xffE4FFF6), + Color(0xffE4FFF6), + Color(0xffE4FFF6), + Color(0xffE4FFF6), + Color(0xffE4FFF6), + Color(0xffFFFFFF), + ], + ), + borderRadius: BorderRadius.all( + Radius.circular(12.w), + ), + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SizedBox(height: 25.w), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + SizedBox(width: 20.w), + GestureDetector( + child: head( + url: + ref + .userCardInfo + ?.userProfile + ?.userAvatar ?? + "", + width: 55.w, + border: Border.all( + color: Colors.white, + width: 2, + ), + headdress: + ref.userCardInfo?.userProfile + ?.getHeaddress() + ?.sourceUrl, + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == widget.userId}&tageId=${widget.userId}", + ); + }, + ), + SizedBox(width: 3.w), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + msgRoleTag( + ref + .userCardInfo + ?.roomRole ?? + "", + width: 16.w, + height: 16.w, + ), + SizedBox(width: 2.w), + socialchatNickNameText( + maxWidth: 115.w, + ref + .userCardInfo + ?.userProfile + ?.userNickname ?? + "", + fontSize: 14.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + type: "", + needScroll: + (ref + .userCardInfo + ?.userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 10, + ), + ], + ), + SizedBox(height: 2.w), + text( + "ID:${ref.userCardInfo?.userProfile?.getID() ?? 0}", + fontSize: 12.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + ), + ], + ), + ], + ), + Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(15.w), + color: Colors.transparent, + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 5.w, + ), + child: Row( + children: [ + SizedBox(width: 20.w,), + netImage( + url: + Provider.of< + SCAppGeneralManager + >( + context, + listen: false, + ) + .findCountryByName( + ref + .userCardInfo + ?.userProfile + ?.countryName ?? + "", + ) + ?.nationalFlag ?? + "", + borderRadius: + BorderRadius.all( + Radius.circular(3.w), + ), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 5.w), + Container( + constraints: BoxConstraints( + maxWidth: 110.w, + ), + child: text( + ref + .userCardInfo + ?.userProfile + ?.countryName ?? + "", + textColor: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + SizedBox(width: 5.w), + Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + ref + .userCardInfo + ?.userProfile + ?.userSex == + 0 + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png", + ), + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 10.w, + vertical: 5.w, + ), + child: Row( + children: [ + xb( + ref + .userCardInfo + ?.userProfile + ?.userSex, + ), + SizedBox(width: 3.w), + text( + "${ref.userCardInfo?.userProfile?.age ?? 0}", + textColor: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + ), + ], + ), + + SizedBox(height: 15.w), + Container( + margin: EdgeInsets.symmetric( + horizontal: 25.w, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + widget.userId != + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id + ? SCDebounceWidget( + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_send_user_message.png", + width: 32.w, + height: 32.w, + ), + SizedBox(height: 5.w), + Container( + alignment: + Alignment.center, + width: 70.w, + child: text( + SCAppLocalizations.of( + context, + )!.sayHi2, + textColor: Color( + 0xff808080, + ), + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + onTap: () async { + Navigator.of(context).pop(); + var conversation = + V2TimConversation( + type: + ConversationType + .V2TIM_C2C, + userID: widget.userId, + conversationID: '', + ); + var bool = + await Provider.of< + RtmProvider + >( + context, + listen: false, + ).startConversation( + conversation, + ); + if (!bool) return; + var json = jsonEncode( + conversation.toJson(), + ); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + }, + ) + : Container(), + widget.userId != + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id + ? SCDebounceWidget( + child: Selector< + SocialChatUserProfileManager, + bool + >( + selector: + (context, provider) => + provider + .userCardInfo + ?.follow ?? + false, + shouldRebuild: + (prev, next) => + prev != next, + builder: ( + context, + follow, + _, + ) { + return Column( + children: [ + Image.asset( + follow + ? "sc_images/room/sc_icon_user_un_follow.png" + : "sc_images/room/sc_icon_user_follow.png", + width: 32.w, + height: 32.w, + ), + SizedBox(height: 5.w), + Container( + alignment: + Alignment + .center, + width: 70.w, + child: text( + // 使用 Flutter 内置 Text + follow + ? SCAppLocalizations.of( + context, + )!.followed + : SCAppLocalizations.of( + context, + )!.follow, // + fontSize: 14.sp, + textColor: Color( + 0xff808080, + ), + fontWeight: + FontWeight + .bold, + ), + ), + ], + ); + }, + ), + onTap: () { + // ✅ 使用正确的 Provider 访问方式 + final provider = Provider.of< + SocialChatUserProfileManager + >(context, listen: false); + provider.followUser( + widget.userId ?? "", + ); + }, + ) + : Container(), + widget.userId != + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id + ? SCDebounceWidget( + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_at_tag_user.png", + width: 32.w, + height: 32.w, + ), + SizedBox(height: 5.w), + Container( + alignment: + Alignment.center, + width: 70.w, + child: text( + SCAppLocalizations.of( + context, + )!.atTag, + textColor: Color( + 0xff808080, + ), + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + onTap: () async { + if (SCRoomUtils.touristCanMsg( + context, + )) { + if ((ref + .userCardInfo + ?.userProfile + ?.userNickname ?? + "") + .isEmpty) { + return; + } + Navigator.of( + context, + ).pop(); + Navigator.push( + context, + PopRoute( + child: RoomMsgInput( + atTextContent: + "${ref.userCardInfo?.userProfile?.userNickname}", + ), + ), + ); + } + }, + ) + : Container(), + widget.userId != + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id + ? SCDebounceWidget( + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_send_user_gift.png", + width: 32.w, + height: 32.w, + ), + SizedBox(height: 5.w), + Container( + alignment: + Alignment.center, + width: 70.w, + child: text( + SCAppLocalizations.of( + context, + )!.send, + textColor: Color( + 0xff808080, + ), + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + onTap: () { + Navigator.of(context).pop(); + SmartDialog.show( + tag: "showGiftControl", + alignment: + Alignment + .bottomCenter, + maskColor: + Colors.transparent, + animationType: + SmartAnimationType + .fade, + clickMaskDismiss: true, + builder: (_) { + return GiftPage( + toUser: + ref + .userCardInfo + ?.userProfile, + ); + }, + ); + }, + ) + : Container(), + ], + ), + ), + SizedBox(height: 35.w), + ], + ), + ], + ), + ), + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id != + widget.userId + ? Positioned( + top:28.w, + right: 15.w, + child: GestureDetector( + child: Image.asset( + "sc_images/room/sc_icon_user_card_report.png", + width: 24.w, + height: 24.w, + color: Colors.black, + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.report}?type=user&tageId=${widget.userId}", + replace: false, + ); + }, + ), + ) + : Container(), + ref.userCardInfo?.roomRole == + SCRoomRolesType.HOMEOWNER.name + ? Container() + : (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id != + widget.userId && + Provider.of( + context, + listen: false, + ).isFz() || + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id != + widget.userId && + Provider.of( + context, + listen: false, + ).isGL()) + ? Positioned( + top:28.w, + right: 45.w, + child: GestureDetector( + child: Image.asset( + "sc_images/room/sc_icon_room_user_card_setting.png", + width: 24.w, + height: 24.w, + color: Colors.black, + ), + onTap: () { + if (userProvider?.userCardInfo != null) { + Navigator.of(context).pop(); + showBottomInBottomDialog( + context!, + RoomUserCardSetting( + roomId: roomId, + userCardInfo: + userProvider?.userCardInfo + ?.copyWith(), + ), + ); + } + }, + ), + ) + : Container(), + ], + ), + ), + ], + ), + ], + ), + ), + ); + }, + ), + ); + } + + String roleName(String role) { + if (role == SCRoomRolesType.HOMEOWNER.name) { + return SCAppLocalizations.of(context)!.owner; + } else if (role == SCRoomRolesType.ADMIN.name) { + return SCAppLocalizations.of(context)!.admin; + } else if (role == SCRoomRolesType.MEMBER.name) { + return SCAppLocalizations.of(context)!.member; + } + return SCAppLocalizations.of(context)!.guest; + } + + _buildEmptyCpItem(bool canAdd) { + return Container( + width: ScreenUtil().screenWidth, + margin: EdgeInsets.symmetric(horizontal: 12.w), + height: 105.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + canAdd + ? "sc_images/person/sc_icon_no_cp_item_bg.png" + : "sc_images/person/sc_icon_no_cp_item_bg4.png", + ), + fit: BoxFit.fill, + ), + ), + child: Container( + margin: EdgeInsets.only(bottom: 13.w), + alignment: AlignmentDirectional.bottomCenter, + child: text( + SCAppLocalizations.of(context)!.noMatchedCP, + fontWeight: FontWeight.w600, + fontSize: 12.sp, + textColor: canAdd ? Color(0xFFFF79A1) : Colors.white, + ), + ), + ); + } + + Widget _buildCpItem(CPRes item) { + return Stack( + alignment: AlignmentDirectional.center, + children: [ + Container( + width: ScreenUtil().screenWidth, + margin: EdgeInsets.symmetric(horizontal: 15.w), + height: 135.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/person/sc_icon_no_cp_item_bg2.png"), + fit: BoxFit.fill, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 68.w), + Container( + alignment: AlignmentDirectional.bottomCenter, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + "sc_images/person/sc_icon_cp_value_tag.png", + height: 18.w, + ), + SizedBox(width: 5.w), + text( + _cpVaFormat(item.cpValue ?? 0), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + textColor: Color(0xFFFF79A1), + ), + ], + ), + ), + SizedBox(height: 8.w), + Container( + alignment: AlignmentDirectional.bottomCenter, + child: text( + SCAppLocalizations.of( + context, + )!.timeSpentTogether(item.days ?? ""), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + textColor: Color(0xFFFF79A1), + ), + ), + Container( + alignment: AlignmentDirectional.bottomCenter, + child: text( + SCAppLocalizations.of(context)!.firstDay(item.firstDay ?? ""), + fontWeight: FontWeight.w600, + fontSize: 11.sp, + textColor: Colors.white, + ), + ), + ], + ), + ), + PositionedDirectional( + top: 30.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + GestureDetector( + child: SizedBox( + height: 50.w, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + netImage( + url: item.meUserAvatar ?? "", + width: 48.w, + shape: BoxShape.circle, + defaultImg: + "sc_images/general/sc_icon_avar_defalt.png", + ), + Image.asset( + "sc_images/person/sc_icon_cp_head_ring.png", + ), + ], + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + replace: false, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == item.meUserId}&tageId=${item.meUserId}", + ); + }, + ), + + Container( + alignment: Alignment.center, + width: 80.w, + height: 15.w, + child: + (item.meUserNickname?.length ?? 0) > 8 + ? Marquee( + text: item.meUserNickname ?? "", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration(milliseconds: 550), + decelerationCurve: Curves.easeOut, + ) + : Text( + item.meUserNickname ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + SizedBox(width: 120.w), + Column( + children: [ + GestureDetector( + child: SizedBox( + height: 50.w, + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + netImage( + url: item.cpUserAvatar ?? "", + width: 48.w, + shape: BoxShape.circle, + defaultImg: + "sc_images/general/sc_icon_avar_defalt.png", + ), + Image.asset( + "sc_images/person/sc_icon_cp_head_ring.png", + ), + ], + ), + ), + onTap: () { + SCNavigatorUtils.push( + context, + replace: false, + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == item.cpUserId}&tageId=${item.cpUserId}", + ); + }, + ), + Container( + alignment: Alignment.center, + width: 80.w, + height: 15.w, + child: + (item.cpUserNickname?.length ?? 0) > 8 + ? Marquee( + text: item.cpUserNickname ?? "", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration(milliseconds: 550), + decelerationCurve: Curves.easeOut, + ) + : Text( + item.cpUserNickname ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + fontWeight: FontWeight.w600, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ); + } + + String getUserCardBg() { + return ""; + } + + String _cpVaFormat(double cpValue) { + int value = cpValue.toInt(); + if (value > 99999) { + return "${(value / 1000).toStringAsFixed(0)}k"; + } + return "$value"; + } +} diff --git a/lib/ui_kit/widgets/room/sc_edit_room_announcement_page.dart b/lib/ui_kit/widgets/room/sc_edit_room_announcement_page.dart new file mode 100644 index 0000000..2d753f5 --- /dev/null +++ b/lib/ui_kit/widgets/room/sc_edit_room_announcement_page.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_screen.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +class SCEditRoomAnnouncementPage extends StatefulWidget { + @override + _SCEditRoomAnnouncementPageState createState() => + _SCEditRoomAnnouncementPageState(); +} + +class _SCEditRoomAnnouncementPageState extends State { + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12.w)), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: ScreenUtil().screenWidth * 0.75, + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration(color: Color(0XFF09372E).withOpacity(0.5)), + child: Stack( + alignment: Alignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 15.w), + text( + SCAppLocalizations.of(context)!.roomAnnouncement, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + textColor: Colors.white, + ), + SizedBox(height: 10.w), + Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 15.w), + padding: EdgeInsets.only( + top: 5.w, + bottom: 10.w, + left: width(12), + right: width(12), + ), + alignment: AlignmentDirectional.centerStart, + height: 110.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.all( + Radius.circular(height(8)), + ), + border: Border.all( + color: SocialChatTheme.primaryLight, + width: 0.5.w, + ), + ), + child: SingleChildScrollView( + child: Text( + Provider.of(context, listen: false) + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomDesc ?? + "", + style: TextStyle( + fontSize: sp(12), + color: Colors.white, + textBaseline: TextBaseline.alphabetic, + ), + ), + ), + ), + ), + ], + ), + SizedBox(height: 25.w), + ], + ), + PositionedDirectional( + top: 10.w, + end: 10.w, + child: GestureDetector( + child: Icon(Icons.close, color: Colors.white, size: 18.w), + onTap: () { + SmartDialog.dismiss(tag: "showEditNotiPage"); + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/seamless_auto_scroll.dart b/lib/ui_kit/widgets/room/seamless_auto_scroll.dart new file mode 100644 index 0000000..2b61331 --- /dev/null +++ b/lib/ui_kit/widgets/room/seamless_auto_scroll.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class SeamlessAutoScroll extends StatefulWidget { + final List items; + final double itemHeight; + final Duration scrollInterval; + + const SeamlessAutoScroll({ + Key? key, + required this.items, + this.itemHeight = 120.0, + this.scrollInterval = const Duration(seconds: 2), + }) : super(key: key); + + @override + _AutoScrollListState createState() => _AutoScrollListState(); +} + +class _AutoScrollListState extends State { + final ScrollController _scrollController = ScrollController(); + Timer? _timer; + int _currentIndex = 0; + + @override + void initState() { + super.initState(); + _startAutoScroll(); + } + + @override + void dispose() { + _timer?.cancel(); + _scrollController.dispose(); + super.dispose(); + } + + void _startAutoScroll() { + _timer = Timer.periodic(widget.scrollInterval, (timer) { + if (_currentIndex < widget.items.length) { + // 滚动到下一个项目 + _scrollController.animateTo( + _currentIndex * widget.itemHeight, + duration: const Duration(milliseconds: 550), + curve: Curves.easeIn, + ); + _currentIndex++; + } else { + // 滚动到底部后回到顶部重新开始 + _currentIndex = 0; + _scrollController.jumpTo(0); + } + }); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 110.w, // 固定容器高度 + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), + child: ListView.builder( + controller: _scrollController, + physics: const NeverScrollableScrollPhysics(), // 禁用手动滚动 + itemCount: widget.items.length, + itemBuilder: (context, index) { + return Container( + height: widget.itemHeight, + alignment: Alignment.centerLeft, + child: widget.items[index], + ); + }, + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart new file mode 100644 index 0000000..b1e529a --- /dev/null +++ b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart @@ -0,0 +1,167 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; + +import '../../../../modules/room/seat/sc_seat_item.dart'; + + + +class RoomSeatWidget extends StatefulWidget { + @override + _RoomSeatWidgetState createState() => _RoomSeatWidgetState(); +} + +class _RoomSeatWidgetState extends State { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, child) { + return ref.roomWheatMap.length == 5 + ? _buildSeat5() + : (ref.roomWheatMap.length == 10 + ? _buildSeat10() + : (ref.roomWheatMap.length == 15 + ? _buildSeat15() + : (ref.roomWheatMap.length == 20 + ? _buildSeat20() + : Container(height: 180.w)))); + }, + ); + } + + _buildSeat5() { + return Container( + margin: EdgeInsets.only(top: 3.w, left: 10.w, right: 10.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 0)), + Expanded(flex: 1, child: SCSeatItem(index: 1)), + Expanded(flex: 1, child: SCSeatItem(index: 2)), + Expanded(flex: 1, child: SCSeatItem(index: 3)), + Expanded(flex: 1, child: SCSeatItem(index: 4)), + ], + ), + ], + ), + ); + } + + _buildSeat10() { + return Container( + margin: EdgeInsets.only(top: 3.w, left: 10.w, right: 10.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 0)), + Expanded(flex: 1, child: SCSeatItem(index: 1)), + Expanded(flex: 1, child: SCSeatItem(index: 2)), + Expanded(flex: 1, child: SCSeatItem(index: 3)), + Expanded(flex: 1, child: SCSeatItem(index: 4)), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 5)), + Expanded(flex: 1, child: SCSeatItem(index: 6)), + Expanded(flex: 1, child: SCSeatItem(index: 7)), + Expanded(flex: 1, child: SCSeatItem(index: 8)), + Expanded(flex: 1, child: SCSeatItem(index: 9)), + ], + ), + ], + ), + ); + } + + _buildSeat15() { + return Container( + margin: EdgeInsets.only(top: 3.w, left: 10.w, right: 10.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 0)), + Expanded(flex: 1, child: SCSeatItem(index: 1)), + Expanded(flex: 1, child: SCSeatItem(index: 2)), + Expanded(flex: 1, child: SCSeatItem(index: 3)), + Expanded(flex: 1, child: SCSeatItem(index: 4)), + ], + ), + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 5)), + Expanded(flex: 1, child: SCSeatItem(index: 6)), + Expanded(flex: 1, child: SCSeatItem(index: 7)), + Expanded(flex: 1, child: SCSeatItem(index: 8)), + Expanded(flex: 1, child: SCSeatItem(index: 9)), + ], + ), + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 10)), + Expanded(flex: 1, child: SCSeatItem(index: 11)), + Expanded(flex: 1, child: SCSeatItem(index: 12)), + Expanded(flex: 1, child: SCSeatItem(index: 13)), + Expanded(flex: 1, child: SCSeatItem(index: 14)), + ], + ), + ], + ), + ); + } + + _buildSeat20() { + return Container( + margin: EdgeInsets.only(top: 3.w, left: 10.w, right: 10.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 0)), + Expanded(flex: 1, child: SCSeatItem(index: 1)), + Expanded(flex: 1, child: SCSeatItem(index: 2)), + Expanded(flex: 1, child: SCSeatItem(index: 3)), + Expanded(flex: 1, child: SCSeatItem(index: 4)), + ], + ), + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 5)), + Expanded(flex: 1, child: SCSeatItem(index: 6)), + Expanded(flex: 1, child: SCSeatItem(index: 7)), + Expanded(flex: 1, child: SCSeatItem(index: 8)), + Expanded(flex: 1, child: SCSeatItem(index: 9)), + ], + ), + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 10)), + Expanded(flex: 1, child: SCSeatItem(index: 11)), + Expanded(flex: 1, child: SCSeatItem(index: 12)), + Expanded(flex: 1, child: SCSeatItem(index: 13)), + Expanded(flex: 1, child: SCSeatItem(index: 14)), + ], + ), + Row( + children: [ + Expanded(flex: 1, child: SCSeatItem(index: 15)), + Expanded(flex: 1, child: SCSeatItem(index: 16)), + Expanded(flex: 1, child: SCSeatItem(index: 17)), + Expanded(flex: 1, child: SCSeatItem(index: 18)), + Expanded(flex: 1, child: SCSeatItem(index: 19)), + ], + ), + ], + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/set_room_role_page.dart b/lib/ui_kit/widgets/room/set_room_role_page.dart new file mode 100644 index 0000000..4a148de --- /dev/null +++ b/lib/ui_kit/widgets/room/set_room_role_page.dart @@ -0,0 +1,220 @@ +import 'dart:ui' as ui; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; + +import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +import '../../components/sc_tts.dart'; +import '../../components/text/sc_text.dart'; + +class SetRoomRolePage extends StatefulWidget { + RoomUserCardRes? userCardInfo; + + SetRoomRolePage(this.userCardInfo); + + @override + _SetRoomRolePageState createState() => _SetRoomRolePageState(); +} + +class _SetRoomRolePageState extends State { + String roomId = ""; + String roomAccount = ""; + String optTips = ""; + RtmProvider? rtmProvider; + @override + void initState() { + super.initState(); + roomId = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + ""; + rtmProvider = Provider.of( + context, + listen: false, + ); + roomAccount = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.roomAccount ?? + ""; + } + + @override + Widget build(BuildContext context) { + optTips = SCAppLocalizations.of(context)!.operationSuccessful; + return ClipRect( + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: ScreenUtil().screenWidth * 0.75, + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Color(0xff09372E).withOpacity(0.5), + borderRadius: BorderRadius.all(Radius.circular(12.w)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 18.w), + Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if(widget.userCardInfo?.roomRole==SCRoomRolesType.ADMIN.name){ + SCTts.show(SCAppLocalizations.of(context)!.alreadyAnAdministrator); + return; + } + Navigator.of(context).pop(); + SCChatRoomRepository() + .changeRoomRole( + roomId, + SCRoomRolesType.ADMIN.name, + widget.userCardInfo?.userProfile?.id ?? "", + AccountStorage().getCurrentUser()?.userProfile?.id ?? + "", + "OTHER_CHANGE", + ) + .whenComplete(() { + SCTts.show(optTips); + Msg msg = Msg( + groupId: roomAccount, + msg: 'ADMIN', + toUser: widget.userCardInfo?.userProfile, + type: SCRoomMsgType.roomRoleChange, + ); + rtmProvider?.dispatchMessage(msg, addLocal: true); + }); + }, + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_room_gly.png", + width: 30.w, + height: 30.w, + ), + SizedBox(height: 10.w), + text( + SCAppLocalizations.of(context)!.admin, + textColor: Color(0xffB1B1B1), + fontSize: 13.sp, + ), + ], + ), + ), + ), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_room_hy.png", + width: 30.w, + height: 30.w, + ), + SizedBox(height: 10.w), + text( + SCAppLocalizations.of(context)!.member, + textColor: Color(0xffB1B1B1), + fontSize: 13.sp, + ), + ], + ), + onTap: () { + if(widget.userCardInfo?.roomRole==SCRoomRolesType.MEMBER.name){ + SCTts.show(SCAppLocalizations.of(context)!.alreadyAnMember); + return; + } + Navigator.of(context).pop(); + SCChatRoomRepository() + .changeRoomRole( + roomId, + SCRoomRolesType.MEMBER.name, + widget.userCardInfo?.userProfile?.id ?? "", + AccountStorage().getCurrentUser()?.userProfile?.id ?? + "", + "OTHER_CHANGE", + ) + .whenComplete(() { + SCTts.show(optTips); + Msg msg = Msg( + groupId: roomAccount, + msg: 'MEMBER', + toUser: widget.userCardInfo?.userProfile, + type: SCRoomMsgType.roomRoleChange, + ); + rtmProvider?.dispatchMessage(msg, addLocal: true); + }); + }, + ), + ), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Column( + children: [ + Image.asset( + "sc_images/room/sc_icon_room_guest.png", + width: 30.w, + height: 30.w, + ), + SizedBox(height: 10.w), + text( + SCAppLocalizations.of(context)!.guest, + textColor: Color(0xffB1B1B1), + fontSize: 13.sp, + ), + ], + ), + onTap: () { + if(widget.userCardInfo?.roomRole==SCRoomRolesType.TOURIST.name){ + SCTts.show(SCAppLocalizations.of(context)!.alreadyAnTourist); + return; + } + Navigator.of(context).pop(); + SCChatRoomRepository() + .changeRoomRole( + roomId, + SCRoomRolesType.TOURIST.name, + widget.userCardInfo?.userProfile?.id ?? "", + AccountStorage().getCurrentUser()?.userProfile?.id ?? + "", + "OTHER_CHANGE", + ) + .whenComplete(() { + SCTts.show(optTips); + Msg msg = Msg( + groupId: roomAccount, + msg: 'TOURIST', + toUser: widget.userCardInfo?.userProfile, + type: SCRoomMsgType.roomRoleChange, + ); + rtmProvider?.dispatchMessage(msg, addLocal: true); + }); + }, + ), + ), + ], + ), + SizedBox(height: 20.w), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart b/lib/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart new file mode 100644 index 0000000..26ef025 --- /dev/null +++ b/lib/ui_kit/widgets/room/switch_model/room_mic_switch_page.dart @@ -0,0 +1,78 @@ +import 'dart:ui' as ui; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/widgets/room/switch_model/room_seat_number_page.dart'; + +class RoomMicSwitchPage extends StatefulWidget { + @override + _RoomMicSwitchPageState createState() => _RoomMicSwitchPageState(); +} + +class _RoomMicSwitchPageState extends State + with SingleTickerProviderStateMixin { + TabController? _tabController; + final List _pages = []; + final List _tabs = []; + + @override + void initState() { + super.initState(); + _pages.add(RoomSeatNumberPage()); + _tabController = TabController(length: _pages.length, vsync: this); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + _tabs.add(Tab(text: SCAppLocalizations.of(context)!.numberOfMic)); + return SafeArea( + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: Container( + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight * 0.75, + color: Color(0xff09372E).withOpacity(0.5), + child: Column( + children: [ + TabBar( + tabAlignment: TabAlignment.center, + labelPadding: EdgeInsets.symmetric(horizontal: 18.w), + labelColor: Colors.white, + isScrollable: true, + unselectedLabelColor: Colors.white54, + labelStyle: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + indicator: BoxDecoration(), + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/switch_model/room_seat_number_page.dart b/lib/ui_kit/widgets/room/switch_model/room_seat_number_page.dart new file mode 100644 index 0000000..b4c9b8d --- /dev/null +++ b/lib/ui_kit/widgets/room/switch_model/room_seat_number_page.dart @@ -0,0 +1,412 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:provider/provider.dart'; + +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; + +class RoomSeatNumberPage extends StatefulWidget { + @override + _RoomSeatNumberPageState createState() => _RoomSeatNumberPageState(); +} + +class _RoomSeatNumberPageState extends State { + num mikeSize = 10; + + @override + void initState() { + super.initState(); + mikeSize = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomSetting?.mikeSize ?? + 10; + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 8.w, vertical: 5.w), + child: Column( + children: [ + SizedBox(height: 5.w), + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_seat_num_bg.png"), + fit: BoxFit.cover, + ), + ), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(12.w), + border: Border.all( + color: + mikeSize == 5 + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 2.w, + ), + ), + child: Stack( + children: [ + Column( + spacing: 5.w, + children: [ + SizedBox(height: 5.w), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + color: Colors.transparent, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + color: Colors.transparent, + ), + text( + textColor: Colors.white, + SCAppLocalizations.of( + context, + )!.classicMic("5"), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + ), + SizedBox(height: 5.w), + ], + ), + ], + ), + ), + onTap: () { + mikeSize = 5; + setState(() {}); + }, + ), + ], + ), + ), + ), + + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_seat_num_bg.png"), + fit: BoxFit.cover, + ), + ), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(12.w), + border: Border.all( + color: + mikeSize == 10 + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 2.w, + ), + ), + child: Stack( + children: [ + Column( + spacing: 5.w, + children: [ + SizedBox(height: 5.w), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + color: Colors.transparent, + ), + text( + textColor: Colors.white, + SCAppLocalizations.of( + context, + )!.classicMic("10"), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + ), + SizedBox(height: 5.w), + ], + ), + ], + ), + ), + onTap: () { + mikeSize = 10; + setState(() {}); + }, + ), + ], + ), + ), + ), + ], + ), + SizedBox(height: 15.w), + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_seat_num_bg.png"), + fit: BoxFit.cover, + ), + ), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(12.w), + border: Border.all( + color: + mikeSize == 15 + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 2.w, + ), + ), + child: Stack( + children: [ + Column( + spacing: 5.w, + children: [ + SizedBox(height: 5.w), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + color: Colors.transparent, + ), + text( + textColor: Colors.white, + SCAppLocalizations.of( + context, + )!.classicMic("15"), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + ), + SizedBox(height: 5.w), + ], + ), + ], + ), + ), + onTap: () { + mikeSize = 15; + setState(() {}); + }, + ), + ], + ), + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_seat_num_bg.png"), + fit: BoxFit.cover, + ), + ), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + color: Colors.white10, + borderRadius: BorderRadius.circular(12.w), + border: Border.all( + color: + mikeSize == 20 + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 2.w, + ), + ), + child: Stack( + children: [ + Column( + spacing: 5.w, + children: [ + SizedBox(height: 5.w), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + Image.asset( + "sc_images/room/sc_icon_room_mic_model_5.png", + height: 25.w, + ), + text( + textColor: Colors.white, + SCAppLocalizations.of( + context, + )!.classicMic("20"), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + ), + SizedBox(height: 5.w), + ], + ), + ], + ), + ), + onTap: () { + mikeSize = 20; + setState(() {}); + }, + ), + ], + ), + ), + ), + ], + ), + Spacer(), + GestureDetector( + child: Container( + alignment: AlignmentDirectional.center, + margin: EdgeInsets.only(bottom: 20.w), + height: 45.w, + width: 180.w, + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(25.w), + ), + child: text( + SCAppLocalizations.of(context)!.save, + textColor: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: + SCAppLocalizations.of(context)!.confirmSwitchMicModelTips, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () { + SCLoadingManager.show(); + JoinRoomRes? room = + Provider.of( + context, + listen: false, + ).currenRoom; + if (room != null) { + SCChatRoomRepository() + .updateRoomSetting( + room.roomProfile?.roomProfile?.id ?? "", + room.roomProfile?.roomProfile?.roomAccount ?? "", + context, + mikeSize: mikeSize, + ) + .then((value) { + var roomSetting = room.roomProfile?.roomSetting; + Provider.of( + context, + listen: false, + ).updateRoomSetting( + roomSetting!.copyWith(mikeSize: mikeSize), + ); + SCTts.show( + SCAppLocalizations.of( + context, + )!.operationSuccessful, + ); + SCLoadingManager.hide(); + SmartDialog.dismiss(tag: "showRoomMicSwitch"); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + } + }, + ); + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room_reward_countdown_timer.dart b/lib/ui_kit/widgets/room_reward_countdown_timer.dart new file mode 100644 index 0000000..acafab2 --- /dev/null +++ b/lib/ui_kit/widgets/room_reward_countdown_timer.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; + +class RoomRewardCountdownTimer extends StatefulWidget { + final DateTime targetDate; + + const RoomRewardCountdownTimer({super.key, required this.targetDate}); + + @override + State createState() => + _RoomRewardCountdownTimerState(); +} + +class _RoomRewardCountdownTimerState extends State { + Timer? _timer; + + // 剩余时间 + Duration _remainingTime = Duration.zero; + + @override + void initState() { + super.initState(); + _startTimer(); + } + + void _startTimer() { + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + setState(() { + _remainingTime = widget.targetDate.difference(DateTime.now()); + // 如果倒计时结束 + if (_remainingTime.isNegative) { + _remainingTime = Duration.zero; + _timer?.cancel(); + } + }); + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + // 格式化数字为两位数 + String _formatNumber(int number) { + return number.toString().padLeft(2, '0'); + } + + @override + Widget build(BuildContext context) { + // 计算天、时、分、秒 + int days = _remainingTime.inDays; + int hours = _remainingTime.inHours.remainder(24); + int minutes = _remainingTime.inMinutes.remainder(60); + int seconds = _remainingTime.inSeconds.remainder(60); + return Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 10.w), + _TimeBox( + value: days < 10 ? "$days" : _formatNumber(days), + label: 'D', + ), + SizedBox(width: 5.w,), + _TimeBox(value: _formatNumber(hours), label: ''), + _buildMaohao(), + _TimeBox(value: _formatNumber(minutes), label: ''), + _buildMaohao(), + _TimeBox(value: _formatNumber(seconds), label: ''), + SizedBox(width: 10.w), + ], + ), + ); + } + + _buildMaohao(){ + return text( + ":", + fontSize: 24.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ); + } +} + +class _TimeBox extends StatelessWidget { + final String value; + final String label; + + const _TimeBox({required this.value, required this.label}); + + @override + Widget build(BuildContext context) { + return Container( + width: 70.w, + height: 70.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/room/sc_icon_room_reward_countdown_item_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + value, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ), + text( + label, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ), + ], + ), + ); + } +} diff --git a/lib/ui_kit/widgets/sc_lk_tap_widget.dart b/lib/ui_kit/widgets/sc_lk_tap_widget.dart new file mode 100644 index 0000000..99de028 --- /dev/null +++ b/lib/ui_kit/widgets/sc_lk_tap_widget.dart @@ -0,0 +1,108 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +// ignore: must_be_immutable +class SCLkTapWidget extends StatefulWidget { + final Widget? child; + final Function? onTap; + Color highlightColor; + final Duration? duration; + final BorderRadius? borderRadius; + + SCLkTapWidget( + {Key? key, + this.child, + this.onTap, + this.highlightColor = Colors.black12, + this.duration, this.borderRadius}) + : super(key: key); + + @override + TapWidgetState createState() { + return new TapWidgetState(); + } +} + +class TapWidgetState extends State + with SingleTickerProviderStateMixin { + late AnimationController _ctl; + late Animation _colorAnimation; + Duration? _duration; + bool get onTapEnable => widget.onTap != null; + + @override + void initState() { + super.initState(); + _duration = widget.duration; + if (_duration == null) { + _duration = Duration(milliseconds: 1); + } + _ctl = AnimationController(vsync: this, duration: _duration); + widget.highlightColor??=Colors.black12; + + _colorAnimation = Tween( + begin: widget.highlightColor.withOpacity(0.0), + end: widget.highlightColor, + ).animate(_ctl); + } + + @override + void dispose() { + _ctl.stop(); + _ctl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + child: AnimatedBuilder( + animation: _ctl, + builder: (BuildContext context, Widget? child) { + return Container( + foregroundDecoration: BoxDecoration( + color: _colorAnimation.value, + borderRadius: widget.borderRadius + ), + child: widget.child, + ); + }, + ), + onTap: (){ + if(widget.onTap!=null){ + widget.onTap!(); + } + }, + onTapDown: (d) { + if (onTapEnable) _ctl.forward(); + }, + onTapUp: (d) { + if (onTapEnable) prepareToIdle(); + }, + onTapCancel: (){ + if (onTapEnable) prepareToIdle(); + }, + ); + } + + void prepareToIdle() { + late AnimationStatusListener listener; + listener = (AnimationStatus statue) { + if (statue == AnimationStatus.completed) { + _ctl.removeStatusListener(listener); + toStart(); + } + }; + _ctl.addStatusListener(listener); + if (!_ctl.isAnimating) { + _ctl.removeStatusListener(listener); + toStart(); + } + } + + void toStart() { + _ctl.stop(); + _ctl.reverse(); + } +} \ No newline at end of file diff --git a/lib/ui_kit/widgets/store/props_store_chatbox_detail_dialog.dart b/lib/ui_kit/widgets/store/props_store_chatbox_detail_dialog.dart new file mode 100644 index 0000000..62f2632 --- /dev/null +++ b/lib/ui_kit/widgets/store/props_store_chatbox_detail_dialog.dart @@ -0,0 +1,244 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import '../../../app/constants/sc_global_config.dart'; +import '../../../app/routes/sc_fluro_navigator.dart'; +import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; +import '../../../modules/wallet/wallet_route.dart'; +import '../../../main.dart'; +import '../../components/sc_debounce_widget.dart'; +import '../../components/sc_compontent.dart'; + +class PropsStoreChatboxDetailDialog extends StatefulWidget { + StoreListRes res; + + //折扣 + double disCount = 1; + + PropsStoreChatboxDetailDialog(this.res, this.disCount); + + @override + _PropsStoreChatboxDetailDialogState createState() => + _PropsStoreChatboxDetailDialogState(); +} + +class _PropsStoreChatboxDetailDialogState + extends State { + PropsPrices? curentPropsPrice; + int currentIndex = 0; + + @override + void initState() { + super.initState(); + currentIndex = 0; + curentPropsPrice = widget.res.propsPrices![currentIndex]; + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + height: 330.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + color: Color(0xff03523a), + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Column( + children: [ + SizedBox(height: 20.w), + netImage( + url: + widget.res.propsResources?.cover ?? "", + height: 95.w, + ), + SizedBox(height: 25.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 15.w), + text( + widget.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 12.sp, + ), + Spacer(), + Image.asset( + "sc_images/general/sc_icon_jb.png", + width: 22.w, + height: 22.w, + ), + widget.disCount < 1 + ? text( + "${curentPropsPrice?.amount}", + textColor: SocialChatTheme.primaryLight, + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ) + : Container(), + widget.disCount < 1 ? SizedBox(width: 5.w) : Container(), + text( + "${((curentPropsPrice?.amount ?? 0) * widget.disCount).toInt()}", + textColor: SocialChatTheme.primaryLight, + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 15.w), + ], + ), + SizedBox(height: 10.w), + SingleChildScrollView( + child: SizedBox( + height: 28.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: widget.res.propsPrices?.length ?? 0, + itemBuilder: (context, index) { + return GestureDetector( + child: Container( + width: 110.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.w), + color: Color(0xff18F2B1).withOpacity(0.1), + border: Border.all( + color: + index == currentIndex + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + ), + child: text( + "${widget.res.propsPrices?[index].days} ${SCAppLocalizations.of(context)!.day}", + textColor: SocialChatTheme.primaryLight, + ), + ), + onTap: () { + currentIndex = index; + curentPropsPrice = widget.res.propsPrices?[index]; + setState(() {}); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 5.w); + }, + ), + ), + ), + SizedBox(height: 20.w), + Container( + color: Colors.black12, + width: ScreenUtil().screenWidth, + height: 0.5.w, + ), + SizedBox(height: 20.w), + Container( + alignment: Alignment.center, + child: Row( + children: [ + Consumer( + builder: (context, ref, child) { + return GestureDetector( + child: Row( + children: [ + SizedBox(width: 10.w), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIcon(), + width: 20.w, + height: 20.w, + ), + SizedBox(width: 5.w), + text( + "${ref.myBalance}", + fontSize: 13.sp, + textColor: SocialChatTheme.primaryLight, + ), + Icon( + Icons.chevron_right, + size: 20.w, + color: SocialChatTheme.primaryLight, + ), + ], + ), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + WalletRoute.recharge, + replace: false, + ); + }, + ); + }, + ), + Spacer(), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12.w), + ), + child: text( + SCAppLocalizations.of(context)!.purchase, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .storePurchasing( + widget.res.id ?? "", + SCPropsType.AVATAR_FRAME.name, + SCCurrencyType.GOLD.name, + "${curentPropsPrice?.days}", + ) + .then((value) { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCLoadingManager.hide(); + SCTts.show( + SCAppLocalizations.of( + context, + )!.purchaseIsSuccessful, + ); + Provider.of( + context, + listen: false, + ).updateBalance(value); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + }, + ), + SizedBox(width: 10.w,) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/store/props_store_headdress_detail_dialog.dart b/lib/ui_kit/widgets/store/props_store_headdress_detail_dialog.dart new file mode 100644 index 0000000..e71555b --- /dev/null +++ b/lib/ui_kit/widgets/store/props_store_headdress_detail_dialog.dart @@ -0,0 +1,250 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:yumi/main.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import '../../../app/constants/sc_global_config.dart'; +import '../../../app/routes/sc_fluro_navigator.dart'; +import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; +import '../../../modules/wallet/wallet_route.dart'; + +class PropsStoreHeaddressDetailDialog extends StatefulWidget { + StoreListRes res; + + //折扣 + double disCount = 1; + + PropsStoreHeaddressDetailDialog(this.res, this.disCount); + + @override + _PropsStoreHeaddressDetailDialogState createState() => + _PropsStoreHeaddressDetailDialogState(); +} + +class _PropsStoreHeaddressDetailDialogState + extends State { + PropsPrices? curentPropsPrice; + int currentIndex = 0; + + @override + void initState() { + super.initState(); + currentIndex = 0; + curentPropsPrice = widget.res.propsPrices![currentIndex]; + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + height: 330.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + color: Color(0xff03523a), + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Column( + children: [ + SizedBox(height: 20.w), + head( + url: + AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? + "", + width: 95.w, + height: 95.w, + headdress: + widget.res.propsResources?.sourceUrl ?? + "" + "", + ), + SizedBox(height: 25.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 15.w), + text( + widget.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 12.sp, + ), + Spacer(), + Image.asset( + "sc_images/general/sc_icon_jb.png", + width: 22.w, + height: 22.w, + ), + widget.disCount < 1 + ? text( + "${curentPropsPrice?.amount}", + textColor: SocialChatTheme.primaryLight, + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ) + : Container(), + widget.disCount < 1 ? SizedBox(width: 5.w) : Container(), + text( + "${((curentPropsPrice?.amount ?? 0) * widget.disCount).toInt()}", + textColor: SocialChatTheme.primaryLight, + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 15.w), + ], + ), + SizedBox(height: 10.w), + SingleChildScrollView( + child: SizedBox( + height: 28.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: widget.res.propsPrices?.length ?? 0, + itemBuilder: (context, index) { + return GestureDetector( + child: Container( + width: 110.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.w), + color: Color(0xff18F2B1).withOpacity(0.1), + border: Border.all( + color: + index == currentIndex + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + ), + child: text( + "${widget.res.propsPrices?[index].days} ${SCAppLocalizations.of(context)!.day}", + textColor: SocialChatTheme.primaryLight, + ), + ), + onTap: () { + currentIndex = index; + curentPropsPrice = widget.res.propsPrices?[index]; + setState(() {}); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 5.w); + }, + ), + ), + ), + SizedBox(height: 20.w), + Container( + color: Colors.black12, + width: ScreenUtil().screenWidth, + height: 0.5.w, + ), + SizedBox(height: 20.w), + Container( + alignment: Alignment.center, + child: Row( + children: [ + Consumer( + builder: (context, ref, child) { + return GestureDetector( + child: Row( + children: [ + SizedBox(width: 10.w), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIcon(), + width: 20.w, + height: 20.w, + ), + SizedBox(width: 5.w), + text( + "${ref.myBalance}", + fontSize: 13.sp, + textColor: SocialChatTheme.primaryLight, + ), + Icon( + Icons.chevron_right, + size: 20.w, + color: SocialChatTheme.primaryLight, + ), + ], + ), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + WalletRoute.recharge, + replace: false, + ); + }, + ); + }, + ), + Spacer(), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12.w), + ), + child: text( + SCAppLocalizations.of(context)!.purchase, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .storePurchasing( + widget.res.id ?? "", + SCPropsType.AVATAR_FRAME.name, + SCCurrencyType.GOLD.name, + "${curentPropsPrice?.days}", + ) + .then((value) { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCLoadingManager.hide(); + SCTts.show( + SCAppLocalizations.of( + context, + )!.purchaseIsSuccessful, + ); + Provider.of( + context, + listen: false, + ).updateBalance(value); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + }, + ), + SizedBox(width: 10.w), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/store/props_store_mountains_detail_dialog.dart b/lib/ui_kit/widgets/store/props_store_mountains_detail_dialog.dart new file mode 100644 index 0000000..3dc2752 --- /dev/null +++ b/lib/ui_kit/widgets/store/props_store_mountains_detail_dialog.dart @@ -0,0 +1,250 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import '../../../app/constants/sc_global_config.dart'; +import '../../../app/routes/sc_fluro_navigator.dart'; +import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; +import '../../../shared/data_sources/sources/local/user_manager.dart'; +import '../../../modules/wallet/wallet_route.dart'; +import '../../../main.dart'; +import '../../components/sc_debounce_widget.dart'; + +class PropsStoreMountainsDetailDialog extends StatefulWidget { + StoreListRes res; + + //折扣 + double disCount = 1; + + PropsStoreMountainsDetailDialog(this.res, this.disCount); + + @override + _PropsStoreMountainsDetailDialogState createState() => + _PropsStoreMountainsDetailDialogState(); +} + +class _PropsStoreMountainsDetailDialogState + extends State { + PropsPrices? curentPropsPrice; + int currentIndex = 0; + + @override + void initState() { + super.initState(); + currentIndex = 0; + curentPropsPrice = widget.res.propsPrices![currentIndex]; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + height: 330.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + color: Color(0xff03523a), + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Column( + children: [ + SizedBox(height: 20.w), + netImage( + url: + widget.res.propsResources?.cover ?? "", + width: 95.w, + height: 95.w, + ), + SizedBox(height: 25.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 15.w), + text( + widget.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 12.sp, + ), + Spacer(), + Image.asset( + "sc_images/general/sc_icon_jb.png", + width: 22.w, + height: 22.w, + ), + widget.disCount < 1 + ? text( + "${curentPropsPrice?.amount}", + textColor: SocialChatTheme.primaryLight, + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ) + : Container(), + widget.disCount < 1 ? SizedBox(width: 5.w) : Container(), + text( + "${((curentPropsPrice?.amount ?? 0) * widget.disCount).toInt()}", + textColor:SocialChatTheme.primaryLight, + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 15.w), + ], + ), + SizedBox(height: 10.w), + SingleChildScrollView( + child: SizedBox( + height: 28.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: widget.res.propsPrices?.length ?? 0, + itemBuilder: (context, index) { + return GestureDetector( + child: Container( + width: 110.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.w), + color: Color(0xff18F2B1).withOpacity(0.1), + border: Border.all( + color: + index == currentIndex + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + ), + child: text( + "${widget.res.propsPrices?[index].days} ${SCAppLocalizations.of(context)!.day}", + textColor: SocialChatTheme.primaryLight, + ), + ), + onTap: () { + currentIndex = index; + curentPropsPrice = widget.res.propsPrices?[index]; + setState(() {}); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 5.w); + }, + ), + ), + ), + SizedBox(height: 20.w), + Container( + color: Colors.black12, + width: ScreenUtil().screenWidth, + height: 0.5.w, + ), + SizedBox(height: 20.w), + Container( + alignment: Alignment.center, + child: Row( + children: [ + Consumer( + builder: (context, ref, child) { + return GestureDetector( + child: Row( + children: [ + SizedBox(width: 10.w), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIcon(), + width: 20.w, + height: 20.w, + ), + SizedBox(width: 5.w), + text( + "${ref.myBalance}", + fontSize: 13.sp, + textColor: SocialChatTheme.primaryLight, + ), + Icon( + Icons.chevron_right, + size: 20.w, + color: SocialChatTheme.primaryLight, + ), + ], + ), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + WalletRoute.recharge, + replace: false, + ); + }, + ); + }, + ), + Spacer(), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12.w), + ), + child: text( + SCAppLocalizations.of(context)!.purchase, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .storePurchasing( + widget.res.id ?? "", + SCPropsType.AVATAR_FRAME.name, + SCCurrencyType.GOLD.name, + "${curentPropsPrice?.days}", + ) + .then((value) { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCLoadingManager.hide(); + SCTts.show( + SCAppLocalizations.of( + context, + )!.purchaseIsSuccessful, + ); + Provider.of( + context, + listen: false, + ).updateBalance(value); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + }, + ), + SizedBox(width: 10.w,) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/store/props_store_theme_detail_dialog.dart b/lib/ui_kit/widgets/store/props_store_theme_detail_dialog.dart new file mode 100644 index 0000000..f887afc --- /dev/null +++ b/lib/ui_kit/widgets/store/props_store_theme_detail_dialog.dart @@ -0,0 +1,250 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/store_list_res.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import '../../../app/constants/sc_global_config.dart'; +import '../../../app/routes/sc_fluro_navigator.dart'; +import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; +import '../../../shared/data_sources/models/enum/sc_props_type.dart'; +import '../../../modules/wallet/wallet_route.dart'; +import '../../../main.dart'; +import '../../components/sc_debounce_widget.dart'; + +class PropsStoreThemeDetailDialog extends StatefulWidget { + StoreListRes res; + + //折扣 + double disCount = 1; + + PropsStoreThemeDetailDialog(this.res, this.disCount); + + @override + _PropsStoreThemeDetailDialogState createState() => + _PropsStoreThemeDetailDialogState(); +} + +class _PropsStoreThemeDetailDialogState + extends State { + PropsPrices? curentPropsPrice; + int currentIndex = 0; + + @override + void initState() { + super.initState(); + currentIndex = 0; + curentPropsPrice = widget.res.propsPrices![currentIndex]; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + height: 330.w, + width: ScreenUtil().screenWidth, + decoration: BoxDecoration( + color: Color(0xff03523a), + borderRadius: BorderRadius.all(Radius.circular(10.w)), + ), + child: Column( + children: [ + SizedBox(height: 20.w), + netImage( + url: + widget.res.propsResources?.cover ?? "", + borderRadius: BorderRadius.circular(10.w), + width: 95.w, + height: 95.w, + ), + SizedBox(height: 25.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 15.w), + text( + widget.res.propsResources?.name ?? "", + textColor: Colors.white, + fontSize: 12.sp, + ), + Spacer(), + Image.asset( + "sc_images/general/sc_icon_jb.png", + width: 22.w, + height: 22.w, + ), + widget.disCount < 1 + ? text( + "${curentPropsPrice?.amount}", + textColor:SocialChatTheme.primaryLight, + fontWeight: FontWeight.w600, + decoration: TextDecoration.lineThrough, + ) + : Container(), + widget.disCount < 1 ? SizedBox(width: 5.w) : Container(), + text( + "${((curentPropsPrice?.amount ?? 0) * widget.disCount).toInt()}", + textColor:SocialChatTheme.primaryLight, + fontSize: 13.sp, + fontWeight: FontWeight.w600, + ), + SizedBox(width: 15.w), + ], + ), + SizedBox(height: 10.w), + SingleChildScrollView( + child: SizedBox( + height: 28.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemCount: widget.res.propsPrices?.length ?? 0, + itemBuilder: (context, index) { + return GestureDetector( + child: Container( + width: 85.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.w), + color: Color(0xff18F2B1).withOpacity(0.1), + border: Border.all( + color: + index == currentIndex + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + ), + child: text( + "${widget.res.propsPrices?[index].days} ${SCAppLocalizations.of(context)!.day}", + textColor: SocialChatTheme.primaryLight, + ), + ), + onTap: () { + currentIndex = index; + curentPropsPrice = widget.res.propsPrices?[index]; + setState(() {}); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 5.w); + }, + ), + ), + ), + SizedBox(height: 20.w), + Container( + color: Colors.black12, + width: ScreenUtil().screenWidth, + height: 0.5.w, + ), + SizedBox(height: 20.w), + Container( + alignment: Alignment.center, + child: Row( + children: [ + Consumer( + builder: (context, ref, child) { + return GestureDetector( + child: Row( + children: [ + SizedBox(width: 10.w), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getStorePageGoldIcon(), + width: 20.w, + height: 20.w, + ), + SizedBox(width: 5.w), + text( + "${ref.myBalance}", + fontSize: 13.sp, + textColor: SocialChatTheme.primaryLight, + ), + Icon( + Icons.chevron_right, + size: 20.w, + color: SocialChatTheme.primaryLight, + ), + ], + ), + onTap: () { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + WalletRoute.recharge, + replace: false, + ); + }, + ); + }, + ), + Spacer(), + SCDebounceWidget( + child: Container( + alignment: Alignment.center, + width: 130.w, + padding: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + color: SocialChatTheme.primaryLight, + borderRadius: BorderRadius.circular(12.w), + ), + child: text( + SCAppLocalizations.of(context)!.purchase, + fontSize: 16.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + onTap: () { + SCLoadingManager.show(); + SCStoreRepositoryImp() + .storePurchasing( + widget.res.id ?? "", + SCPropsType.AVATAR_FRAME.name, + SCCurrencyType.GOLD.name, + "${curentPropsPrice?.days}", + ) + .then((value) { + SmartDialog.dismiss(tag: "showPropsDetail"); + SCLoadingManager.hide(); + SCTts.show( + SCAppLocalizations.of( + context, + )!.purchaseIsSuccessful, + ); + Provider.of( + context, + listen: false, + ).updateBalance(value); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + }, + ), + SizedBox(width: 10.w,) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/test.dart b/lib/ui_kit/widgets/test.dart new file mode 100644 index 0000000..19c2357 --- /dev/null +++ b/lib/ui_kit/widgets/test.dart @@ -0,0 +1,326 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'dart:math'; +import 'dart:async'; + +import 'package:yumi/ui_kit/widgets/headdress/headdress_widget.dart'; +import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart'; + +// 红包数据模型 +class RedPacket { + double x; // x坐标 + double y; // y坐标 + double speed; // 下落速度 + double size; // 大小 + double rotation; // 旋转角度 + double rotationSpeed; // 旋转速度 + Color color; // 红包颜色 + bool isCollected; // 是否被收集 + String id; // 唯一标识符 + + RedPacket({ + required this.x, + required this.y, + required this.speed, + required this.size, + required this.rotation, + required this.rotationSpeed, + required this.color, + this.isCollected = false, + required this.id, + }); +} + +class RedPacketRainPage extends StatefulWidget { + @override + _RedPacketRainPageState createState() => _RedPacketRainPageState(); +} + +class _RedPacketRainPageState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + final Random _random = Random(); + final List _redPackets = []; + final int _maxPackets = 50; + DateTime? _startTime; + int _durationSeconds = 30; + int _collectedCount = 0; + + final List _redColors = [ + Colors.red[400]!, + Colors.red[500]!, + Colors.red[600]!, + Colors.red[700]!, + Colors.red[800]!, + Colors.red[900]!, + ]; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 350), + )..addListener(_updateRain); + + _startRain(); + + Future.delayed(Duration(seconds: 3),(){ + RoomEntranceQueueManager().addToQueue(AnimationQueueItem(resource: "sc_images/room/entrance/test3.svga",dynamicData: {"textReplacements":{"02":"3123123"}})); + }); + } + + void _startRain() { + _startTime = DateTime.now(); + _collectedCount = 0; + _redPackets.clear(); + _controller.repeat(); + + Timer(Duration(seconds: _durationSeconds), () { + if (mounted) { + _controller.stop(); + } + }); + } + + void _updateRain() { + if (!mounted) return; + + final screenWidth = MediaQuery.of(context).size.width; + final screenHeight = MediaQuery.of(context).size.height; + + // 随机生成新红包 + if (_redPackets.length < _maxPackets && _random.nextDouble() < 0.3) { + _createNewPacket(screenWidth, screenHeight); + } + + // 更新现有红包位置 + for (int i = _redPackets.length - 1; i >= 0; i--) { + final packet = _redPackets[i]; + + if (!packet.isCollected) { + packet.y += packet.speed; + packet.rotation += packet.rotationSpeed; + + if (packet.y > screenHeight + 100) { + _redPackets.removeAt(i); + continue; + } + } + } + + setState(() {}); + } + + void _createNewPacket(double screenWidth, double screenHeight) { + _redPackets.add( + RedPacket( + x: _random.nextDouble() * screenWidth, + y: -50, + speed: 2 + _random.nextDouble() * 3, + size: 30 + _random.nextDouble() * 20, + rotation: _random.nextDouble() * 2 * pi, + rotationSpeed: (_random.nextDouble() - 0.5) * 0.1, + color: _redColors[_random.nextInt(_redColors.length)], + id: '${DateTime.now().millisecondsSinceEpoch}_${_random.nextInt(1000)}', + ), + ); + } + + void _collectPacket(String packetId) { + final packetIndex = _redPackets.indexWhere( + (packet) => packet.id == packetId, + ); + + if (packetIndex != -1 && !_redPackets[packetIndex].isCollected) { + setState(() { + _redPackets[packetIndex].isCollected = true; + _collectedCount++; + }); + + // 收集动画效果 + Future.delayed(Duration(milliseconds: 350), () { + if (mounted) { + setState(() { + _redPackets.removeWhere((packet) => packet.id == packetId); + }); + } + }); + } + } + + @override + void dispose() { + _controller.dispose(); + _redPackets.clear(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final remainingTime = + _startTime != null + ? max( + 0, + _durationSeconds - + DateTime.now().difference(_startTime!).inSeconds, + ) + : _durationSeconds; + + return Scaffold( + backgroundColor: Colors.black, + body: RoomEntranceWidget( + height: 90.w, + ), + ); + } + + void _handleTap(Offset position) { + // 从后往前遍历,这样点击检测会优先处理最上层的红包(最后绘制的) + for (int i = _redPackets.length - 1; i >= 0; i--) { + final packet = _redPackets[i]; + if (!packet.isCollected && _isPointInRedPacket(position, packet)) { + _collectPacket(packet.id); + break; // 只处理最上层的一个红包 + } + } + } + + bool _isPointInRedPacket(Offset point, RedPacket packet) { + // 简化的点击区域检测 - 圆形区域 + final distance = sqrt( + pow(point.dx - packet.x, 2) + pow(point.dy - packet.y, 2), + ); + return distance < packet.size; + } + + Widget _buildInfoCard(String title, String value) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.red), + ), + child: Column( + children: [ + Text(title, style: TextStyle(color: Colors.white, fontSize: 10)), + Text( + value, + style: TextStyle( + color: Colors.red, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} + +class RedPacketPainter extends CustomPainter { + final List redPackets; + + RedPacketPainter({required this.redPackets}); + + @override + void paint(Canvas canvas, Size size) { + final textPainter = TextPainter(textDirection: TextDirection.ltr); + + for (final packet in redPackets) { + if (packet.isCollected) { + _drawCollectedPacket(canvas, packet); + } else { + _drawRedPacket(canvas, packet, textPainter); + } + } + } + + void _drawRedPacket( + Canvas canvas, + RedPacket packet, + TextPainter textPainter, + ) { + canvas.save(); + canvas.translate(packet.x, packet.y); + canvas.rotate(packet.rotation); + + // 绘制红包主体 + final packetPaint = + Paint() + ..color = packet.color + ..style = PaintingStyle.fill; + + final rect = Rect.fromCenter( + center: Offset.zero, + width: packet.size, + height: packet.size * 1.5, + ); + + final rrect = RRect.fromRectAndRadius(rect, Radius.circular(8)); + canvas.drawRRect(rrect, packetPaint); + + // 红包中间的黄色条纹 + final stripePaint = + Paint() + ..color = Colors.yellow[700]! + ..style = PaintingStyle.fill; + + final stripeRect = Rect.fromCenter( + center: Offset.zero, + width: packet.size * 0.8, + height: packet.size * 0.2, + ); + canvas.drawRRect( + RRect.fromRectAndRadius(stripeRect, Radius.circular(4)), + stripePaint, + ); + + // 红包文字 + textPainter.text = TextSpan( + text: '福', + style: TextStyle( + color: Colors.yellow[700], + fontSize: packet.size * 0.4, + fontWeight: FontWeight.bold, + ), + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset(-textPainter.width / 2, -textPainter.height / 2), + ); + + canvas.restore(); + } + + void _drawCollectedPacket(Canvas canvas, RedPacket packet) { + final progress = (DateTime.now().millisecondsSinceEpoch % 300) / 300; + final scale = 1.0 - progress; + final opacity = 1.0 - progress; + + canvas.save(); + canvas.translate(packet.x, packet.y); + canvas.scale(scale); + + final packetPaint = + Paint() + ..color = packet.color.withOpacity(opacity) + ..style = PaintingStyle.fill; + + final rect = Rect.fromCenter( + center: Offset.zero, + width: packet.size, + height: packet.size * 1.5, + ); + + final rrect = RRect.fromRectAndRadius(rect, Radius.circular(8)); + canvas.drawRRect(rrect, packetPaint); + + canvas.restore(); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/CHANGELOG.md b/local_packages/flutter_foreground_task-9.1.0/CHANGELOG.md new file mode 100644 index 0000000..77e7578 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/CHANGELOG.md @@ -0,0 +1,540 @@ +## 9.1.0 + +* [**FEAT**] Support manual foregroundServiceType via serviceTypes in startService + +## 9.0.0 + +* [**CHORE**] Bump minimum supported SDK version to `Flutter 3.22/Dart 3.4` +* [**CHORE**] Bump `kotlin_version(1.7.10 -> 1.9.10)`, `gradle(7.3.0 -> 8.6.0)` for Android 15 +* [**FEAT**] Add `isTimeout` param to the onDestroy callback +* [**FIX**] Fix "null object" error [#332](https://github.com/Dev-hwang/flutter_foreground_task/issues/332) +* [**FIX**] Fix "Reply already submitted" error [#330](https://github.com/Dev-hwang/flutter_foreground_task/issues/330) +* [**FIX**] Prevent crash by catching exceptions during foreground service start +* Check [migration_documentation](./documentation/migration_documentation.md) for changes + +## 8.17.0 + +* [**FEAT**] Allow `onNotificationPressed` to trigger without `SYSTEM_ALERT_WINDOW` permission + - Do not use the `launchApp` function inside the `onNotificationPressed` callback + - Instead, use the `notificationInitialRoute` option of the `startService` or `updateService` functions + +## 8.16.0 + +* [**BREAKING**] Change `ServiceRequestResult` class to `sealed class` for improved code readability +* [**BREAKING**] Change method for customizing notification icon +* [**FEAT**] Add `copyWith` function for models reuse +* Check [migration_documentation](./documentation/migration_documentation.md) for changes + +## 8.14.0 + +* [**FEAT**] Support quickboot for HTC devices + +## 8.13.0 + +* [**CHORE**] Downgrade iOS minimumVersion from `13.0` to `12.0` + +## 8.12.0 + +* [**FEAT**] Add `setOnlyAlertOnce` option to AndroidNotificationOptions [pr-#287](https://github.com/Dev-hwang/flutter_foreground_task/pull/287) +* [**CHANGE**] Change notification channel importance from `DEFAULT` to `LOW` + +## 8.11.0 + +* [**FEAT**] Allow sending `Set` collection using `FlutterForegroundTask.sendDataToMain` + +## 8.10.4 + +* [**FIX**] Fixed an issue where main function was called repeatedly when there was no callback to start +* [**FIX**] Handle exceptions that occur when service component cannot be found + +## 8.10.2 + +* [**FIX**] Fixed an issue where exception was thrown when flutterEngine failed to start [#282](https://github.com/Dev-hwang/flutter_foreground_task/issues/282) +* [**FIX**] Fixed an issue with flutterEngine not being destroyed properly + +## 8.10.0 + +* [**BREAKING**] Change `onStart`, `onDestroy` callback return type from `void` to `Future` + - The onRepeatEvent callback is called when the onStart asynchronous operation has finished + - Now you can access network, database, and other plugins in onDestroy callback [#276](https://github.com/Dev-hwang/flutter_foreground_task/issues/276) + - Check [migration_documentation](./documentation/migration_documentation.md) for changes + +## 8.9.0 + +* [**CHANGE**] Ignore `autoRunOnBoot` option when service is stopped by developer +* [**CHANGE**] Ignore `autoRunOnBoot` option when android:stopWithTask is set to true +* [**FEAT**] Add TaskStarter to check who started the task [#276](https://github.com/Dev-hwang/flutter_foreground_task/issues/276) + - Add `starter` parameter to `onStart` callback of TaskHandler + - `.developer`: The task has been started by the developer (startService, restartService, updateService) + - `.system`: The task has been started by the system (reboot, app-updates, AlarmManager-restart) +* [**FEAT-iOS**] Allow background app refresh + - ~~Bump iOS minimumVersion to 13.0~~ + - You need to add `BGTaskSchedulerPermittedIdentifiers` key in `ios/Runner/info.plist` file + - Check [Getting started-iOS](https://pub.dev/packages/flutter_foreground_task#baby_chick-ios) for more details + +## 8.8.1+1 + +* [**DOCS**] Update example to see two-way communication flow + +## 8.8.1 + +* [**FIX**] Fix issue related to `openIgnoreBatteryOptimizationSettings` [#275](https://github.com/Dev-hwang/flutter_foreground_task/issues/275) + +## 8.8.0 + +* [**REMOVE**] Remove notification permission request code in startService + - Notification permission request is required before starting the service + - Check step-4 of [readme](https://github.com/Dev-hwang/flutter_foreground_task?tab=readme-ov-file#hatched_chick-step-by-step) +* [**FIX-Android**] Fix issue with calling onRepeatEvent without waiting for repeat delay after onStart + - If use the `eventAction: ForegroundTaskEventAction.repeat(millis)` + - flow: `onStart` - (millis) - `onRepeatEvent` - (millis) - `onRepeatEvent`.. + +## 8.7.0 + +* [**FEAT**] Allow permission settings page to be opened even if permission is granted + - openIgnoreBatteryOptimizationSettings() + - openSystemAlertWindowSettings() + - openAlarmsAndRemindersSettings() + +## 8.6.0 + +* [**BREAKING**] Change the way to set the task intervals (for increase scalability) + - Remove `interval`, `isOnceEvent` option in ForegroundTaskOptions model + - Add `eventAction` option with ForegroundTaskEventAction constructor + - Check [migration_documentation](./documentation/migration_documentation.md) for changes + +## 8.5.0 + +* [**FEAT**] Add `openAlarmsAndRemindersSettings` utility + - This utility allows the Android OS to immediately restart service in doze mode. + - required `android.permission.SCHEDULE_EXACT_ALARM` permission. + - When you call this function, will be gone to the settings page. So you need to explain to the user why set it. + - Check [utility_documentation](./documentation/utility_documentation.md) for more details. +* [**FEAT**] Add `canScheduleExactAlarms` utility + - Returns whether the `android.permission.SCHEDULE_EXACT_ALARM` permission is granted. + - In some cases, permission is granted automatically. Don't worry :) + +## 8.3.1 + +* [**FIX**] Fixed an issue where Map collection could not be cast in onReceiveData [#258](https://github.com/Dev-hwang/flutter_foreground_task/issues/258) + +## 8.3.0 + +* [**FEAT**] Add showBadge option to AndroidNotificationOptions. +* [**DEPRECATED**] Deprecated AndroidNotificationOptions.id. Use startService(serviceId) instead. +* [**DOCS**] Separate models, utility, migration documentation. + +## 8.2.0 + +* [**CHORE-AOS**] Down Android minSdkVersion to 21 + +## 8.1.0 + +* [**FIX-AOS**] Fixed an issue where notification text was not visible on Android 7.1 and below +* [**FEAT-iOS**] Implement onNotificationButtonPressed +* [**FEAT-iOS**] Implement onNotificationPressed +* [**FEAT-iOS**] Implement onNotificationDismissed +* [**FEAT-iOS**] Implement notification permission request and check function + - Request notification permission in the `Example._requestPermissions` function + +## 8.0.0 + +* [**BREAKING**] Redesign the communication method between TaskHandler and UI + - Fixed an issue where `SendPort.send` does not work when calling `receivePort` getter function after app restart [#244](https://github.com/Dev-hwang/flutter_foreground_task/issues/244) + - Allow task data to be listened on multiple pages + - Check [migration_documentation](./documentation/migration_documentation.md) for changes + +## 7.5.2 + +* [**FIX**] Fixed an issue that caused poor performance and delays in TaskHandler +* [**FIX-iOS**] Fixed an issue where onDestroy was not called when an app was closed in recent apps + +## 7.5.0+1 + +* [**DOCS**] Update readme + - Update documentation on how to pass timestamp to UI isolate + - Update `WithForegroundTask` documentation comments + +## 7.5.0 + +* [**FEAT**] Support notificationText with multiple lines [#182](https://github.com/Dev-hwang/flutter_foreground_task/issues/182) +* [**REMOVE**] Remove `WillStartForegroundTask` widget + - This widget is designed to start a service when the app is minimized. + - This widget had an ambiguous purpose, it caused a lot of problems because the widget controlled the service. + - These issues have led me to remove this widget. Please implement it yourself if necessary. + +## 7.4.3 + +* [**FEAT-Recommended**] Improve restart alarm that occurs when the service terminates abnormally + +## 7.4.2 + +* [**FIX**] Fixed an issue where notification events were handled on both when using plugin in multiple apps [#137](https://github.com/Dev-hwang/flutter_foreground_task/issues/137) +* [**FIX**] Fixed an issue where the service did not restart when Android OS forcibly terminated the service [#223](https://github.com/Dev-hwang/flutter_foreground_task/issues/223) + +## 7.4.0 + +* [**FEAT**] Add result class to handle service request errors + - The return value of service request functions changed from boolean to `ServiceRequestResult` +* [**FEAT**] Add function to send data to TaskHandler + - You can send data from UI to TaskHandler using `FlutterForegroundTask.sendData` function + +## 7.2.0 + +* [**FEAT**] Add ability to restart service when app is deleted from recent app list + - To restart service on Android 12+, you must allow the `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission +* [**REMOVE**] Remove `isSticky` from AndroidNotificationOptions + - The isSticky option is automatically set by the service based on the Manifest android:stopWithTask + +## 7.1.0 + +* [**FEAT-Recommended**] Allow updateService to be processed without service restart + +## 7.0.0 + +* [**CHORE**] Updates minimum supported SDK version to `Flutter 3.10` / `Dart 3.0` +* [**BREAKING**] Change timezone(local > UTC) of timestamp in TaskHandler callback +* [**BREAKING**] Remove `iconData`, `buttons` from AndroidNotificationOptions + - Can use this options in the startService function + - Check [migration_documentation](./documentation/migration_documentation.md) for changes +* [**FEAT**] Add ability to update notification icon and buttons for Android platform +* [**FEAT**] Add `onNotificationDismissed` callback for Android 14 + +## 6.5.0 + +* [**CHORE**] Bump Android minSdkVersion to 23 +* [**FIX**] Fixed an issue where notification actions did not work on Android 14 + +## 6.4.0 + +* [**REMOVE**] Remove `foregroundServiceType` option +* [**FEAT**] Add `autoRunOnMyPackageReplaced` option + +## 6.3.0 + +* [**FEAT**] ~~Add option to allow multiple foregroundServiceType~~ +* [**DOCS**] Update readme + +## 6.2.0 + +* [**FEAT**] Support AGP 8 +* [**FEAT**] ~~Add `foregroundServiceType` option to specify foreground service type on Android 14 and higher~~ + +## 6.1.3 + +* [**DOCS**] Update readme [#192](https://github.com/Dev-hwang/flutter_foreground_task/issues/192) +* [**CHORE**] Remove platform dependency +* [**CHORE**] Update dependencies + +## 6.1.2 + +* [**FIX**] Fix issue where SecurityException occurred when registering runtime receiver [#175](https://github.com/Dev-hwang/flutter_foreground_task/issues/175) + +## 6.1.1 + +* [**TEST**] Add assertions to service options + +## 6.1.0 + +* [**BREAKING**] Remove future return of TaskHandler callback function +* [**FIX**] Fix issue where isRunningService is not updated after calling onDestroy +* [**FIX**] Fix storage data not syncing between isolates +* [**FIX**] Fix the service could not be started when the notification channel information is empty +* [**CHORE**] Upgrade dependencies - shared_preferences + +## 6.0.0+1 + +* [**REFACTOR**] Move required permissions on the Android platform inside the plugin + +## 6.0.0 + +* [**BREAKING**] Rename the callback function to clarify what information the event provides + - Rename `onButtonPressed` to `onNotificationButtonPressed` + - Rename `onEvent` to `onRepeatEvent` +* [**FEAT**] Add textColor option to NotificationButton +* [**FEAT**] Add ability to change task options while service is running + +## 5.2.1 + +* [**FIX**] Fix issue where service could not be started in the background + +## 5.2.0 + +* [**FEAT**] Add id option to AndroidNotificationOptions +* [**FEAT**] The WillStartForegroundTask widget supports receiving data + +## 5.0.0 + +* [**CHORE**] Update dependency constraints to `sdk: '>=2.18.0 <4.0.0'` `flutter: '>=3.3.0'` +* [**FEAT**] Add notification permission request func for Android 13 + - `FlutterForegroundTask.checkNotificationPermission()` + - `FlutterForegroundTask.requestNotificationPermission()` +* [**DOCS**] Update documentation to the latest version +* [**FIX**] Fix service not starting when notification permission is denied + +## 4.2.0 + +* [**FEAT**] Add notification permission request func for Android 13 + - According to the [official documentation](https://developer.android.com/develop/ui/views/notifications/notification-permission), starting with Android 13 and higher, you need to request notification permission to expose foreground service notifications. + - In this version, notification permission requests occur when the `startService` function is called. + - Add `` permission to your `AndroidManifest.xml` file. + +## 4.1.0 + +* [**CHORE**] Bump Android coroutines version to 1.6.4 +* [**CHANGE**] Change the way get receivePort from asynchronous to synchronous [#128](https://github.com/Dev-hwang/flutter_foreground_task/issues/128) + - Can register and get receivePort without starting the service. + - From now on, register receivePort before starting the service. Please check the readme and example. +* [**FIX**] Fix issue where the results of the service start and stop functions did not match the service status + +## 4.0.1 + +* [**FIX**] Fix mounted error [#133](https://github.com/Dev-hwang/flutter_foreground_task/issues/133) + +## 4.0.0 + +* [**CHORE**] Bump Android Gradle version to 7.1.2 +* [**CHORE**] Update minimum Flutter version to 3.0.0 [#130](https://github.com/Dev-hwang/flutter_foreground_task/issues/130) [#131](https://github.com/Dev-hwang/flutter_foreground_task/issues/131) +* [**DOCS**] Update readme [#125](https://github.com/Dev-hwang/flutter_foreground_task/issues/125) + +## 3.10.0 + +* [**FEAT**] Add `isOnceEvent` option to `ForegroundTaskOptions.class`. +* [**DOCS**] Add `entry-point` pragma. +* [**REFACTOR**] Refactor code using plugin_platform_interface. + +## 3.9.0 + +* [**FEAT**] Add `allowWakeLock` option to `ForegroundTaskOptions.class`. +* [**FEAT**] Add `forceOpen` option to `openSystemAlertWindowSettings()`. + +## 3.8.2 + +* Fix issue with SharedPreferences won't provide updated data from main isolate while running on background. Thanks @h2210316651 + +## 3.8.1 + +* Fix issue where sendPort returned null when restartService called. + +## 3.8.0 + +* Upgrade Coroutine library. +* Upgrade shared_preferences plugin. +* Separate the SendPort registration code from the foreground service related functions. + - Register a SendPort object only when the user needs a ReceivePort. + - Please see [this page](https://github.com/Dev-hwang/flutter_foreground_task/blob/1cfc23160eb352fbfa74f7dfbe34ff714a83fffc/example/lib/main.dart#L147) for a modified example. +* [**FEAT**] Add `isAppOnForeground` function. + - Returns whether the app is in the foreground. + - This can be used when you want to perform some function when the app is in the foreground. +* [**FEAT**] Add `setOnLockScreenVisibility` function. Thanks @Techno-Disaster + - Toggles lockScreen visibility. + - If set to true, launchApp can be run from the lockscreen. + +## 3.7.3 + +* [[#61](https://github.com/Dev-hwang/flutter_foreground_task/issues/61)] Add code to prevent ForegroundServiceStartNotAllowedException. +* [[#78](https://github.com/Dev-hwang/flutter_foreground_task/issues/78)] Fix compilation errors for Flutter 3.0.0 + +## 3.7.2 + +* [[#62](https://github.com/Dev-hwang/flutter_foreground_task/issues/62)] Fix issues with SharedPreferences. + +## 3.7.1 + +* Add SYSTEM_ALERT_WINDOW permission request function. +* Provide a way to use notification press handler on Android 10+. + - https://developer.android.com/guide/components/activities/background-starts + - You can use the SYSTEM_ALERT_WINDOW permission to work around the above Restrictions. + +## 3.7.0 + +* Add notification press handler for Android platform. +* Add sendPort parameter to onDestroy function of TaskHandler. +* Add receivePort getter function. +* Clarify the meaning of the dev message. +* Example updates. + +## 3.6.0 + +* Upgrade Flutter SDK minimum version to 2.8.1 +* Upgrade shared_preferences: ^2.0.13 +* Add `backgroundColor` option for AndroidNotificationOptions. +* Add `getAllData` function. +* Fixed the problem that data not related to the service is deleted when clearAllData() is called. +* Fixed the problem that the notification button did not work when using a specific button id. + +## 3.5.5 + +* Downgrade Android minSdkVersion to 21. + +## 3.5.4 + +* [[#42](https://github.com/Dev-hwang/flutter_foreground_task/issues/42)] Only minimize app on pop when there is no route to pop. + +## 3.5.3 + +* Upgrade shared_preferences: ^2.0.11 + +## 3.5.2 + +* [**iOS**] Fixed an issue where notifications not related to the service were removed. +* [**iOS**] Improved compatibility with other plugins that use notifications. + - Additional settings are required, so please check the Readme-Getting started. + +## 3.5.1 + +* Add process exit code to prevent memory leak. +* Fix dart formatting issues. + +## 3.5.0 + +* Upgrade shared_preferences: ^2.0.9 +* Can now add action buttons to Android notification. + +## 3.4.1 + +* [**Bug**] Fixed an issue where lockMode(wakeLock, wifiLock) was not properly released when the service was forcibly shutdown. +* [**Bug**] Fixed an issue where foreground service notification UX was delayed on Android version 12. + +## 3.4.0 + +* Add wakeLock to keep the CPU active in the background. + - Need to add `android.permission.WAKE_LOCK` permission to `AndroidManifest.xml` file. +* Add wifiLock to keep the Wi-Fi radio awake in the background. + - Enable or disable can be set with `allowWifiLock` of `ForegroundTaskOptions` class. + +## 3.3.0 + +* Add `requestIgnoreBatteryOptimization` function. +* Change onWillStart type from `ValueGetter` to `AsyncValueGetter`. + +## 3.2.3 + +* [**Bug**] Fixed an issue where IllegalArgumentException was thrown when starting the service on Android version 11 and higher. +* Bump Android minSdkVersion to 23. +* Bump Android compileSdkVersion to 31. + +## 3.2.2 + +* [**Bug**] Fixed an issue where RemoteServiceException occurred intermittently. + +## 3.2.1 + +* [**iOS**] Fixed an issue where all data stored in `UserDefaults.standard` was removed when the stopService function was called. + +## 3.2.0 + +* Add `restartService` function. You can now restart the service to get the new `ReceivePort`. +* Improve service-related function code. A return value has been added to check if the function was properly requested. + +## 3.1.0 + +* Upgrade shared_preferences: ^2.0.8 +* Add `required keyword` to parameters of saveData func. +* Add `isSticky` notification option for Android. + +## 3.0.0 + +* [**BREAKING**] The way you start the foreground service and register tasks has changed. Check [readme](https://pub.dev/packages/flutter_foreground_task#how-to-use) for more information. +* [**BREAKING**] Change function name from `start` to `startService`. +* [**BREAKING**] Change function name from `update` to `updateService`. +* [**BREAKING**] Change function name from `stop` to `stopService`. +* [**BREAKING**] Change function name from `isRunningTask` to `isRunningService`. +* Added functions for data management. +* Fixed an issue where notifications were not removed when the service was stopped. +* Fixed incorrect documentation. + +## 2.2.1 + +* Fixed playSound option not working properly in the background. + +## 2.2.0 + +* Implement background task on iOS platform. Please check Readme as setup is required. +* Implement a service restart function to deal with unexpected errors. +* Remove `notification_options.dart`. +* Add `android_notification_options.dart`. +* Add `ios_notification_options.dart`. +* Change the `playSound` default value from `true` to `false`. + +## 2.1.0 + +* Fixed duplicate call to startForegroundTask function. +* Optimize android native code. +* Add `sendPort` parameter to TaskCallback. + +## 2.0.5+1 + +* Update README.md + +## 2.0.5 + +* Fix `callbackHandle` type casting error. + +## 2.0.4 + +* Add utility methods related to battery optimization. +* Add `showWhen` option. +* Add `visibility` option. +* Migrate example to null safety. + +## 2.0.3 + +* Add `autoRunOnBoot` field to `ForegroundTaskOptions`. Check the readme for more details. + +## 2.0.2 + +* Add `onDestroy` to clean up used resources in callback functions. + +## 2.0.1 + +* Change the notification icon setting method. +* Improved the code so that the notification icon is displayed properly even when using the resource shrinker. + +## 2.0.0 + +* [**BREAKING**] Remove singleton `instance` of `FlutterForegroundTask`. +* [**BREAKING**] `TaskCallback` return type changed from `void` to `Future`. +* [**BREAKING**] All functions of `FlutterForegroundTask` are applied as static. +* [**BREAKING**] The way foreground task are registered has changed. See the readme for details. +* Add `printDevLog` option. +* Update README.md +* Update Example. + +## 1.0.9 + +* Add `icon` field to `NotificationOptions`. +* Change the model's `toMap` function name to `toJson`. + +## 1.0.8 + +* Add `FlutterForegroundTask.instance.update()` function. +* Update README.md + +## 1.0.7 + +* Fix incorrect comments and documents. +* Add `enableVibration` notification options. +* Add `playSound` notification options. + +## 1.0.5 + +* Fix an issue where `RemoteServiceException` occurs. + +## 1.0.4 + +* Add `WillStartForegroundTask` widget. + +## 1.0.3 + +* Fix incorrect comments and documents. +* Add `channelImportance` notification options. +* Add `priority` notification options. + +## 1.0.1 + +* Add `WithForegroundTask` widget. + +## 1.0.0 + +* Initial release. diff --git a/local_packages/flutter_foreground_task-9.1.0/LICENSE b/local_packages/flutter_foreground_task-9.1.0/LICENSE new file mode 100644 index 0000000..7d88a66 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Dev-hwang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/local_packages/flutter_foreground_task-9.1.0/README.md b/local_packages/flutter_foreground_task-9.1.0/README.md new file mode 100644 index 0000000..e4bab21 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/README.md @@ -0,0 +1,616 @@ +This plugin is used to implement a foreground service on the Android platform. + +

+pub +build +MIT license +

+ +## Features + +* Can perform repetitive tasks with the foreground service. +* Supports two-way communication between the foreground service and UI(main isolate). +* Provides a widget that minimize the app without closing it when the user presses the soft back button. +* Provides useful utilities that can use while performing tasks. +* Provides an option to automatically resume the foreground service on boot. + +## Support version + +- Flutter: `3.22.0+` +- Dart: `3.4.0+` +- Android: `5.0+ (minSdkVersion: 21)` +- iOS: `12.0+` + +## Getting started + +To use this plugin, add `flutter_foreground_task` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). For example: + +```yaml +dependencies: + flutter_foreground_task: ^9.1.0 +``` + +After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly. + +### :baby_chick: Android + +This plugin requires `Kotlin version 1.9.10+` and `Gradle version 8.6.0+`. Please refer to the migration documentation for more details. + +- [project/settings.gradle](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/example/android/settings.gradle) +- [project/gradle-wrapper.properties](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/example/android/gradle/wrapper/gradle-wrapper.properties) +- [app/build.gradle](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/example/android/app/build.gradle) +- [migration_documentation](https://github.com/Dev-hwang/flutter_foreground_task/blob/master/documentation/migration_documentation.md) + +Open the `AndroidManifest.xml` file and declare the service tag inside the `` tag as follows. +If you want the foreground service to run only when the app is running, add `android:stopWithTask="true"`. +As mentioned in the Android guidelines, to start a FG service on Android 14+, you must declare `android:foregroundServiceType`. + +* [`camera`](https://developer.android.com/about/versions/14/changes/fgs-types-required#camera) +* [`connectedDevice`](https://developer.android.com/about/versions/14/changes/fgs-types-required#connected-device) +* [`dataSync`](https://developer.android.com/about/versions/14/changes/fgs-types-required#data-sync) +* [`health`](https://developer.android.com/about/versions/14/changes/fgs-types-required#health) +* [`location`](https://developer.android.com/about/versions/14/changes/fgs-types-required#location) +* [`mediaPlayback`](https://developer.android.com/about/versions/14/changes/fgs-types-required#media) +* [`mediaProjection`](https://developer.android.com/about/versions/14/changes/fgs-types-required#media-projection) +* [`microphone`](https://developer.android.com/about/versions/14/changes/fgs-types-required#microphone) +* [`phoneCall`](https://developer.android.com/about/versions/14/changes/fgs-types-required#phone-call) +* [`remoteMessaging`](https://developer.android.com/about/versions/14/changes/fgs-types-required#remote-messaging) +* [`shortService`](https://developer.android.com/about/versions/14/changes/fgs-types-required#short-service) +* [`specialUse`](https://developer.android.com/about/versions/14/changes/fgs-types-required#special-use) +* [`systemExempted`](https://developer.android.com/about/versions/14/changes/fgs-types-required#system-exempted) + +``` + + + + + + + + + + + +``` + +> [!CAUTION] +> Check [runtime requirements](https://developer.android.com/about/versions/14/changes/fgs-types-required#system-runtime-checks) before starting the service. If this requirement is not met, the foreground service cannot be started. + +> [!CAUTION] +> Android 15 introduces a new timeout behavior to `dataSync` for apps targeting Android 15 (API level 35) or higher. +> The system permits an app's `dataSync` services to run for a total of 6 hours in a 24-hour period. +> However, if the user brings the app to the foreground, the timer resets and the app has 6 hours available. +> +> There are new restrictions on `BOOT_COMPLETED(autoRunOnBoot)` broadcast receivers launching foreground services. +> `BOOT_COMPLETED` receivers are not allowed to launch the following types of foreground services: +> - [dataSync](https://developer.android.com/develop/background-work/services/fg-service-types#data-sync) +> - [camera](https://developer.android.com/develop/background-work/services/fg-service-types#camera) +> - [mediaPlayback](https://developer.android.com/develop/background-work/services/fg-service-types#media) +> - [phoneCall](https://developer.android.com/develop/background-work/services/fg-service-types#phone-call) +> - [microphone](https://developer.android.com/about/versions/14/changes/fgs-types-required#microphone) +> +> You can find how to test this behavior and more details at this [link](https://developer.android.com/about/versions/15/behavior-changes-15#fgs-hardening). + +### :baby_chick: iOS + +You can also run `flutter_foreground_task` on the iOS platform. However, it has the following limitations. + +* If you force close an app in recent apps, the task will be destroyed immediately. +* The task cannot be started automatically on boot like Android OS. +* The task runs in the background for approximately 30 seconds every 15 minutes. This may take longer than 15 minutes due to iOS limitations. + +**Info.plist**: + +Add the key below to `ios/Runner/info.plist` file so that the task can run in the background. + +```text +BGTaskSchedulerPermittedIdentifiers + + com.pravera.flutter_foreground_task.refresh + +UIBackgroundModes + + fetch + +``` + +**Objective-C**: + +To use this plugin developed in Swift in a project using Objective-C, you need to add a bridge header. +If there is no `ios/Runner/Runner-Bridging-Header.h` file in your project, check this [page](https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift). + +Open the `ios/Runner/AppDelegate.swift` file and add the commented code. + +```objc +#import "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +// this +#import + +// this +void registerPlugins(NSObject* registry) { + [GeneratedPluginRegistrant registerWithRegistry:registry]; +} + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + + // this + [FlutterForegroundTaskPlugin setPluginRegistrantCallback:registerPlugins]; + if (@available(iOS 10.0, *)) { + [UNUserNotificationCenter currentNotificationCenter].delegate = (id) self; + } + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end +``` + +**Swift**: + +Declare the import statement below in the `ios/Runner/Runner-Bridging-Header.h` file. + +```objc +#import +``` + +Open the `ios/Runner/AppDelegate.swift` file and add the commented code. + +```swift +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + + // this + SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback { registry in + GeneratedPluginRegistrant.register(with: registry) + } + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + } + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} +``` + +## How to use + +### :hatched_chick: step by step + +1. Initialize port for communication between TaskHandler and UI. + +```dart +void main() { + // Initialize port for communication between TaskHandler and UI. + FlutterForegroundTask.initCommunicationPort(); + runApp(const ExampleApp()); +} +``` + +2. Write a `TaskHandler` and a `callback` to request starting a TaskHandler. + +```dart +// The callback function should always be a top-level or static function. +@pragma('vm:entry-point') +void startCallback() { + FlutterForegroundTask.setTaskHandler(MyTaskHandler()); +} + +class MyTaskHandler extends TaskHandler { + // Called when the task is started. + @override + Future onStart(DateTime timestamp, TaskStarter starter) async { + print('onStart(starter: ${starter.name})'); + } + + // Called based on the eventAction set in ForegroundTaskOptions. + @override + void onRepeatEvent(DateTime timestamp) { + // Send data to main isolate. + final Map data = { + "timestampMillis": timestamp.millisecondsSinceEpoch, + }; + FlutterForegroundTask.sendDataToMain(data); + } + + // Called when the task is destroyed. + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async { + print('onDestroy(isTimeout: $isTimeout)'); + } + + // Called when data is sent using `FlutterForegroundTask.sendDataToTask`. + @override + void onReceiveData(Object data) { + print('onReceiveData: $data'); + } + + // Called when the notification button is pressed. + @override + void onNotificationButtonPressed(String id) { + print('onNotificationButtonPressed: $id'); + } + + // Called when the notification itself is pressed. + @override + void onNotificationPressed() { + print('onNotificationPressed'); + } + + // Called when the notification itself is dismissed. + @override + void onNotificationDismissed() { + print('onNotificationDismissed'); + } +} +``` + +3. Add a callback to receive data sent from the TaskHandler. If the screen or controller is disposed, be sure to call the `removeTaskDataCallback` function. + +```dart +void _onReceiveTaskData(Object data) { + if (data is Map) { + final dynamic timestampMillis = data["timestampMillis"]; + if (timestampMillis != null) { + final DateTime timestamp = + DateTime.fromMillisecondsSinceEpoch(timestampMillis, isUtc: true); + print('timestamp: ${timestamp.toString()}'); + } + } +} + +@override +void initState() { + super.initState(); + // Add a callback to receive data sent from the TaskHandler. + FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData); +} + +@override +void dispose() { + // Remove a callback to receive data sent from the TaskHandler. + FlutterForegroundTask.removeTaskDataCallback(_onReceiveTaskData); + super.dispose(); +} +``` + +4. Request permissions and initialize the service. + +```dart +Future _requestPermissions() async { + // Android 13+, you need to allow notification permission to display foreground service notification. + // + // iOS: If you need notification, ask for permission. + final NotificationPermission notificationPermission = + await FlutterForegroundTask.checkNotificationPermission(); + if (notificationPermission != NotificationPermission.granted) { + await FlutterForegroundTask.requestNotificationPermission(); + } + + if (Platform.isAndroid) { + // Android 12+, there are restrictions on starting a foreground service. + // + // To restart the service on device reboot or unexpected problem, you need to allow below permission. + if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { + // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission. + await FlutterForegroundTask.requestIgnoreBatteryOptimization(); + } + + // Use this utility only if you provide services that require long-term survival, + // such as exact alarm service, healthcare service, or Bluetooth communication. + // + // This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission. + // Using this permission may make app distribution difficult due to Google policy. + if (!await FlutterForegroundTask.canScheduleExactAlarms) { + // When you call this function, will be gone to the settings page. + // So you need to explain to the user why set it. + await FlutterForegroundTask.openAlarmsAndRemindersSettings(); + } + } +} + +void _initService() { + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'foreground_service', + channelName: 'Foreground Service Notification', + channelDescription: + 'This notification appears when the foreground service is running.', + onlyAlertOnce: true, + ), + iosNotificationOptions: const IOSNotificationOptions( + showNotification: false, + playSound: false, + ), + foregroundTaskOptions: ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(5000), + autoRunOnBoot: true, + autoRunOnMyPackageReplaced: true, + allowWakeLock: true, + allowWifiLock: true, + ), + ); +} + +@override +void initState() { + super.initState(); + // Add a callback to receive data sent from the TaskHandler. + FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData); + + WidgetsBinding.instance.addPostFrameCallback((_) { + // Request permissions and initialize the service. + _requestPermissions(); + _initService(); + }); +} +``` + +5. Use `FlutterForegroundTask.startService` to start the service. `startService` provides the following options: +* `serviceId`: The unique ID that identifies the service. +* `notificationTitle`: The title to display in the notification. +* `notificationText`: The text to display in the notification. +* `notificationIcon`: The icon to display in the notification. Go to [this page](./documentation/customize_notification_icon.md) to customize. +* `notificationButtons`: The buttons to display in the notification. (can add 0~3 buttons) +* `notificationInitialRoute`: Initial route to be used when the app is launched via a notification. Works the same as the `launchApp` utility. +* `callback`: A top-level function that calls the setTaskHandler function. + +```dart +Future _startService() async { + if (await FlutterForegroundTask.isRunningService) { + return FlutterForegroundTask.restartService(); + } else { + return FlutterForegroundTask.startService( + // You can manually specify the foregroundServiceType for the service + // to be started, as shown in the comment below. + // serviceTypes: [ + // ForegroundServiceTypes.dataSync, + // ForegroundServiceTypes.remoteMessaging, + // ], + serviceId: 256, + notificationTitle: 'Foreground Service is running', + notificationText: 'Tap to return to the app', + notificationIcon: null, + notificationButtons: [ + const NotificationButton(id: 'btn_hello', text: 'hello'), + ], + notificationInitialRoute: '/', + callback: startCallback, + ); + } +} +``` + +> [!NOTE] +> iOS Platform, `notificationButtons` is not displayed directly in notification. +> When the user slides down the notification, the button is displayed, so you need to guide the user on how to use it. +> https://developer.apple.com/documentation/usernotifications/declaring-your-actionable-notification-types + +6. Use `FlutterForegroundTask.updateService` to update the service. The options are the same as the start function. + +```dart +final ForegroundTaskOptions defaultTaskOptions = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(5000), + autoRunOnBoot: true, + autoRunOnMyPackageReplaced: true, + allowWakeLock: true, + allowWifiLock: true, +); + +@pragma('vm:entry-point') +void startCallback() { + FlutterForegroundTask.setTaskHandler(FirstTaskHandler()); +} + +class FirstTaskHandler extends TaskHandler { + int _count = 0; + + @override + Future onStart(DateTime timestamp, TaskStarter starter) async { + // some code + } + + @override + void onRepeatEvent(DateTime timestamp) { + _count++; + + if (_count == 10) { + FlutterForegroundTask.updateService( + foregroundTaskOptions: defaultTaskOptions.copyWith( + eventAction: ForegroundTaskEventAction.repeat(1000), + ), + callback: updateCallback, + ); + return; + } + + FlutterForegroundTask.updateService( + notificationTitle: 'Hello FirstTaskHandler :)', + notificationText: timestamp.toString(), + ); + + // Send data to main isolate. + final Map data = { + "timestampMillis": timestamp.millisecondsSinceEpoch, + }; + FlutterForegroundTask.sendDataToMain(data); + } + + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async { + // some code + } +} + +@pragma('vm:entry-point') +void updateCallback() { + FlutterForegroundTask.setTaskHandler(SecondTaskHandler()); +} + +class SecondTaskHandler extends TaskHandler { + @override + Future onStart(DateTime timestamp, TaskStarter starter) async { + // some code + } + + @override + void onRepeatEvent(DateTime timestamp) { + FlutterForegroundTask.updateService( + notificationTitle: 'Hello SecondTaskHandler :)', + notificationText: timestamp.toString(), + ); + + // Send data to main isolate. + final Map data = { + "timestampMillis": timestamp.millisecondsSinceEpoch, + }; + FlutterForegroundTask.sendDataToMain(data); + } + + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async { + // some code + } +} +``` + +7. If you no longer use the service, call `FlutterForegroundTask.stopService`. + +```dart +Future _stopService() { + return FlutterForegroundTask.stopService(); +} +``` + +### :hatched_chick: deepening + +This plugin supports two-way communication between TaskHandler and UI(main isolate). + +The send function can only send primitive type(int, double, bool), String, Collection(Map, List) provided by Flutter. + +If you want to send a custom object, send it in String format using jsonEncode and jsonDecode. + +JSON and serialization >> https://docs.flutter.dev/data-and-backend/serialization/json + +```dart +// TaskHandler +@override +Future onStart(DateTime timestamp, TaskStarter starter) async { + // TaskHandler -> Main(UI) + FlutterForegroundTask.sendDataToMain(Object); +} + +// Main(UI) +void _onReceiveTaskData(Object data) { + print('onReceiveTaskData: $data'); +} +``` + +```dart +// Main(UI) +void _sendDataToTask() { + // Main(UI) -> TaskHandler + // + // The Map collection can only be sent in json format, such as Map. + FlutterForegroundTask.sendDataToTask(Object); +} + +// TaskHandler +@override +void onReceiveData(Object data) { + print('onReceiveData: $data'); + + // You can cast it to any type you want using the Collection.cast function. + if (data is List) { + final List intList = data.cast(); + } +} +``` + +And there are some functions for storing and managing data that are only used in this plugin. + +```dart +void function() async { + await FlutterForegroundTask.getData(key: String); + await FlutterForegroundTask.getAllData(); + await FlutterForegroundTask.saveData(key: String, value: Object); + await FlutterForegroundTask.removeData(key: String); + await FlutterForegroundTask.clearAllData(); +} +``` + +If the plugin you want to use provides a stream, use it like this: + +```dart +class MyTaskHandler extends TaskHandler { + StreamSubscription? _streamSubscription; + + @override + Future onStart(DateTime timestamp, TaskStarter starter) async { + _streamSubscription = FlLocation.getLocationStream().listen((location) { + final String message = '${location.latitude}, ${location.longitude}'; + FlutterForegroundTask.updateService(notificationText: message); + + // Send data to main isolate. + final String locationJson = jsonEncode(location.toJson()); + FlutterForegroundTask.sendDataToMain(locationJson); + }); + } + + @override + void onRepeatEvent(DateTime timestamp) { + // not use + } + + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async { + _streamSubscription?.cancel(); + _streamSubscription = null; + } +} +``` + +### :hatched_chick: other example + +#### [`internal_plugin_service`](https://github.com/Dev-hwang/flutter_foreground_task_example/tree/main/internal_plugin_service) +An example of using the platform channel in project with `flutter_foreground_task`. + +#### [`location_service`](https://github.com/Dev-hwang/flutter_foreground_task_example/tree/main/location_service) +An example of a background location service implementation using `flutter_foreground_task` and `fl_location`. + +#### [`record_service`](https://github.com/Dev-hwang/flutter_foreground_task_example/tree/main/record_service) +An example of a voice record service implementation using `flutter_foreground_task` and `record`. + +#### [`geofencing_service`](https://github.com/Dev-hwang/flutter_foreground_task_example/tree/main/geofencing_service) +An example of a background geofencing service implementation using `flutter_foreground_task` and `geofencing_api`. + +#### [`pedometer_service`](https://github.com/Dev-hwang/flutter_foreground_task_example/tree/main/pedometer_service) +An example of a pedometer service implementation using `flutter_foreground_task` and `pedometer`. + +## More Documentation + +Go [here](./documentation/models_documentation.md) to learn about the `models` provided by this plugin. + +Go [here](./documentation/utility_documentation.md) to learn about the `utility` provided by this plugin. + +Go [here](./documentation/migration_documentation.md) to `migrate` to the new version. + +## Support + +If you find any bugs or issues while using the plugin, please register an issues on [GitHub](https://github.com/Dev-hwang/flutter_foreground_task/issues). You can also contact us at . diff --git a/local_packages/flutter_foreground_task-9.1.0/analysis_options.yaml b/local_packages/flutter_foreground_task-9.1.0/analysis_options.yaml new file mode 100644 index 0000000..34b3fec --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/analysis_options.yaml @@ -0,0 +1,8 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false + avoid_function_literals_in_foreach_calls: false + constant_identifier_names: false + prefer_void_to_null: false diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/checksums.lock b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/checksums.lock new file mode 100644 index 0000000..f8c8f3b Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/checksums.lock differ diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/md5-checksums.bin b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/md5-checksums.bin new file mode 100644 index 0000000..41118aa Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/md5-checksums.bin differ diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/sha1-checksums.bin b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/sha1-checksums.bin new file mode 100644 index 0000000..5b59222 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/checksums/sha1-checksums.bin differ diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/dependencies-accessors/gc.properties b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/dependencies-accessors/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/fileChanges/last-build.bin b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/fileChanges/last-build.bin differ diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/fileHashes/fileHashes.lock b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/fileHashes/fileHashes.lock new file mode 100644 index 0000000..f0791cb Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/fileHashes/fileHashes.lock differ diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/gc.properties b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/8.7/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..66353b4 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/buildOutputCleanup/cache.properties b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..855d5d6 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Thu Apr 09 19:26:31 CST 2026 +gradle.version=8.7 diff --git a/local_packages/flutter_foreground_task-9.1.0/android/.gradle/vcs-1/gc.properties b/local_packages/flutter_foreground_task-9.1.0/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/flutter_foreground_task-9.1.0/android/build.gradle b/local_packages/flutter_foreground_task-9.1.0/android/build.gradle new file mode 100644 index 0000000..9a76517 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/build.gradle @@ -0,0 +1,73 @@ +group 'com.pravera.flutter_foreground_task' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '2.1.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.6.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'com.pravera.flutter_foreground_task' + } + + compileSdk 36 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } + + dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' + implementation 'androidx.core:core-ktx:1.15.0' + + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen { false } + showStandardStreams = true + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/gradle.properties b/local_packages/flutter_foreground_task-9.1.0/android/gradle.properties new file mode 100644 index 0000000..46c1f16 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/local_packages/flutter_foreground_task-9.1.0/android/gradle/wrapper/gradle-wrapper.properties b/local_packages/flutter_foreground_task-9.1.0/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2a2cd08 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/local_packages/flutter_foreground_task-9.1.0/android/settings.gradle b/local_packages/flutter_foreground_task-9.1.0/android/settings.gradle new file mode 100644 index 0000000..437ef8e --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_foreground_task' diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/AndroidManifest.xml b/local_packages/flutter_foreground_task-9.1.0/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9eec3e4 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskLifecycleListener.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskLifecycleListener.kt new file mode 100644 index 0000000..c8f3f11 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskLifecycleListener.kt @@ -0,0 +1,31 @@ +package com.pravera.flutter_foreground_task + +import io.flutter.embedding.engine.FlutterEngine + +/** A listener that can listen to the task lifecycle events. */ +interface FlutterForegroundTaskLifecycleListener { + /** + * Each time a task starts, a new FlutterEngine is created. + * + * This is called before [onTaskStart], + * Initialize the service you want to use in the task. (like PlatformChannel initialization) + */ + fun onEngineCreate(flutterEngine: FlutterEngine?) + + /** Called when the task is started. */ + fun onTaskStart(starter: FlutterForegroundTaskStarter) + + /** Called based on the eventAction set in ForegroundTaskOptions. */ + fun onTaskRepeatEvent() + + /** Called when the task is destroyed. */ + fun onTaskDestroy() + + /** + * If one task is finished or replaced by another, the FlutterEngine is destroyed. + * + * This is called after [onTaskDestroy], + * where dispose the service that was initialized in [onEngineCreate]. + */ + fun onEngineWillDestroy() +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskPlugin.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskPlugin.kt new file mode 100644 index 0000000..ad424d9 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskPlugin.kt @@ -0,0 +1,80 @@ +package com.pravera.flutter_foreground_task + +import android.content.Intent +import com.pravera.flutter_foreground_task.service.ForegroundService +import com.pravera.flutter_foreground_task.service.ForegroundServiceManager +import com.pravera.flutter_foreground_task.service.NotificationPermissionManager +import com.pravera.flutter_foreground_task.service.ServiceProvider +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.PluginRegistry.NewIntentListener + +/** FlutterForegroundTaskPlugin */ +class FlutterForegroundTaskPlugin : FlutterPlugin, ActivityAware, ServiceProvider, NewIntentListener { + companion object { + fun addTaskLifecycleListener(listener: FlutterForegroundTaskLifecycleListener) { + ForegroundService.addTaskLifecycleListener(listener) + } + + fun removeTaskLifecycleListener(listener: FlutterForegroundTaskLifecycleListener) { + ForegroundService.removeTaskLifecycleListener(listener) + } + } + + private lateinit var notificationPermissionManager: NotificationPermissionManager + private lateinit var foregroundServiceManager: ForegroundServiceManager + + private var activityBinding: ActivityPluginBinding? = null + private lateinit var methodCallHandler: MethodCallHandlerImpl + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + notificationPermissionManager = NotificationPermissionManager() + foregroundServiceManager = ForegroundServiceManager() + + methodCallHandler = MethodCallHandlerImpl(binding.applicationContext, this) + methodCallHandler.init(binding.binaryMessenger) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + if (::methodCallHandler.isInitialized) { + methodCallHandler.dispose() + } + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + methodCallHandler.setActivity(binding.activity) + binding.addRequestPermissionsResultListener(notificationPermissionManager) + binding.addActivityResultListener(methodCallHandler) + binding.addOnNewIntentListener(this) + activityBinding = binding + + val intent = binding.activity.intent + ForegroundService.handleNotificationContentIntent(intent) + } + + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } + + override fun onDetachedFromActivity() { + activityBinding?.removeRequestPermissionsResultListener(notificationPermissionManager) + activityBinding?.removeActivityResultListener(methodCallHandler) + activityBinding?.removeOnNewIntentListener(this) + activityBinding = null + methodCallHandler.setActivity(null) + } + + override fun onNewIntent(intent: Intent): Boolean { + ForegroundService.handleNotificationContentIntent(intent) + return true + } + + override fun getNotificationPermissionManager() = notificationPermissionManager + + override fun getForegroundServiceManager() = foregroundServiceManager +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskPluginChannel.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskPluginChannel.kt new file mode 100644 index 0000000..c9a7211 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskPluginChannel.kt @@ -0,0 +1,11 @@ +package com.pravera.flutter_foreground_task + +import android.app.Activity +import io.flutter.plugin.common.BinaryMessenger + +/** FlutterForegroundTaskPluginChannel */ +interface FlutterForegroundTaskPluginChannel { + fun init(messenger: BinaryMessenger) + fun setActivity(activity: Activity?) + fun dispose() +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskStarter.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskStarter.kt new file mode 100644 index 0000000..183fbc6 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/FlutterForegroundTaskStarter.kt @@ -0,0 +1,6 @@ +package com.pravera.flutter_foreground_task + +enum class FlutterForegroundTaskStarter { + DEVELOPER, + SYSTEM +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/MethodCallHandlerImpl.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/MethodCallHandlerImpl.kt new file mode 100644 index 0000000..daca41b --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/MethodCallHandlerImpl.kt @@ -0,0 +1,204 @@ +package com.pravera.flutter_foreground_task + +import android.app.Activity +import android.content.Context +import android.content.Intent + +import com.pravera.flutter_foreground_task.errors.ActivityNotAttachedException +import com.pravera.flutter_foreground_task.models.NotificationPermission +import com.pravera.flutter_foreground_task.service.NotificationPermissionCallback +import com.pravera.flutter_foreground_task.service.ServiceProvider +import com.pravera.flutter_foreground_task.utils.ErrorHandleUtils +import com.pravera.flutter_foreground_task.utils.PluginUtils + +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry +import java.util.UUID +import kotlin.Exception + +/** MethodCallHandlerImpl */ +class MethodCallHandlerImpl(private val context: Context, private val provider: ServiceProvider) : + MethodChannel.MethodCallHandler, + FlutterForegroundTaskPluginChannel, + PluginRegistry.ActivityResultListener { + private lateinit var channel: MethodChannel + + private var activity: Activity? = null + private var methodCodes: MutableMap = mutableMapOf() + private var methodResults: MutableMap = mutableMapOf() + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + val args = call.arguments + try { + when (call.method) { + "checkNotificationPermission" -> { + checkActivityNull().let { + val status = provider.getNotificationPermissionManager().checkPermission(it) + result.success(status.ordinal) + } + } + + "requestNotificationPermission" -> { + checkActivityNull().let { + val callback = object : NotificationPermissionCallback { + override fun onResult(permissionStatus: NotificationPermission) { + result.success(permissionStatus.ordinal) + } + + override fun onError(exception: Exception) { + ErrorHandleUtils.handleMethodCallError(result, exception) + } + } + provider.getNotificationPermissionManager().requestPermission(it, callback) + } + } + + "startService" -> { + provider.getForegroundServiceManager().start(context, args) + result.success(true) + } + + "restartService" -> { + provider.getForegroundServiceManager().restart(context) + result.success(true) + } + + "updateService" -> { + provider.getForegroundServiceManager().update(context, args) + result.success(true) + } + + "stopService" -> { + provider.getForegroundServiceManager().stop(context) + result.success(true) + } + + "sendData" -> provider.getForegroundServiceManager().sendData(args) + + "isRunningService" -> + result.success(provider.getForegroundServiceManager().isRunningService()) + + "attachedActivity" -> result.success(activity != null) + + "minimizeApp" -> { + checkActivityNull().let { + PluginUtils.minimizeApp(it) + } + } + + "launchApp" -> { + if (args is String?) { + PluginUtils.launchApp(context, args) + } + } + + "isAppOnForeground" -> result.success(PluginUtils.isAppOnForeground(context)) + + "setOnLockScreenVisibility" -> { + checkActivityNull().let { + if (args is Boolean) { + PluginUtils.setOnLockScreenVisibility(it, args) + } + } + } + + "wakeUpScreen" -> PluginUtils.wakeUpScreen(context) + + "isIgnoringBatteryOptimizations" -> + result.success(PluginUtils.isIgnoringBatteryOptimizations(context)) + + "openIgnoreBatteryOptimizationSettings" -> { + checkActivityNull().let { + val requestCode = UUID.randomUUID().hashCode() and 0xFFFF + methodCodes[requestCode] = RequestCode.OPEN_IGNORE_BATTERY_OPTIMIZATION_SETTINGS + methodResults[requestCode] = result + PluginUtils.openIgnoreBatteryOptimizationSettings(it, requestCode) + } + } + + "requestIgnoreBatteryOptimization" -> { + checkActivityNull().let { + val requestCode = UUID.randomUUID().hashCode() and 0xFFFF + methodCodes[requestCode] = RequestCode.REQUEST_IGNORE_BATTERY_OPTIMIZATION + methodResults[requestCode] = result + PluginUtils.requestIgnoreBatteryOptimization(it, requestCode) + } + } + + "canDrawOverlays" -> result.success(PluginUtils.canDrawOverlays(context)) + + "openSystemAlertWindowSettings" -> { + checkActivityNull().let { + val requestCode = UUID.randomUUID().hashCode() and 0xFFFF + methodCodes[requestCode] = RequestCode.OPEN_SYSTEM_ALERT_WINDOW_SETTINGS + methodResults[requestCode] = result + PluginUtils.openSystemAlertWindowSettings(it, requestCode) + } + } + + "canScheduleExactAlarms" -> + result.success(PluginUtils.canScheduleExactAlarms(context)) + + "openAlarmsAndRemindersSettings" -> { + checkActivityNull().let { + val requestCode = UUID.randomUUID().hashCode() and 0xFFFF + methodCodes[requestCode] = RequestCode.OPEN_ALARMS_AND_REMINDER_SETTINGS + methodResults[requestCode] = result + PluginUtils.openAlarmsAndRemindersSettings(it, requestCode) + } + } + + else -> result.notImplemented() + } + } catch (e: Exception) { + ErrorHandleUtils.handleMethodCallError(result, e) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + val methodCode = methodCodes[requestCode] + val methodResult = methodResults[requestCode] + methodCodes.remove(requestCode) + methodResults.remove(requestCode) + + if (methodCode == null || methodResult == null) { + return true + } + + when (methodCode) { + RequestCode.OPEN_IGNORE_BATTERY_OPTIMIZATION_SETTINGS -> + methodResult.success(PluginUtils.isIgnoringBatteryOptimizations(context)) + RequestCode.REQUEST_IGNORE_BATTERY_OPTIMIZATION -> + methodResult.success(PluginUtils.isIgnoringBatteryOptimizations(context)) + RequestCode.OPEN_SYSTEM_ALERT_WINDOW_SETTINGS -> + methodResult.success(PluginUtils.canDrawOverlays(context)) + RequestCode.OPEN_ALARMS_AND_REMINDER_SETTINGS -> + methodResult.success(PluginUtils.canScheduleExactAlarms(context)) + } + return true + } + + override fun init(messenger: BinaryMessenger) { + channel = MethodChannel(messenger, "flutter_foreground_task/methods") + channel.setMethodCallHandler(this) + } + + override fun setActivity(activity: Activity?) { + this.activity = activity + } + + override fun dispose() { + if (::channel.isInitialized) { + channel.setMethodCallHandler(null) + } + } + + private fun checkActivityNull(): Activity { + if (activity == null) { + throw ActivityNotAttachedException() + } + return activity!! + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/PreferencesKey.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/PreferencesKey.kt new file mode 100644 index 0000000..51adce6 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/PreferencesKey.kt @@ -0,0 +1,58 @@ +package com.pravera.flutter_foreground_task + +/** + * Key values for data stored in SharedPreferences. + * + * @author Dev-hwang + * @version 1.0 + */ +object PreferencesKey { + private const val prefix = "com.pravera.flutter_foreground_task.prefs." + + // permissions + const val NOTIFICATION_PERMISSION_STATUS_PREFS = prefix + "NOTIFICATION_PERMISSION_STATUS" + + // service status + const val FOREGROUND_SERVICE_STATUS_PREFS = prefix + "FOREGROUND_SERVICE_STATUS" + const val FOREGROUND_SERVICE_ACTION = "foregroundServiceAction" + + // service types + const val FOREGROUND_SERVICE_TYPES_PREFS = prefix + "FOREGROUND_SERVICE_TYPES" + const val FOREGROUND_SERVICE_TYPES = "serviceTypes" + + // notification options + const val NOTIFICATION_OPTIONS_PREFS = prefix + "NOTIFICATION_OPTIONS" + const val SERVICE_ID = "serviceId" + const val NOTIFICATION_ID = "notificationId" + const val NOTIFICATION_CHANNEL_ID = "notificationChannelId" + const val NOTIFICATION_CHANNEL_NAME = "notificationChannelName" + const val NOTIFICATION_CHANNEL_DESC = "notificationChannelDescription" + const val NOTIFICATION_CHANNEL_IMPORTANCE = "notificationChannelImportance" + const val NOTIFICATION_PRIORITY = "notificationPriority" + const val ENABLE_VIBRATION = "enableVibration" + const val PLAY_SOUND = "playSound" + const val SHOW_WHEN = "showWhen" + const val SHOW_BADGE = "showBadge" + const val ONLY_ALERT_ONCE = "onlyAlertOnce" + const val VISIBILITY = "visibility" + + // notification content + const val NOTIFICATION_CONTENT_TITLE = "notificationContentTitle" + const val NOTIFICATION_CONTENT_TEXT = "notificationContentText" + const val NOTIFICATION_CONTENT_ICON = "icon" + const val NOTIFICATION_CONTENT_BUTTONS = "buttons" + const val NOTIFICATION_INITIAL_ROUTE = "initialRoute" + + // task options + const val FOREGROUND_TASK_OPTIONS_PREFS = prefix + "FOREGROUND_TASK_OPTIONS" + const val TASK_EVENT_ACTION = "taskEventAction" // new + const val INTERVAL = "interval" // deprecated + const val IS_ONCE_EVENT = "isOnceEvent" // deprecated + const val AUTO_RUN_ON_BOOT = "autoRunOnBoot" + const val AUTO_RUN_ON_MY_PACKAGE_REPLACED = "autoRunOnMyPackageReplaced" + const val ALLOW_WAKE_LOCK = "allowWakeLock" + const val ALLOW_WIFI_LOCK = "allowWifiLock" + + // task data + const val CALLBACK_HANDLE = "callbackHandle" +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/RequestCode.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/RequestCode.kt new file mode 100644 index 0000000..53660e4 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/RequestCode.kt @@ -0,0 +1,17 @@ +package com.pravera.flutter_foreground_task + +object RequestCode { + // permissions + const val REQUEST_NOTIFICATION_PERMISSION = 100 + + // utils + const val REQUEST_IGNORE_BATTERY_OPTIMIZATION = 200 + const val OPEN_IGNORE_BATTERY_OPTIMIZATION_SETTINGS = 201 + const val OPEN_SYSTEM_ALERT_WINDOW_SETTINGS = 202 + const val OPEN_ALARMS_AND_REMINDER_SETTINGS = 203 + + // service + const val SET_RESTART_SERVICE_ALARM = 300 + const val NOTIFICATION_PRESSED = 301 + const val NOTIFICATION_DISMISSED = 302 +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ActivityNotAttachedException.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ActivityNotAttachedException.kt new file mode 100644 index 0000000..178e04e --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ActivityNotAttachedException.kt @@ -0,0 +1,4 @@ +package com.pravera.flutter_foreground_task.errors + +class ActivityNotAttachedException : + Exception("Cannot call method because Activity is not attached to FlutterEngine.") diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/NotSupportedException.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/NotSupportedException.kt new file mode 100644 index 0000000..74bf7fe --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/NotSupportedException.kt @@ -0,0 +1,3 @@ +package com.pravera.flutter_foreground_task.errors + +class NotSupportedException(message: String) : Exception(message) diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/PermissionRequestCancelledException.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/PermissionRequestCancelledException.kt new file mode 100644 index 0000000..4d8480a --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/PermissionRequestCancelledException.kt @@ -0,0 +1,4 @@ +package com.pravera.flutter_foreground_task.errors + +class PermissionRequestCancelledException : + Exception("The permission request dialog was closed or the request was cancelled.") diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ServiceAlreadyStartedException.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ServiceAlreadyStartedException.kt new file mode 100644 index 0000000..590c70d --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ServiceAlreadyStartedException.kt @@ -0,0 +1,3 @@ +package com.pravera.flutter_foreground_task.errors + +class ServiceAlreadyStartedException : Exception("The service has already started.") diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ServiceNotStartedException.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ServiceNotStartedException.kt new file mode 100644 index 0000000..f2b23df --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/errors/ServiceNotStartedException.kt @@ -0,0 +1,3 @@ +package com.pravera.flutter_foreground_task.errors + +class ServiceNotStartedException : Exception("The service is not started.") diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceAction.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceAction.kt new file mode 100644 index 0000000..b346152 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceAction.kt @@ -0,0 +1,19 @@ +package com.pravera.flutter_foreground_task.models + +/** + * Intent action for foreground service control. + * + * @author Dev-hwang + * @version 1.0 + */ +object ForegroundServiceAction { + private const val prefix = "com.pravera.flutter_foreground_task.action." + + const val API_START = prefix + "api_start" + const val API_RESTART = prefix + "api_restart" + const val API_UPDATE = prefix + "api_update" + const val API_STOP = prefix + "api_stop" + + const val REBOOT = prefix + "reboot" + const val RESTART = prefix + "restart" +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceStatus.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceStatus.kt new file mode 100644 index 0000000..0557516 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceStatus.kt @@ -0,0 +1,32 @@ +package com.pravera.flutter_foreground_task.models + +import android.content.Context +import com.pravera.flutter_foreground_task.PreferencesKey as PrefsKey + +data class ForegroundServiceStatus(val action: String) { + companion object { + fun getData(context: Context): ForegroundServiceStatus { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_SERVICE_STATUS_PREFS, Context.MODE_PRIVATE) + + val action = prefs.getString(PrefsKey.FOREGROUND_SERVICE_ACTION, null) + ?: ForegroundServiceAction.API_STOP + + return ForegroundServiceStatus(action = action) + } + + fun setData(context: Context, action: String) { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_SERVICE_STATUS_PREFS, Context.MODE_PRIVATE) + + with(prefs.edit()) { + putString(PrefsKey.FOREGROUND_SERVICE_ACTION, action) + commit() + } + } + } + + fun isCorrectlyStopped(): Boolean { + return action == ForegroundServiceAction.API_STOP + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt new file mode 100644 index 0000000..4976abc --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundServiceTypes.kt @@ -0,0 +1,77 @@ +package com.pravera.flutter_foreground_task.models + +import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build +import com.pravera.flutter_foreground_task.PreferencesKey + +data class ForegroundServiceTypes(val value: Int) { + companion object { + fun getData(context: Context): ForegroundServiceTypes { + val prefs = context.getSharedPreferences( + PreferencesKey.FOREGROUND_SERVICE_TYPES_PREFS, Context.MODE_PRIVATE) + + val value = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + prefs.getInt(PreferencesKey.FOREGROUND_SERVICE_TYPES, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) + } else { + prefs.getInt(PreferencesKey.FOREGROUND_SERVICE_TYPES, 0) // none + } + + return ForegroundServiceTypes(value = value) + } + + fun setData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PreferencesKey.FOREGROUND_SERVICE_TYPES_PREFS, Context.MODE_PRIVATE) + + var value = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val serviceTypes = map?.get(PreferencesKey.FOREGROUND_SERVICE_TYPES) as? List<*> + if (serviceTypes != null) { + for (serviceType in serviceTypes) { + getForegroundServiceTypeFlag(serviceType)?.let { + value = value or it + } + } + } + } + + // not none type + if (value > 0) { + with(prefs.edit()) { + putInt(PreferencesKey.FOREGROUND_SERVICE_TYPES, value) + commit() + } + } + } + + fun clearData(context: Context) { + val prefs = context.getSharedPreferences( + PreferencesKey.FOREGROUND_SERVICE_TYPES_PREFS, Context.MODE_PRIVATE) + + with(prefs.edit()) { + clear() + commit() + } + } + + private fun getForegroundServiceTypeFlag(type: Any?): Int? { + return when (type) { + 0 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA else null + 1 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE else null + 2 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else null + 3 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH else null + 4 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION else null + 5 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK else null + 6 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION else null + 7 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE else null + 8 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL else null + 9 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING else null + 10 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE else null + 11 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE else null + 12 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED else null + else -> null + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskData.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskData.kt new file mode 100644 index 0000000..ba279d3 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskData.kt @@ -0,0 +1,56 @@ +package com.pravera.flutter_foreground_task.models + +import android.content.Context +import com.pravera.flutter_foreground_task.PreferencesKey as PrefsKey + +data class ForegroundTaskData(val callbackHandle: Long?) { + companion object { + fun getData(context: Context): ForegroundTaskData { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val callbackHandle = if (prefs.contains(PrefsKey.CALLBACK_HANDLE)) { + prefs.getLong(PrefsKey.CALLBACK_HANDLE, 0L) + } else { + null + } + + return ForegroundTaskData(callbackHandle = callbackHandle) + } + + fun setData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val callbackHandle = "${map?.get(PrefsKey.CALLBACK_HANDLE)}".toLongOrNull() + + with(prefs.edit()) { + remove(PrefsKey.CALLBACK_HANDLE) + callbackHandle?.let { putLong(PrefsKey.CALLBACK_HANDLE, it) } + commit() + } + } + + fun updateData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val callbackHandle = "${map?.get(PrefsKey.CALLBACK_HANDLE)}".toLongOrNull() + + with(prefs.edit()) { + callbackHandle?.let { putLong(PrefsKey.CALLBACK_HANDLE, it) } + commit() + } + } + + fun clearData(context: Context) { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + + with(prefs.edit()) { + clear() + commit() + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskEventAction.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskEventAction.kt new file mode 100644 index 0000000..b18a527 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskEventAction.kt @@ -0,0 +1,45 @@ +package com.pravera.flutter_foreground_task.models + +import org.json.JSONObject +import java.util.Objects + +data class ForegroundTaskEventAction( + val type: ForegroundTaskEventType, + val interval: Long +) { + companion object { + private const val TASK_EVENT_TYPE_KEY = "taskEventType" + private const val TASK_EVENT_INTERVAL_KEY = "taskEventInterval" + + fun fromJsonString(jsonString: String): ForegroundTaskEventAction { + val jsonObj = JSONObject(jsonString) + + val type: ForegroundTaskEventType = if (jsonObj.isNull(TASK_EVENT_TYPE_KEY)) { + ForegroundTaskEventType.NOTHING + } else { + val value = jsonObj.getInt(TASK_EVENT_TYPE_KEY) + ForegroundTaskEventType.fromValue(value) + } + + val interval: Long = if (jsonObj.isNull(TASK_EVENT_INTERVAL_KEY)) { + 5000L + } else { + val value = jsonObj.getInt(TASK_EVENT_INTERVAL_KEY) + value.toLong() + } + + return ForegroundTaskEventAction(type = type, interval = interval) + } + } + + override fun equals(other: Any?): Boolean { + if (other == null || other !is ForegroundTaskEventAction) { + return false + } + return this.type.value == other.type.value && this.interval == other.interval + } + + override fun hashCode(): Int { + return Objects.hash(type.value, interval) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskEventType.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskEventType.kt new file mode 100644 index 0000000..e7f23d9 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskEventType.kt @@ -0,0 +1,12 @@ +package com.pravera.flutter_foreground_task.models + +enum class ForegroundTaskEventType(val value: Int) { + NOTHING(1), + ONCE(2), + REPEAT(3); + + companion object { + fun fromValue(value: Int) = + ForegroundTaskEventType.values().firstOrNull { it.value == value } ?: NOTHING + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskOptions.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskOptions.kt new file mode 100644 index 0000000..0b44973 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/ForegroundTaskOptions.kt @@ -0,0 +1,107 @@ +package com.pravera.flutter_foreground_task.models + +import android.content.Context +import org.json.JSONObject +import com.pravera.flutter_foreground_task.PreferencesKey as PrefsKey + +data class ForegroundTaskOptions( + val eventAction: ForegroundTaskEventAction, + val autoRunOnBoot: Boolean, + val autoRunOnMyPackageReplaced: Boolean, + val allowWakeLock: Boolean, + val allowWifiLock: Boolean +) { + companion object { + fun getData(context: Context): ForegroundTaskOptions { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val eventActionJsonString = prefs.getString(PrefsKey.TASK_EVENT_ACTION, null) + val eventAction: ForegroundTaskEventAction = if (eventActionJsonString != null) { + ForegroundTaskEventAction.fromJsonString(eventActionJsonString) + } else { + // for deprecated api + val oldIsOnceEvent = prefs.getBoolean(PrefsKey.IS_ONCE_EVENT, false) + val oldInterval = prefs.getLong(PrefsKey.INTERVAL, 5000L) + if (oldIsOnceEvent) { + ForegroundTaskEventAction(ForegroundTaskEventType.ONCE, oldInterval) + } else { + ForegroundTaskEventAction(ForegroundTaskEventType.REPEAT, oldInterval) + } + } + + val autoRunOnBoot = prefs.getBoolean(PrefsKey.AUTO_RUN_ON_BOOT, false) + val autoRunOnMyPackageReplaced = prefs.getBoolean(PrefsKey.AUTO_RUN_ON_MY_PACKAGE_REPLACED, false) + val allowWakeLock = prefs.getBoolean(PrefsKey.ALLOW_WAKE_LOCK, true) + val allowWifiLock = prefs.getBoolean(PrefsKey.ALLOW_WIFI_LOCK, false) + + return ForegroundTaskOptions( + eventAction = eventAction, + autoRunOnBoot = autoRunOnBoot, + autoRunOnMyPackageReplaced = autoRunOnMyPackageReplaced, + allowWakeLock = allowWakeLock, + allowWifiLock = allowWifiLock + ) + } + + fun setData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val eventActionJson = map?.get(PrefsKey.TASK_EVENT_ACTION) as? Map<*, *> + var eventActionJsonString: String? = null + if (eventActionJson != null) { + eventActionJsonString = JSONObject(eventActionJson).toString() + } + + val autoRunOnBoot = map?.get(PrefsKey.AUTO_RUN_ON_BOOT) as? Boolean ?: false + val autoRunOnMyPackageReplaced = map?.get(PrefsKey.AUTO_RUN_ON_MY_PACKAGE_REPLACED) as? Boolean ?: false + val allowWakeLock = map?.get(PrefsKey.ALLOW_WAKE_LOCK) as? Boolean ?: true + val allowWifiLock = map?.get(PrefsKey.ALLOW_WIFI_LOCK) as? Boolean ?: false + + with(prefs.edit()) { + putString(PrefsKey.TASK_EVENT_ACTION, eventActionJsonString) + putBoolean(PrefsKey.AUTO_RUN_ON_BOOT, autoRunOnBoot) + putBoolean(PrefsKey.AUTO_RUN_ON_MY_PACKAGE_REPLACED, autoRunOnMyPackageReplaced) + putBoolean(PrefsKey.ALLOW_WAKE_LOCK, allowWakeLock) + putBoolean(PrefsKey.ALLOW_WIFI_LOCK, allowWifiLock) + commit() + } + } + + fun updateData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val eventActionJson = map?.get(PrefsKey.TASK_EVENT_ACTION) as? Map<*, *> + var eventActionJsonString: String? = null + if (eventActionJson != null) { + eventActionJsonString = JSONObject(eventActionJson).toString() + } + + val autoRunOnBoot = map?.get(PrefsKey.AUTO_RUN_ON_BOOT) as? Boolean + val autoRunOnMyPackageReplaced = map?.get(PrefsKey.AUTO_RUN_ON_MY_PACKAGE_REPLACED) as? Boolean + val allowWakeLock = map?.get(PrefsKey.ALLOW_WAKE_LOCK) as? Boolean + val allowWifiLock = map?.get(PrefsKey.ALLOW_WIFI_LOCK) as? Boolean + + with(prefs.edit()) { + eventActionJsonString?.let { putString(PrefsKey.TASK_EVENT_ACTION, it) } + autoRunOnBoot?.let { putBoolean(PrefsKey.AUTO_RUN_ON_BOOT, it) } + autoRunOnMyPackageReplaced?.let { putBoolean(PrefsKey.AUTO_RUN_ON_MY_PACKAGE_REPLACED, it) } + allowWakeLock?.let { putBoolean(PrefsKey.ALLOW_WAKE_LOCK, it) } + allowWifiLock?.let { putBoolean(PrefsKey.ALLOW_WIFI_LOCK, it) } + commit() + } + } + + fun clearData(context: Context) { + val prefs = context.getSharedPreferences( + PrefsKey.FOREGROUND_TASK_OPTIONS_PREFS, Context.MODE_PRIVATE) + + with(prefs.edit()) { + clear() + commit() + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationButton.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationButton.kt new file mode 100644 index 0000000..0b98323 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationButton.kt @@ -0,0 +1,49 @@ +package com.pravera.flutter_foreground_task.models + +import org.json.JSONObject +import java.util.Objects + +data class NotificationButton( + val id: String, + val text: String, + val textColorRgb: String? +) { + companion object { + private const val ID_KEY = "id" + private const val TEXT_KEY = "text" + private const val TEXT_COLOR_RGB_KEY = "textColorRgb" + + fun fromJSONObject(jsonObj: JSONObject): NotificationButton { + val id: String = if (jsonObj.isNull(ID_KEY)) { + "unknown" + } else { + jsonObj.getString(ID_KEY) + } + + val text: String = if (jsonObj.isNull(TEXT_KEY)) { + "" + } else { + jsonObj.getString(TEXT_KEY) + } + + val textColorRgb: String? = if (jsonObj.isNull(TEXT_COLOR_RGB_KEY)) { + null + } else { + jsonObj.getString(TEXT_COLOR_RGB_KEY) + } + + return NotificationButton(id = id, text = text, textColorRgb = textColorRgb) + } + } + + override fun equals(other: Any?): Boolean { + if (other == null || other !is NotificationButton) { + return false + } + return this.id == other.id && this.text == other.text && this.textColorRgb == other.textColorRgb + } + + override fun hashCode(): Int { + return Objects.hash(id, text, textColorRgb) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationContent.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationContent.kt new file mode 100644 index 0000000..ec37d54 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationContent.kt @@ -0,0 +1,122 @@ +package com.pravera.flutter_foreground_task.models + +import android.content.Context +import com.pravera.flutter_foreground_task.PreferencesKey as PrefsKey +import org.json.JSONArray +import org.json.JSONObject + +data class NotificationContent( + val title: String, + val text: String, + val icon: NotificationIcon?, + val buttons: List, + val initialRoute: String? +) { + companion object { + fun getData(context: Context): NotificationContent { + val prefs = context.getSharedPreferences( + PrefsKey.NOTIFICATION_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val title = prefs.getString(PrefsKey.NOTIFICATION_CONTENT_TITLE, null) ?: "" + val text = prefs.getString(PrefsKey.NOTIFICATION_CONTENT_TEXT, null) ?: "" + + val iconJsonString = prefs.getString(PrefsKey.NOTIFICATION_CONTENT_ICON, null) + var icon: NotificationIcon? = null + if (iconJsonString != null) { + icon = NotificationIcon.fromJsonString(iconJsonString) + } + + val buttonsJsonString = prefs.getString(PrefsKey.NOTIFICATION_CONTENT_BUTTONS, null) + val buttons: MutableList = mutableListOf() + if (buttonsJsonString != null) { + val buttonsJsonArr = JSONArray(buttonsJsonString) + for (i in 0 until buttonsJsonArr.length()) { + val buttonJsonObj = buttonsJsonArr.getJSONObject(i) + buttons.add(NotificationButton.fromJSONObject(buttonJsonObj)) + } + } + + val initialRoute = prefs.getString(PrefsKey.NOTIFICATION_INITIAL_ROUTE, null) + + return NotificationContent( + title = title, + text = text, + icon = icon, + buttons = buttons, + initialRoute = initialRoute + ) + } + + fun setData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PrefsKey.NOTIFICATION_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val title = map?.get(PrefsKey.NOTIFICATION_CONTENT_TITLE) as? String ?: "" + val text = map?.get(PrefsKey.NOTIFICATION_CONTENT_TEXT) as? String ?: "" + + val iconJson = map?.get(PrefsKey.NOTIFICATION_CONTENT_ICON) as? Map<*, *> + var iconJsonString: String? = null + if (iconJson != null) { + iconJsonString = JSONObject(iconJson).toString() + } + + val buttonsJson = map?.get(PrefsKey.NOTIFICATION_CONTENT_BUTTONS) as? List<*> + var buttonsJsonString: String? = null + if (buttonsJson != null) { + buttonsJsonString = JSONArray(buttonsJson).toString() + } + + val initialRoute = map?.get(PrefsKey.NOTIFICATION_INITIAL_ROUTE) as? String + + with(prefs.edit()) { + putString(PrefsKey.NOTIFICATION_CONTENT_TITLE, title) + putString(PrefsKey.NOTIFICATION_CONTENT_TEXT, text) + putString(PrefsKey.NOTIFICATION_CONTENT_ICON, iconJsonString) + putString(PrefsKey.NOTIFICATION_CONTENT_BUTTONS, buttonsJsonString) + putString(PrefsKey.NOTIFICATION_INITIAL_ROUTE, initialRoute) + commit() + } + } + + fun updateData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PrefsKey.NOTIFICATION_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val title = map?.get(PrefsKey.NOTIFICATION_CONTENT_TITLE) as? String + val text = map?.get(PrefsKey.NOTIFICATION_CONTENT_TEXT) as? String + + val iconJson = map?.get(PrefsKey.NOTIFICATION_CONTENT_ICON) as? Map<*, *> + var iconJsonString: String? = null + if (iconJson != null) { + iconJsonString = JSONObject(iconJson).toString() + } + + val buttonsJson = map?.get(PrefsKey.NOTIFICATION_CONTENT_BUTTONS) as? List<*> + var buttonsJsonString: String? = null + if (buttonsJson != null) { + buttonsJsonString = JSONArray(buttonsJson).toString() + } + + val initialRoute = map?.get(PrefsKey.NOTIFICATION_INITIAL_ROUTE) as? String + + with(prefs.edit()) { + title?.let { putString(PrefsKey.NOTIFICATION_CONTENT_TITLE, it) } + text?.let { putString(PrefsKey.NOTIFICATION_CONTENT_TEXT, it) } + iconJsonString?.let { putString(PrefsKey.NOTIFICATION_CONTENT_ICON, it) } + buttonsJsonString?.let { putString(PrefsKey.NOTIFICATION_CONTENT_BUTTONS, it) } + initialRoute?.let { putString(PrefsKey.NOTIFICATION_INITIAL_ROUTE, it) } + commit() + } + } + + fun clearData(context: Context) { + val prefs = context.getSharedPreferences( + PrefsKey.NOTIFICATION_OPTIONS_PREFS, Context.MODE_PRIVATE) + + with(prefs.edit()) { + clear() + commit() + } + } + } +} \ No newline at end of file diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationIcon.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationIcon.kt new file mode 100644 index 0000000..327444d --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationIcon.kt @@ -0,0 +1,34 @@ +package com.pravera.flutter_foreground_task.models + +import org.json.JSONObject + +data class NotificationIcon( + val metaDataName: String, + val backgroundColorRgb: String? +) { + companion object { + private const val META_DATA_NAME_KEY = "metaDataName" + private const val BACKGROUND_COLOR_RGB_KEY = "backgroundColorRgb" + + fun fromJsonString(jsonString: String): NotificationIcon { + val jsonObj = JSONObject(jsonString) + + val metaDataName: String = if (jsonObj.isNull(META_DATA_NAME_KEY)) { + "" + } else { + jsonObj.getString(META_DATA_NAME_KEY) + } + + val backgroundColorRgb: String? = if (jsonObj.isNull(BACKGROUND_COLOR_RGB_KEY)) { + null + } else { + jsonObj.getString(BACKGROUND_COLOR_RGB_KEY) + } + + return NotificationIcon( + metaDataName = metaDataName, + backgroundColorRgb = backgroundColorRgb + ) + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationOptions.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationOptions.kt new file mode 100644 index 0000000..f83cb4d --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationOptions.kt @@ -0,0 +1,100 @@ +package com.pravera.flutter_foreground_task.models + +import android.content.Context +import com.pravera.flutter_foreground_task.PreferencesKey as PrefsKey + +data class NotificationOptions( + val serviceId: Int, + val channelId: String, + val channelName: String, + val channelDescription: String?, + val channelImportance: Int, + val priority: Int, + val enableVibration: Boolean, + val playSound: Boolean, + val showWhen: Boolean, + val showBadge: Boolean, + val onlyAlertOnce: Boolean, + val visibility: Int +) { + companion object { + fun getData(context: Context): NotificationOptions { + val prefs = context.getSharedPreferences( + PrefsKey.NOTIFICATION_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val serviceId = prefs.getInt(PrefsKey.SERVICE_ID, prefs.getInt(PrefsKey.NOTIFICATION_ID, 1000)) + val channelId = prefs.getString(PrefsKey.NOTIFICATION_CHANNEL_ID, null) ?: "foreground_service" + val channelName = prefs.getString(PrefsKey.NOTIFICATION_CHANNEL_NAME, null) ?: "Foreground Service" + val channelDesc = prefs.getString(PrefsKey.NOTIFICATION_CHANNEL_DESC, null) + val channelImportance = prefs.getInt(PrefsKey.NOTIFICATION_CHANNEL_IMPORTANCE, 2) + val priority = prefs.getInt(PrefsKey.NOTIFICATION_PRIORITY, -1) + val enableVibration = prefs.getBoolean(PrefsKey.ENABLE_VIBRATION, false) + val playSound = prefs.getBoolean(PrefsKey.PLAY_SOUND, false) + val showWhen = prefs.getBoolean(PrefsKey.SHOW_WHEN, false) + val showBadge = prefs.getBoolean(PrefsKey.SHOW_BADGE, false) + val onlyAlertOnce = prefs.getBoolean(PrefsKey.ONLY_ALERT_ONCE, false) + val visibility = prefs.getInt(PrefsKey.VISIBILITY, 1) + + return NotificationOptions( + serviceId = serviceId, + channelId = channelId, + channelName = channelName, + channelDescription = channelDesc, + channelImportance = channelImportance, + priority = priority, + enableVibration = enableVibration, + playSound = playSound, + showWhen = showWhen, + showBadge = showBadge, + onlyAlertOnce = onlyAlertOnce, + visibility = visibility + ) + } + + fun setData(context: Context, map: Map<*, *>?) { + val prefs = context.getSharedPreferences( + PrefsKey.NOTIFICATION_OPTIONS_PREFS, Context.MODE_PRIVATE) + + val serviceId = map?.get(PrefsKey.SERVICE_ID) as? Int + ?: map?.get(PrefsKey.NOTIFICATION_ID) as? Int + ?: 1000 + val channelId = map?.get(PrefsKey.NOTIFICATION_CHANNEL_ID) as? String + val channelName = map?.get(PrefsKey.NOTIFICATION_CHANNEL_NAME) as? String + val channelDesc = map?.get(PrefsKey.NOTIFICATION_CHANNEL_DESC) as? String + val channelImportance = map?.get(PrefsKey.NOTIFICATION_CHANNEL_IMPORTANCE) as? Int ?: 2 + val priority = map?.get(PrefsKey.NOTIFICATION_PRIORITY) as? Int ?: -1 + val enableVibration = map?.get(PrefsKey.ENABLE_VIBRATION) as? Boolean ?: false + val playSound = map?.get(PrefsKey.PLAY_SOUND) as? Boolean ?: false + val showWhen = map?.get(PrefsKey.SHOW_WHEN) as? Boolean ?: false + val showBadge = map?.get(PrefsKey.SHOW_BADGE) as? Boolean ?: false + val onlyAlertOnce = map?.get(PrefsKey.ONLY_ALERT_ONCE) as? Boolean ?: false + val visibility = map?.get(PrefsKey.VISIBILITY) as? Int ?: 1 + + with(prefs.edit()) { + putInt(PrefsKey.SERVICE_ID, serviceId) + putString(PrefsKey.NOTIFICATION_CHANNEL_ID, channelId) + putString(PrefsKey.NOTIFICATION_CHANNEL_NAME, channelName) + putString(PrefsKey.NOTIFICATION_CHANNEL_DESC, channelDesc) + putInt(PrefsKey.NOTIFICATION_CHANNEL_IMPORTANCE, channelImportance) + putInt(PrefsKey.NOTIFICATION_PRIORITY, priority) + putBoolean(PrefsKey.ENABLE_VIBRATION, enableVibration) + putBoolean(PrefsKey.PLAY_SOUND, playSound) + putBoolean(PrefsKey.SHOW_WHEN, showWhen) + putBoolean(PrefsKey.SHOW_BADGE, showBadge) + putBoolean(PrefsKey.ONLY_ALERT_ONCE, onlyAlertOnce) + putInt(PrefsKey.VISIBILITY, visibility) + commit() + } + } + + fun clearData(context: Context) { + val prefs = context.getSharedPreferences( + PrefsKey.NOTIFICATION_OPTIONS_PREFS, Context.MODE_PRIVATE) + + with(prefs.edit()) { + clear() + commit() + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationPermission.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationPermission.kt new file mode 100644 index 0000000..6fd3ec8 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/models/NotificationPermission.kt @@ -0,0 +1,7 @@ +package com.pravera.flutter_foreground_task.models + +enum class NotificationPermission { + GRANTED, + DENIED, + PERMANENTLY_DENIED +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt new file mode 100644 index 0000000..9b2b445 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundService.kt @@ -0,0 +1,601 @@ +package com.pravera.flutter_foreground_task.service + +import android.annotation.SuppressLint +import android.app.* +import android.content.* +import android.content.pm.PackageManager +import android.graphics.Color +import android.net.wifi.WifiManager +import android.os.* +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.pravera.flutter_foreground_task.FlutterForegroundTaskLifecycleListener +import com.pravera.flutter_foreground_task.RequestCode +import com.pravera.flutter_foreground_task.models.* +import com.pravera.flutter_foreground_task.utils.ForegroundServiceUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** + * A service class for implementing foreground service. + * + * @author Dev-hwang + * @version 1.0 + */ +class ForegroundService : Service() { + companion object { + private val TAG = ForegroundService::class.java.simpleName + + private const val ACTION_NOTIFICATION_PRESSED = "onNotificationPressed" + private const val ACTION_NOTIFICATION_DISMISSED = "onNotificationDismissed" + private const val ACTION_NOTIFICATION_BUTTON_PRESSED = "onNotificationButtonPressed" + private const val ACTION_RECEIVE_DATA = "onReceiveData" + private const val INTENT_DATA_NAME = "intentData" + + private val _isRunningServiceState = MutableStateFlow(false) + val isRunningServiceState = _isRunningServiceState.asStateFlow() + + private var task: ForegroundTask? = null + private var taskLifecycleListeners = ForegroundTaskLifecycleListeners() + + fun addTaskLifecycleListener(listener: FlutterForegroundTaskLifecycleListener) { + taskLifecycleListeners.addListener(listener) + } + + fun removeTaskLifecycleListener(listener: FlutterForegroundTaskLifecycleListener) { + taskLifecycleListeners.removeListener(listener) + } + + fun handleNotificationContentIntent(intent: Intent?) { + if (intent == null) return + + try { + // Check if the given intent is a LaunchIntent. + val isLaunchIntent = (intent.action == Intent.ACTION_MAIN) && + (intent.categories?.contains(Intent.CATEGORY_LAUNCHER) == true) + if (!isLaunchIntent) return + + val data = intent.getStringExtra(INTENT_DATA_NAME) + if (data == ACTION_NOTIFICATION_PRESSED) { + task?.invokeMethod(data, null) + } + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } + + fun sendData(data: Any?) { + if (isRunningServiceState.value) { + task?.invokeMethod(ACTION_RECEIVE_DATA, data) + } + } + } + + private lateinit var foregroundServiceStatus: ForegroundServiceStatus + private lateinit var foregroundServiceTypes: ForegroundServiceTypes + private lateinit var foregroundTaskOptions: ForegroundTaskOptions + private lateinit var foregroundTaskData: ForegroundTaskData + private lateinit var notificationOptions: NotificationOptions + private lateinit var notificationContent: NotificationContent + private var prevForegroundTaskOptions: ForegroundTaskOptions? = null + private var prevForegroundTaskData: ForegroundTaskData? = null + private var prevNotificationOptions: NotificationOptions? = null + private var prevNotificationContent: NotificationContent? = null + + private var wakeLock: PowerManager.WakeLock? = null + private var wifiLock: WifiManager.WifiLock? = null + + private var isTimeout: Boolean = false + + // A broadcast receiver that handles intents that occur in the foreground service. + private var broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null) return + + try { + // This intent has not sent from the current package. + val iPackageName = intent.`package` + val cPackageName = packageName + if (iPackageName != cPackageName) { + Log.d(TAG, "This intent has not sent from the current package. ($iPackageName != $cPackageName)") + return + } + + val action = intent.action ?: return + val data = intent.getStringExtra(INTENT_DATA_NAME) + task?.invokeMethod(action, data) + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } + } + + override fun onCreate() { + super.onCreate() + registerBroadcastReceiver() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + isTimeout = false + loadDataFromPreferences() + + var action = foregroundServiceStatus.action + val isSetStopWithTaskFlag = ForegroundServiceUtils.isSetStopWithTaskFlag(this) + + if (action == ForegroundServiceAction.API_STOP) { + stopForegroundService() + return START_NOT_STICKY + } + + try { + if (intent == null) { + ForegroundServiceStatus.setData(this, ForegroundServiceAction.RESTART) + foregroundServiceStatus = ForegroundServiceStatus.getData(this) + action = foregroundServiceStatus.action + } + + when (action) { + ForegroundServiceAction.API_START, + ForegroundServiceAction.API_RESTART -> { + startForegroundService() + createForegroundTask() + } + ForegroundServiceAction.API_UPDATE -> { + updateNotification() + val prevCallbackHandle = prevForegroundTaskData?.callbackHandle + val currCallbackHandle = foregroundTaskData.callbackHandle + if (prevCallbackHandle != currCallbackHandle) { + createForegroundTask() + } else { + val prevEventAction = prevForegroundTaskOptions?.eventAction + val currEventAction = foregroundTaskOptions.eventAction + if (prevEventAction != currEventAction) { + updateForegroundTask() + } + } + } + ForegroundServiceAction.REBOOT, + ForegroundServiceAction.RESTART -> { + startForegroundService() + createForegroundTask() + Log.d(TAG, "The service has been restarted by Android OS.") + } + } + } catch (e: Exception) { + Log.e(TAG, e.message, e) + stopForegroundService() + } + + return if (isSetStopWithTaskFlag) { + START_NOT_STICKY + } else { + START_STICKY + } + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onDestroy() { + super.onDestroy() + val isTimeout = this.isTimeout + destroyForegroundTask(isTimeout) + stopForegroundService() + unregisterBroadcastReceiver() + + var isCorrectlyStopped = false + if (::foregroundServiceStatus.isInitialized) { + isCorrectlyStopped = foregroundServiceStatus.isCorrectlyStopped() + } + if (!isCorrectlyStopped && !ForegroundServiceUtils.isSetStopWithTaskFlag(this)) { + Log.e(TAG, "The service will be restarted after 5 seconds because it wasn't properly stopped.") + RestartReceiver.setRestartAlarm(this, 5000) + } + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + if (ForegroundServiceUtils.isSetStopWithTaskFlag(this)) { + stopSelf() + } else { + RestartReceiver.setRestartAlarm(this, 1000) + } + } + + override fun onTimeout(startId: Int) { + super.onTimeout(startId) + isTimeout = true + stopForegroundService() + Log.e(TAG, "The service(id: $startId) timed out and was terminated by the system.") + } + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + override fun onTimeout(startId: Int, fgsType: Int) { + super.onTimeout(startId, fgsType) + isTimeout = true + stopForegroundService() + Log.e(TAG, "The service(id: $startId) timed out and was terminated by the system.") + } + + private fun loadDataFromPreferences() { + foregroundServiceStatus = ForegroundServiceStatus.getData(applicationContext) + foregroundServiceTypes = ForegroundServiceTypes.getData(applicationContext) + if (::foregroundTaskOptions.isInitialized) { prevForegroundTaskOptions = foregroundTaskOptions } + foregroundTaskOptions = ForegroundTaskOptions.getData(applicationContext) + if (::foregroundTaskData.isInitialized) { prevForegroundTaskData = foregroundTaskData } + foregroundTaskData = ForegroundTaskData.getData(applicationContext) + if (::notificationOptions.isInitialized) { prevNotificationOptions = notificationOptions } + notificationOptions = NotificationOptions.getData(applicationContext) + if (::notificationContent.isInitialized) { prevNotificationContent = notificationContent } + notificationContent = NotificationContent.getData(applicationContext) + } + + private fun registerBroadcastReceiver() { + val intentFilter = IntentFilter().apply { + addAction(ACTION_NOTIFICATION_BUTTON_PRESSED) + addAction(ACTION_NOTIFICATION_PRESSED) + addAction(ACTION_NOTIFICATION_DISMISSED) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED) + } else { + registerReceiver(broadcastReceiver, intentFilter) + } + } + + private fun unregisterBroadcastReceiver() { + unregisterReceiver(broadcastReceiver) + } + + @SuppressLint("WrongConstant", "SuspiciousIndentation") + private fun startForegroundService() { + RestartReceiver.cancelRestartAlarm(this) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel() + } + + val serviceId = notificationOptions.serviceId + val notification = createNotification() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(serviceId, notification, foregroundServiceTypes.value) + } else { + startForeground(serviceId, notification) + } + + releaseLockMode() + acquireLockMode() + + _isRunningServiceState.update { true } + } + + private fun stopForegroundService() { + RestartReceiver.cancelRestartAlarm(this) + + releaseLockMode() + stopForeground(true) + stopSelf() + + _isRunningServiceState.update { false } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel() { + val channelId = notificationOptions.channelId + val channelName = notificationOptions.channelName + val channelDesc = notificationOptions.channelDescription + val channelImportance = notificationOptions.channelImportance + + val nm = getSystemService(NotificationManager::class.java) + if (nm.getNotificationChannel(channelId) == null) { + val channel = NotificationChannel(channelId, channelName, channelImportance).apply { + if (channelDesc != null) { + description = channelDesc + } + enableVibration(notificationOptions.enableVibration) + if (!notificationOptions.playSound) { + setSound(null, null) + } + setShowBadge(notificationOptions.showBadge) + } + nm.createNotificationChannel(channel) + } + } + + private fun createNotification(): Notification { + // notification icon + val icon = notificationContent.icon + val iconResId = getIconResId(icon) + val iconBackgroundColor = icon?.backgroundColorRgb?.let(::getRgbColor) + + // notification intent + val contentIntent = getContentIntent() + val deleteIntent = getDeleteIntent() + + // notification actions + var needsRebuildButtons = false + val prevButtons = prevNotificationContent?.buttons + val currButtons = notificationContent.buttons + if (prevButtons != null) { + if (prevButtons.size != currButtons.size) { + needsRebuildButtons = true + } else { + for (i in currButtons.indices) { + if (prevButtons[i] != currButtons[i]) { + needsRebuildButtons = true + break + } + } + } + } else { + needsRebuildButtons = true + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val builder = Notification.Builder(this, notificationOptions.channelId) + builder.setOngoing(true) + builder.setShowWhen(notificationOptions.showWhen) + builder.setSmallIcon(iconResId) + builder.setContentIntent(contentIntent) + builder.setContentTitle(notificationContent.title) + builder.setContentText(notificationContent.text) + builder.style = Notification.BigTextStyle() + builder.setVisibility(notificationOptions.visibility) + builder.setOnlyAlertOnce(notificationOptions.onlyAlertOnce) + if (iconBackgroundColor != null) { + builder.setColor(iconBackgroundColor) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + builder.setDeleteIntent(deleteIntent) + } + + val actions = buildNotificationActions(currButtons, needsRebuildButtons) + for (action in actions) { + builder.addAction(action) + } + + return builder.build() + } else { + val builder = NotificationCompat.Builder(this, notificationOptions.channelId) + builder.setOngoing(true) + builder.setShowWhen(notificationOptions.showWhen) + builder.setSmallIcon(iconResId) + builder.setContentIntent(contentIntent) + builder.setContentTitle(notificationContent.title) + builder.setContentText(notificationContent.text) + builder.setStyle(NotificationCompat.BigTextStyle().bigText(notificationContent.text)) + builder.setVisibility(notificationOptions.visibility) + builder.setOnlyAlertOnce(notificationOptions.onlyAlertOnce) + if (iconBackgroundColor != null) { + builder.color = iconBackgroundColor + } + if (!notificationOptions.enableVibration) { + builder.setVibrate(longArrayOf(0L)) + } + if (!notificationOptions.playSound) { + builder.setSound(null) + } + builder.priority = notificationOptions.priority + + val actions = buildNotificationCompatActions(currButtons, needsRebuildButtons) + for (action in actions) { + builder.addAction(action) + } + + return builder.build() + } + } + + private fun updateNotification() { + val serviceId = notificationOptions.serviceId + val notification = createNotification() + val nm = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + getSystemService(NotificationManager::class.java) + } else { + // crash 23+ + ContextCompat.getSystemService(this, NotificationManager::class.java) + } + nm?.notify(serviceId, notification) + } + + @SuppressLint("WakelockTimeout") + private fun acquireLockMode() { + if (foregroundTaskOptions.allowWakeLock && (wakeLock == null || wakeLock?.isHeld == false)) { + wakeLock = + (applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ForegroundService:WakeLock").apply { + setReferenceCounted(false) + acquire() + } + } + } + + if (foregroundTaskOptions.allowWifiLock && (wifiLock == null || wifiLock?.isHeld == false)) { + wifiLock = + (applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager).run { + createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "ForegroundService:WifiLock").apply { + setReferenceCounted(false) + acquire() + } + } + } + } + + private fun releaseLockMode() { + wakeLock?.let { + if (it.isHeld) { + it.release() + wakeLock = null + } + } + + wifiLock?.let { + if (it.isHeld) { + it.release() + wifiLock = null + } + } + } + + private fun createForegroundTask() { + destroyForegroundTask() + + task = ForegroundTask( + context = this, + serviceStatus = foregroundServiceStatus, + taskData = foregroundTaskData, + taskEventAction = foregroundTaskOptions.eventAction, + taskLifecycleListener = taskLifecycleListeners + ) + } + + private fun updateForegroundTask() { + task?.update(taskEventAction = foregroundTaskOptions.eventAction) + } + + private fun destroyForegroundTask(isTimeout: Boolean = false) { + task?.destroy(isTimeout) + task = null + } + + private fun getIconResId(icon: NotificationIcon?): Int { + try { + val packageManager = applicationContext.packageManager + val packageName = applicationContext.packageName + val appInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) + + // application icon + if (icon == null) { + return appInfo.icon + } + + // custom icon + val metaData = appInfo.metaData + if (metaData != null) { + return metaData.getInt(icon.metaDataName) + } + + return 0 + } catch (e: Exception) { + Log.e(TAG, "getIconResId($icon)", e) + return 0 + } + } + + private fun getContentIntent(): PendingIntent { + val packageManager = applicationContext.packageManager + val packageName = applicationContext.packageName + val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply { + putExtra(INTENT_DATA_NAME, ACTION_NOTIFICATION_PRESSED) + + // set initialRoute + val initialRoute = notificationContent.initialRoute + if (initialRoute != null) { + putExtra("route", initialRoute) + } + } + + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + + return PendingIntent.getActivity(this, RequestCode.NOTIFICATION_PRESSED, intent, flags) + } + + private fun getDeleteIntent(): PendingIntent { + val intent = Intent(ACTION_NOTIFICATION_DISMISSED).apply { + setPackage(packageName) + } + + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + + return PendingIntent.getBroadcast(this, RequestCode.NOTIFICATION_DISMISSED, intent, flags) + } + + private fun getRgbColor(rgb: String): Int? { + val rgbSet = rgb.split(",") + return if (rgbSet.size == 3) { + Color.rgb(rgbSet[0].toInt(), rgbSet[1].toInt(), rgbSet[2].toInt()) + } else { + null + } + } + + private fun getTextSpan(text: String, color: Int?): Spannable { + return if (color != null) { + SpannableString(text).apply { + setSpan(ForegroundColorSpan(color), 0, length, 0) + } + } else { + SpannableString(text) + } + } + + private fun buildNotificationActions( + buttons: List, + needsRebuild: Boolean = false + ): List { + val actions = mutableListOf() + for (i in buttons.indices) { + val intent = Intent(ACTION_NOTIFICATION_BUTTON_PRESSED).apply { + setPackage(packageName) + putExtra(INTENT_DATA_NAME, buttons[i].id) + } + var flags = PendingIntent.FLAG_IMMUTABLE + if (needsRebuild) { + flags = flags or PendingIntent.FLAG_CANCEL_CURRENT + } + val textColor = buttons[i].textColorRgb?.let(::getRgbColor) + val text = getTextSpan(buttons[i].text, textColor) + val pendingIntent = + PendingIntent.getBroadcast(this, i + 1, intent, flags) + val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Notification.Action.Builder(null, text, pendingIntent).build() + } else { + Notification.Action.Builder(0, text, pendingIntent).build() + } + actions.add(action) + } + + return actions + } + + private fun buildNotificationCompatActions( + buttons: List, + needsRebuild: Boolean = false + ): List { + val actions = mutableListOf() + for (i in buttons.indices) { + val intent = Intent(ACTION_NOTIFICATION_BUTTON_PRESSED).apply { + setPackage(packageName) + putExtra(INTENT_DATA_NAME, buttons[i].id) + } + var flags = PendingIntent.FLAG_IMMUTABLE + if (needsRebuild) { + flags = flags or PendingIntent.FLAG_CANCEL_CURRENT + } + val textColor = buttons[i].textColorRgb?.let(::getRgbColor) + val text = getTextSpan(buttons[i].text, textColor) + val pendingIntent = + PendingIntent.getBroadcast(this, i + 1, intent, flags) + val action = NotificationCompat.Action.Builder(0, text, pendingIntent).build() + actions.add(action) + } + + return actions + } +} \ No newline at end of file diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundServiceManager.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundServiceManager.kt new file mode 100644 index 0000000..7742481 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundServiceManager.kt @@ -0,0 +1,91 @@ +package com.pravera.flutter_foreground_task.service + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import com.pravera.flutter_foreground_task.errors.ServiceAlreadyStartedException +import com.pravera.flutter_foreground_task.errors.ServiceNotStartedException +import com.pravera.flutter_foreground_task.models.ForegroundServiceAction +import com.pravera.flutter_foreground_task.models.ForegroundServiceStatus +import com.pravera.flutter_foreground_task.models.ForegroundServiceTypes +import com.pravera.flutter_foreground_task.models.ForegroundTaskData +import com.pravera.flutter_foreground_task.models.ForegroundTaskOptions +import com.pravera.flutter_foreground_task.models.NotificationContent +import com.pravera.flutter_foreground_task.models.NotificationOptions + +/** + * A class that provides foreground service control and management functions. + * + * @author Dev-hwang + * @version 1.0 + */ +class ForegroundServiceManager { + /** Start the foreground service. */ + fun start(context: Context, arguments: Any?) { + if (isRunningService()) { + throw ServiceAlreadyStartedException() + } + + val nIntent = Intent(context, ForegroundService::class.java) + val argsMap = arguments as? Map<*, *> + ForegroundServiceStatus.setData(context, ForegroundServiceAction.API_START) + ForegroundServiceTypes.setData(context, argsMap) + NotificationOptions.setData(context, argsMap) + ForegroundTaskOptions.setData(context, argsMap) + ForegroundTaskData.setData(context, argsMap) + NotificationContent.setData(context, argsMap) + ContextCompat.startForegroundService(context, nIntent) + } + + /** Restart the foreground service. */ + fun restart(context: Context) { + if (!isRunningService()) { + throw ServiceNotStartedException() + } + + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.API_RESTART) + ContextCompat.startForegroundService(context, nIntent) + } + + /** Update the foreground service. */ + fun update(context: Context, arguments: Any?) { + if (!isRunningService()) { + throw ServiceNotStartedException() + } + + val nIntent = Intent(context, ForegroundService::class.java) + val argsMap = arguments as? Map<*, *> + ForegroundServiceStatus.setData(context, ForegroundServiceAction.API_UPDATE) + ForegroundTaskOptions.updateData(context, argsMap) + ForegroundTaskData.updateData(context, argsMap) + NotificationContent.updateData(context, argsMap) + ContextCompat.startForegroundService(context, nIntent) + } + + /** Stop the foreground service. */ + fun stop(context: Context) { + if (!isRunningService()) { + throw ServiceNotStartedException() + } + + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.API_STOP) + ForegroundServiceTypes.clearData(context) + NotificationOptions.clearData(context) + ForegroundTaskOptions.clearData(context) + ForegroundTaskData.clearData(context) + NotificationContent.clearData(context) + ContextCompat.startForegroundService(context, nIntent) + } + + /** Send data to TaskHandler. */ + fun sendData(data: Any?) { + if (data != null) { + ForegroundService.sendData(data) + } + } + + /** Returns whether the foreground service is running. */ + fun isRunningService(): Boolean = ForegroundService.isRunningServiceState.value +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTask.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTask.kt new file mode 100644 index 0000000..fa9461c --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTask.kt @@ -0,0 +1,205 @@ +package com.pravera.flutter_foreground_task.service + +import android.content.Context +import android.util.Log +import com.pravera.flutter_foreground_task.FlutterForegroundTaskLifecycleListener +import com.pravera.flutter_foreground_task.FlutterForegroundTaskStarter +import com.pravera.flutter_foreground_task.models.ForegroundServiceAction +import com.pravera.flutter_foreground_task.models.ForegroundServiceStatus +import com.pravera.flutter_foreground_task.models.ForegroundTaskData +import com.pravera.flutter_foreground_task.models.ForegroundTaskEventAction +import com.pravera.flutter_foreground_task.models.ForegroundTaskEventType +import io.flutter.FlutterInjector +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor +import io.flutter.embedding.engine.loader.FlutterLoader +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.view.FlutterCallbackInformation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ForegroundTask( + context: Context, + private val serviceStatus: ForegroundServiceStatus, + private val taskData: ForegroundTaskData, + private var taskEventAction: ForegroundTaskEventAction, + private val taskLifecycleListener: FlutterForegroundTaskLifecycleListener, +) : MethodChannel.MethodCallHandler { + companion object { + private val TAG = ForegroundTask::class.java.simpleName + + private const val ACTION_TASK_START = "onStart" + private const val ACTION_TASK_REPEAT_EVENT = "onRepeatEvent" + private const val ACTION_TASK_DESTROY = "onDestroy" + } + + private val flutterEngine: FlutterEngine + private val flutterLoader: FlutterLoader + private val backgroundChannel: MethodChannel + private var repeatTask: Job? = null + private var isDestroyed: Boolean = false + + init { + // create flutter engine + flutterEngine = FlutterEngine(context) + flutterLoader = FlutterInjector.instance().flutterLoader() + if (!flutterLoader.initialized()) { + flutterLoader.startInitialization(context) + } + flutterLoader.ensureInitializationComplete(context, null) + taskLifecycleListener.onEngineCreate(flutterEngine) + + // create background channel + val messenger = flutterEngine.dartExecutor.binaryMessenger + backgroundChannel = MethodChannel(messenger, "flutter_foreground_task/background") + backgroundChannel.setMethodCallHandler(this) + + // execute callback + val callbackHandle = taskData.callbackHandle + if (callbackHandle != null) { + val bundlePath = flutterLoader.findAppBundlePath() + val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) + val dartCallback = DartExecutor.DartCallback(context.assets, bundlePath, callbackInfo) + flutterEngine.dartExecutor.executeDartCallback(dartCallback) + } + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "start" -> start() + else -> result.notImplemented() + } + } + + private fun start() { + runIfNotDestroyed { + runIfCallbackHandleExists { + val serviceAction = serviceStatus.action + val starter = if (serviceAction == ForegroundServiceAction.API_START || + serviceAction == ForegroundServiceAction.API_RESTART || + serviceAction == ForegroundServiceAction.API_UPDATE) { + FlutterForegroundTaskStarter.DEVELOPER + } else { + FlutterForegroundTaskStarter.SYSTEM + } + + backgroundChannel.invokeMethod(ACTION_TASK_START, starter.ordinal) { + runIfNotDestroyed { + startRepeatTask() + } + } + taskLifecycleListener.onTaskStart(starter) + } + } + } + + private fun invokeTaskRepeatEvent() { + backgroundChannel.invokeMethod(ACTION_TASK_REPEAT_EVENT, null) + taskLifecycleListener.onTaskRepeatEvent() + } + + private fun startRepeatTask() { + stopRepeatTask() + + val type = taskEventAction.type + val interval = taskEventAction.interval + + if (type == ForegroundTaskEventType.NOTHING) { + return + } + + if (type == ForegroundTaskEventType.ONCE) { + invokeTaskRepeatEvent() + return + } + + repeatTask = CoroutineScope(Dispatchers.Default).launch { + while (true) { + delay(interval) + withContext(Dispatchers.Main) { + try { + invokeTaskRepeatEvent() + } catch (e: Exception) { + Log.e(TAG, "repeatTask", e) + } + } + } + } + } + + private fun stopRepeatTask() { + repeatTask?.cancel() + repeatTask = null + } + + fun invokeMethod(method: String, data: Any?) { + runIfNotDestroyed { + backgroundChannel.invokeMethod(method, data) + } + } + + fun update(taskEventAction: ForegroundTaskEventAction) { + runIfNotDestroyed { + runIfCallbackHandleExists { + this.taskEventAction = taskEventAction + startRepeatTask() + } + } + } + + fun destroy(isTimeout: Boolean) { + runIfNotDestroyed { + stopRepeatTask() + + backgroundChannel.setMethodCallHandler(null) + if (taskData.callbackHandle == null) { + taskLifecycleListener.onEngineWillDestroy() + flutterEngine.destroy() + } else { + backgroundChannel.invokeMethod(ACTION_TASK_DESTROY, isTimeout) { + flutterEngine.destroy() + } + taskLifecycleListener.onTaskDestroy() + taskLifecycleListener.onEngineWillDestroy() + } + + isDestroyed = true + } + } + + private fun runIfCallbackHandleExists(call: () -> Unit) { + if (taskData.callbackHandle == null) { + return + } + call() + } + + private fun runIfNotDestroyed(call: () -> Unit) { + if (isDestroyed) { + return + } + call() + } + + private fun MethodChannel.invokeMethod(method: String, data: Any?, onComplete: () -> Unit = {}) { + val callback = object : MethodChannel.Result { + override fun success(result: Any?) { + onComplete() + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + onComplete() + } + + override fun notImplemented() { + onComplete() + } + } + invokeMethod(method, data, callback) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTaskLifecycleListeners.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTaskLifecycleListeners.kt new file mode 100644 index 0000000..964f661 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ForegroundTaskLifecycleListeners.kt @@ -0,0 +1,49 @@ +package com.pravera.flutter_foreground_task.service + +import com.pravera.flutter_foreground_task.FlutterForegroundTaskLifecycleListener +import com.pravera.flutter_foreground_task.FlutterForegroundTaskStarter +import io.flutter.embedding.engine.FlutterEngine + +class ForegroundTaskLifecycleListeners : FlutterForegroundTaskLifecycleListener { + private var listeners = mutableListOf() + + fun addListener(listener: FlutterForegroundTaskLifecycleListener) { + if (!listeners.contains(listener)) { + listeners.add(listener) + } + } + + fun removeListener(listener: FlutterForegroundTaskLifecycleListener) { + listeners.remove(listener) + } + + override fun onEngineCreate(flutterEngine: FlutterEngine?) { + for (listener in listeners) { + listener.onEngineCreate(flutterEngine) + } + } + + override fun onTaskStart(starter: FlutterForegroundTaskStarter) { + for (listener in listeners) { + listener.onTaskStart(starter) + } + } + + override fun onTaskRepeatEvent() { + for (listener in listeners) { + listener.onTaskRepeatEvent() + } + } + + override fun onTaskDestroy() { + for (listener in listeners) { + listener.onTaskDestroy() + } + } + + override fun onEngineWillDestroy() { + for (listener in listeners) { + listener.onEngineWillDestroy() + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/NotificationPermissionCallback.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/NotificationPermissionCallback.kt new file mode 100644 index 0000000..61dbda0 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/NotificationPermissionCallback.kt @@ -0,0 +1,8 @@ +package com.pravera.flutter_foreground_task.service + +import com.pravera.flutter_foreground_task.models.NotificationPermission + +interface NotificationPermissionCallback { + fun onResult(permissionStatus: NotificationPermission) + fun onError(exception: Exception) +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/NotificationPermissionManager.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/NotificationPermissionManager.kt new file mode 100644 index 0000000..1fcf047 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/NotificationPermissionManager.kt @@ -0,0 +1,115 @@ +package com.pravera.flutter_foreground_task.service + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.pravera.flutter_foreground_task.PreferencesKey +import com.pravera.flutter_foreground_task.RequestCode +import com.pravera.flutter_foreground_task.errors.PermissionRequestCancelledException +import com.pravera.flutter_foreground_task.models.NotificationPermission +import io.flutter.plugin.common.PluginRegistry + +class NotificationPermissionManager : PluginRegistry.RequestPermissionsResultListener { + private var activity: Activity? = null + private var callback: NotificationPermissionCallback? = null + + fun checkPermission(activity: Activity): NotificationPermission { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return NotificationPermission.GRANTED + } + + val permission = Manifest.permission.POST_NOTIFICATIONS + if (activity.isPermissionGranted(permission)) { + return NotificationPermission.GRANTED + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val prevPermissionStatus = activity.getPrevPermissionStatus(permission) + if (prevPermissionStatus != null + && prevPermissionStatus == NotificationPermission.PERMANENTLY_DENIED + && !activity.shouldShowRequestPermissionRationale(permission)) { + return NotificationPermission.PERMANENTLY_DENIED + } + } + return NotificationPermission.DENIED + } + } + + fun requestPermission(activity: Activity, callback: NotificationPermissionCallback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + callback.onResult(NotificationPermission.GRANTED) + return + } + + this.activity = activity + this.callback = callback + + ActivityCompat.requestPermissions( + activity, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + RequestCode.REQUEST_NOTIFICATION_PERMISSION + ) + } + + private fun Context.isPermissionGranted(permission: String): Boolean { + return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED + } + + private fun Context.setPrevPermissionStatus(permission: String, status: NotificationPermission) { + val prefs = getSharedPreferences( + PreferencesKey.NOTIFICATION_PERMISSION_STATUS_PREFS, Context.MODE_PRIVATE) ?: return + with (prefs.edit()) { + putString(permission, status.toString()) + commit() + } + } + + private fun Context.getPrevPermissionStatus(permission: String): NotificationPermission? { + val prefs = getSharedPreferences( + PreferencesKey.NOTIFICATION_PERMISSION_STATUS_PREFS, Context.MODE_PRIVATE) ?: return null + val value = prefs.getString(permission, null) ?: return null + return NotificationPermission.valueOf(value) + } + + private fun disposeReference() { + this.activity = null + this.callback = null + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray): Boolean { + if (grantResults.isEmpty()) { + callback?.onError(PermissionRequestCancelledException()) + disposeReference() + return false + } + + val permission: String + val permissionIndex: Int + var permissionStatus = NotificationPermission.DENIED + when (requestCode) { + RequestCode.REQUEST_NOTIFICATION_PERMISSION -> { + permission = Manifest.permission.POST_NOTIFICATIONS + permissionIndex = permissions.indexOf(permission) + if (permissionIndex >= 0 + && grantResults[permissionIndex] == PackageManager.PERMISSION_GRANTED) { + permissionStatus = NotificationPermission.GRANTED + } else { + if (activity?.shouldShowRequestPermissionRationale(permission) == false) { + permissionStatus = NotificationPermission.PERMANENTLY_DENIED + } + } + } + else -> return false + } + + activity?.setPrevPermissionStatus(permission, permissionStatus) + callback?.onResult(permissionStatus) + disposeReference() + return true + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RebootReceiver.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RebootReceiver.kt new file mode 100644 index 0000000..9365305 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RebootReceiver.kt @@ -0,0 +1,75 @@ +package com.pravera.flutter_foreground_task.service + +import android.app.ForegroundServiceStartNotAllowedException +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.content.ContextCompat +import com.pravera.flutter_foreground_task.models.ForegroundServiceAction +import com.pravera.flutter_foreground_task.models.ForegroundServiceStatus +import com.pravera.flutter_foreground_task.models.ForegroundTaskOptions +import com.pravera.flutter_foreground_task.utils.ForegroundServiceUtils + +/** + * The receiver that receives the BOOT_COMPLETED and MY_PACKAGE_REPLACED intent. + * + * @author Dev-hwang + * @version 1.0 + */ +class RebootReceiver : BroadcastReceiver() { + companion object { + private val TAG = RebootReceiver::class.java.simpleName + } + + override fun onReceive(context: Context?, intent: Intent?) { + if (context == null || intent == null) return + + // Ignore autoRunOnBoot option when android:stopWithTask is set to true. + if (ForegroundServiceUtils.isSetStopWithTaskFlag(context)) { + return + } + + // Ignore autoRunOnBoot option when service is stopped by developer. + val serviceStatus = ForegroundServiceStatus.getData(context) + if (serviceStatus.isCorrectlyStopped()) { + return + } + + val options = ForegroundTaskOptions.getData(context) + + // Check whether to start the service at boot intent. + if ((intent.action == Intent.ACTION_BOOT_COMPLETED || + intent.action == "android.intent.action.QUICKBOOT_POWERON") && options.autoRunOnBoot) { + return startForegroundService(context) + } + + // Check whether to start the service on my package replaced intent. + if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED && options.autoRunOnMyPackageReplaced) { + return startForegroundService(context) + } + } + + private fun startForegroundService(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.REBOOT) + ContextCompat.startForegroundService(context, nIntent) + } catch (e: ForegroundServiceStartNotAllowedException) { + Log.e(TAG, "Foreground service start not allowed exception: ${e.message}") + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } else { + try { + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.REBOOT) + ContextCompat.startForegroundService(context, nIntent) + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RestartReceiver.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RestartReceiver.kt new file mode 100644 index 0000000..8ff56ac --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/RestartReceiver.kt @@ -0,0 +1,103 @@ +package com.pravera.flutter_foreground_task.service + +import android.app.ActivityManager +import android.app.AlarmManager +import android.app.ForegroundServiceStartNotAllowedException +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.content.ContextCompat +import com.pravera.flutter_foreground_task.RequestCode +import com.pravera.flutter_foreground_task.models.ForegroundServiceAction +import com.pravera.flutter_foreground_task.models.ForegroundServiceStatus +import com.pravera.flutter_foreground_task.utils.PluginUtils + +/** + * The receiver that receives restart alarm event. + * + * @author Dev-hwang + * @version 1.0 + */ +class RestartReceiver : BroadcastReceiver() { + companion object { + private val TAG = RestartReceiver::class.java.simpleName + + fun setRestartAlarm(context: Context, millis: Int) { + val triggerTime = System.currentTimeMillis() + millis + + val intent = Intent(context, RestartReceiver::class.java) + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_MUTABLE + } + val operation = PendingIntent.getBroadcast( + context, RequestCode.SET_RESTART_SERVICE_ALARM, intent, flags) + + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + PluginUtils.canScheduleExactAlarms(context)) { + val info = AlarmManager.AlarmClockInfo(triggerTime, operation) + alarmManager.setAlarmClock(info, operation) + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, triggerTime, operation) + } + } + + fun cancelRestartAlarm(context: Context) { + val intent = Intent(context, RestartReceiver::class.java) + var flags = PendingIntent.FLAG_CANCEL_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_MUTABLE + } + val operation = PendingIntent.getBroadcast( + context, RequestCode.SET_RESTART_SERVICE_ALARM, intent, flags) + + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + alarmManager.cancel(operation) + } + } + + override fun onReceive(context: Context?, intent: Intent?) { + if (context == null) return + + val serviceStatus = ForegroundServiceStatus.getData(context) + if (serviceStatus.isCorrectlyStopped()) { + return + } + + val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val isRunningService = manager.getRunningServices(Integer.MAX_VALUE) + .any { it.service.className == ForegroundService::class.java.name } + if (isRunningService) { + return + } + + val isIgnoringBatteryOptimizations = PluginUtils.isIgnoringBatteryOptimizations(context) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isIgnoringBatteryOptimizations) { + Log.w(TAG, "Turn off battery optimization to restart service in the background.") + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART) + ContextCompat.startForegroundService(context, nIntent) + } catch (e: ForegroundServiceStartNotAllowedException) { + Log.e(TAG, "Foreground service start not allowed exception: ${e.message}") + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } else { + try { + val nIntent = Intent(context, ForegroundService::class.java) + ForegroundServiceStatus.setData(context, ForegroundServiceAction.RESTART) + ContextCompat.startForegroundService(context, nIntent) + } catch (e: Exception) { + Log.e(TAG, e.message, e) + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ServiceProvider.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ServiceProvider.kt new file mode 100644 index 0000000..9c67535 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/service/ServiceProvider.kt @@ -0,0 +1,7 @@ +package com.pravera.flutter_foreground_task.service + +/** ServiceProvider */ +interface ServiceProvider { + fun getNotificationPermissionManager(): NotificationPermissionManager + fun getForegroundServiceManager(): ForegroundServiceManager +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ErrorHandleUtils.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ErrorHandleUtils.kt new file mode 100644 index 0000000..33648a6 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ErrorHandleUtils.kt @@ -0,0 +1,20 @@ +package com.pravera.flutter_foreground_task.utils + +import io.flutter.plugin.common.MethodChannel + +/** + * Utilities for error handling. + * + * @author Dev-hwang + * @version 1.0 + */ +class ErrorHandleUtils { + companion object { + /** Handles errors that occur in MethodChannel. */ + fun handleMethodCallError(result: MethodChannel.Result, exception: Exception) { + val name = exception.javaClass.simpleName + val message = exception.message + result.error(name, message, null) + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt new file mode 100644 index 0000000..28325b5 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/ForegroundServiceUtils.kt @@ -0,0 +1,30 @@ +package com.pravera.flutter_foreground_task.utils + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException +import android.content.pm.ServiceInfo +import android.util.Log +import com.pravera.flutter_foreground_task.service.ForegroundService + +class ForegroundServiceUtils { + companion object { + private val TAG = ForegroundServiceUtils::class.java.simpleName + + fun isSetStopWithTaskFlag(context: Context): Boolean { + return try { + val pm = context.packageManager + val cName = ComponentName(context, ForegroundService::class.java) + val flags = pm.getServiceInfo(cName, PackageManager.GET_META_DATA).flags + (flags and ServiceInfo.FLAG_STOP_WITH_TASK) == 1 + } catch (e: NameNotFoundException) { + Log.e(TAG, "isSetStopWithTaskFlag >> The service component cannot be found on the system.") + true + } catch (e: Exception) { + Log.e(TAG, "isSetStopWithTaskFlag >> $e") + true + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/PluginUtils.kt b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/PluginUtils.kt new file mode 100644 index 0000000..0aa385e --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/android/src/main/kotlin/com/pravera/flutter_foreground_task/utils/PluginUtils.kt @@ -0,0 +1,172 @@ +package com.pravera.flutter_foreground_task.utils + +import android.app.Activity +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo +import android.app.ActivityOptions +import android.app.AlarmManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import android.view.WindowManager +import com.pravera.flutter_foreground_task.errors.NotSupportedException + +/** + * Utilities supported in the plugin. + * + * @author Dev-hwang + * @version 1.0 + */ +class PluginUtils { + companion object { + /** Returns whether the app is in the foreground. */ + fun isAppOnForeground(context: Context): Boolean { + val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val processes: MutableList = am.runningAppProcesses + ?: return false + + val packageName = context.packageName + for (process in processes) { + if (process.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && process.processName.equals(packageName)) { + return true + } + } + + return false + } + + /** Minimize the app to the background. */ + fun minimizeApp(activity: Activity) { + activity.moveTaskToBack(true) + } + + /** Launch the app at [route] if it is not running otherwise open it. */ + fun launchApp(context: Context, route: String?) { + val pm = context.packageManager + val launchIntent = pm.getLaunchIntentForPackage(context.packageName) + if (launchIntent != null) { + launchIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + if (route != null) { + launchIntent.putExtra("route", route) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val options = ActivityOptions.makeBasic().apply { + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + } + context.startActivity(launchIntent, options.toBundle()) + } else { + context.startActivity(launchIntent) + } + } + } + + /** Toggle on lockscreen visibility. */ + fun setOnLockScreenVisibility(activity: Activity, isVisible: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + activity.setShowWhenLocked(isVisible) + } else { + if (isVisible) { + activity.window?.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + } else { + activity.window?.clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + } + } + } + + /** Wake up the screen of a device that is turned off. */ + fun wakeUpScreen(context: Context) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + val serviceFlag = PowerManager.SCREEN_BRIGHT_WAKE_LOCK + .or(PowerManager.ACQUIRE_CAUSES_WAKEUP) + .or(PowerManager.ON_AFTER_RELEASE) + + val newWakeLock = powerManager.newWakeLock(serviceFlag, "PluginUtils:WakeLock") + newWakeLock.acquire(1000) + newWakeLock.release() + } + + /** Returns whether the app has been excluded from battery optimization. */ + fun isIgnoringBatteryOptimizations(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + powerManager.isIgnoringBatteryOptimizations(context.packageName) + } else { + true + } + } + + /** Open the settings page where you can set ignore battery optimization. */ + fun openIgnoreBatteryOptimizationSettings(activity: Activity, requestCode: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + activity.startActivityForResult(intent, requestCode) + } else { + throw NotSupportedException("only supports Android 6.0+") + } + } + + /** Request to ignore battery optimization. */ + fun requestIgnoreBatteryOptimization(activity: Activity, requestCode: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val intent = Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:" + activity.packageName) + ) + activity.startActivityForResult(intent, requestCode) + } else { + throw NotSupportedException("only supports Android 6.0+") + } + } + + /** Returns whether the "android.permission.SYSTEM_ALERT_WINDOW" permission was granted. */ + fun canDrawOverlays(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Settings.canDrawOverlays(context) + } else { + true + } + } + + /** Open the settings page where you can allow/deny the "android.permission.SYSTEM_ALERT_WINDOW" permission. */ + fun openSystemAlertWindowSettings(activity: Activity, requestCode: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val intent = Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + activity.packageName) + ) + activity.startActivityForResult(intent, requestCode) + } else { + throw NotSupportedException("only supports Android 6.0+") + } + } + + /** Returns whether the "android.permission.SCHEDULE_EXACT_ALARM" permission is granted. */ + fun canScheduleExactAlarms(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + alarmManager.canScheduleExactAlarms() + } else { + true + } + } + + /** Open the alarms & reminders settings page. */ + fun openAlarmsAndRemindersSettings(activity: Activity, requestCode: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val intent = Intent( + Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, + Uri.parse("package:" + activity.packageName) + ) + activity.startActivityForResult(intent, requestCode) + } else { + throw NotSupportedException("only supports Android 12.0+") + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/documentation/customize_notification_icon.md b/local_packages/flutter_foreground_task-9.1.0/documentation/customize_notification_icon.md new file mode 100644 index 0000000..2e0e890 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/documentation/customize_notification_icon.md @@ -0,0 +1,54 @@ +Go to [`page`][1] and generate the notification icon. +You can also use the [`image generator`][2] provided by Android Studio. + +Move the downloaded icon image file to the `android/app/src/res/drawable` folder. + +If successful, it will appear as shown in the image below: + +image + +Next, you need to add meta-data in the `AndroidManifest.xml` file so that the plugin can reference the icon resource. + +```xml + + + + + + + +``` + +The name of the meta-data can be customized, but it is recommended to set it to a unique name combined with your package name. + +Finally, you can create a NotificationIcon object to change the notification icon when starting or updating the service. + +```dart +void start() { + FlutterForegroundTask.startService( + notificationTitle: 'notificationTitle', + notificationText: 'notificationText', + notificationIcon: const NotificationIcon( + metaDataName: 'com.your_package.service.HEART_ICON', + backgroundColor: Colors.orange, + ), + ); +} + +void update() { + FlutterForegroundTask.updateService( + notificationIcon: const NotificationIcon( + metaDataName: 'com.your_package.service.HEART_ICON', + backgroundColor: Colors.purple, + ), + ); +} +``` + +[1]: https://romannurik.github.io/AndroidAssetStudio/icons-notification.html#source.type=clipart&source.clipart=ac_unit&source.space.trim=1&source.space.pad=0.15&name=ic_notification +[2]: https://developer.android.com/studio/write/create-app-icons#create-notification diff --git a/local_packages/flutter_foreground_task-9.1.0/documentation/migration_documentation.md b/local_packages/flutter_foreground_task-9.1.0/documentation/migration_documentation.md new file mode 100644 index 0000000..7206001 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/documentation/migration_documentation.md @@ -0,0 +1,251 @@ +## Migration + +### ver 9.0.0 + +- chore: Bump minimum supported SDK version to `Flutter 3.22/Dart 3.4`. + +``` +environment: + // sdk: ">=3.0.0 <4.0.0" + // flutter: ">=3.10.0" + sdk: ^3.4.0 + flutter: ">=3.22.0" +``` + +- chore: Bump `kotlin_version(1.7.10 -> 1.9.10)`, `gradle(7.3.0 -> 8.6.0)` for Android 15. + +``` +[android/settings.gradle] +plugins { + // id "com.android.application" version "7.3.0" apply false + // id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.10" apply false +} + +[android/gradle/wrapper/gradle-wrapper.properties] +// distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip + +[android/app/build.gradle] +android { + // compileSdk 34 + compileSdk 35 + + compileOptions { + // sourceCompatibility JavaVersion.VERSION_1_8 + // targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + // jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_11 + } + + defaultConfig { + // targetSdkVersion 34 + targetSdkVersion 35 + } +} +``` + +- feat: Add `isTimeout` param to the onDestroy callback. + +```dart +// from +@override +Future onDestroy(DateTime timestamp) async {} + +// to +@override +Future onDestroy(DateTime timestamp, bool isTimeout) async {} +``` + +### ver 8.16.0 + +- Change `ServiceRequestResult` class to `sealed class` for improved code readability. + +```dart +void startService() { + final ServiceRequestResult result = + await FlutterForegroundTask.startService( + serviceId: 100, + notificationTitle: 'notificationTitle', + notificationText: 'notificationText', + callback: startLocationService, + ); + + // before: The distinction between success and failure of the request is unclear. + if (result.success) { + // The error should not be accessible when the request is successful. + final Object? error = result.error; + } else { + // Handle error + final Object? error = result.error; + } + + // after: The distinction between success and failure of the request is clear. + switch (result) { + case ServiceRequestSuccess(): + // The error cannot be accessed. + // final Object error = result.error; + print('success'); + case ServiceRequestFailure(): + // The error can only be accessed when the request fails, + // and no null check is required. + final Object error = result.error; + print('failure($error)'); + } +} +``` + +- Change method for customizing notification icon. [guide page](./customize_notification_icon.md) + +```dart +void startService() { + // before: There was an issue in the foreground service where only the white icon was displayed + // because the icon resource could not be referenced. + await FlutterForegroundTask.startService( + notificationTitle: 'notificationTitle', + notificationText: 'notificationText', + notificationIcon: const NotificationIconData( + resType: ResourceType.drawable, + resPrefix: ResourcePrefix.ic, + name: 'snow', + backgroundColor: Colors.orange, + ), + ); + + // after: Enabled static reference to the icon resource through meta-data. + await FlutterForegroundTask.startService( + notificationTitle: 'notificationTitle', + notificationText: 'notificationText', + notificationIcon: const NotificationIcon( + metaDataName: 'com.your_package.service.SNOW_ICON', + backgroundColor: Colors.orange, + ), + ); +} +``` + +### ver 8.10.0 + +- Change onStart, onDestroy callback return type from `void` to `Future`. + +```dart +// before +@override +void onStart(DateTime timestamp, TaskStarter starter) { } + +@override +void onDestroy(DateTime timestamp) { } + +// after +@override +Future onStart(DateTime timestamp, TaskStarter starter) async { } + +@override +Future onDestroy(DateTime timestamp) async { } +``` + +### ver 8.6.0 + +- Remove `interval`, `isOnceEvent` option in ForegroundTaskOptions model. +- Add `eventAction` option with ForegroundTaskEventAction constructor. + +```dart +// before +FlutterForegroundTask.init( + foregroundTaskOptions: ForegroundTaskOptions( + interval: 5000, + isOnceEvent: false, + ), +); + +// after +FlutterForegroundTask.init( + // ForegroundTaskEventAction.nothing() : Not use onRepeatEvent callback. + // ForegroundTaskEventAction.once() : Call onRepeatEvent only once. + // ForegroundTaskEventAction.repeat(interval) : Call onRepeatEvent at milliseconds interval. + foregroundTaskOptions: ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(5000), + ), +); +``` + +### ver 8.0.0 + +- The `sendPort` parameter was removed from the service callback(onStart, onRepeatEvent, onDestroy). + +```dart +// before +void onStart(DateTime timestamp, SendPort? sendPort) { + sendPort?.send(Object); +} + +// after +void onStart(DateTime timestamp) { + // Send data to main isolate. + FlutterForegroundTask.sendDataToMain(Object); +} +``` + +- `FlutterForegroundTask.receivePort` getter function was removed. + +```dart +// before +final ReceivePort? receivePort = FlutterForegroundTask.receivePort; +receivePort?.listen(_onReceiveTaskData) +receivePort?.close(); + +// after +void main() { + // Initialize port for communication between TaskHandler and UI. + FlutterForegroundTask.initCommunicationPort(); + runApp(const ExampleApp()); +} + +FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData); +FlutterForegroundTask.removeTaskDataCallback(_onReceiveTaskData); +``` + +- `sendData` renamed to `sendDataToTask`. + +```dart +// before +FlutterForegroundTask.sendData(Object); + +// after +FlutterForegroundTask.sendDataToTask(Object); +``` + +### ver 7.0.0 + +- Remove `iconData`, `buttons` from AndroidNotificationOptions. + +```dart +// before +FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'foreground_service', + channelName: 'Foreground Service Notification', + iconData: null, + buttons: [ + const NotificationButton(id: 'btn_hello', text: 'hello'), + ], + ), +); + +// after +FlutterForegroundTask.startService( + notificationTitle: 'Foreground Service is running', + notificationText: 'Tap to return to the app', + notificationIcon: null, + notificationButtons: [ + const NotificationButton(id: 'btn_hello', text: 'hello'), + ], + callback: startCallback, +) +``` diff --git a/local_packages/flutter_foreground_task-9.1.0/documentation/models_documentation.md b/local_packages/flutter_foreground_task-9.1.0/documentation/models_documentation.md new file mode 100644 index 0000000..c2f6202 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/documentation/models_documentation.md @@ -0,0 +1,140 @@ +## Models + +### :chicken: AndroidNotificationOptions + +Notification options for Android platform. + +| Property | Description | +|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| `channelId` | Unique ID of the notification channel. It is set only once for the first time on Android 8.0+. | +| `channelName` | The name of the notification channel. It is set only once for the first time on Android 8.0+. | +| `channelDescription` | The description of the notification channel. It is set only once for the first time on Android 8.0+. | +| `channelImportance` | The importance of the notification channel. The default is `NotificationChannelImportance.LOW`. It is set only once for the first time on Android 8.0+. | +| `priority` | Priority of notifications for Android 7.1 and lower. The default is `NotificationPriority.LOW`. | +| `enableVibration` | Whether to enable vibration when creating notifications. The default is `false`. It is set only once for the first time on Android 8.0+. | +| `playSound` | Whether to play sound when creating notifications. The default is `false`. It is set only once for the first time on Android 8.0+. | +| `showWhen` | Whether to show the timestamp when the notification was created in the content view. The default is `false`. | +| `showBadge` | Whether to show the badge near the app icon when service is started. The default is `false`. It is set only once for the first time on Android 8.0+. | +| `onlyAlertOnce` | Whether to only alert once when the notification is created. The default is `false`. | +| `visibility` | Control the level of detail displayed in notifications on the lock screen. The default is `NotificationVisibility.VISIBILITY_PUBLIC`. | + +### :chicken: IOSNotificationOptions + +Notification options for iOS platform. + +| Property | Description | +|--------------------|----------------------------------------------------------------------------| +| `showNotification` | Whether to show notifications. The default is `true`. | +| `playSound` | Whether to play sound when creating notifications. The default is `false`. | + +### :chicken: ForegroundTaskOptions + +Data class with foreground task options. + +| Property | Description | +|------------------------------|----------------------------------------------------------------------------------------------------------------| +| `eventAction` | The action of onRepeatEvent in `TaskHandler`. | +| `autoRunOnBoot` | Whether to automatically run foreground task on boot. The default is `false`. | +| `autoRunOnMyPackageReplaced` | Whether to automatically run foreground task when the app is updated to a new version. The default is `false`. | +| `allowWakeLock` | Whether to keep the CPU turned on. The default is `true`. | +| `allowWifiLock` | Allows an application to keep the Wi-Fi radio awake. The default is `false`. | + +### :chicken: ForegroundTaskEventAction + +A class that defines the action of onRepeatEvent in `TaskHandler`. + +| Constructor | Description | +|--------------------|------------------------------------------------| +| `nothing()` | Not use onRepeatEvent callback. | +| `once()` | Call onRepeatEvent only once. | +| `repeat(interval)` | Call onRepeatEvent at milliseconds `interval`. | + +### :chicken: NotificationIcon + +A data class for dynamically changing the notification icon. + +| Property | Description | +|-------------------|------------------------------------------------------------------------------------------------| +| `metaDataName` | The name of the meta-data in the manifest that contains the drawable icon resource identifier. | +| `backgroundColor` | The background color for the notification icon. | + +### :chicken: NotificationButton + +The button to display in the notification. + +| Property | Description | +|-------------|--------------------------------------------| +| `id` | The button identifier. | +| `text` | The text to display on the button. | +| `textColor` | The button text color. (only work Android) | + +### :chicken: NotificationChannelImportance + +The importance of the notification channel. +See https://developer.android.com/training/notify-user/channels?hl=ko#importance + +| Value | Description | +|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `NONE` | A notification with no importance: does not show in the shade. | +| `MIN` | Min notification importance: only shows in the shade, below the fold. | +| `LOW` | Low notification importance: shows in the shade, and potentially in the status bar (see shouldHideSilentStatusBarIcons()), but is not audibly intrusive. | +| `DEFAULT` | Default notification importance: shows everywhere, makes noise, but does not visually intrude. | +| `HIGH` | Higher notification importance: shows everywhere, makes noise and peeks. May use full screen intents. | +| `MAX` | Max notification importance: same as HIGH, but generally not used. | + +### :chicken: NotificationPriority + +Priority of notifications for Android 7.1 and lower. + +| Value | Description | +|-----------|--------------------------------------------------------------------------| +| `MIN` | No sound and does not appear in the status bar. | +| `LOW` | No sound. | +| `DEFAULT` | Makes a sound. | +| `HIGH` | Makes a sound and appears as a heads-up notification. | +| `MAX` | Same as HIGH, but used when you want to notify notification immediately. | + +### :chicken: NotificationVisibility + +The level of detail displayed in notifications on the lock screen. + +| Value | Description | +|----------------------|----------------------------------------------------------------------------------------------------------------| +| `VISIBILITY_PUBLIC` | Show this notification in its entirety on all lockscreens. | +| `VISIBILITY_SECRET` | Do not reveal any part of this notification on a secure lockscreen. | +| `VISIBILITY_PRIVATE` | Show this notification on all lockscreens, but conceal sensitive or private information on secure lockscreens. | + +### :chicken: NotificationPermission + +enum class: Represents the result of a notification permission request. + +| value | Description | +|----------------------|------------------------------------------------------| +| `granted` | Notification permission has been granted. | +| `denied` | Notification permission has been denied. | +| `permanently_denied` | Notification permission has been permanently denied. | + +### :chicken: ServiceRequestResult + +sealed class: Represents the result of a service request. + +| child | Description | +|---------------------------------------|-------------------------------------| +| `ServiceRequestSuccess()` | The service request was successful. | +| `ServiceRequestFailure(Object error)` | The service request failed. | + +| error | Description | +|----------------------------------|----------------------------------------------------------------------------------------------------------------| +| `ServiceAlreadyStartedException` | The service has already started. | +| `ServiceNotInitializedException` | Not initialized. Please call this function after calling the init function. | +| `ServiceNotStartedException` | The service is not started. | +| `ServiceTimeoutException` | The service request timed out. (ref: https://developer.android.com/guide/components/services#StartingAService) | + +### :chicken: TaskStarter + +The starter that started the task. + +| Value | Description | +|-------------|---------------------------------------------| +| `developer` | The task has been started by the developer. | +| `system` | The task has been started by the system. | diff --git a/local_packages/flutter_foreground_task-9.1.0/documentation/utility_documentation.md b/local_packages/flutter_foreground_task-9.1.0/documentation/utility_documentation.md new file mode 100644 index 0000000..563aff6 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/documentation/utility_documentation.md @@ -0,0 +1,158 @@ +## Utility + +### :lollipop: minimizeApp + +Minimize the app to the background. + +> **Warning** +> It only works when the app is in the foreground. + +```dart +void function() => FlutterForegroundTask.minimizeApp(); +``` + +### :lollipop: launchApp (Android) + +Launch the app at `route` if it is not running otherwise open it. + +It is also possible to pass a route to this function but the route will only +be loaded if the app is not already running. + +This function requires the `android.permission.SYSTEM_ALERT_WINDOW` permission and +requires using the `openSystemAlertWindowSettings()` function to grant the permission. + +```dart +void requestPermission() async { + if (!await FlutterForegroundTask.canDrawOverlays) { + await FlutterForegroundTask.openSystemAlertWindowSettings(); + } +} + +void function() => FlutterForegroundTask.launchApp([route]); +``` + +### :lollipop: setOnLockScreenVisibility (Android) + +Toggles lockScreen visibility. + +> **Warning** +> It only works when the app is in the foreground. + +```dart +void function() => FlutterForegroundTask.setOnLockScreenVisibility(true); +``` + +### :lollipop: isAppOnForeground + +Returns whether the app is in the foreground. + +```dart +Future function() => FlutterForegroundTask.isAppOnForeground; +``` + +### :lollipop: wakeUpScreen (Android) + +Wake up the screen of a device that is turned off. + +```dart +void function() => FlutterForegroundTask.wakeUpScreen(); +``` + +### :lollipop: isIgnoringBatteryOptimizations (Android) + +Returns whether the app has been excluded from battery optimization. + +```dart +Future function() => FlutterForegroundTask.isIgnoringBatteryOptimizations; +``` + +### :lollipop: openIgnoreBatteryOptimizationSettings (Android) + +Open the settings page where you can set ignore battery optimization. + +> **Warning** +> It only works when the app is in the foreground. + +```dart +Future function() => FlutterForegroundTask.openIgnoreBatteryOptimizationSettings(); +``` + +### :lollipop: requestIgnoreBatteryOptimization (Android) + +Request to ignore battery optimization. + +This function requires the `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission. + +> **Warning** +> It only works when the app is in the foreground. + +```dart +Future function() => FlutterForegroundTask.requestIgnoreBatteryOptimization(); +``` + +### :lollipop: canDrawOverlays (Android) + +Returns whether the `android.permission.SYSTEM_ALERT_WINDOW` permission is granted. + +```dart +Future function() => FlutterForegroundTask.canDrawOverlays; +``` + +### :lollipop: openSystemAlertWindowSettings (Android) + +Open the settings page where you can allow/deny the `android.permission.SYSTEM_ALERT_WINDOW` +permission. + +> **Warning** +> It only works when the app is in the foreground. + +```dart +Future function() => FlutterForegroundTask.openSystemAlertWindowSettings(); +``` + +### :lollipop: checkNotificationPermission + +Returns notification permission status. + +> **Warning** +> It only works when the app is in the foreground. + +```dart +Future function() => FlutterForegroundTask.checkNotificationPermission(); +``` + +### :lollipop: requestNotificationPermission + +Request notification permission. + +> **Warning** +> It only works when the app is in the foreground. + +```dart +Future function() => FlutterForegroundTask.requestNotificationPermission(); +``` + +### :lollipop: canScheduleExactAlarms (Android) + +Returns whether the `android.permission.SCHEDULE_EXACT_ALARM` permission is granted. + +```dart +Future function() => FlutterForegroundTask.canScheduleExactAlarms; +``` + +### :lollipop: openAlarmsAndRemindersSettings (Android) + +Open the alarms & reminders settings page. + +Use this utility only if you provide services that require long-term survival, +such as exact alarm service, healthcare service, or Bluetooth communication. + +This utility requires the `android.permission.SCHEDULE_EXACT_ALARM` permission. +Using this permission may make app distribution difficult due to Google policy. + +> **Warning** +> It only works when the app is in the foreground. + +```dart +Future function() => FlutterForegroundTask.openAlarmsAndRemindersSettings(); +``` diff --git a/local_packages/flutter_foreground_task-9.1.0/example/README.md b/local_packages/flutter_foreground_task-9.1.0/example/README.md new file mode 100644 index 0000000..58c9057 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/README.md @@ -0,0 +1,16 @@ +# flutter_foreground_task_example + +Demonstrates how to use the flutter_foreground_task plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/local_packages/flutter_foreground_task-9.1.0/example/analysis_options.yaml b/local_packages/flutter_foreground_task-9.1.0/example/analysis_options.yaml new file mode 100644 index 0000000..34b3fec --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/analysis_options.yaml @@ -0,0 +1,8 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + avoid_print: false + avoid_function_literals_in_foreach_calls: false + constant_identifier_names: false + prefer_void_to_null: false diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/checksums/checksums.lock b/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/checksums/checksums.lock new file mode 100644 index 0000000..7b721bf Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/checksums/checksums.lock differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/fileChanges/last-build.bin b/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/fileChanges/last-build.bin differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/fileHashes/fileHashes.lock b/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/fileHashes/fileHashes.lock new file mode 100644 index 0000000..0ba1ea2 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/fileHashes/fileHashes.lock differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/gc.properties b/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/8.7/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/vcs-1/gc.properties b/local_packages/flutter_foreground_task-9.1.0/example/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/build.gradle b/local_packages/flutter_foreground_task-9.1.0/example/android/app/build.gradle new file mode 100644 index 0000000..7b223d4 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/build.gradle @@ -0,0 +1,73 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.pravera.flutter_foreground_task_example" + + compileSdk 36 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + 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.pravera.flutter_foreground_task_example" + minSdkVersion 21 + targetSdkVersion 35 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + + debug { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile( + 'proguard-android-optimize.txt'), + 'proguard-rules.pro' + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/proguard-rules.pro b/local_packages/flutter_foreground_task-9.1.0/example/android/app/proguard-rules.pro new file mode 100644 index 0000000..b77317f --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/proguard-rules.pro @@ -0,0 +1,7 @@ +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } +-dontwarn io.flutter.embedding.** diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/debug/AndroidManifest.xml b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..f47bf35 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/AndroidManifest.xml b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..86ed672 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/kotlin/com/pravera/flutter_foreground_task_example/MainActivity.kt b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/kotlin/com/pravera/flutter_foreground_task_example/MainActivity.kt new file mode 100644 index 0000000..83e279d --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/kotlin/com/pravera/flutter_foreground_task_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.pravera.flutter_foreground_task_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-hdpi/ic_heart.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-hdpi/ic_heart.png new file mode 100644 index 0000000..61f9992 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-hdpi/ic_heart.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-hdpi/ic_home.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-hdpi/ic_home.png new file mode 100644 index 0000000..962de2a Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-hdpi/ic_home.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-mdpi/ic_heart.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-mdpi/ic_heart.png new file mode 100644 index 0000000..25baa09 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-mdpi/ic_heart.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-mdpi/ic_home.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-mdpi/ic_home.png new file mode 100644 index 0000000..f74a6c7 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-mdpi/ic_home.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-v21/launch_background.xml b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..1cb7aa2 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xhdpi/ic_heart.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xhdpi/ic_heart.png new file mode 100644 index 0000000..7560ef0 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xhdpi/ic_heart.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xhdpi/ic_home.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xhdpi/ic_home.png new file mode 100644 index 0000000..d7a4ffa Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xhdpi/ic_home.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxhdpi/ic_heart.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxhdpi/ic_heart.png new file mode 100644 index 0000000..da0c3aa Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxhdpi/ic_heart.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxhdpi/ic_home.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxhdpi/ic_home.png new file mode 100644 index 0000000..8c2ee5f Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxhdpi/ic_home.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxxhdpi/ic_heart.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxxhdpi/ic_heart.png new file mode 100644 index 0000000..af5cce9 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxxhdpi/ic_heart.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxxhdpi/ic_home.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxxhdpi/ic_home.png new file mode 100644 index 0000000..e2716a3 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable-xxxhdpi/ic_home.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable/launch_background.xml b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..8403758 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/values-night/styles.xml b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..827f12f --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/values/styles.xml b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..ccf2854 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/profile/AndroidManifest.xml b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..0cc9a4a --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/build.gradle b/local_packages/flutter_foreground_task-9.1.0/example/android/build.gradle new file mode 100644 index 0000000..d37c869 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/build.gradle @@ -0,0 +1,18 @@ +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/local_packages/flutter_foreground_task-9.1.0/example/android/gradle.properties b/local_packages/flutter_foreground_task-9.1.0/example/android/gradle.properties new file mode 100644 index 0000000..46c1f16 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/gradle/wrapper/gradle-wrapper.properties b/local_packages/flutter_foreground_task-9.1.0/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6636b63 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/local_packages/flutter_foreground_task-9.1.0/example/android/settings.gradle b/local_packages/flutter_foreground_task-9.1.0/example/android/settings.gradle new file mode 100644 index 0000000..f5f5a5e --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.10" apply false +} + +include ":app" diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/AppFrameworkInfo.plist b/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..e041d38 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/Debug.xcconfig b/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..553cfac --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/Release.xcconfig b/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..4b6e1f6 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Podfile b/local_packages/flutter_foreground_task-9.1.0/example/ios/Podfile new file mode 100644 index 0000000..eb95098 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.pbxproj b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e308e33 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,557 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 37B6493A4744452DDC4DEBF7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A32D55799B66D9DF641C7ED8 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2EDD805CC77857CB5704E03A /* 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 = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8BA7286763F4F5A5D4D4791F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9DCDDA24B41ECFF3C6781683 /* 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 = ""; }; + A32D55799B66D9DF641C7ED8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 37B6493A4744452DDC4DEBF7 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2105D355AEE26F31BDDE1572 /* Pods */ = { + isa = PBXGroup; + children = ( + 8BA7286763F4F5A5D4D4791F /* Pods-Runner.debug.xcconfig */, + 9DCDDA24B41ECFF3C6781683 /* Pods-Runner.release.xcconfig */, + 2EDD805CC77857CB5704E03A /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 2105D355AEE26F31BDDE1572 /* Pods */, + A07F075276D23FBA8C723152 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + A07F075276D23FBA8C723152 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A32D55799B66D9DF641C7ED8 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 8D396599160DECB90EDD47BE /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 6EA6E717D2A4A7547969F4C2 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + 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"; + }; + 6EA6E717D2A4A7547969F4C2 /* [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; + }; + 8D396599160DECB90EDD47BE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 2BHAXN696Y; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.pravera.flutterForegroundTaskExam; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 2BHAXN696Y; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.pravera.flutterForegroundTaskExam; + 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_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 2BHAXN696Y; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.pravera.flutterForegroundTaskExam; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..c4b79bd --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..fc6bf80 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..af0309c --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..9a5e54c --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..17ccc03 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..fc6bf80 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..af0309c --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/AppDelegate.swift b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..f59a287 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,21 @@ +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + + SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback { registry in + GeneratedPluginRegistrant.register(with: registry) + } + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self + } + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..1950fd8 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..d08a4de --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..65a94b5 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..497371e --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Base.lproj/Main.storyboard b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..bbb83ca --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Info.plist b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Info.plist new file mode 100644 index 0000000..2d8e057 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Info.plist @@ -0,0 +1,57 @@ + + + + + BGTaskSchedulerPermittedIdentifiers + + com.pravera.flutter_foreground_task.refresh + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_foreground_task_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Runner-Bridging-Header.h b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..d660cc1 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,2 @@ +#import "GeneratedPluginRegistrant.h" +#import diff --git a/local_packages/flutter_foreground_task-9.1.0/example/lib/main.dart b/local_packages/flutter_foreground_task-9.1.0/example/lib/main.dart new file mode 100644 index 0000000..74d03fc --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/lib/main.dart @@ -0,0 +1,304 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; + +void main() { + // Initialize port for communication between TaskHandler and UI. + FlutterForegroundTask.initCommunicationPort(); + runApp(const ExampleApp()); +} + +// The callback function should always be a top-level or static function. +@pragma('vm:entry-point') +void startCallback() { + FlutterForegroundTask.setTaskHandler(MyTaskHandler()); +} + +class MyTaskHandler extends TaskHandler { + static const String incrementCountCommand = 'incrementCount'; + + int _count = 0; + + void _incrementCount() { + _count++; + + // Update notification content. + FlutterForegroundTask.updateService( + notificationTitle: 'Hello MyTaskHandler :)', + notificationText: 'count: $_count', + ); + + // Send data to main isolate. + FlutterForegroundTask.sendDataToMain(_count); + } + + // Called when the task is started. + @override + Future onStart(DateTime timestamp, TaskStarter starter) async { + print('onStart(starter: ${starter.name})'); + _incrementCount(); + } + + // Called based on the eventAction set in ForegroundTaskOptions. + @override + void onRepeatEvent(DateTime timestamp) { + _incrementCount(); + } + + // Called when the task is destroyed. + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async { + print('onDestroy(isTimeout: $isTimeout)'); + } + + // Called when data is sent using `FlutterForegroundTask.sendDataToTask`. + @override + void onReceiveData(Object data) { + print('onReceiveData: $data'); + if (data == incrementCountCommand) { + _incrementCount(); + } + } + + // Called when the notification button is pressed. + @override + void onNotificationButtonPressed(String id) { + print('onNotificationButtonPressed: $id'); + } + + // Called when the notification itself is pressed. + @override + void onNotificationPressed() { + print('onNotificationPressed'); + } + + // Called when the notification itself is dismissed. + @override + void onNotificationDismissed() { + print('onNotificationDismissed'); + } +} + +class ExampleApp extends StatelessWidget { + const ExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + routes: { + '/': (context) => const ExamplePage(), + '/second': (context) => const SecondPage(), + }, + initialRoute: '/', + ); + } +} + +class ExamplePage extends StatefulWidget { + const ExamplePage({super.key}); + + @override + State createState() => _ExamplePageState(); +} + +class _ExamplePageState extends State { + final ValueNotifier _taskDataListenable = ValueNotifier(null); + + Future _requestPermissions() async { + // Android 13+, you need to allow notification permission to display foreground service notification. + // + // iOS: If you need notification, ask for permission. + final NotificationPermission notificationPermission = + await FlutterForegroundTask.checkNotificationPermission(); + if (notificationPermission != NotificationPermission.granted) { + await FlutterForegroundTask.requestNotificationPermission(); + } + + if (Platform.isAndroid) { + // Android 12+, there are restrictions on starting a foreground service. + // + // To restart the service on device reboot or unexpected problem, you need to allow below permission. + if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { + // This function requires `android.permission.REQUEST_IGNORE_BSCTERY_OPTIMIZATIONS` permission. + await FlutterForegroundTask.requestIgnoreBatteryOptimization(); + } + + // Use this utility only if you provide services that require long-term survival, + // such as exact alarm service, healthcare service, or Bluetooth communication. + // + // This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission. + // Using this permission may make app distribution difficult due to Google policy. + if (!await FlutterForegroundTask.canScheduleExactAlarms) { + // When you call this function, will be gone to the settings page. + // So you need to explain to the user why set it. + await FlutterForegroundTask.openAlarmsAndRemindersSettings(); + } + } + } + + void _initService() { + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + channelId: 'foreground_service', + channelName: 'Foreground Service Notification', + channelDescription: + 'This notification appears when the foreground service is running.', + onlyAlertOnce: true, + ), + iosNotificationOptions: const IOSNotificationOptions( + showNotification: false, + playSound: false, + ), + foregroundTaskOptions: ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(5000), + autoRunOnBoot: true, + autoRunOnMyPackageReplaced: true, + allowWakeLock: true, + allowWifiLock: true, + ), + ); + } + + Future _startService() async { + if (await FlutterForegroundTask.isRunningService) { + return FlutterForegroundTask.restartService(); + } else { + return FlutterForegroundTask.startService( + // You can manually specify the foregroundServiceType for the service + // to be started, as shown in the comment below. + // serviceTypes: [ + // ForegroundServiceTypes.dataSync, + // ForegroundServiceTypes.remoteMessaging, + // ], + serviceId: 256, + notificationTitle: 'Foreground Service is running', + notificationText: 'Tap to return to the app', + notificationIcon: null, + notificationButtons: [ + const NotificationButton(id: 'btn_hello', text: 'hello'), + ], + notificationInitialRoute: '/second', + callback: startCallback, + ); + } + } + + Future _stopService() { + return FlutterForegroundTask.stopService(); + } + + void _onReceiveTaskData(Object data) { + print('onReceiveTaskData: $data'); + _taskDataListenable.value = data; + } + + void _incrementCount() { + FlutterForegroundTask.sendDataToTask(MyTaskHandler.incrementCountCommand); + } + + @override + void initState() { + super.initState(); + // Add a callback to receive data sent from the TaskHandler. + FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData); + + WidgetsBinding.instance.addPostFrameCallback((_) { + // Request permissions and initialize the service. + _requestPermissions(); + _initService(); + }); + } + + @override + void dispose() { + // Remove a callback to receive data sent from the TaskHandler. + FlutterForegroundTask.removeTaskDataCallback(_onReceiveTaskData); + _taskDataListenable.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // ** optional ** + // A widget that minimize the app without closing it when the user presses + // the soft back button. It only works when the service is running. + // + // This widget must be declared above the [Scaffold] widget. + return WithForegroundTask( + child: Scaffold( + appBar: AppBar( + title: const Text('Flutter Foreground Task'), + centerTitle: true, + ), + body: SafeArea( + child: Column( + children: [ + Expanded(child: _buildCommunicationDataText()), + _buildServiceControlButtons(), + ], + ), + ), + ), + ); + } + + Widget _buildCommunicationDataText() { + return ValueListenableBuilder( + valueListenable: _taskDataListenable, + builder: (context, data, _) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('You received data from TaskHandler:'), + Text('$data', style: Theme.of(context).textTheme.headlineMedium), + ], + ), + ); + }, + ); + } + + Widget _buildServiceControlButtons() { + buttonBuilder(String text, {VoidCallback? onPressed}) { + return ElevatedButton( + onPressed: onPressed, + child: Text(text), + ); + } + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buttonBuilder('start service', onPressed: _startService), + buttonBuilder('stop service', onPressed: _stopService), + buttonBuilder('increment count', onPressed: _incrementCount), + ], + ), + ); + } +} + +class SecondPage extends StatelessWidget { + const SecondPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Second Page'), + centerTitle: true, + ), + body: Center( + child: ElevatedButton( + onPressed: Navigator.of(context).pop, + child: const Text('pop this page'), + ), + ), + ); + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/example/pubspec.yaml b/local_packages/flutter_foreground_task-9.1.0/example/pubspec.yaml new file mode 100644 index 0000000..1a13eec --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/example/pubspec.yaml @@ -0,0 +1,67 @@ +name: flutter_foreground_task_example +description: Demonstrates how to use the flutter_foreground_task plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ^3.4.0 + flutter: ">=3.22.0" + +dependencies: + flutter: + sdk: flutter + + flutter_foreground_task: + # When depending on this package from a real application you should use: + # flutter_foreground_task: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^5.0.0 + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskLifecycleListener.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskLifecycleListener.swift new file mode 100644 index 0000000..283e957 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskLifecycleListener.swift @@ -0,0 +1,35 @@ +// +// FlutterForegroundTaskLifecycleListener.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 7/15/24. +// + +import Foundation +import Flutter + +/** A listener that can listen to the task lifecycle events. */ +public protocol FlutterForegroundTaskLifecycleListener : AnyObject { + /** + * Each time a task starts, a new FlutterEngine is created. + * + * This is called before onTaskStart, Initialize the service you want to use in the task. (like PlatformChannel initialization) + */ + func onEngineCreate(flutterEngine: FlutterEngine?) + + /** Called when the task is started. */ + func onTaskStart(starter: FlutterForegroundTaskStarter) + + /** Called based on the eventAction set in ForegroundTaskOptions. */ + func onTaskRepeatEvent() + + /** Called when the task is destroyed. */ + func onTaskDestroy() + + /** + * If one task is finished or replaced by another, the FlutterEngine is destroyed. + * + * This is called after onTaskDestroy, where dispose the service that was initialized in onEngineCreate. + */ + func onEngineWillDestroy() +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskPlugin.h b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskPlugin.h new file mode 100644 index 0000000..4ce9adb --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface FlutterForegroundTaskPlugin : NSObject +@end diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskPlugin.m b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskPlugin.m new file mode 100644 index 0000000..f490a5f --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskPlugin.m @@ -0,0 +1,18 @@ +#import "FlutterForegroundTaskPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "flutter_foreground_task-Swift.h" +#endif + +@implementation FlutterForegroundTaskPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftFlutterForegroundTaskPlugin registerWithRegistrar:registrar]; +} ++ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { + [SwiftFlutterForegroundTaskPlugin setPluginRegistrantCallback:callback]; +} +@end diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskStarter.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskStarter.swift new file mode 100644 index 0000000..350047e --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/FlutterForegroundTaskStarter.swift @@ -0,0 +1,13 @@ +// +// FlutterForegroundTaskStarter.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/23/24. +// + +import Foundation + +public enum FlutterForegroundTaskStarter : Int { + case DEVELOPER = 0 + case SYSTEM = 1 +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/PreferencesKey.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/PreferencesKey.swift new file mode 100644 index 0000000..c4bed31 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/PreferencesKey.swift @@ -0,0 +1,26 @@ +// +// BackgroundServicePrefsKey.swift +// flutter_foreground_task +// +// Created by WOO JIN HWANG on 2021/08/11. +// + +// service status +let BACKGROUND_SERVICE_ACTION = "backgroundServiceAction" + +// notification options +let SHOW_NOTIFICATION = "showNotification" +let PLAY_SOUND = "playSound" + +// notification content +let NOTIFICATION_CONTENT_TITLE = "notificationContentTitle" +let NOTIFICATION_CONTENT_TEXT = "notificationContentText" +let NOTIFICATION_CONTENT_BUTTONS = "buttons" + +// task options +let TASK_EVENT_ACTION = "taskEventAction" // new +let INTERVAL = "interval" // deprecated +let IS_ONCE_EVENT = "isOnceEvent" // deprecated + +// task data +let CALLBACK_HANDLE = "callbackHandle" diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift new file mode 100644 index 0000000..51900c0 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/SwiftFlutterForegroundTaskPlugin.swift @@ -0,0 +1,191 @@ +import Flutter +import UIKit +import BackgroundTasks + +public class SwiftFlutterForegroundTaskPlugin: NSObject, FlutterPlugin { + // ====================== Plugin ====================== + static private(set) var registerPlugins: FlutterPluginRegistrantCallback? = nil + + private var notificationPermissionManager: NotificationPermissionManager? = nil + private var backgroundServiceManager: BackgroundServiceManager? = nil + + private var foregroundChannel: FlutterMethodChannel? = nil + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = SwiftFlutterForegroundTaskPlugin() + instance.initServices() + instance.initChannels(registrar.messenger()) + registrar.addApplicationDelegate(instance) + } + + public static func setPluginRegistrantCallback(_ callback: @escaping FlutterPluginRegistrantCallback) { + registerPlugins = callback + } + + public static func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { + BackgroundService.sharedInstance.addTaskLifecycleListener(listener) + } + + public static func removeTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { + BackgroundService.sharedInstance.removeTaskLifecycleListener(listener) + } + + private func initServices() { + notificationPermissionManager = NotificationPermissionManager() + backgroundServiceManager = BackgroundServiceManager() + } + + private func initChannels(_ messenger: FlutterBinaryMessenger) { + foregroundChannel = FlutterMethodChannel(name: "flutter_foreground_task/methods", binaryMessenger: messenger) + foregroundChannel?.setMethodCallHandler(onMethodCall) + } + + private func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments + do { + switch call.method { + case "checkNotificationPermission": + notificationPermissionManager!.checkPermission { permission in + result(permission.rawValue) + } + case "requestNotificationPermission": + notificationPermissionManager!.requestPermission { permission in + result(permission.rawValue) + } + case "startService": + try backgroundServiceManager!.start(arguments: args) + result(true) + case "restartService": + try backgroundServiceManager!.restart(arguments: args) + result(true) + case "updateService": + try backgroundServiceManager!.update(arguments: args) + result(true) + case "stopService": + try backgroundServiceManager!.stop() + result(true) + case "sendData": + backgroundServiceManager!.sendData(data: args) + case "isRunningService": + result(backgroundServiceManager!.isRunningService()) + case "minimizeApp": + UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil) + case "isAppOnForeground": + result(UIApplication.shared.applicationState == .active) + default: + result(FlutterMethodNotImplemented) + } + } catch { + let code = String(describing: error.self) + let message = error.localizedDescription + let flutterError = FlutterError(code: code, message: message, details: nil) + result(flutterError) + } + } + + // ================== App Lifecycle =================== + public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable : Any] = [:]) -> Bool { + UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) + if #available(iOS 13.0, *) { + SwiftFlutterForegroundTaskPlugin.registerAppRefresh() + } + return true + } + + public func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool { + completionHandler(.newData) + return true + } + + public func applicationDidEnterBackground(_ application: UIApplication) { + if #available(iOS 13.0, *) { + SwiftFlutterForegroundTaskPlugin.scheduleAppRefresh() + } + } + + public func applicationWillTerminate(_ application: UIApplication) { + if !BackgroundService.sharedInstance.isRunningService { + return + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.APP_TERMINATE) + BackgroundService.sharedInstance.run() + + // Chance to handle onDestroy before app terminates + sleep(5) + } + + // ================= Service Delegate ================= + @available(iOS 10.0, *) + public func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + BackgroundService.sharedInstance.userNotificationCenter(center, response, completionHandler) + } + + @available(iOS 10.0, *) + public func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + BackgroundService.sharedInstance.userNotificationCenter(center, notification, completionHandler) + } + + // ============== Background App Refresh ============== + public static var refreshIdentifier: String = "com.pravera.flutter_foreground_task.refresh" + + @available(iOS 13.0, *) + private static func registerAppRefresh() { + BGTaskScheduler.shared.register(forTaskWithIdentifier: refreshIdentifier, using: nil) { task in + handleAppRefresh(task: task as! BGAppRefreshTask) + } + } + + @available(iOS 13.0, *) + private static func scheduleAppRefresh() { + let request = BGAppRefreshTaskRequest(identifier: refreshIdentifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + print("Could not schedule app refresh: \(error)") + } + } + + @available(iOS 13.0, *) + private static func cancelAppRefresh() { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshIdentifier) + } + + @available(iOS 13.0, *) + private static func handleAppRefresh(task: BGAppRefreshTask) { + let queue = OperationQueue() + let operation = AppRefreshOperation() + + task.expirationHandler = { + operation.cancel() + } + + operation.completionBlock = { + // Schedule a new refresh task + scheduleAppRefresh() + + task.setTaskCompleted(success: true) + } + + queue.addOperation(operation) + } +} + +class AppRefreshOperation: Operation { + override func main() { + let semaphore = DispatchSemaphore(value: 0) + + // avoid non-platform thread + DispatchQueue.main.asyncAfter(deadline: .now() + 25) { + semaphore.signal() + } + + semaphore.wait() + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/errors/ServiceError.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/errors/ServiceError.swift new file mode 100644 index 0000000..e3633fe --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/errors/ServiceError.swift @@ -0,0 +1,30 @@ +// +// ServiceError.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 7/17/24. +// + +import Foundation + +enum ServiceError { + case ServiceArgumentNullException + case ServiceAlreadyStartedException + case ServiceNotStartedException + case ServiceNotSupportedException +} + +extension ServiceError : LocalizedError { + public var errorDescription: String? { + switch self { + case .ServiceArgumentNullException: + return NSLocalizedString("The required argument was not passed to the service.", comment: "ServiceArgumentNullException") + case .ServiceAlreadyStartedException: + return NSLocalizedString("The service has already started.", comment: "ServiceAlreadyStartedException") + case .ServiceNotStartedException: + return NSLocalizedString("The service is not started.", comment: "ServiceNotStartedException") + case .ServiceNotSupportedException: + return NSLocalizedString("The current iOS version does not support the service.", comment: "ServiceNotSupportedException") + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/BackgroundServiceAction.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/BackgroundServiceAction.swift new file mode 100644 index 0000000..0db333c --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/BackgroundServiceAction.swift @@ -0,0 +1,17 @@ +// +// BackgroundServiceAction.swift +// flutter_foreground_task +// +// Created by WOO JIN HWANG on 2021/08/11. +// + +import Foundation + +enum BackgroundServiceAction: String { + case API_START + case API_RESTART + case API_UPDATE + case API_STOP + + case APP_TERMINATE +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/BackgroundServiceStatus.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/BackgroundServiceStatus.swift new file mode 100644 index 0000000..227ce74 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/BackgroundServiceStatus.swift @@ -0,0 +1,25 @@ +// +// BackgroundServiceStatus.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/13/24. +// + +import Foundation + +struct BackgroundServiceStatus { + let action: BackgroundServiceAction + + static func getData() -> BackgroundServiceStatus { + let prefs = UserDefaults.standard + let actionValue = prefs.string(forKey: BACKGROUND_SERVICE_ACTION) ?? BackgroundServiceAction.API_STOP.rawValue + let action = BackgroundServiceAction(rawValue: actionValue)! + + return BackgroundServiceStatus(action: action) + } + + static func setData(action: BackgroundServiceAction) { + let prefs = UserDefaults.standard + prefs.set(action.rawValue, forKey: BACKGROUND_SERVICE_ACTION) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskData.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskData.swift new file mode 100644 index 0000000..276e587 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskData.swift @@ -0,0 +1,42 @@ +// +// ForegroundTaskData.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/24/24. +// + +import Foundation + +struct ForegroundTaskData { + let callbackHandle: Int64? + + static func getData() -> ForegroundTaskData { + let prefs = UserDefaults.standard + + let callbackHandle = prefs.object(forKey: CALLBACK_HANDLE) as? Int64 + + return ForegroundTaskData(callbackHandle: callbackHandle) + } + + static func setData(args: Dictionary) { + let prefs = UserDefaults.standard + + prefs.removeObject(forKey: CALLBACK_HANDLE) + if let callbackHandle = args[CALLBACK_HANDLE] as? Int64 { + prefs.set(callbackHandle, forKey: CALLBACK_HANDLE) + } + } + + static func updateData(args: Dictionary) { + let prefs = UserDefaults.standard + + if let callbackHandle = args[CALLBACK_HANDLE] as? Int64 { + prefs.set(callbackHandle, forKey: CALLBACK_HANDLE) + } + } + + static func clearData() { + let prefs = UserDefaults.standard + prefs.removeObject(forKey: CALLBACK_HANDLE) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskEventAction.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskEventAction.swift new file mode 100644 index 0000000..209f486 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskEventAction.swift @@ -0,0 +1,39 @@ +// +// ForegroundTaskEventAction.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/28/24. +// + +import Foundation + +private let TASK_EVENT_TYPE_KEY = "taskEventType" +private let TASK_EVENT_INTERVAL_KEY = "taskEventInterval" + +struct ForegroundTaskEventAction: Equatable { + let type: ForegroundTaskEventType + let interval: Int + + static func fromJsonString(_ jsonString: String) -> ForegroundTaskEventAction { + var type: ForegroundTaskEventType = .NOTHING + var interval: Int = 5000 + + if let jsonData = jsonString.data(using: .utf8) { + if let jsonObj = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? Dictionary { + if let _type = jsonObj[TASK_EVENT_TYPE_KEY] as? Int { + type = ForegroundTaskEventType.fromValue(_type) + } + + if let _interval = jsonObj[TASK_EVENT_INTERVAL_KEY] as? Int { + interval = _interval + } + } + } + + return ForegroundTaskEventAction(type: type, interval: interval) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.type.rawValue == rhs.type.rawValue && lhs.interval == rhs.interval + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskEventType.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskEventType.swift new file mode 100644 index 0000000..bbc2f2b --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskEventType.swift @@ -0,0 +1,18 @@ +// +// ForegroundTaskEventType.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/28/24. +// + +import Foundation + +enum ForegroundTaskEventType: Int { + case NOTHING = 1 + case ONCE = 2 + case REPEAT = 3 + + static func fromValue(_ value: Int) -> ForegroundTaskEventType { + return ForegroundTaskEventType(rawValue: value) ?? .NOTHING + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskOptions.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskOptions.swift new file mode 100644 index 0000000..7215978 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/ForegroundTaskOptions.swift @@ -0,0 +1,64 @@ +// +// ForegroundTaskOptions.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/24/24. +// + +import Foundation + +struct ForegroundTaskOptions { + let eventAction: ForegroundTaskEventAction + + static func getData() -> ForegroundTaskOptions { + let prefs = UserDefaults.standard + + let eventActionJsonString = prefs.string(forKey: TASK_EVENT_ACTION) + let eventAction: ForegroundTaskEventAction + if eventActionJsonString != nil { + eventAction = ForegroundTaskEventAction.fromJsonString(eventActionJsonString!) + } else { + // for deprecated api + let oldIsOnceEvent = prefs.bool(forKey: IS_ONCE_EVENT) + let oldInterval = prefs.integer(forKey: INTERVAL) + if oldIsOnceEvent { + eventAction = ForegroundTaskEventAction(type: .ONCE, interval: oldInterval) + } else { + eventAction = ForegroundTaskEventAction(type: .REPEAT, interval: oldInterval) + } + } + + return ForegroundTaskOptions(eventAction: eventAction) + } + + static func setData(args: Dictionary) { + let prefs = UserDefaults.standard + + if let eventActionJson = args[TASK_EVENT_ACTION] as? Dictionary { + if let eventActionJsonData = try? JSONSerialization.data(withJSONObject: eventActionJson, options: []) { + if let eventActionJsonString = String(data: eventActionJsonData, encoding: .utf8) { + prefs.set(eventActionJsonString, forKey: TASK_EVENT_ACTION) + } + } + } + } + + static func updateData(args: Dictionary) { + let prefs = UserDefaults.standard + + if let eventActionJson = args[TASK_EVENT_ACTION] as? Dictionary { + if let eventActionJsonData = try? JSONSerialization.data(withJSONObject: eventActionJson, options: []) { + if let eventActionJsonString = String(data: eventActionJsonData, encoding: .utf8) { + prefs.set(eventActionJsonString, forKey: TASK_EVENT_ACTION) + } + } + } + } + + static func clearData() { + let prefs = UserDefaults.standard + prefs.removeObject(forKey: TASK_EVENT_ACTION) // new + prefs.removeObject(forKey: INTERVAL) // deprecated + prefs.removeObject(forKey: IS_ONCE_EVENT) // deprecated + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationButton.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationButton.swift new file mode 100644 index 0000000..c5f1e8b --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationButton.swift @@ -0,0 +1,33 @@ +// +// NotificationButton.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/5/24. +// + +import Foundation + +private let ID_KEY = "id" +private let TEXT_KEY = "text" + +struct NotificationButton { + let id: String + let text: String + + static func fromJSONObject(_ jsonObj: Any) -> NotificationButton { + var id: String = "unknown" + var text: String = "" + + if let _jsonObj = jsonObj as? Dictionary { + if let _id = _jsonObj[ID_KEY] as? String { + id = _id + } + + if let _text = _jsonObj[TEXT_KEY] as? String { + text = _text + } + } + + return NotificationButton(id: id, text: text) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationContent.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationContent.swift new file mode 100644 index 0000000..e47a0c8 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationContent.swift @@ -0,0 +1,79 @@ +// +// NotificationContent.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/2/24. +// + +import Foundation + +struct NotificationContent { + let title: String + let text: String + let buttons: Array + + static func getData() -> NotificationContent { + let prefs = UserDefaults.standard + + let title = prefs.string(forKey: NOTIFICATION_CONTENT_TITLE) ?? "" + let text = prefs.string(forKey: NOTIFICATION_CONTENT_TEXT) ?? "" + + var buttons: [NotificationButton] = [] + if let buttonsJsonString = prefs.string(forKey: NOTIFICATION_CONTENT_BUTTONS) { + if let buttonsJsonData = buttonsJsonString.data(using: .utf8) { + if let buttonsJsonArr = try? JSONSerialization.jsonObject(with: buttonsJsonData, options: []) as? [Any] { + for buttonJsonObj in buttonsJsonArr { + buttons.append(NotificationButton.fromJSONObject(buttonJsonObj)) + } + } + } + } + + return NotificationContent(title: title, text: text, buttons: buttons) + } + + static func setData(args: Dictionary) { + let prefs = UserDefaults.standard + + let title = args[NOTIFICATION_CONTENT_TITLE] as? String ?? "" + prefs.set(title, forKey: NOTIFICATION_CONTENT_TITLE) + + let text = args[NOTIFICATION_CONTENT_TEXT] as? String ?? "" + prefs.set(text, forKey: NOTIFICATION_CONTENT_TEXT) + + if let buttonsJson = args[NOTIFICATION_CONTENT_BUTTONS] as? [Any] { + if let buttonsJsonData = try? JSONSerialization.data(withJSONObject: buttonsJson, options: []) { + if let buttonsJsonString = String(data: buttonsJsonData, encoding: .utf8) { + prefs.set(buttonsJsonString, forKey: NOTIFICATION_CONTENT_BUTTONS) + } + } + } + } + + static func updateData(args: Dictionary) { + let prefs = UserDefaults.standard + + if let title = args[NOTIFICATION_CONTENT_TITLE] as? String { + prefs.set(title, forKey: NOTIFICATION_CONTENT_TITLE) + } + + if let text = args[NOTIFICATION_CONTENT_TEXT] as? String { + prefs.set(text, forKey: NOTIFICATION_CONTENT_TEXT) + } + + if let buttonsJson = args[NOTIFICATION_CONTENT_BUTTONS] as? [Any] { + if let buttonsJsonData = try? JSONSerialization.data(withJSONObject: buttonsJson, options: []) { + if let buttonsJsonString = String(data: buttonsJsonData, encoding: .utf8) { + prefs.set(buttonsJsonString, forKey: NOTIFICATION_CONTENT_BUTTONS) + } + } + } + } + + static func clearData() { + let prefs = UserDefaults.standard + prefs.removeObject(forKey: NOTIFICATION_CONTENT_TITLE) + prefs.removeObject(forKey: NOTIFICATION_CONTENT_TEXT) + prefs.removeObject(forKey: NOTIFICATION_CONTENT_BUTTONS) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationOptions.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationOptions.swift new file mode 100644 index 0000000..0880b20 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationOptions.swift @@ -0,0 +1,38 @@ +// +// NotificationOptions.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/2/24. +// + +import Foundation + +struct NotificationOptions { + let showNotification: Bool + let playSound: Bool + + static func getData() -> NotificationOptions { + let prefs = UserDefaults.standard + + let showNotification = prefs.bool(forKey: SHOW_NOTIFICATION) + let playSound = prefs.bool(forKey: PLAY_SOUND) + + return NotificationOptions(showNotification: showNotification, playSound: playSound) + } + + static func setData(args: Dictionary) { + let prefs = UserDefaults.standard + + let showNotification = args[SHOW_NOTIFICATION] as? Bool ?? false + let playSound = args[PLAY_SOUND] as? Bool ?? false + + prefs.set(showNotification, forKey: SHOW_NOTIFICATION) + prefs.set(playSound, forKey: PLAY_SOUND) + } + + static func clearData() { + let prefs = UserDefaults.standard + prefs.removeObject(forKey: SHOW_NOTIFICATION) + prefs.removeObject(forKey: PLAY_SOUND) + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationPermission.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationPermission.swift new file mode 100644 index 0000000..ce7777a --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/models/NotificationPermission.swift @@ -0,0 +1,13 @@ +// +// NotificationPermission.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/6/24. +// + +import Foundation + +enum NotificationPermission : Int { + case GRANTED = 0 + case DENIED = 1 +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/BackgroundService.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/BackgroundService.swift new file mode 100644 index 0000000..89bc061 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/BackgroundService.swift @@ -0,0 +1,206 @@ +// +// BackgroundService.swift +// flutter_foreground_task +// +// Created by WOO JIN HWANG on 2021/08/11. +// + +import Flutter +import Foundation +import UserNotifications + +private let NOTIFICATION_ID = "flutter_foreground_task/notification" +private let NOTIFICATION_CATEGORY_ID = "flutter_foreground_task/notification_category" + +private let ACTION_RECEIVE_DATA = "onReceiveData" +private let ACTION_NOTIFICATION_BUTTON_PRESSED = "onNotificationButtonPressed" +private let ACTION_NOTIFICATION_PRESSED = "onNotificationPressed" +private let ACTION_NOTIFICATION_DISMISSED = "onNotificationDismissed" + +@available(iOS 10.0, *) +class BackgroundService: NSObject { + static let sharedInstance = BackgroundService() + + private(set) var isRunningService: Bool = false + + private var foregroundTask: ForegroundTask? = nil + private var taskLifecycleListeners = ForegroundTaskLifecycleListeners() + + func sendData(data: Any?) { + if isRunningService { + foregroundTask?.invokeMethod(ACTION_RECEIVE_DATA, arguments: data) + } + } + + func addTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { + taskLifecycleListeners.addListener(listener) + } + + func removeTaskLifecycleListener(_ listener: FlutterForegroundTaskLifecycleListener) { + taskLifecycleListeners.removeListener(listener) + } + + private let notificationCenter: UNUserNotificationCenter + private let notificationPermissionManager: NotificationPermissionManager + private var canReceiveNotificationResponse: Bool = false + + private var backgroundServiceStatus: BackgroundServiceStatus + private var notificationOptions: NotificationOptions + private var notificationContent: NotificationContent + private var prevForegroundTaskOptions: ForegroundTaskOptions? + private var currForegroundTaskOptions: ForegroundTaskOptions + private var prevForegroundTaskData: ForegroundTaskData? + private var currForegroundTaskData: ForegroundTaskData + + override init() { + notificationCenter = UNUserNotificationCenter.current() + notificationPermissionManager = NotificationPermissionManager() + backgroundServiceStatus = BackgroundServiceStatus.getData() + notificationOptions = NotificationOptions.getData() + notificationContent = NotificationContent.getData() + currForegroundTaskOptions = ForegroundTaskOptions.getData() + currForegroundTaskData = ForegroundTaskData.getData() + super.init() + } + + func run() { + backgroundServiceStatus = BackgroundServiceStatus.getData() + notificationOptions = NotificationOptions.getData() + notificationContent = NotificationContent.getData() + prevForegroundTaskOptions = currForegroundTaskOptions + currForegroundTaskOptions = ForegroundTaskOptions.getData() + prevForegroundTaskData = currForegroundTaskData + currForegroundTaskData = ForegroundTaskData.getData() + + switch backgroundServiceStatus.action { + case .API_START, .API_RESTART: + requestNotification() + createForegroundTask() + isRunningService = true + break + case .API_UPDATE: + requestNotification() + let prevCallbackHandle = prevForegroundTaskData?.callbackHandle + let currCallbackHandle = currForegroundTaskData.callbackHandle + if prevCallbackHandle != currCallbackHandle { + createForegroundTask() + } else { + let prevEventAction = prevForegroundTaskOptions?.eventAction + let currEventAction = currForegroundTaskOptions.eventAction + if prevEventAction != currEventAction { + updateForegroundTask() + } + } + break + case .API_STOP, .APP_TERMINATE: + destroyForegroundTask() + removeAllNotification() + isRunningService = false + break + } + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + _ response: UNNotificationResponse, + _ completionHandler: @escaping () -> Void) { + // If it is not a notification requested by this plugin, the processing below is ignored. + if response.notification.request.identifier != NOTIFICATION_ID { return } + + // Prevents duplicate processing due to the `registrar.addApplicationDelegate`. + if !canReceiveNotificationResponse { return } + canReceiveNotificationResponse = false + + let actionId = response.actionIdentifier + if notificationContent.buttons.contains(where: { $0.id == actionId }) { + foregroundTask?.invokeMethod(ACTION_NOTIFICATION_BUTTON_PRESSED, arguments: actionId) + } else if actionId == UNNotificationDefaultActionIdentifier { + foregroundTask?.invokeMethod(ACTION_NOTIFICATION_PRESSED, arguments: nil) + } else if actionId == UNNotificationDismissActionIdentifier { + foregroundTask?.invokeMethod(ACTION_NOTIFICATION_DISMISSED, arguments: nil) + } + + completionHandler() + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + _ notification: UNNotification, + _ completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + // If it is not a notification requested by this plugin, the processing below is ignored. + if notification.request.identifier != NOTIFICATION_ID { return } + + if notificationOptions.playSound { + completionHandler([.alert, .sound]) + } else { + completionHandler([.alert]) + } + + // Prevents duplicate processing due to the `registrar.addApplicationDelegate`. + canReceiveNotificationResponse = true + } + + private func setNotificationActions() { + var actions: [UNNotificationAction] = [] + for button in notificationContent.buttons { + let action = UNNotificationAction(identifier: button.id, title: button.text) + actions.append(action) + } + + let category = UNNotificationCategory( + identifier: NOTIFICATION_CATEGORY_ID, + actions: actions, + intentIdentifiers: [], + options: .customDismissAction + ) + + notificationCenter.setNotificationCategories([category]) + } + + private func requestNotification() { + if !notificationOptions.showNotification { + return + } + + notificationPermissionManager.checkPermission { permission in + if permission == NotificationPermission.DENIED { + return + } + + let content = UNMutableNotificationContent() + content.title = self.notificationContent.title + content.body = self.notificationContent.text + content.categoryIdentifier = NOTIFICATION_CATEGORY_ID + if self.notificationOptions.playSound { + content.sound = .default + } + self.setNotificationActions() + + let request = UNNotificationRequest(identifier: NOTIFICATION_ID, content: content, trigger: nil) + self.notificationCenter.add(request, withCompletionHandler: nil) + } + } + + private func removeAllNotification() { + notificationCenter.removePendingNotificationRequests(withIdentifiers: [NOTIFICATION_ID]) + notificationCenter.removeDeliveredNotifications(withIdentifiers: [NOTIFICATION_ID]) + } + + private func createForegroundTask() { + destroyForegroundTask() + + foregroundTask = ForegroundTask( + serviceStatus: backgroundServiceStatus, + taskData: currForegroundTaskData, + taskEventAction: currForegroundTaskOptions.eventAction, + taskLifecycleListener: taskLifecycleListeners + ) + } + + private func updateForegroundTask() { + foregroundTask?.update(taskEventAction: currForegroundTaskOptions.eventAction) + } + + private func destroyForegroundTask() { + foregroundTask?.destroy() + foregroundTask = nil + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/BackgroundServiceManager.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/BackgroundServiceManager.swift new file mode 100644 index 0000000..7d3a294 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/BackgroundServiceManager.swift @@ -0,0 +1,96 @@ +// +// BackgroundServiceManager.swift +// flutter_foreground_task +// +// Created by WOO JIN HWANG on 2021/08/10. +// + +import Flutter +import Foundation + +class BackgroundServiceManager: NSObject { + func start(arguments: Any?) throws { + if #available(iOS 10.0, *) { + if BackgroundService.sharedInstance.isRunningService { + throw ServiceError.ServiceAlreadyStartedException + } + + guard let args = arguments as? Dictionary else { + throw ServiceError.ServiceArgumentNullException + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_START) + NotificationOptions.setData(args: args) + NotificationContent.setData(args: args) + ForegroundTaskOptions.setData(args: args) + ForegroundTaskData.setData(args: args) + BackgroundService.sharedInstance.run() + } else { + throw ServiceError.ServiceNotSupportedException + } + } + + func restart(arguments: Any?) throws { + if #available(iOS 10.0, *) { + if !BackgroundService.sharedInstance.isRunningService { + throw ServiceError.ServiceNotStartedException + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_RESTART) + BackgroundService.sharedInstance.run() + } else { + throw ServiceError.ServiceNotSupportedException + } + } + + func update(arguments: Any?) throws { + if #available(iOS 10.0, *) { + if !BackgroundService.sharedInstance.isRunningService { + throw ServiceError.ServiceNotStartedException + } + + guard let args = arguments as? Dictionary else { + throw ServiceError.ServiceArgumentNullException + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_UPDATE) + NotificationContent.updateData(args: args) + ForegroundTaskOptions.updateData(args: args) + ForegroundTaskData.updateData(args: args) + BackgroundService.sharedInstance.run() + } else { + throw ServiceError.ServiceNotSupportedException + } + } + + func stop() throws { + if #available(iOS 10.0, *) { + if !BackgroundService.sharedInstance.isRunningService { + throw ServiceError.ServiceNotStartedException + } + + BackgroundServiceStatus.setData(action: BackgroundServiceAction.API_STOP) + NotificationOptions.clearData() + NotificationContent.clearData() + ForegroundTaskOptions.clearData() + ForegroundTaskData.clearData() + BackgroundService.sharedInstance.run() + } else { + throw ServiceError.ServiceNotSupportedException + } + } + + func sendData(data: Any?) { + if data != nil { + BackgroundService.sharedInstance.sendData(data: data) + } + } + + func isRunningService() -> Bool { + if #available(iOS 10.0, *) { + return BackgroundService.sharedInstance.isRunningService + } else { + return false + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/ForegroundTask.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/ForegroundTask.swift new file mode 100644 index 0000000..94633cf --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/ForegroundTask.swift @@ -0,0 +1,192 @@ +// +// ForegroundTask.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/24/24. +// + +import Foundation + +private let BG_ISOLATE_NAME = "flutter_foreground_task/backgroundIsolate" +private let BG_CHANNEL_NAME = "flutter_foreground_task/background" + +private let ACTION_TASK_START = "onStart" +private let ACTION_TASK_REPEAT_EVENT = "onRepeatEvent" +private let ACTION_TASK_DESTROY = "onDestroy" + +class ForegroundTask { + private let serviceStatus: BackgroundServiceStatus + private let taskData: ForegroundTaskData + private var taskEventAction: ForegroundTaskEventAction + private let taskLifecycleListener: FlutterForegroundTaskLifecycleListener + + private var flutterEngine: FlutterEngine? = nil + private var backgroundChannel: FlutterMethodChannel? = nil + private var repeatTask: Timer? = nil + private var isDestroyed: Bool = false + + init( + serviceStatus: BackgroundServiceStatus, + taskData: ForegroundTaskData, + taskEventAction: ForegroundTaskEventAction, + taskLifecycleListener: FlutterForegroundTaskLifecycleListener + ) { + self.serviceStatus = serviceStatus + self.taskData = taskData + self.taskEventAction = taskEventAction + self.taskLifecycleListener = taskLifecycleListener + initialize() + } + + private func initialize() { + guard let registerPlugins = SwiftFlutterForegroundTaskPlugin.registerPlugins else { + print("Please register the registerPlugins function using the SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback.") + return + } + + guard let callbackHandle = taskData.callbackHandle else { + // no callback -> Unlike Android, the flutter engine does not start. + return + } + + // lookup callback + let callbackInfo = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) + guard let entrypoint = callbackInfo?.callbackName else { + print("Entrypoint not found in callback information.") + return + } + guard let libraryURI = callbackInfo?.callbackLibraryPath else { + print("LibraryURI not found in callback information.") + return + } + + // create flutter engine & execute callback + let flutterEngine = FlutterEngine(name: BG_ISOLATE_NAME, project: nil, allowHeadlessExecution: true) + let isRunningEngine = flutterEngine.run(withEntrypoint: entrypoint, libraryURI: libraryURI) + + if isRunningEngine { + // register plugins + registerPlugins(flutterEngine) + taskLifecycleListener.onEngineCreate(flutterEngine: flutterEngine) + + // create background channel + let messenger = flutterEngine.binaryMessenger + let backgroundChannel = FlutterMethodChannel(name: BG_CHANNEL_NAME, binaryMessenger: messenger) + backgroundChannel.setMethodCallHandler(onMethodCall) + + self.flutterEngine = flutterEngine + self.backgroundChannel = backgroundChannel + } + } + + func onMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "start": + start() + default: + result(FlutterMethodNotImplemented) + } + } + + private func start() { + runIfNotDestroyed { + runIfCallbackHandleExists { + let serviceAction = serviceStatus.action + let starter: FlutterForegroundTaskStarter + if serviceAction == .API_START || serviceAction == .API_RESTART || serviceAction == .API_UPDATE { + starter = .DEVELOPER + } else { + starter = .SYSTEM + } + + backgroundChannel?.invokeMethod(ACTION_TASK_START, arguments: starter.rawValue) { _ in + self.runIfNotDestroyed { + self.startRepeatTask() + } + } + taskLifecycleListener.onTaskStart(starter: starter) + } + } + } + + private func invokeTaskRepeatEvent() { + backgroundChannel?.invokeMethod(ACTION_TASK_REPEAT_EVENT, arguments: nil) + taskLifecycleListener.onTaskRepeatEvent() + } + + private func startRepeatTask() { + stopRepeatTask() + + let type = taskEventAction.type + let interval = TimeInterval(Double(taskEventAction.interval) / 1000) + + if type == .NOTHING { + return + } + + if type == .ONCE { + invokeTaskRepeatEvent() + return + } + + repeatTask = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in + self.invokeTaskRepeatEvent() + } + } + + private func stopRepeatTask() { + repeatTask?.invalidate() + repeatTask = nil + } + + func invokeMethod(_ method: String, arguments: Any?) { + runIfNotDestroyed { + backgroundChannel?.invokeMethod(method, arguments: arguments) + } + } + + func update(taskEventAction: ForegroundTaskEventAction) { + runIfNotDestroyed { + runIfCallbackHandleExists { + self.taskEventAction = taskEventAction + startRepeatTask() + } + } + } + + func destroy() { + runIfNotDestroyed { + stopRepeatTask() + + backgroundChannel?.setMethodCallHandler(nil) + if taskData.callbackHandle == nil { + taskLifecycleListener.onEngineWillDestroy() + flutterEngine?.destroyContext() + flutterEngine = nil + } else { + backgroundChannel?.invokeMethod(ACTION_TASK_DESTROY, arguments: nil) { _ in + self.flutterEngine?.destroyContext() + self.flutterEngine = nil + } + taskLifecycleListener.onTaskDestroy() + taskLifecycleListener.onEngineWillDestroy() + } + + isDestroyed = true + } + } + + private func runIfCallbackHandleExists(call: () -> Void) { + if taskData.callbackHandle == nil { + return + } + call() + } + + private func runIfNotDestroyed(call: () -> Void) { + if isDestroyed { + return + } + call() + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/ForegroundTaskLifecycleListeners.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/ForegroundTaskLifecycleListeners.swift new file mode 100644 index 0000000..dd880be --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/ForegroundTaskLifecycleListeners.swift @@ -0,0 +1,54 @@ +// +// ForegroundTaskLifecycleListeners.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 9/24/24. +// + +import Foundation + +class ForegroundTaskLifecycleListeners : FlutterForegroundTaskLifecycleListener { + private var listeners: Array = [] + + func addListener(_ listener: FlutterForegroundTaskLifecycleListener) { + if listeners.contains(where: { $0 === listener }) == false { + listeners.append(listener) + } + } + + func removeListener(_ listener: FlutterForegroundTaskLifecycleListener) { + if let index = listeners.firstIndex(where: { $0 === listener }) { + listeners.remove(at: index) + } + } + + func onEngineCreate(flutterEngine: FlutterEngine?) { + for listener in listeners { + listener.onEngineCreate(flutterEngine: flutterEngine) + } + } + + func onTaskStart(starter: FlutterForegroundTaskStarter) { + for listener in listeners { + listener.onTaskStart(starter: starter) + } + } + + func onTaskRepeatEvent() { + for listener in listeners { + listener.onTaskRepeatEvent() + } + } + + func onTaskDestroy() { + for listener in listeners { + listener.onTaskDestroy() + } + } + + func onEngineWillDestroy() { + for listener in listeners { + listener.onEngineWillDestroy() + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/NotificationPermissionManager.swift b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/NotificationPermissionManager.swift new file mode 100644 index 0000000..e2857ab --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/Classes/service/NotificationPermissionManager.swift @@ -0,0 +1,38 @@ +// +// NotificationPermissionManager.swift +// flutter_foreground_task +// +// Created by Woo Jin Hwang on 8/6/24. +// + +import Foundation + +class NotificationPermissionManager { + func checkPermission(completion: @escaping (NotificationPermission) -> Void) { + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .authorized: + completion(NotificationPermission.GRANTED) + case .denied, .ephemeral, .notDetermined, .provisional: + completion(NotificationPermission.DENIED) + @unknown default: + completion(NotificationPermission.DENIED) + } + } + } + + func requestPermission(completion: @escaping (NotificationPermission) -> Void) { + let options = UNAuthorizationOptions(arrayLiteral: .alert, .sound) + UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in + if error != nil { + completion(NotificationPermission.DENIED) + } else { + if (granted) { + completion(NotificationPermission.GRANTED) + } else { + completion(NotificationPermission.DENIED) + } + } + } + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/ios/flutter_foreground_task.podspec b/local_packages/flutter_foreground_task-9.1.0/ios/flutter_foreground_task.podspec new file mode 100644 index 0000000..e918410 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/ios/flutter_foreground_task.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_foreground_task.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_foreground_task' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin.' + s.description = <<-DESC +A new Flutter plugin. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '8.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_already_started_exception.dart b/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_already_started_exception.dart new file mode 100644 index 0000000..f969129 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_already_started_exception.dart @@ -0,0 +1,9 @@ +class ServiceAlreadyStartedException implements Exception { + ServiceAlreadyStartedException( + [this.message = 'The service has already started.']); + + final String message; + + @override + String toString() => 'ServiceAlreadyStartedException: $message'; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_not_initialized_exception.dart b/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_not_initialized_exception.dart new file mode 100644 index 0000000..0e822cc --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_not_initialized_exception.dart @@ -0,0 +1,10 @@ +class ServiceNotInitializedException implements Exception { + ServiceNotInitializedException( + [this.message = + 'Not initialized. Please call this function after calling the init function.']); + + final String message; + + @override + String toString() => 'ServiceNotInitializedException: $message'; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_not_started_exception.dart b/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_not_started_exception.dart new file mode 100644 index 0000000..c8ccec7 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_not_started_exception.dart @@ -0,0 +1,8 @@ +class ServiceNotStartedException implements Exception { + ServiceNotStartedException([this.message = 'The service is not started.']); + + final String message; + + @override + String toString() => 'ServiceNotStartedException: $message'; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_timeout_exception.dart b/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_timeout_exception.dart new file mode 100644 index 0000000..cdbbaac --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/errors/service_timeout_exception.dart @@ -0,0 +1,10 @@ +class ServiceTimeoutException implements Exception { + ServiceTimeoutException( + [this.message = + 'The service request timed out. (ref: https://developer.android.com/guide/components/services#StartingAService)']); + + final String message; + + @override + String toString() => 'ServiceTimeoutException: $message'; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task.dart b/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task.dart new file mode 100644 index 0000000..a0ed093 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task.dart @@ -0,0 +1,424 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'dart:ui'; + +import 'package:flutter/widgets.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'flutter_foreground_task_platform_interface.dart'; +import 'errors/service_already_started_exception.dart'; +import 'errors/service_not_initialized_exception.dart'; +import 'errors/service_not_started_exception.dart'; +import 'errors/service_timeout_exception.dart'; +import 'models/foreground_service_types.dart'; +import 'models/foreground_task_options.dart'; +import 'models/notification_button.dart'; +import 'models/notification_icon.dart'; +import 'models/notification_options.dart'; +import 'models/notification_permission.dart'; +import 'models/service_request_result.dart'; +import 'task_handler.dart'; +import 'utils/utility.dart'; + +export 'errors/service_already_started_exception.dart'; +export 'errors/service_not_initialized_exception.dart'; +export 'errors/service_not_started_exception.dart'; +export 'errors/service_timeout_exception.dart'; +export 'models/foreground_service_types.dart'; +export 'models/foreground_task_event_action.dart'; +export 'models/foreground_task_options.dart'; +export 'models/notification_button.dart'; +export 'models/notification_channel_importance.dart'; +export 'models/notification_icon.dart'; +export 'models/notification_options.dart'; +export 'models/notification_permission.dart'; +export 'models/notification_priority.dart'; +export 'models/notification_visibility.dart'; +export 'models/service_request_result.dart'; +export 'ui/with_foreground_task.dart'; +export 'task_handler.dart'; + +const String _kPortName = 'flutter_foreground_task/isolateComPort'; +const String _kPrefsKeyPrefix = 'com.pravera.flutter_foreground_task.prefs.'; + +typedef DataCallback = void Function(Object data); + +/// A class that implements foreground task and provides useful utilities. +class FlutterForegroundTask { + // ====================== Service ====================== + + @visibleForTesting + static AndroidNotificationOptions? androidNotificationOptions; + + @visibleForTesting + static IOSNotificationOptions? iosNotificationOptions; + + @visibleForTesting + static ForegroundTaskOptions? foregroundTaskOptions; + + @visibleForTesting + static bool isInitialized = false; + + @visibleForTesting + static bool skipServiceResponseCheck = false; + + // platform instance: MethodChannelFlutterForegroundTask + static FlutterForegroundTaskPlatform get _platform => + FlutterForegroundTaskPlatform.instance; + + /// Resets class's static values to allow for testing of service flow. + @visibleForTesting + static void resetStatic() { + androidNotificationOptions = null; + iosNotificationOptions = null; + foregroundTaskOptions = null; + isInitialized = false; + skipServiceResponseCheck = false; + + receivePort?.close(); + receivePort = null; + streamSubscription?.cancel(); + streamSubscription = null; + dataCallbacks.clear(); + } + + /// Initialize the [FlutterForegroundTask]. + static void init({ + required AndroidNotificationOptions androidNotificationOptions, + required IOSNotificationOptions iosNotificationOptions, + required ForegroundTaskOptions foregroundTaskOptions, + }) { + FlutterForegroundTask.androidNotificationOptions = + androidNotificationOptions; + FlutterForegroundTask.iosNotificationOptions = iosNotificationOptions; + FlutterForegroundTask.foregroundTaskOptions = foregroundTaskOptions; + FlutterForegroundTask.isInitialized = true; + } + + /// Start the foreground service. + static Future startService({ + int? serviceId, + List? serviceTypes, + required String notificationTitle, + required String notificationText, + NotificationIcon? notificationIcon, + List? notificationButtons, + String? notificationInitialRoute, + Function? callback, + }) async { + try { + if (!isInitialized) { + throw ServiceNotInitializedException(); + } + + if (await isRunningService) { + throw ServiceAlreadyStartedException(); + } + + await _platform.startService( + androidNotificationOptions: androidNotificationOptions!, + iosNotificationOptions: iosNotificationOptions!, + foregroundTaskOptions: foregroundTaskOptions!, + serviceId: serviceId, + serviceTypes: serviceTypes, + notificationTitle: notificationTitle, + notificationText: notificationText, + notificationIcon: notificationIcon, + notificationButtons: notificationButtons, + notificationInitialRoute: notificationInitialRoute, + callback: callback, + ); + + if (!skipServiceResponseCheck) { + await checkServiceStateChange(target: true); + } + + return const ServiceRequestSuccess(); + } catch (error) { + return ServiceRequestFailure(error: error); + } + } + + /// Restart the foreground service. + static Future restartService() async { + try { + if (!(await isRunningService)) { + throw ServiceNotStartedException(); + } + + await _platform.restartService(); + + return const ServiceRequestSuccess(); + } catch (error) { + return ServiceRequestFailure(error: error); + } + } + + /// Update the foreground service. + static Future updateService({ + ForegroundTaskOptions? foregroundTaskOptions, + String? notificationTitle, + String? notificationText, + NotificationIcon? notificationIcon, + List? notificationButtons, + String? notificationInitialRoute, + Function? callback, + }) async { + try { + if (!(await isRunningService)) { + throw ServiceNotStartedException(); + } + + await _platform.updateService( + foregroundTaskOptions: foregroundTaskOptions, + notificationText: notificationText, + notificationTitle: notificationTitle, + notificationIcon: notificationIcon, + notificationButtons: notificationButtons, + notificationInitialRoute: notificationInitialRoute, + callback: callback, + ); + + return const ServiceRequestSuccess(); + } catch (error) { + return ServiceRequestFailure(error: error); + } + } + + /// Stop the foreground service. + static Future stopService() async { + try { + if (!(await isRunningService)) { + throw ServiceNotStartedException(); + } + + await _platform.stopService(); + + if (!skipServiceResponseCheck) { + await checkServiceStateChange(target: false); + } + + return const ServiceRequestSuccess(); + } catch (error) { + return ServiceRequestFailure(error: error); + } + } + + @visibleForTesting + static Future checkServiceStateChange({required bool target}) async { + // official doc: Once the service has been created, the service must call its startForeground() method within 5 seconds. + // ref: https://developer.android.com/guide/components/services#StartingAService + final bool isCompleted = await Utility.instance.completedWithinDeadline( + deadline: const Duration(seconds: 5), + future: () async { + return target == await isRunningService; + }, + ); + + if (!isCompleted) { + throw ServiceTimeoutException(); + } + } + + /// Returns whether the foreground service is running. + static Future get isRunningService => _platform.isRunningService; + + /// Set up the task handler and start the foreground task. + /// + /// It must always be called from a top-level function, otherwise foreground task will not work. + static void setTaskHandler(TaskHandler handler) => + _platform.setTaskHandler(handler); + + // =================== Communication =================== + + @visibleForTesting + static ReceivePort? receivePort; + + @visibleForTesting + static StreamSubscription? streamSubscription; + + @visibleForTesting + static final List dataCallbacks = []; + + /// Initialize port for communication between TaskHandler and UI. + static void initCommunicationPort() { + final ReceivePort newReceivePort = ReceivePort(); + final SendPort newSendPort = newReceivePort.sendPort; + + IsolateNameServer.removePortNameMapping(_kPortName); + if (IsolateNameServer.registerPortWithName(newSendPort, _kPortName)) { + streamSubscription?.cancel(); + receivePort?.close(); + + receivePort = newReceivePort; + streamSubscription = receivePort?.listen((data) { + for (final DataCallback callback in dataCallbacks.toList()) { + callback.call(data); + } + }); + } + } + + /// Add a callback to receive data sent from the [TaskHandler]. + static void addTaskDataCallback(DataCallback callback) { + if (!dataCallbacks.contains(callback)) { + dataCallbacks.add(callback); + } + } + + /// Remove a callback to receive data sent from the [TaskHandler]. + static void removeTaskDataCallback(DataCallback callback) { + dataCallbacks.remove(callback); + } + + /// Send data to [TaskHandler]. + static void sendDataToTask(Object data) => _platform.sendDataToTask(data); + + /// Send date to main isolate. + static void sendDataToMain(Object data) { + final SendPort? sendPort = IsolateNameServer.lookupPortByName(_kPortName); + sendPort?.send(data); + } + + // ====================== Storage ====================== + + /// Get the stored data with [key]. + static Future getData({required String key}) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.reload(); + + final Object? data = prefs.get(_kPrefsKeyPrefix + key); + + return (data is T) ? data : null; + } + + /// Get all stored data. + static Future> getAllData() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.reload(); + + final Map dataList = {}; + for (final String prefsKey in prefs.getKeys()) { + if (prefsKey.contains(_kPrefsKeyPrefix)) { + final Object? data = prefs.get(prefsKey); + if (data != null) { + final String originKey = prefsKey.replaceAll(_kPrefsKeyPrefix, ''); + dataList[originKey] = data; + } + } + } + + return dataList; + } + + /// Save data with [key]. + static Future saveData({ + required String key, + required Object value, + }) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.reload(); + + final String prefsKey = _kPrefsKeyPrefix + key; + if (value is int) { + return prefs.setInt(prefsKey, value); + } else if (value is double) { + return prefs.setDouble(prefsKey, value); + } else if (value is String) { + return prefs.setString(prefsKey, value); + } else if (value is bool) { + return prefs.setBool(prefsKey, value); + } else { + return false; + } + } + + /// Remove data with [key]. + static Future removeData({required String key}) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.reload(); + + return prefs.remove(_kPrefsKeyPrefix + key); + } + + /// Clears all stored data. + static Future clearAllData() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.reload(); + + for (final String prefsKey in prefs.getKeys()) { + if (prefsKey.contains(_kPrefsKeyPrefix)) { + await prefs.remove(prefsKey); + } + } + + return true; + } + + // ====================== Utility ====================== + + /// Minimize the app to the background. + static void minimizeApp() => _platform.minimizeApp(); + + /// Launch the app at [route] if it is not running otherwise open it. + /// + /// It is also possible to pass a route to this function but the route will only + /// be loaded if the app is not already running. + /// + /// This function requires the "android.permission.SYSTEM\_ALERT\_WINDOW" permission and + /// requires using the `openSystemAlertWindowSettings()` function to grant the permission. + static void launchApp([String? route]) => _platform.launchApp(route); + + /// Toggles lockScreen visibility. + static void setOnLockScreenVisibility(bool isVisible) => + _platform.setOnLockScreenVisibility(isVisible); + + /// Returns whether the app is in the foreground. + static Future get isAppOnForeground => _platform.isAppOnForeground; + + /// Wake up the screen of a device that is turned off. + static void wakeUpScreen() => _platform.wakeUpScreen(); + + /// Returns whether the app has been excluded from battery optimization. + static Future get isIgnoringBatteryOptimizations => + _platform.isIgnoringBatteryOptimizations; + + /// Open the settings page where you can set ignore battery optimization. + static Future openIgnoreBatteryOptimizationSettings() => + _platform.openIgnoreBatteryOptimizationSettings(); + + /// Request to ignore battery optimization. + /// + /// This function requires the "android.permission.REQUEST\_IGNORE\_BSCTERY\_OPTIMIZSCIONS" permission. + static Future requestIgnoreBatteryOptimization() => + _platform.requestIgnoreBatteryOptimization(); + + /// Returns whether the "android.permission.SYSTEM\_ALERT\_WINDOW" permission is granted. + static Future get canDrawOverlays => _platform.canDrawOverlays; + + /// Open the settings page where you can allow/deny the "android.permission.SYSTEM\_ALERT\_WINDOW" permission. + static Future openSystemAlertWindowSettings() => + _platform.openSystemAlertWindowSettings(); + + /// Returns notification permission status. + static Future checkNotificationPermission() => + _platform.checkNotificationPermission(); + + /// Request notification permission. + static Future requestNotificationPermission() => + _platform.requestNotificationPermission(); + + /// Returns whether the "android.permission.SCHEDULE\_EXACT\_ALARM" permission is granted. + static Future get canScheduleExactAlarms => + _platform.canScheduleExactAlarms; + + /// Open the alarms & reminders settings page. + /// + /// Use this utility only if you provide services that require long-term survival, + /// such as exact alarm service, healthcare service, or Bluetooth communication. + /// + /// This utility requires the "android.permission.SCHEDULE\_EXACT\_ALARM" permission. + /// Using this permission may make app distribution difficult due to Google policy. + static Future openAlarmsAndRemindersSettings() => + _platform.openAlarmsAndRemindersSettings(); +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task_method_channel.dart b/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task_method_channel.dart new file mode 100644 index 0000000..54ddd41 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task_method_channel.dart @@ -0,0 +1,276 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer' as dev; +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:platform/platform.dart'; + +import 'flutter_foreground_task_platform_interface.dart'; +import 'models/foreground_service_types.dart'; +import 'models/foreground_task_options.dart'; +import 'models/notification_button.dart'; +import 'models/notification_icon.dart'; +import 'models/notification_options.dart'; +import 'models/notification_permission.dart'; +import 'models/service_options.dart'; +import 'task_handler.dart'; + +/// An implementation of [FlutterForegroundTaskPlatform] that uses method channels. +class MethodChannelFlutterForegroundTask extends FlutterForegroundTaskPlatform { + @visibleForTesting + final MethodChannel mMDChannel = + const MethodChannel('flutter_foreground_task/methods'); + + @visibleForTesting + final MethodChannel mBGChannel = + const MethodChannel('flutter_foreground_task/background'); + + @visibleForTesting + Platform platform = const LocalPlatform(); + + // ====================== Service ====================== + + @override + Future startService({ + required AndroidNotificationOptions androidNotificationOptions, + required IOSNotificationOptions iosNotificationOptions, + required ForegroundTaskOptions foregroundTaskOptions, + int? serviceId, + List? serviceTypes, + required String notificationTitle, + required String notificationText, + NotificationIcon? notificationIcon, + List? notificationButtons, + String? notificationInitialRoute, + Function? callback, + }) async { + final Map optionsJson = ServiceStartOptions( + serviceId: serviceId, + serviceTypes: serviceTypes, + androidNotificationOptions: androidNotificationOptions, + iosNotificationOptions: iosNotificationOptions, + foregroundTaskOptions: foregroundTaskOptions, + notificationContentTitle: notificationTitle, + notificationContentText: notificationText, + notificationIcon: notificationIcon, + notificationButtons: notificationButtons, + notificationInitialRoute: notificationInitialRoute, + callback: callback, + ).toJson(platform); + + await mMDChannel.invokeMethod('startService', optionsJson); + } + + @override + Future restartService() async { + await mMDChannel.invokeMethod('restartService'); + } + + @override + Future updateService({ + ForegroundTaskOptions? foregroundTaskOptions, + String? notificationTitle, + String? notificationText, + NotificationIcon? notificationIcon, + List? notificationButtons, + String? notificationInitialRoute, + Function? callback, + }) async { + final Map optionsJson = ServiceUpdateOptions( + foregroundTaskOptions: foregroundTaskOptions, + notificationContentTitle: notificationTitle, + notificationContentText: notificationText, + notificationIcon: notificationIcon, + notificationButtons: notificationButtons, + notificationInitialRoute: notificationInitialRoute, + callback: callback, + ).toJson(platform); + + await mMDChannel.invokeMethod('updateService', optionsJson); + } + + @override + Future stopService() async { + await mMDChannel.invokeMethod('stopService'); + } + + @override + Future get isRunningService async { + return await mMDChannel.invokeMethod('isRunningService'); + } + + @override + Future get attachedActivity async { + if (platform.isAndroid) { + return await mMDChannel.invokeMethod('attachedActivity'); + } + return true; + } + + @override + void setTaskHandler(TaskHandler handler) { + // Binding the framework to the flutter engine. + WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + + // Set the method call handler for the background channel. + mBGChannel.setMethodCallHandler((call) async { + await onBackgroundChannel(call, handler); + }); + + mBGChannel.invokeMethod('start'); + } + + @visibleForTesting + Future onBackgroundChannel(MethodCall call, TaskHandler handler) async { + final DateTime timestamp = DateTime.timestamp(); + + switch (call.method) { + case 'onStart': + final TaskStarter starter = TaskStarter.fromIndex(call.arguments); + await handler.onStart(timestamp, starter); + break; + case 'onRepeatEvent': + handler.onRepeatEvent(timestamp); + break; + case 'onDestroy': + final bool isTimeout = call.arguments ?? false; + await handler.onDestroy(timestamp, isTimeout); + break; + case 'onReceiveData': + dynamic data = call.arguments; + if (data is List || data is Map || data is Set) { + try { + data = jsonDecode(jsonEncode(data)); + } catch (e, s) { + dev.log('onReceiveData error: $e\n$s'); + } + } + handler.onReceiveData(data); + break; + case 'onNotificationButtonPressed': + final String id = call.arguments.toString(); + handler.onNotificationButtonPressed(id); + break; + case 'onNotificationDismissed': + handler.onNotificationDismissed(); + break; + case 'onNotificationPressed': + handler.onNotificationPressed(); + break; + } + } + + // =================== Communication =================== + + @override + void sendDataToTask(Object data) { + mMDChannel.invokeMethod('sendData', data); + } + + // ====================== Utility ====================== + + @override + void minimizeApp() { + mMDChannel.invokeMethod('minimizeApp'); + } + + @override + void launchApp([String? route]) { + if (platform.isAndroid) { + mMDChannel.invokeMethod('launchApp', route); + } + } + + @override + void setOnLockScreenVisibility(bool isVisible) { + if (platform.isAndroid) { + mMDChannel.invokeMethod('setOnLockScreenVisibility', isVisible); + } + } + + @override + Future get isAppOnForeground async { + return await mMDChannel.invokeMethod('isAppOnForeground'); + } + + @override + void wakeUpScreen() { + if (platform.isAndroid) { + mMDChannel.invokeMethod('wakeUpScreen'); + } + } + + @override + Future get isIgnoringBatteryOptimizations async { + if (platform.isAndroid) { + return await mMDChannel.invokeMethod('isIgnoringBatteryOptimizations'); + } + return true; + } + + @override + Future openIgnoreBatteryOptimizationSettings() async { + if (platform.isAndroid) { + return await mMDChannel + .invokeMethod('openIgnoreBatteryOptimizationSettings'); + } + return true; + } + + @override + Future requestIgnoreBatteryOptimization() async { + if (platform.isAndroid) { + return await mMDChannel.invokeMethod('requestIgnoreBatteryOptimization'); + } + return true; + } + + @override + Future get canDrawOverlays async { + if (platform.isAndroid) { + return await mMDChannel.invokeMethod('canDrawOverlays'); + } + return true; + } + + @override + Future openSystemAlertWindowSettings() async { + if (platform.isAndroid) { + return await mMDChannel.invokeMethod('openSystemAlertWindowSettings'); + } + return true; + } + + @override + Future checkNotificationPermission() async { + final int result = + await mMDChannel.invokeMethod('checkNotificationPermission'); + return NotificationPermission.fromIndex(result); + } + + @override + Future requestNotificationPermission() async { + final int result = + await mMDChannel.invokeMethod('requestNotificationPermission'); + return NotificationPermission.fromIndex(result); + } + + @override + Future get canScheduleExactAlarms async { + if (platform.isAndroid) { + return await mMDChannel.invokeMethod('canScheduleExactAlarms'); + } + return true; + } + + @override + Future openAlarmsAndRemindersSettings() async { + if (platform.isAndroid) { + return await mMDChannel.invokeMethod('openAlarmsAndRemindersSettings'); + } + return true; + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task_platform_interface.dart b/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task_platform_interface.dart new file mode 100644 index 0000000..5748512 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/flutter_foreground_task_platform_interface.dart @@ -0,0 +1,156 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'flutter_foreground_task_method_channel.dart'; +import 'models/foreground_service_types.dart'; +import 'models/foreground_task_options.dart'; +import 'models/notification_button.dart'; +import 'models/notification_icon.dart'; +import 'models/notification_options.dart'; +import 'models/notification_permission.dart'; +import 'task_handler.dart'; + +abstract class FlutterForegroundTaskPlatform extends PlatformInterface { + /// Constructs a FlutterForegroundTaskPlatform. + FlutterForegroundTaskPlatform() : super(token: _token); + + static final Object _token = Object(); + + static FlutterForegroundTaskPlatform _instance = + MethodChannelFlutterForegroundTask(); + + /// The default instance of [FlutterForegroundTaskPlatform] to use. + /// + /// Defaults to [MethodChannelFlutterForegroundTask]. + static FlutterForegroundTaskPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [FlutterForegroundTaskPlatform] when + /// they register themselves. + static set instance(FlutterForegroundTaskPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + // ====================== Service ====================== + + Future startService({ + required AndroidNotificationOptions androidNotificationOptions, + required IOSNotificationOptions iosNotificationOptions, + required ForegroundTaskOptions foregroundTaskOptions, + int? serviceId, + List? serviceTypes, + required String notificationTitle, + required String notificationText, + NotificationIcon? notificationIcon, + List? notificationButtons, + String? notificationInitialRoute, + Function? callback, + }) { + throw UnimplementedError('startService() has not been implemented.'); + } + + Future restartService() { + throw UnimplementedError('restartService() has not been implemented.'); + } + + Future updateService({ + ForegroundTaskOptions? foregroundTaskOptions, + String? notificationTitle, + String? notificationText, + NotificationIcon? notificationIcon, + List? notificationButtons, + String? notificationInitialRoute, + Function? callback, + }) { + throw UnimplementedError('updateService() has not been implemented.'); + } + + Future stopService() { + throw UnimplementedError('stopService() has not been implemented.'); + } + + Future get isRunningService { + throw UnimplementedError('isRunningService has not been implemented.'); + } + + Future get attachedActivity { + throw UnimplementedError('attachedActivity has not been implemented.'); + } + + void setTaskHandler(TaskHandler handler) { + throw UnimplementedError('setTaskHandler() has not been implemented.'); + } + + // =================== Communication =================== + + void sendDataToTask(Object data) { + throw UnimplementedError('sendDataToTask() has not been implemented.'); + } + + // ====================== Utility ====================== + + void minimizeApp() { + throw UnimplementedError('minimizeApp() has not been implemented.'); + } + + void launchApp([String? route]) { + throw UnimplementedError('launchApp() has not been implemented.'); + } + + void setOnLockScreenVisibility(bool isVisible) { + throw UnimplementedError( + 'setOnLockScreenVisibility() has not been implemented.'); + } + + Future get isAppOnForeground { + throw UnimplementedError('isAppOnForeground has not been implemented.'); + } + + void wakeUpScreen() { + throw UnimplementedError('wakeUpScreen() has not been implemented.'); + } + + Future get isIgnoringBatteryOptimizations { + throw UnimplementedError( + 'isIgnoringBatteryOptimizations has not been implemented.'); + } + + Future openIgnoreBatteryOptimizationSettings() { + throw UnimplementedError( + 'openIgnoreBatteryOptimizationSettings() has not been implemented.'); + } + + Future requestIgnoreBatteryOptimization() { + throw UnimplementedError( + 'requestIgnoreBatteryOptimization() has not been implemented.'); + } + + Future get canDrawOverlays { + throw UnimplementedError('canDrawOverlays has not been implemented.'); + } + + Future openSystemAlertWindowSettings() { + throw UnimplementedError( + 'openSystemAlertWindowSettings() has not been implemented.'); + } + + Future checkNotificationPermission() { + throw UnimplementedError( + 'checkNotificationPermission() has not been implemented.'); + } + + Future requestNotificationPermission() { + throw UnimplementedError( + 'requestNotificationPermission() has not been implemented.'); + } + + Future get canScheduleExactAlarms { + throw UnimplementedError( + 'canScheduleExactAlarms has not been implemented.'); + } + + Future openAlarmsAndRemindersSettings() { + throw UnimplementedError( + 'openAlarmsAndRemindersSettings() has not been implemented.'); + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_service_types.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_service_types.dart new file mode 100644 index 0000000..5baa1e7 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_service_types.dart @@ -0,0 +1,54 @@ +/// https://developer.android.com/about/versions/14/changes/fgs-types-required#system-exempted +class ForegroundServiceTypes { + /// Constructs an instance of [ForegroundServiceTypes]. + const ForegroundServiceTypes(this.rawValue); + + /// Continue to access the camera from the background, such as video chat apps that allow for multitasking. + static const camera = ForegroundServiceTypes(0); + + /// Interactions with external devices that require a Bluetooth, NFC, IR, USB, or network connection. + static const connectedDevice = ForegroundServiceTypes(1); + + /// Data transfer operations, such as the following: + /// + /// * Data upload or download + /// * Backup-and-restore operations + /// * Import or export operations + /// * Fetch data + /// * Local file processing + /// * Transfer data between a device and the cloud over a network + static const dataSync = ForegroundServiceTypes(2); + + /// Any long-running use cases to support apps in the fitness category such as exercise trackers. + static const health = ForegroundServiceTypes(3); + + /// Long-running use cases that require location access, such as navigation and location sharing. + static const location = ForegroundServiceTypes(4); + + /// Continue audio or video playback from the background. Support Digital Video Recording (DVR) functionality on Android TV. + static const mediaPlayback = ForegroundServiceTypes(5); + + /// Project content to non-primary display or external device using the MediaProjection APIs. This content doesn't have to be exclusively media content. + static const mediaProjection = ForegroundServiceTypes(6); + + /// Continue microphone capture from the background, such as voice recorders or communication apps. + static const microphone = ForegroundServiceTypes(7); + + /// Continue an ongoing call using the ConnectionService APIs. + static const phoneCall = ForegroundServiceTypes(8); + + /// Transfer text messages from one device to another. Assists with continuity of a user's messaging tasks when they switch devices. + static const remoteMessaging = ForegroundServiceTypes(9); + + /// Quickly finish critical work that cannot be interrupted or postponed. + static const shortService = ForegroundServiceTypes(10); + + /// Covers any valid foreground service use cases that aren't covered by the other foreground service types. + static const specialUse = ForegroundServiceTypes(11); + + /// Reserved for system applications and specific system integrations, to continue to use foreground services. + static const systemExempted = ForegroundServiceTypes(12); + + /// The raw value of [ForegroundServiceTypes]. + final int rawValue; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_task_event_action.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_task_event_action.dart new file mode 100644 index 0000000..53266c4 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_task_event_action.dart @@ -0,0 +1,50 @@ +/// A class that defines the action of onRepeatEvent in [TaskHandler]. +class ForegroundTaskEventAction { + ForegroundTaskEventAction._private({ + required this.type, + this.interval, + }); + + /// Not use onRepeatEvent callback. + factory ForegroundTaskEventAction.nothing() => + ForegroundTaskEventAction._private(type: ForegroundTaskEventType.nothing); + + /// Call onRepeatEvent only once. + factory ForegroundTaskEventAction.once() => + ForegroundTaskEventAction._private(type: ForegroundTaskEventType.once); + + /// Call onRepeatEvent at milliseconds [interval]. + factory ForegroundTaskEventAction.repeat(int interval) => + ForegroundTaskEventAction._private( + type: ForegroundTaskEventType.repeat, interval: interval); + + /// The type for [ForegroundTaskEventAction]. + final ForegroundTaskEventType type; + + /// The interval(in milliseconds) at which onRepeatEvent is invoked. + final int? interval; + + /// Returns the data fields of [ForegroundTaskEventAction] in JSON format. + Map toJson() { + return { + 'taskEventType': type.value, + 'taskEventInterval': interval, + }; + } +} + +/// The type for [ForegroundTaskEventAction]. +enum ForegroundTaskEventType { + /// Not use onRepeatEvent callback. + nothing(1), + + /// Call onRepeatEvent only once. + once(2), + + /// Call onRepeatEvent at milliseconds interval. + repeat(3); + + const ForegroundTaskEventType(this.value); + + final int value; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_task_options.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_task_options.dart new file mode 100644 index 0000000..03d142c --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/foreground_task_options.dart @@ -0,0 +1,62 @@ +import 'foreground_task_event_action.dart'; + +/// Data class with foreground task options. +class ForegroundTaskOptions { + /// Constructs an instance of [ForegroundTaskOptions]. + const ForegroundTaskOptions({ + required this.eventAction, + this.autoRunOnBoot = false, + this.autoRunOnMyPackageReplaced = false, + this.allowWakeLock = true, + this.allowWifiLock = false, + }); + + /// The action of onRepeatEvent in [TaskHandler]. + final ForegroundTaskEventAction eventAction; + + /// Whether to automatically run foreground task on boot. + /// The default is `false`. + final bool autoRunOnBoot; + + /// Whether to automatically run foreground task when the app is updated to a new version. + /// The default is `false`. + final bool autoRunOnMyPackageReplaced; + + /// Whether to keep the CPU turned on. + /// The default is `true`. + final bool allowWakeLock; + + /// Allows an application to keep the Wi-Fi radio awake. + /// The default is `false`. + /// + /// https://developer.android.com/reference/android/net/wifi/WifiManager.WifiLock.html + final bool allowWifiLock; + + /// Returns the data fields of [ForegroundTaskOptions] in JSON format. + Map toJson() { + return { + 'taskEventAction': eventAction.toJson(), + 'autoRunOnBoot': autoRunOnBoot, + 'autoRunOnMyPackageReplaced': autoRunOnMyPackageReplaced, + 'allowWakeLock': allowWakeLock, + 'allowWifiLock': allowWifiLock, + }; + } + + /// Creates a copy of the object replaced with new values. + ForegroundTaskOptions copyWith({ + ForegroundTaskEventAction? eventAction, + bool? autoRunOnBoot, + bool? autoRunOnMyPackageReplaced, + bool? allowWakeLock, + bool? allowWifiLock, + }) => + ForegroundTaskOptions( + eventAction: eventAction ?? this.eventAction, + autoRunOnBoot: autoRunOnBoot ?? this.autoRunOnBoot, + autoRunOnMyPackageReplaced: + autoRunOnMyPackageReplaced ?? this.autoRunOnMyPackageReplaced, + allowWakeLock: allowWakeLock ?? this.allowWakeLock, + allowWifiLock: allowWifiLock ?? this.allowWifiLock, + ); +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_button.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_button.dart new file mode 100644 index 0000000..014d3c1 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_button.dart @@ -0,0 +1,44 @@ +import 'dart:ui'; + +import 'package:flutter_foreground_task/utils/color_extension.dart'; + +/// The button to display in the notification. +class NotificationButton { + /// Constructs an instance of [NotificationButton]. + const NotificationButton({ + required this.id, + required this.text, + this.textColor, + }) : assert(id.length > 0), + assert(text.length > 0); + + /// The button identifier. + final String id; + + /// The text to display on the button. + final String text; + + /// The button text color. (only work Android) + final Color? textColor; + + /// Returns the data fields of [NotificationButton] in JSON format. + Map toJson() { + return { + 'id': id, + 'text': text, + 'textColorRgb': textColor?.toRgbString, + }; + } + + /// Creates a copy of the object replaced with new values. + NotificationButton copyWith({ + String? id, + String? text, + Color? textColor, + }) => + NotificationButton( + id: id ?? this.id, + text: text ?? this.text, + textColor: textColor ?? this.textColor, + ); +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_channel_importance.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_channel_importance.dart new file mode 100644 index 0000000..864e0ff --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_channel_importance.dart @@ -0,0 +1,33 @@ +/// The importance of the notification channel. +/// See https://developer.android.com/training/notify-user/channels?hl=ko#importance +class NotificationChannelImportance { + /// Constructs an instance of [NotificationChannelImportance]. + const NotificationChannelImportance(this.rawValue); + + /// A notification with no importance: does not show in the shade. + static const NotificationChannelImportance NONE = + NotificationChannelImportance(0); + + /// Min notification importance: only shows in the shade, below the fold. + static const NotificationChannelImportance MIN = + NotificationChannelImportance(1); + + /// Low notification importance: shows in the shade, and potentially in the status bar (see shouldHideSilentStatusBarIcons()), but is not audibly intrusive. + static const NotificationChannelImportance LOW = + NotificationChannelImportance(2); + + /// Default notification importance: shows everywhere, makes noise, but does not visually intrude. + static const NotificationChannelImportance DEFAULT = + NotificationChannelImportance(3); + + /// Higher notification importance: shows everywhere, makes noise and peeks. May use full screen intents. + static const NotificationChannelImportance HIGH = + NotificationChannelImportance(4); + + /// Max notification importance: same as HIGH, but generally not used. + static const NotificationChannelImportance MAX = + NotificationChannelImportance(5); + + /// The raw value of [NotificationChannelImportance]. + final int rawValue; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_icon.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_icon.dart new file mode 100644 index 0000000..983c512 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_icon.dart @@ -0,0 +1,36 @@ +import 'dart:ui'; + +import 'package:flutter_foreground_task/utils/color_extension.dart'; + +/// A data class for dynamically changing the notification icon. +class NotificationIcon { + /// Constructs an instance of [NotificationIcon]. + const NotificationIcon({ + required this.metaDataName, + this.backgroundColor, + }) : assert(metaDataName.length > 0); + + /// The name of the meta-data in the manifest that contains the drawable icon resource identifier. + final String metaDataName; + + /// The background color for the notification icon. + final Color? backgroundColor; + + /// Returns the data fields of [NotificationIcon] in JSON format. + Map toJson() { + return { + 'metaDataName': metaDataName, + 'backgroundColorRgb': backgroundColor?.toRgbString, + }; + } + + /// Creates a copy of the object replaced with new values. + NotificationIcon copyWith({ + String? metaDataName, + Color? backgroundColor, + }) => + NotificationIcon( + metaDataName: metaDataName ?? this.metaDataName, + backgroundColor: backgroundColor ?? this.backgroundColor, + ); +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_options.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_options.dart new file mode 100644 index 0000000..d2316cc --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_options.dart @@ -0,0 +1,162 @@ +import 'notification_channel_importance.dart'; +import 'notification_priority.dart'; +import 'notification_visibility.dart'; + +/// Notification options for Android platform. +class AndroidNotificationOptions { + /// Constructs an instance of [AndroidNotificationOptions]. + AndroidNotificationOptions({ + @Deprecated('Use startService(serviceId) instead.') this.id, + required this.channelId, + required this.channelName, + this.channelDescription, + this.channelImportance = NotificationChannelImportance.LOW, + this.priority = NotificationPriority.LOW, + this.enableVibration = false, + this.playSound = false, + this.showWhen = false, + this.showBadge = false, + this.onlyAlertOnce = false, + this.visibility = NotificationVisibility.VISIBILITY_PUBLIC, + }) : assert(channelId.isNotEmpty), + assert(channelName.isNotEmpty); + + /// Unique ID of the notification. + final int? id; + + /// Unique ID of the notification channel. + /// + /// It is set only once for the first time on Android 8.0+. + final String channelId; + + /// The name of the notification channel. + /// + /// It is set only once for the first time on Android 8.0+. + final String channelName; + + /// The description of the notification channel. + /// + /// It is set only once for the first time on Android 8.0+. + final String? channelDescription; + + /// The importance of the notification channel. + /// The default is `NotificationChannelImportance.LOW`. + /// + /// It is set only once for the first time on Android 8.0+. + final NotificationChannelImportance channelImportance; + + /// Priority of notifications for Android 7.1 and lower. + /// The default is `NotificationPriority.LOW`. + final NotificationPriority priority; + + /// Whether to enable vibration when creating notifications. + /// The default is `false`. + /// + /// It is set only once for the first time on Android 8.0+. + final bool enableVibration; + + /// Whether to play sound when creating notifications. + /// The default is `false`. + /// + /// It is set only once for the first time on Android 8.0+. + final bool playSound; + + /// Whether to show the timestamp when the notification was created in the content view. + /// The default is `false`. + final bool showWhen; + + /// Whether to show the badge near the app icon when service is started. + /// The default is `false`. + /// + /// It is set only once for the first time on Android 8.0+. + final bool showBadge; + + /// Whether to only alert once when the notification is created. + /// The default is `false`. + final bool onlyAlertOnce; + + /// Control the level of detail displayed in notifications on the lock screen. + /// The default is `NotificationVisibility.VISIBILITY_PUBLIC`. + final NotificationVisibility visibility; + + /// Returns the data fields of [AndroidNotificationOptions] in JSON format. + Map toJson() { + return { + 'notificationId': id, + 'notificationChannelId': channelId, + 'notificationChannelName': channelName, + 'notificationChannelDescription': channelDescription, + 'notificationChannelImportance': channelImportance.rawValue, + 'notificationPriority': priority.rawValue, + 'enableVibration': enableVibration, + 'playSound': playSound, + 'showWhen': showWhen, + 'showBadge': showBadge, + 'onlyAlertOnce': onlyAlertOnce, + 'visibility': visibility.rawValue, + }; + } + + /// Creates a copy of the object replaced with new values. + AndroidNotificationOptions copyWith({ + String? channelId, + String? channelName, + String? channelDescription, + NotificationChannelImportance? channelImportance, + NotificationPriority? priority, + bool? enableVibration, + bool? playSound, + bool? showWhen, + bool? showBadge, + bool? onlyAlertOnce, + NotificationVisibility? visibility, + }) => + AndroidNotificationOptions( + channelId: channelId ?? this.channelId, + channelName: channelName ?? this.channelName, + channelDescription: channelDescription ?? this.channelDescription, + channelImportance: channelImportance ?? this.channelImportance, + priority: priority ?? this.priority, + enableVibration: enableVibration ?? this.enableVibration, + playSound: playSound ?? this.playSound, + showWhen: showWhen ?? this.showWhen, + showBadge: showBadge ?? this.showBadge, + onlyAlertOnce: onlyAlertOnce ?? this.onlyAlertOnce, + visibility: visibility ?? this.visibility, + ); +} + +/// Notification options for iOS platform. +class IOSNotificationOptions { + /// Constructs an instance of [IOSNotificationOptions]. + const IOSNotificationOptions({ + this.showNotification = true, + this.playSound = false, + }); + + /// Whether to show notifications. + /// The default is `true`. + final bool showNotification; + + /// Whether to play sound when creating notifications. + /// The default is `false`. + final bool playSound; + + /// Returns the data fields of [IOSNotificationOptions] in JSON format. + Map toJson() { + return { + 'showNotification': showNotification, + 'playSound': playSound, + }; + } + + /// Creates a copy of the object replaced with new values. + IOSNotificationOptions copyWith({ + bool? showNotification, + bool? playSound, + }) => + IOSNotificationOptions( + showNotification: showNotification ?? this.showNotification, + playSound: playSound ?? this.playSound, + ); +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_permission.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_permission.dart new file mode 100644 index 0000000..1064e58 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_permission.dart @@ -0,0 +1,14 @@ +/// Represents the result of a notification permission request. +enum NotificationPermission { + /// Notification permission has been granted. + granted, + + /// Notification permission has been denied. + denied, + + /// Notification permission has been permanently denied. + permanently_denied; + + static NotificationPermission fromIndex(int? index) => NotificationPermission + .values[index ?? NotificationPermission.denied.index]; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_priority.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_priority.dart new file mode 100644 index 0000000..f70aaa6 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_priority.dart @@ -0,0 +1,23 @@ +/// Priority of notifications for Android 7.1 and lower. +class NotificationPriority { + /// Constructs an instance of [NotificationPriority]. + const NotificationPriority(this.rawValue); + + /// No sound and does not appear in the status bar. + static const NotificationPriority MIN = NotificationPriority(-2); + + /// No sound. + static const NotificationPriority LOW = NotificationPriority(-1); + + /// Makes a sound. + static const NotificationPriority DEFAULT = NotificationPriority(0); + + /// Makes a sound and appears as a heads-up notification. + static const NotificationPriority HIGH = NotificationPriority(1); + + /// Same as HIGH, but used when you want to notify notification immediately. + static const NotificationPriority MAX = NotificationPriority(2); + + /// The raw value of [NotificationPriority]. + final int rawValue; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_visibility.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_visibility.dart new file mode 100644 index 0000000..7c01c6f --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/notification_visibility.dart @@ -0,0 +1,20 @@ +/// The level of detail displayed in notifications on the lock screen. +class NotificationVisibility { + /// Constructs an instance of [NotificationVisibility]. + const NotificationVisibility(this.rawValue); + + /// Show this notification in its entirety on all lockscreens. + static const NotificationVisibility VISIBILITY_PUBLIC = + NotificationVisibility(1); + + /// Do not reveal any part of this notification on a secure lockscreen. + static const NotificationVisibility VISIBILITY_SECRET = + NotificationVisibility(-1); + + /// Show this notification on all lockscreens, but conceal sensitive or private information on secure lockscreens. + static const NotificationVisibility VISIBILITY_PRIVSCE = + NotificationVisibility(0); + + /// The raw value of [NotificationVisibility]. + final int rawValue; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/service_options.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/service_options.dart new file mode 100644 index 0000000..8f9bf0b --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/service_options.dart @@ -0,0 +1,104 @@ +import 'dart:ui'; + +import 'package:platform/platform.dart'; + +import 'foreground_service_types.dart'; +import 'foreground_task_options.dart'; +import 'notification_button.dart'; +import 'notification_icon.dart'; +import 'notification_options.dart'; + +class ServiceStartOptions { + const ServiceStartOptions({ + this.serviceId, + this.serviceTypes, + required this.androidNotificationOptions, + required this.iosNotificationOptions, + required this.foregroundTaskOptions, + required this.notificationContentTitle, + required this.notificationContentText, + this.notificationIcon, + this.notificationButtons, + this.notificationInitialRoute, + this.callback, + }); + + final int? serviceId; + final List? serviceTypes; + final AndroidNotificationOptions androidNotificationOptions; + final IOSNotificationOptions iosNotificationOptions; + final ForegroundTaskOptions foregroundTaskOptions; + final String notificationContentTitle; + final String notificationContentText; + final NotificationIcon? notificationIcon; + final List? notificationButtons; + final String? notificationInitialRoute; + final Function? callback; + + Map toJson(Platform platform) { + final Map json = { + 'serviceId': serviceId, + 'serviceTypes': serviceTypes?.map((e) => e.rawValue).toList(), + ...foregroundTaskOptions.toJson(), + 'notificationContentTitle': notificationContentTitle, + 'notificationContentText': notificationContentText, + 'icon': notificationIcon?.toJson(), + 'buttons': notificationButtons?.map((e) => e.toJson()).toList(), + 'initialRoute': notificationInitialRoute, + }; + + if (platform.isAndroid) { + json.addAll(androidNotificationOptions.toJson()); + } else if (platform.isIOS) { + json.addAll(iosNotificationOptions.toJson()); + } + + if (callback != null) { + json['callbackHandle'] = + PluginUtilities.getCallbackHandle(callback!)?.toRawHandle(); + } + + return json; + } +} + +class ServiceUpdateOptions { + const ServiceUpdateOptions({ + required this.foregroundTaskOptions, + required this.notificationContentTitle, + required this.notificationContentText, + this.notificationIcon, + this.notificationButtons, + this.notificationInitialRoute, + this.callback, + }); + + final ForegroundTaskOptions? foregroundTaskOptions; + final String? notificationContentTitle; + final String? notificationContentText; + final NotificationIcon? notificationIcon; + final List? notificationButtons; + final String? notificationInitialRoute; + final Function? callback; + + Map toJson(Platform platform) { + final Map json = { + 'notificationContentTitle': notificationContentTitle, + 'notificationContentText': notificationContentText, + 'icon': notificationIcon?.toJson(), + 'buttons': notificationButtons?.map((e) => e.toJson()).toList(), + 'initialRoute': notificationInitialRoute, + }; + + if (foregroundTaskOptions != null) { + json.addAll(foregroundTaskOptions!.toJson()); + } + + if (callback != null) { + json['callbackHandle'] = + PluginUtilities.getCallbackHandle(callback!)?.toRawHandle(); + } + + return json; + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/models/service_request_result.dart b/local_packages/flutter_foreground_task-9.1.0/lib/models/service_request_result.dart new file mode 100644 index 0000000..3b67689 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/models/service_request_result.dart @@ -0,0 +1,17 @@ +/// Represents the result of a service request. +sealed class ServiceRequestResult { + const ServiceRequestResult(); +} + +/// The service request was successful. +final class ServiceRequestSuccess extends ServiceRequestResult { + const ServiceRequestSuccess(); +} + +/// The service request failed. +final class ServiceRequestFailure extends ServiceRequestResult { + const ServiceRequestFailure({required this.error}); + + /// The error that occurred when the service request failed. + final Object error; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/task_handler.dart b/local_packages/flutter_foreground_task-9.1.0/lib/task_handler.dart new file mode 100644 index 0000000..3c64afb --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/task_handler.dart @@ -0,0 +1,44 @@ +import 'flutter_foreground_task.dart'; + +/// A class that implements a task handler. +abstract class TaskHandler { + /// Called when the task is started. + Future onStart(DateTime timestamp, TaskStarter starter); + + /// Called based on the eventAction set in [ForegroundTaskOptions]. + /// + /// - .nothing() : Not use onRepeatEvent callback. + /// - .once() : Call onRepeatEvent only once. + /// - .repeat(interval) : Call onRepeatEvent at milliseconds interval. + void onRepeatEvent(DateTime timestamp); + + /// Called when the task is destroyed. + Future onDestroy(DateTime timestamp, bool isTimeout); + + /// Called when data is sent using [FlutterForegroundTask.sendDataToTask]. + void onReceiveData(Object data) {} + + /// Called when the notification button is pressed. + void onNotificationButtonPressed(String id) {} + + /// Called when the notification itself is pressed. + void onNotificationPressed() {} + + /// Called when the notification itself is dismissed. + /// + /// - AOS: only work Android 14+ + /// + /// - iOS: only work iOS 12+ + void onNotificationDismissed() {} +} + +/// The starter that started the task. +enum TaskStarter { + /// The task has been started by the developer. + developer, + + /// The task has been started by the system. + system; + + static TaskStarter fromIndex(int index) => TaskStarter.values[index]; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/ui/with_foreground_task.dart b/local_packages/flutter_foreground_task-9.1.0/lib/ui/with_foreground_task.dart new file mode 100644 index 0000000..fefb9d8 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/ui/with_foreground_task.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; + +/// A widget that minimize the app without closing it when the user presses the soft back button. +/// It only works when the service is running. +/// +/// This widget must be declared above the [Scaffold] widget. +class WithForegroundTask extends StatefulWidget { + /// A child widget that contains the [Scaffold] widget. + final Widget child; + + const WithForegroundTask({super.key, required this.child}); + + @override + State createState() => _WithForegroundTaskState(); +} + +class _WithForegroundTaskState extends State { + Future _onWillPop() async { + final bool canPop = mounted ? Navigator.canPop(context) : false; + if (!canPop && await FlutterForegroundTask.isRunningService) { + FlutterForegroundTask.minimizeApp(); + return false; + } + + return true; + } + + @override + Widget build(BuildContext context) => + WillPopScope(onWillPop: _onWillPop, child: widget.child); +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/utils/color_extension.dart b/local_packages/flutter_foreground_task-9.1.0/lib/utils/color_extension.dart new file mode 100644 index 0000000..2905cd4 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/utils/color_extension.dart @@ -0,0 +1,5 @@ +import 'dart:ui'; + +extension ColorExtension on Color { + String get toRgbString => '$red,$green,$blue'; +} diff --git a/local_packages/flutter_foreground_task-9.1.0/lib/utils/utility.dart b/local_packages/flutter_foreground_task-9.1.0/lib/utils/utility.dart new file mode 100644 index 0000000..8231df1 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/lib/utils/utility.dart @@ -0,0 +1,26 @@ +final class Utility { + Utility._(); + + static Utility instance = Utility._(); + + Future completedWithinDeadline({ + required Duration deadline, + required Future Function() future, + Duration tick = const Duration(milliseconds: 100), + }) async { + final Stopwatch stopwatch = Stopwatch()..start(); + bool completed = false; + await Future.doWhile(() async { + completed = await future(); + if (completed || + stopwatch.elapsedMilliseconds > deadline.inMilliseconds) { + return false; + } else { + await Future.delayed(tick); + return true; + } + }); + + return completed; + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/pubspec.yaml b/local_packages/flutter_foreground_task-9.1.0/pubspec.yaml new file mode 100644 index 0000000..38e59d3 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/pubspec.yaml @@ -0,0 +1,29 @@ +name: flutter_foreground_task +description: This plugin is used to implement a foreground service on the Android platform. +version: 9.1.0 +homepage: https://github.com/Dev-hwang/flutter_foreground_task + +environment: + sdk: ^3.4.0 + flutter: ">=3.22.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.8 + shared_preferences: ^2.5.3 + platform: ^3.1.6 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + plugin: + platforms: + android: + package: com.pravera.flutter_foreground_task + pluginClass: FlutterForegroundTaskPlugin + ios: + pluginClass: FlutterForegroundTaskPlugin diff --git a/local_packages/flutter_foreground_task-9.1.0/test/dummy/service_dummy_data.dart b/local_packages/flutter_foreground_task-9.1.0/test/dummy/service_dummy_data.dart new file mode 100644 index 0000000..23895bd --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/test/dummy/service_dummy_data.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_foreground_task/models/service_options.dart'; +import 'package:platform/platform.dart'; + +@pragma('vm:entry-point') +void testCallback() { + print('test'); +} + +class ServiceDummyData { + final AndroidNotificationOptions androidNotificationOptions = + AndroidNotificationOptions( + channelId: 'test_channel', + channelName: 'Test Channel', + channelDescription: 'Test Channel Description', + channelImportance: NotificationChannelImportance.LOW, + priority: NotificationPriority.LOW, + enableVibration: false, + playSound: false, + showWhen: false, + showBadge: false, + visibility: NotificationVisibility.VISIBILITY_PUBLIC, + ); + + final IOSNotificationOptions iosNotificationOptions = + const IOSNotificationOptions( + showNotification: false, + playSound: false, + ); + + final ForegroundTaskOptions foregroundTaskOptions = ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(1000), + autoRunOnBoot: true, + autoRunOnMyPackageReplaced: true, + allowWifiLock: true, + allowWakeLock: true, + ); + + final int serviceId = 200; + + final String notificationTitle = 'title'; + + final String notificationText = 'test'; + + final NotificationIcon notificationIcon = const NotificationIcon( + metaDataName: 'test', + backgroundColor: Colors.orange, + ); + + final List notificationButtons = [ + const NotificationButton( + id: 'id_test1', text: 'test1', textColor: Colors.purple), + const NotificationButton( + id: 'id_test2', text: 'test2', textColor: Colors.green), + ]; + + Map getStartServiceArgs(Platform platform) { + return ServiceStartOptions( + serviceId: serviceId, + androidNotificationOptions: androidNotificationOptions, + iosNotificationOptions: iosNotificationOptions, + foregroundTaskOptions: foregroundTaskOptions, + notificationContentTitle: notificationTitle, + notificationContentText: notificationText, + notificationIcon: notificationIcon, + notificationButtons: notificationButtons, + callback: testCallback, + ).toJson(platform); + } + + Map getUpdateServiceArgs(Platform platform) { + return ServiceUpdateOptions( + foregroundTaskOptions: foregroundTaskOptions, + notificationContentTitle: notificationTitle, + notificationContentText: notificationText, + notificationIcon: notificationIcon, + notificationButtons: notificationButtons, + callback: testCallback, + ).toJson(platform); + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/test/service_api_test.dart b/local_packages/flutter_foreground_task-9.1.0/test/service_api_test.dart new file mode 100644 index 0000000..ea1615d --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/test/service_api_test.dart @@ -0,0 +1,633 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_method_channel.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform/platform.dart'; + +import 'dummy/service_dummy_data.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final ServiceDummyData dummyData = ServiceDummyData(); + + late MethodChannelFlutterForegroundTask platformChannel; + late ServiceApiMethodCallHandler methodCallHandler; + + setUp(() { + platformChannel = MethodChannelFlutterForegroundTask(); + FlutterForegroundTaskPlatform.instance = platformChannel; + FlutterForegroundTask.resetStatic(); + + methodCallHandler = + ServiceApiMethodCallHandler(() => platformChannel.platform); + + // method channel + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + platformChannel.mMDChannel, + methodCallHandler.onMethodCall, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformChannel.mMDChannel, null); + }); + + group('Android', () { + final Platform platform = FakePlatform(operatingSystem: Platform.android); + + test('init', () { + platformChannel.platform = platform; + + expect(FlutterForegroundTask.isInitialized, false); + expect(FlutterForegroundTask.androidNotificationOptions, isNull); + expect(FlutterForegroundTask.iosNotificationOptions, isNull); + expect(FlutterForegroundTask.foregroundTaskOptions, isNull); + + _init(dummyData); + + expect(FlutterForegroundTask.isInitialized, true); + expect( + FlutterForegroundTask.androidNotificationOptions, + dummyData.androidNotificationOptions, + ); + expect( + FlutterForegroundTask.iosNotificationOptions, + dummyData.iosNotificationOptions, + ); + expect( + FlutterForegroundTask.foregroundTaskOptions, + dummyData.foregroundTaskOptions, + ); + }); + + test('startService', () async { + platformChannel.platform = platform; + FlutterForegroundTask.skipServiceResponseCheck = true; + + _init(dummyData); + + final ServiceRequestResult result = await _startService(dummyData); + expect(result, isA()); + expect( + methodCallHandler.log.last, + isMethodCall( + ServiceApiMethod.startService, + arguments: dummyData.getStartServiceArgs(platform), + ), + ); + }); + + test('startService (error: ServiceNotInitializedException)', () async { + platformChannel.platform = platform; + + final ServiceRequestResult result = await _startService(dummyData); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('startService (error: ServiceAlreadyStartedException)', () async { + platformChannel.platform = platform; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + final ServiceRequestResult result2 = await _startService(dummyData); + expect(result2, isA()); + expect( + (result2 as ServiceRequestFailure).error, + isA(), + ); + }); + + test('startService (error: ServiceTimeoutException)', () async { + platformChannel.platform = platform; + + _init(dummyData); + + // set test + methodCallHandler.timeoutTest = true; + + final ServiceRequestResult result = await _startService(dummyData); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('restartService', () async { + platformChannel.platform = platform; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + final ServiceRequestResult result2 = await _restartService(); + expect(result2, isA()); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.restartService, arguments: null), + ); + }); + + test('restartService (error: ServiceNotStartedException)', () async { + platformChannel.platform = platform; + + final ServiceRequestResult result = await _restartService(); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('updateService', () async { + platformChannel.platform = platform; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + final ServiceRequestResult result2 = await _updateService(dummyData); + expect(result2, isA()); + expect( + methodCallHandler.log.last, + isMethodCall( + ServiceApiMethod.updateService, + arguments: dummyData.getUpdateServiceArgs(platform), + ), + ); + }); + + test('updateService (error: ServiceNotStartedException)', () async { + platformChannel.platform = platform; + + final ServiceRequestResult result = await _updateService(dummyData); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('stopService', () async { + platformChannel.platform = platform; + FlutterForegroundTask.skipServiceResponseCheck = true; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + final ServiceRequestResult result2 = await _stopService(); + expect(result2, isA()); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.stopService, arguments: null), + ); + }); + + test('stopService (error: ServiceNotStartedException)', () async { + platformChannel.platform = platform; + + final ServiceRequestResult result = await _stopService(); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('stopService (error: ServiceTimeoutException)', () async { + platformChannel.platform = platform; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + // set test + methodCallHandler.timeoutTest = true; + + final ServiceRequestResult result2 = await _stopService(); + expect(result2, isA()); + expect( + (result2 as ServiceRequestFailure).error, + isA(), + ); + }); + + test('isRunningService', () async { + platformChannel.platform = platform; + + _init(dummyData); + expect(await _isRunningService, false); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + + await _startService(dummyData); + expect(await _isRunningService, true); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + + await _restartService(); + expect(await _isRunningService, true); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + + await _updateService(dummyData); + expect(await _isRunningService, true); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + + await _stopService(); + expect(await _isRunningService, false); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + }); + }); + + group('iOS', () { + final Platform platform = FakePlatform(operatingSystem: Platform.iOS); + + test('init', () { + platformChannel.platform = platform; + + expect(FlutterForegroundTask.isInitialized, false); + expect(FlutterForegroundTask.androidNotificationOptions, isNull); + expect(FlutterForegroundTask.iosNotificationOptions, isNull); + expect(FlutterForegroundTask.foregroundTaskOptions, isNull); + + _init(dummyData); + + expect(FlutterForegroundTask.isInitialized, true); + expect( + FlutterForegroundTask.androidNotificationOptions, + dummyData.androidNotificationOptions, + ); + expect( + FlutterForegroundTask.iosNotificationOptions, + dummyData.iosNotificationOptions, + ); + expect( + FlutterForegroundTask.foregroundTaskOptions, + dummyData.foregroundTaskOptions, + ); + }); + + test('startService', () async { + platformChannel.platform = platform; + FlutterForegroundTask.skipServiceResponseCheck = true; + + _init(dummyData); + + final ServiceRequestResult result = await _startService(dummyData); + expect(result, isA()); + expect( + methodCallHandler.log.last, + isMethodCall( + ServiceApiMethod.startService, + arguments: dummyData.getStartServiceArgs(platform), + ), + ); + }); + + test('startService (error: ServiceNotInitializedException)', () async { + platformChannel.platform = platform; + + final ServiceRequestResult result = await _startService(dummyData); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('startService (error: ServiceAlreadyStartedException)', () async { + platformChannel.platform = platform; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + final ServiceRequestResult result2 = await _startService(dummyData); + expect(result2, isA()); + expect( + (result2 as ServiceRequestFailure).error, + isA(), + ); + }); + + test('startService (error: ServiceTimeoutException)', () async { + platformChannel.platform = platform; + + _init(dummyData); + + // set test + methodCallHandler.timeoutTest = true; + + final ServiceRequestResult result = await _startService(dummyData); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('restartService', () async { + platformChannel.platform = platform; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + final ServiceRequestResult result2 = await _restartService(); + expect(result2, isA()); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.restartService, arguments: null), + ); + }); + + test('restartService (error: ServiceNotStartedException)', () async { + platformChannel.platform = platform; + + final ServiceRequestResult result = await _restartService(); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('updateService', () async { + platformChannel.platform = platform; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + final ServiceRequestResult result2 = await _updateService(dummyData); + expect(result2, isA()); + expect( + methodCallHandler.log.last, + isMethodCall( + ServiceApiMethod.updateService, + arguments: dummyData.getUpdateServiceArgs(platform), + ), + ); + }); + + test('updateService (error: ServiceNotStartedException)', () async { + platformChannel.platform = platform; + + final ServiceRequestResult result = await _updateService(dummyData); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('stopService', () async { + platformChannel.platform = platform; + FlutterForegroundTask.skipServiceResponseCheck = true; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + final ServiceRequestResult result2 = await _stopService(); + expect(result2, isA()); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.stopService, arguments: null), + ); + }); + + test('stopService (error: ServiceNotStartedException)', () async { + platformChannel.platform = platform; + + final ServiceRequestResult result = await _stopService(); + expect(result, isA()); + expect( + (result as ServiceRequestFailure).error, + isA(), + ); + }); + + test('stopService (error: ServiceTimeoutException)', () async { + platformChannel.platform = platform; + + _init(dummyData); + + final ServiceRequestResult result1 = await _startService(dummyData); + expect(result1, isA()); + + // set test + methodCallHandler.timeoutTest = true; + + final ServiceRequestResult result2 = await _stopService(); + expect(result2, isA()); + expect( + (result2 as ServiceRequestFailure).error, + isA(), + ); + }); + + test('isRunningService', () async { + platformChannel.platform = platform; + + _init(dummyData); + expect(await _isRunningService, false); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + + await _startService(dummyData); + expect(await _isRunningService, true); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + + await _restartService(); + expect(await _isRunningService, true); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + + await _updateService(dummyData); + expect(await _isRunningService, true); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + + await _stopService(); + expect(await _isRunningService, false); + expect( + methodCallHandler.log.last, + isMethodCall(ServiceApiMethod.isRunningService, arguments: null), + ); + }); + }); +} + +void _init(ServiceDummyData dummyData) { + FlutterForegroundTask.init( + androidNotificationOptions: dummyData.androidNotificationOptions, + iosNotificationOptions: dummyData.iosNotificationOptions, + foregroundTaskOptions: dummyData.foregroundTaskOptions, + ); +} + +Future _startService(ServiceDummyData dummyData) { + return FlutterForegroundTask.startService( + serviceId: dummyData.serviceId, + notificationTitle: dummyData.notificationTitle, + notificationText: dummyData.notificationText, + notificationIcon: dummyData.notificationIcon, + notificationButtons: dummyData.notificationButtons, + callback: testCallback, + ); +} + +Future _restartService() { + return FlutterForegroundTask.restartService(); +} + +Future _updateService(ServiceDummyData dummyData) { + return FlutterForegroundTask.updateService( + foregroundTaskOptions: dummyData.foregroundTaskOptions, + notificationTitle: dummyData.notificationTitle, + notificationText: dummyData.notificationText, + notificationIcon: dummyData.notificationIcon, + notificationButtons: dummyData.notificationButtons, + callback: testCallback, + ); +} + +Future _stopService() { + return FlutterForegroundTask.stopService(); +} + +Future get _isRunningService { + return FlutterForegroundTask.isRunningService; +} + +class ServiceApiMethod { + static const String startService = 'startService'; + static const String restartService = 'restartService'; + static const String updateService = 'updateService'; + static const String stopService = 'stopService'; + static const String isRunningService = 'isRunningService'; + static const String attachedActivity = 'attachedActivity'; + + static Set getImplementation(Platform platform) { + if (platform.isAndroid) { + return { + startService, + restartService, + updateService, + stopService, + isRunningService, + attachedActivity, + }; + } else if (platform.isIOS) { + return { + startService, + restartService, + updateService, + stopService, + isRunningService, + }; + } + + return {}; + } +} + +class ServiceApiMethodCallHandler { + ServiceApiMethodCallHandler(this._platformGetter); + + final ValueGetter _platformGetter; + + final List log = []; + + bool timeoutTest = false; + + bool _isRunningService = false; + + // unimplemented: throw UnimplementedError + void _checkImplementation(String method) { + final Platform platform = _platformGetter(); + if (!ServiceApiMethod.getImplementation(platform).contains(method)) { + throw UnimplementedError( + 'Unimplemented method on ${platform.operatingSystem}: $method'); + } + } + + Future? onMethodCall(MethodCall methodCall) async { + final String method = methodCall.method; + _checkImplementation(method); + + log.add(methodCall); + + if (method == ServiceApiMethod.startService) { + if (!timeoutTest) { + _isRunningService = true; + } + return Future.value(); + } else if (method == ServiceApiMethod.restartService) { + if (!timeoutTest) { + _isRunningService = true; + } + return Future.value(); + } else if (method == ServiceApiMethod.updateService) { + if (!timeoutTest) { + _isRunningService = true; + } + return Future.value(); + } else if (method == ServiceApiMethod.stopService) { + if (!timeoutTest) { + _isRunningService = false; + } + return Future.value(); + } else if (method == ServiceApiMethod.isRunningService) { + return _isRunningService; + } else if (method == ServiceApiMethod.attachedActivity) { + return true; + } + + throw UnimplementedError(); + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/test/storage_test.dart b/local_packages/flutter_foreground_task-9.1.0/test/storage_test.dart new file mode 100644 index 0000000..b1dd474 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/test/storage_test.dart @@ -0,0 +1,130 @@ +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const TestData testString = TestData('testString', 'hello'); + const TestData testBool = TestData('testBool', false); + const TestData testInt = TestData('testInt', 1234); + const TestData testDouble = TestData('testDouble', 1.432); + + late TestDataStorage storage; + + setUp(() { + SharedPreferences.setMockInitialValues({}); + storage = TestDataStorage(); + }); + + test('saveData', () async { + expect(await storage.saveData(data: testString), true); + expect(await storage.saveData(data: testBool), true); + expect(await storage.saveData(data: testInt), true); + expect(await storage.saveData(data: testDouble), true); + }); + + test('getData', () async { + await storage.saveData(data: testString); + await storage.saveData(data: testBool); + await storage.saveData(data: testInt); + await storage.saveData(data: testDouble); + + final TestData? actualString = await storage.getData(key: testString.key); + expect(actualString, testString); + final TestData? actualBool = await storage.getData(key: testBool.key); + expect(actualBool, testBool); + final TestData? actualInt = await storage.getData(key: testInt.key); + expect(actualInt, testInt); + final TestData? actualDouble = await storage.getData(key: testDouble.key); + expect(actualDouble, testDouble); + }); + + test('getAllData', () async { + await storage.saveData(data: testString); + await storage.saveData(data: testBool); + await storage.saveData(data: testInt); + await storage.saveData(data: testDouble); + + final Map allData = await storage.getAllData(); + expect(allData, containsData(testString)); + expect(allData, containsData(testBool)); + expect(allData, containsData(testInt)); + expect(allData, containsData(testDouble)); + }); + + test('removeData', () async { + await storage.saveData(data: testString); + await storage.saveData(data: testInt); + + await storage.removeData(key: testInt.key); + final Map allData = await storage.getAllData(); + expect(allData.length, 1); + expect(allData, containsData(testString)); + expect(allData, isNot(containsData(testInt))); + }); + + test('clearAllData', () async { + await storage.saveData(data: testString); + await storage.saveData(data: testDouble); + + await storage.clearAllData(); + final Map allData = await storage.getAllData(); + expect(allData, isEmpty); + }); + + test('If the key already exists, the value will be replaced.', () async { + await storage.saveData(data: testString); + + // replace value + final TestData newTestString = TestData(testString.key, 'bye'); + await storage.saveData(data: newTestString); + + final TestData? replacedData = await storage.getData(key: testString.key); + expect(replacedData, newTestString); + }); +} + +Matcher containsData(TestData data) { + return containsPair(data.key, data.value); +} + +class TestData { + const TestData(this.key, this.value); + + final String key; + final T value; + + @override + bool operator ==(Object other) => + other is TestData && key == other.key && value == other.value; + + @override + int get hashCode => key.hashCode ^ value.hashCode; +} + +class TestDataStorage { + Future saveData({required TestData data}) { + return FlutterForegroundTask.saveData(key: data.key, value: data.value); + } + + Future?> getData({required String key}) async { + final T? value = await FlutterForegroundTask.getData(key: key); + if (value == null) { + return null; + } + return TestData(key, value); + } + + Future> getAllData() { + return FlutterForegroundTask.getAllData(); + } + + Future removeData({required String key}) { + return FlutterForegroundTask.removeData(key: key); + } + + Future clearAllData() { + return FlutterForegroundTask.clearAllData(); + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/test/task_handler_test.dart b/local_packages/flutter_foreground_task-9.1.0/test/task_handler_test.dart new file mode 100644 index 0000000..5ff5095 --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/test/task_handler_test.dart @@ -0,0 +1,321 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_method_channel.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MethodChannelFlutterForegroundTask platformChannel; + late TestTaskHandler taskHandler; + + setUp(() { + platformChannel = MethodChannelFlutterForegroundTask(); + FlutterForegroundTaskPlatform.instance = platformChannel; + FlutterForegroundTask.resetStatic(); + + taskHandler = TestTaskHandler(); + + // method channel + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + platformChannel.mMDChannel, + (MethodCall methodCall) async { + final String method = methodCall.method; + if (method == 'sendData') { + final dynamic data = methodCall.arguments; + platformChannel.mBGChannel + .invokeMethod(TaskEventMethod.onReceiveData, data); + } + return; + }, + ); + + // background channel + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + platformChannel.mBGChannel, + (MethodCall methodCall) { + return platformChannel.onBackgroundChannel(methodCall, taskHandler); + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformChannel.mMDChannel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformChannel.mBGChannel, null); + }); + + group('TaskHandler', () { + test('onStart', () async { + const String method = TaskEventMethod.onStart; + const TaskStarter starter = TaskStarter.developer; + + await platformChannel.mBGChannel.invokeMethod(method, starter.index); + expect(taskHandler.log.last, isTaskEvent(method, starter.index)); + }); + + test('onRepeatEvent', () async { + const String method = TaskEventMethod.onRepeatEvent; + + await platformChannel.mBGChannel.invokeMethod(method); + expect(taskHandler.log.last, isTaskEvent(method)); + }); + + test('onDestroy', () async { + const String method = TaskEventMethod.onDestroy; + + await platformChannel.mBGChannel.invokeMethod(method); + expect(taskHandler.log.last, isTaskEvent(method, false)); + }); + + test('onReceiveData', () async { + const String method = TaskEventMethod.onReceiveData; + + const String stringData = 'hello'; + await platformChannel.mBGChannel.invokeMethod(method, stringData); + expect(taskHandler.log.last, isTaskEvent(method, stringData)); + + const int intData = 1234; + await platformChannel.mBGChannel.invokeMethod(method, intData); + expect(taskHandler.log.last, isTaskEvent(method, intData)); + + const double doubleData = 1.234; + await platformChannel.mBGChannel.invokeMethod(method, doubleData); + expect(taskHandler.log.last, isTaskEvent(method, doubleData)); + + const bool boolData = false; + await platformChannel.mBGChannel.invokeMethod(method, boolData); + expect(taskHandler.log.last, isTaskEvent(method, boolData)); + + const List listData = [1, 2, 3]; + await platformChannel.mBGChannel.invokeMethod(method, listData); + expect(taskHandler.log.last, isTaskEvent(method, listData)); + + const Map mapData = {'message': 'hello', 'data': 1}; + await platformChannel.mBGChannel.invokeMethod(method, mapData); + expect(taskHandler.log.last, isTaskEvent(method, mapData)); + }); + + test('onNotificationButtonPressed', () async { + const String method = TaskEventMethod.onNotificationButtonPressed; + + const String data = 'id_hello'; + await platformChannel.mBGChannel.invokeMethod(method, data); + expect(taskHandler.log.last, isTaskEvent(method, data)); + }); + + test('onNotificationPressed', () async { + const String method = TaskEventMethod.onNotificationPressed; + + await platformChannel.mBGChannel.invokeMethod(method); + expect(taskHandler.log.last, isTaskEvent(method)); + }); + + test('onNotificationDismissed', () async { + const String method = TaskEventMethod.onNotificationDismissed; + + await platformChannel.mBGChannel.invokeMethod(method); + expect(taskHandler.log.last, isTaskEvent(method)); + }); + }); + + group('CommunicationPort', () { + test('initCommunicationPort', () { + FlutterForegroundTask.initCommunicationPort(); + expect(FlutterForegroundTask.receivePort, isNotNull); + expect(FlutterForegroundTask.streamSubscription, isNotNull); + expect(FlutterForegroundTask.dataCallbacks, isEmpty); + }); + + test('addTaskDataCallback (case: other callback)', () { + FlutterForegroundTask.addTaskDataCallback((_) {}); + expect(FlutterForegroundTask.dataCallbacks.length, 1); + + FlutterForegroundTask.addTaskDataCallback((_) {}); + expect(FlutterForegroundTask.dataCallbacks.length, 2); + }); + + test('addTaskDataCallback (case: same callback)', () { + dataCallback(_) {} + + FlutterForegroundTask.addTaskDataCallback(dataCallback); + expect(FlutterForegroundTask.dataCallbacks.length, 1); + + FlutterForegroundTask.addTaskDataCallback(dataCallback); + expect(FlutterForegroundTask.dataCallbacks.length, 1); + }); + + test('removeTaskDataCallback', () { + dataCallback1(_) {} + dataCallback2(_) {} + + FlutterForegroundTask.addTaskDataCallback(dataCallback1); + FlutterForegroundTask.addTaskDataCallback(dataCallback2); + expect(FlutterForegroundTask.dataCallbacks.length, 2); + + FlutterForegroundTask.removeTaskDataCallback(dataCallback1); + expect(FlutterForegroundTask.dataCallbacks.length, 1); + + FlutterForegroundTask.removeTaskDataCallback(dataCallback2); + expect(FlutterForegroundTask.dataCallbacks.length, 0); + }); + + test('sendDataToTask', () { + const String method = TaskEventMethod.onReceiveData; + + const String stringData = 'hello'; + FlutterForegroundTask.sendDataToTask(stringData); + expect(taskHandler.log.last, isTaskEvent(method, stringData)); + + const int intData = 1234; + FlutterForegroundTask.sendDataToTask(intData); + expect(taskHandler.log.last, isTaskEvent(method, intData)); + + const double doubleData = 1.234; + FlutterForegroundTask.sendDataToTask(doubleData); + expect(taskHandler.log.last, isTaskEvent(method, doubleData)); + + const bool boolData = false; + FlutterForegroundTask.sendDataToTask(boolData); + expect(taskHandler.log.last, isTaskEvent(method, boolData)); + + const List listData = [1, 2, 3]; + FlutterForegroundTask.sendDataToTask(listData); + expect(taskHandler.log.last, isTaskEvent(method, listData)); + + const Map mapData = {'message': 'hello', 'data': 1}; + FlutterForegroundTask.sendDataToTask(mapData); + expect(taskHandler.log.last, isTaskEvent(method, mapData)); + }); + }); +} + +Matcher isTaskEvent(String method, [dynamic data]) { + return _IsTaskEvent(method, data); +} + +class _IsTaskEvent extends Matcher { + const _IsTaskEvent(this.method, this.data); + + final String method; + final dynamic data; + + @override + bool matches(dynamic item, Map matchState) { + if (item is! TaskEvent) { + return false; + } + if (item.method != method) { + return false; + } + return _deepEquals(item.data, data); + } + + bool _deepEquals(dynamic a, dynamic b) { + if (a == b) { + return true; + } + if (a is List) { + return b is List && _deepEqualsList(a, b); + } + if (a is Map) { + return b is Map && _deepEqualsMap(a, b); + } + return false; + } + + bool _deepEqualsList(List a, List b) { + if (a.length != b.length) { + return false; + } + for (int i = 0; i < a.length; i++) { + if (!_deepEquals(a[i], b[i])) { + return false; + } + } + return true; + } + + bool _deepEqualsMap(Map a, Map b) { + if (a.length != b.length) { + return false; + } + for (final dynamic key in a.keys) { + if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) { + return false; + } + } + return true; + } + + @override + Description describe(Description description) { + return description + .add('has method: ') + .addDescriptionOf(method) + .add(' with data: ') + .addDescriptionOf(data); + } +} + +class TaskEvent { + const TaskEvent({required this.method, this.data}); + + final String method; + final dynamic data; +} + +class TaskEventMethod { + static const String onStart = 'onStart'; + static const String onRepeatEvent = 'onRepeatEvent'; + static const String onDestroy = 'onDestroy'; + static const String onReceiveData = 'onReceiveData'; + static const String onNotificationButtonPressed = + 'onNotificationButtonPressed'; + static const String onNotificationPressed = 'onNotificationPressed'; + static const String onNotificationDismissed = 'onNotificationDismissed'; +} + +class TestTaskHandler extends TaskHandler { + final List log = []; + + @override + Future onStart(DateTime timestamp, TaskStarter starter) async { + log.add(TaskEvent(method: TaskEventMethod.onStart, data: starter.index)); + } + + @override + void onRepeatEvent(DateTime timestamp) { + log.add(const TaskEvent(method: TaskEventMethod.onRepeatEvent)); + } + + @override + Future onDestroy(DateTime timestamp, bool isTimeout) async { + log.add(TaskEvent(method: TaskEventMethod.onDestroy, data: isTimeout)); + } + + @override + void onReceiveData(Object data) { + log.add(TaskEvent(method: TaskEventMethod.onReceiveData, data: data)); + } + + @override + void onNotificationButtonPressed(String id) { + log.add(TaskEvent( + method: TaskEventMethod.onNotificationButtonPressed, data: id)); + } + + @override + void onNotificationPressed() { + log.add(const TaskEvent(method: TaskEventMethod.onNotificationPressed)); + } + + @override + void onNotificationDismissed() { + log.add(const TaskEvent(method: TaskEventMethod.onNotificationDismissed)); + } +} diff --git a/local_packages/flutter_foreground_task-9.1.0/test/utility_test.dart b/local_packages/flutter_foreground_task-9.1.0/test/utility_test.dart new file mode 100644 index 0000000..a360f1c --- /dev/null +++ b/local_packages/flutter_foreground_task-9.1.0/test/utility_test.dart @@ -0,0 +1,495 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_method_channel.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform/platform.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MethodChannelFlutterForegroundTask platformChannel; + late UtilityMethodCallHandler methodCallHandler; + + setUp(() { + platformChannel = MethodChannelFlutterForegroundTask(); + FlutterForegroundTaskPlatform.instance = platformChannel; + FlutterForegroundTask.resetStatic(); + + methodCallHandler = + UtilityMethodCallHandler(() => platformChannel.platform); + + // method channel + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + platformChannel.mMDChannel, + methodCallHandler.onMethodCall, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(platformChannel.mMDChannel, null); + }); + + group('Android', () { + const String platform = Platform.android; + + test('minimizeApp', () { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + FlutterForegroundTask.minimizeApp(); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.minimizeApp, arguments: null), + ); + }); + + test('launchApp', () { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + FlutterForegroundTask.launchApp(); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.launchApp, arguments: null), + ); + + FlutterForegroundTask.launchApp('/root'); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.launchApp, arguments: '/root'), + ); + }); + + test('setOnLockScreenVisibility', () { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + FlutterForegroundTask.setOnLockScreenVisibility(true); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.setOnLockScreenVisibility, + arguments: true, + ), + ); + + FlutterForegroundTask.setOnLockScreenVisibility(false); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.setOnLockScreenVisibility, + arguments: false, + ), + ); + }); + + test('isAppOnForeground', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = await FlutterForegroundTask.isAppOnForeground; + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.isAppOnForeground, arguments: null), + ); + }); + + test('wakeUpScreen', () { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + FlutterForegroundTask.wakeUpScreen(); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.wakeUpScreen, arguments: null), + ); + }); + + test('isIgnoringBatteryOptimizations', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.isIgnoringBatteryOptimizations; + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.isIgnoringBatteryOptimizations, + arguments: null, + ), + ); + }); + + test('openIgnoreBatteryOptimizationSettings', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.openIgnoreBatteryOptimizationSettings(); + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.openIgnoreBatteryOptimizationSettings, + arguments: null, + ), + ); + }); + + test('requestIgnoreBatteryOptimization', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.requestIgnoreBatteryOptimization(); + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.requestIgnoreBatteryOptimization, + arguments: null, + ), + ); + }); + + test('canDrawOverlays', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = await FlutterForegroundTask.canDrawOverlays; + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.canDrawOverlays, arguments: null), + ); + }); + + test('openSystemAlertWindowSettings', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.openSystemAlertWindowSettings(); + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.openSystemAlertWindowSettings, + arguments: null, + ), + ); + }); + + test('checkNotificationPermission', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final NotificationPermission permission = + await FlutterForegroundTask.checkNotificationPermission(); + expect(permission, NotificationPermission.granted); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.checkNotificationPermission, + arguments: null, + ), + ); + }); + + test('requestNotificationPermission', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final NotificationPermission permission = + await FlutterForegroundTask.requestNotificationPermission(); + expect(permission, NotificationPermission.granted); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.requestNotificationPermission, + arguments: null, + ), + ); + }); + + test('canScheduleExactAlarms', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = await FlutterForegroundTask.canScheduleExactAlarms; + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.canScheduleExactAlarms, arguments: null), + ); + }); + + test('openAlarmsAndRemindersSettings', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.openAlarmsAndRemindersSettings(); + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.openAlarmsAndRemindersSettings, + arguments: null, + ), + ); + }); + }); + + group('iOS', () { + const String platform = Platform.iOS; + + test('minimizeApp', () { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + FlutterForegroundTask.minimizeApp(); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.minimizeApp, arguments: null), + ); + }); + + test('launchApp', () { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + FlutterForegroundTask.launchApp(); + expect(methodCallHandler.log, isEmpty); + + FlutterForegroundTask.launchApp('/root'); + expect(methodCallHandler.log, isEmpty); + }); + + test('setOnLockScreenVisibility', () { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + FlutterForegroundTask.setOnLockScreenVisibility(true); + expect(methodCallHandler.log, isEmpty); + + FlutterForegroundTask.setOnLockScreenVisibility(false); + expect(methodCallHandler.log, isEmpty); + }); + + test('isAppOnForeground', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = await FlutterForegroundTask.isAppOnForeground; + expect(result, false); + expect( + methodCallHandler.log.last, + isMethodCall(UtilityMethod.isAppOnForeground, arguments: null), + ); + }); + + test('wakeUpScreen', () { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + FlutterForegroundTask.wakeUpScreen(); + expect(methodCallHandler.log, isEmpty); + }); + + test('isIgnoringBatteryOptimizations', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.isIgnoringBatteryOptimizations; + expect(result, true); + expect(methodCallHandler.log, isEmpty); + }); + + test('openIgnoreBatteryOptimizationSettings', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.openIgnoreBatteryOptimizationSettings(); + expect(result, true); + expect(methodCallHandler.log, isEmpty); + }); + + test('requestIgnoreBatteryOptimization', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.requestIgnoreBatteryOptimization(); + expect(result, true); + expect(methodCallHandler.log, isEmpty); + }); + + test('canDrawOverlays', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = await FlutterForegroundTask.canDrawOverlays; + expect(result, true); + expect(methodCallHandler.log, isEmpty); + }); + + test('openSystemAlertWindowSettings', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.openSystemAlertWindowSettings(); + expect(result, true); + expect(methodCallHandler.log, isEmpty); + }); + + test('checkNotificationPermission', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final NotificationPermission permission = + await FlutterForegroundTask.checkNotificationPermission(); + expect(permission, NotificationPermission.granted); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.checkNotificationPermission, + arguments: null, + ), + ); + }); + + test('requestNotificationPermission', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final NotificationPermission permission = + await FlutterForegroundTask.requestNotificationPermission(); + expect(permission, NotificationPermission.granted); + expect( + methodCallHandler.log.last, + isMethodCall( + UtilityMethod.requestNotificationPermission, + arguments: null, + ), + ); + }); + + test('canScheduleExactAlarms', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = await FlutterForegroundTask.canScheduleExactAlarms; + expect(result, true); + expect(methodCallHandler.log, isEmpty); + }); + + test('openAlarmsAndRemindersSettings', () async { + platformChannel.platform = FakePlatform(operatingSystem: platform); + + final bool result = + await FlutterForegroundTask.openAlarmsAndRemindersSettings(); + expect(result, true); + expect(methodCallHandler.log, isEmpty); + }); + }); +} + +class UtilityMethod { + static const String minimizeApp = 'minimizeApp'; + static const String launchApp = 'launchApp'; + static const String setOnLockScreenVisibility = 'setOnLockScreenVisibility'; + static const String isAppOnForeground = 'isAppOnForeground'; + static const String wakeUpScreen = 'wakeUpScreen'; + static const String isIgnoringBatteryOptimizations = + 'isIgnoringBatteryOptimizations'; + static const String openIgnoreBatteryOptimizationSettings = + 'openIgnoreBatteryOptimizationSettings'; + static const String requestIgnoreBatteryOptimization = + 'requestIgnoreBatteryOptimization'; + static const String canDrawOverlays = 'canDrawOverlays'; + static const String openSystemAlertWindowSettings = + 'openSystemAlertWindowSettings'; + static const String checkNotificationPermission = + 'checkNotificationPermission'; + static const String requestNotificationPermission = + 'requestNotificationPermission'; + static const String canScheduleExactAlarms = 'canScheduleExactAlarms'; + static const String openAlarmsAndRemindersSettings = + 'openAlarmsAndRemindersSettings'; + + static Set getImplementation(Platform platform) { + if (platform.isAndroid) { + return { + minimizeApp, + launchApp, + setOnLockScreenVisibility, + isAppOnForeground, + wakeUpScreen, + isIgnoringBatteryOptimizations, + openIgnoreBatteryOptimizationSettings, + requestIgnoreBatteryOptimization, + canDrawOverlays, + openSystemAlertWindowSettings, + checkNotificationPermission, + requestNotificationPermission, + canScheduleExactAlarms, + openAlarmsAndRemindersSettings, + }; + } else if (platform.isIOS) { + return { + minimizeApp, + isAppOnForeground, + checkNotificationPermission, + requestNotificationPermission, + }; + } + + return {}; + } +} + +class UtilityMethodCallHandler { + UtilityMethodCallHandler(this._platformGetter); + + final ValueGetter _platformGetter; + + final List log = []; + + // unimplemented: throw UnimplementedError + void _checkImplementation(String method) { + final Platform platform = _platformGetter(); + if (!UtilityMethod.getImplementation(platform).contains(method)) { + throw UnimplementedError( + 'Unimplemented method on ${platform.operatingSystem}: $method'); + } + } + + Future? onMethodCall(MethodCall methodCall) async { + final String method = methodCall.method; + _checkImplementation(method); + + log.add(methodCall); + + final dynamic arguments = methodCall.arguments; + if (method == UtilityMethod.minimizeApp) { + return Future.value(); + } else if (method == UtilityMethod.launchApp) { + return Future.value(); + } else if (method == UtilityMethod.setOnLockScreenVisibility) { + if (arguments == null) { + throw ArgumentError('The isVisible argument cannot be found.'); + } + if (arguments is! bool) { + throw ArgumentError('The isVisible argument is not of type boolean.'); + } + return Future.value(); + } else if (method == UtilityMethod.isAppOnForeground) { + return false; + } else if (method == UtilityMethod.wakeUpScreen) { + return Future.value(); + } else if (method == UtilityMethod.isIgnoringBatteryOptimizations) { + return false; + } else if (method == UtilityMethod.openIgnoreBatteryOptimizationSettings) { + return false; + } else if (method == UtilityMethod.requestIgnoreBatteryOptimization) { + return false; + } else if (method == UtilityMethod.canDrawOverlays) { + return false; + } else if (method == UtilityMethod.openSystemAlertWindowSettings) { + return false; + } else if (method == UtilityMethod.checkNotificationPermission) { + return NotificationPermission.granted.index; + } else if (method == UtilityMethod.requestNotificationPermission) { + return NotificationPermission.granted.index; + } else if (method == UtilityMethod.canScheduleExactAlarms) { + return false; + } else if (method == UtilityMethod.openAlarmsAndRemindersSettings) { + return false; + } + + throw UnimplementedError(); + } +} diff --git a/local_packages/on_audio_query-2.9.0/CHANGELOG.md b/local_packages/on_audio_query-2.9.0/CHANGELOG.md new file mode 100644 index 0000000..e40280c --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/CHANGELOG.md @@ -0,0 +1,1155 @@ +## [[2.9.0](https://github.com/LucJosin/on_audio_query/releases/tag/2.9.0)] + +### Features + +- **Added** support to Dart 3. + +## [[2.8.1](https://github.com/LucJosin/on_audio_query/releases/tag/2.8.1)] + +### Fixes + +- **Fixed** broken pubspec links. - [#115](https://github.com/LucJosin/on_audio_query/issues/115) + +#### iOS + +- **Fixed** wrong name of podspec in iOS. - [#116](https://github.com/LucJosin/on_audio_query/issues/116) + +### Changes + +- **Updated** dart-analyzer to support cache +- **Updated** README + +## [[2.8.0](https://github.com/LucJosin/on_audio_query/releases/tag/2.8.0)] + +### Features + +- **Added** `showDetailedLog`. + +### Changes + +- **Moved** `android` and `ios` into separated folders. +- **Replaced** `/details` with `/src`. + +### ⚠ Important Changes + +#### Android + +- **Updated** kotlin version from `1.4.32` to `1.6.10`. - [#110](https://github.com/LucJosin/on_audio_query/issues/110) +- **Updated** kotlin coroutines version from `1.5.2-native-mt` to `1.6.4`. + +## [[2.7.0](https://github.com/LucJosin/on_audio_query/releases/tag/2.7.0)] - [03.29.2023] + +### Features + +- **Added** `[LogType]`. +- **Added** `[LogConfig]`. +- **Added** `[PermissionController]` **(Native)** +- **Added** `[PluginProvider]` **(Native)** +- **Added** `[setLogConfig]` method. +- **Added** `[checkAndRequest]` method. +- **Added** `[controller]` to `[QueryArtworkWidget]`. +- **Added** `[retryRequest]` param to `[checkAndRequest]` and `[permissionsRequest]`. + +### Fixes + +#### Android + +- **Fixed** crash after request permission. - [#68](https://github.com/LucJosin/on_audio_query/issues/68) +- **Fixed** quality always being sent as `200` using `[queryArtwork]`. + +### Changes + +- **Updated** example. +- **Renamed** natives files/folders. +- **Reduced** the default `artwork` resolution (from 100 to 50). +- **Updated** `[QueryArtworkWidget]` params. +- **Updated** quality assert on `[QueryArtworkWidget]`. + +### ⚠ Important Changes + +- **Updated** application permission check. + - If application doesn't have permission to access the library, will throw a PlatformException. +- **Updated** `quality` param from `[QueryArtworkWidget]`. + - This param cannot be defined as null anymore and, by default, will be set to `50`. +- **Updated** minimum supported **Dart** version. + - Increase minimum version from `2.12` to `2.17`. + +## [2.6.2] - [03.03.2023] + +### Fixes + +#### Android + +- **Fixed** incompatibility with `Android 13`. - [#91](https://github.com/LucJosin/on_audio_query/issues/91) - Thanks [@ruchit-7span](https://github.com/ruchit-7span) + +## [2.6.1] - [05.17.2022] + +### Fixes + +#### Android + +- **Fixed** incompatibility with `Flutter 3`. - [#78](https://github.com/LucJosin/on_audio_query/issues/78) + +## [2.6.0] - [02.01.2022] + +### Features + +- **Added** `[scanMedia]` method that will scan the given path and update the `[Android]` MediaStore. + +### Fixes + +- **Fixed** media showing when calling `[querySongs]` even after deleting with `[dart:io]`. - [#67](https://github.com/LucJosin/on_audio_query/issues/67) + +### Changes + +- **Updated** some required packages. + +### Documentation + +- Updated `README` documentation. +- Updated `DEPRECATED` documentation. +- Updated `PLATFORMS` documentation. +- Updated some `broken` links. + +## [2.5.3+1] - [01.20.2022] + +### Changes + +- **Updated** all Github links. + +## [2.5.3] - [11.10.2021] + +### Fixes + +#### IOS + +- **Fixed** song/artist/album from `Apple Music` returning when 'querying' - [#61](https://github.com/LucJosin/on_audio_query/issues/61) +- **Fixed** wrong `artistId` returning from `[AlbumModel]` - [#60](https://github.com/LucJosin/on_audio_query/issues/60) + +### Documentation + +- Updated `README` documentation. + +## [2.5.2] - [10.25.2021] + +### Fixes + +#### Android + +- **Fixed** wrong value returning from: - [#56](https://github.com/LucJosin/on_audio_query/issues/56) + - `[is_music]`. + - `[is_alarm]`. + - `[is_notification]`. + - `[is_ringtone]`. + - `[is_podcast]`. + - `[is_audiobook]`. + +### Documentation + +- Updated `README` documentation. + +## [2.5.1] - [10.19.2021] + +### Fixes + +#### Dart + +- **Fixed** wrong value returning from `[artistId]` when using `[AlbumModel]`. - [#54](https://github.com/LucJosin/on_audio_query/issues/54) + +#### Android + +- **Fixed** missing songs from `[queryAudiosFrom]` when using `GENRE`. - [#46](https://github.com/LucJosin/on_audio_query/issues/46) + +### Documentation + +- Updated `README` documentation. + +### ⚠ Important Changes + +#### Dart + +- Now `[artistId]` from `[AlbumModel]` return a `[int]`. + +## [2.5.0] - [10.15.2021] + +### Release + +- `[2.5.0]` release. + +### Features + +#### Dart + +- **Added** `errorBuilder` and `frameBuilder` to `[QueryArtworkWidget]`. + +### Fixes + +#### Web + +- **Fixed** empty result when using `[querySongs]`. +- **Fixed** error when decoding some images. + +See all development [changes](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md): + +- [2.5.0-alpha.0](#250-alpha0---10152021) + +## [2.5.0-alpha.0] - [10.15.2021] + +### Features + +#### All platforms + +- **Added** `artwork` to genres. - [#41](https://github.com/LucJosin/on_audio_query/issues/41) +- **Added** `sortType`, `orderType` and `ignoreCase` to `[queryAudiosFrom]`. + +#### Android + +- Re-**Added** `path` parameter to `[querySongs]`. - [#48](https://github.com/LucJosin/on_audio_query/issues/48) + +#### Web + +- **Added** `path` parameter to `[querySongs]`. + +### Fixes + +#### Android + +- **Fixed** empty `Uint8List` when using `[queryArtwork]` on Android 7. - [#47](https://github.com/LucJosin/on_audio_query/issues/47) +- **Fixed** null `albumId` when using Android 9 or below. - [#53](https://github.com/LucJosin/on_audio_query/issues/53) + +### Documentation + +- Updated `README` documentation. New `[queryAudiosFrom]` section. +- Updated `DEPRECATED` documentation. + +### Changes + +- Downgraded `Kotlin` and `Gradle` version. - [#51](https://github.com/LucJosin/on_audio_query/issues/51) + +### ⚠ Important Changes + +#### @Deprecated + +- `[albumId]` from `[AlbumModel]`. + - Use `[id]` instead. + +## [2.4.2] - [10.01.2021] + +### Fixes + +#### IOS + +- **Fixed** no artwork returning from `[queryArtwork]` when using `ArtworkType.ALBUM`. - [#45](https://github.com/LucJosin/on_audio_query/issues/45) + +### Documentation + +- Updated `README` documentation. + +## [2.4.1] - [09.29.2021] + +### Fixes + +#### Dart + +- **Fixed** wrong type of `numOfSongs` from `[SongModel]`. - [#39](https://github.com/LucJosin/on_audio_query/issues/39) + +#### IOS + +- **Fixed** wrong filter configuration when using `[queryWithFilters]`. +- **Fixed** crash when using any `'query'` method with a null `sortType`. - [#43](https://github.com/LucJosin/on_audio_query/issues/43) +- **Fixed** error with wrong `[MPMediaQuery]` filter. - [#38](https://github.com/LucJosin/on_audio_query/issues/38) + +### Documentation + +- Updated `README` documentation. + +## [2.4.0] - [09.28.2021] + +### Features + +#### Android + +- **Added** a better 'search' method to `[queryWithFilters]`, now the query uses 'contains' when 'querying'. - [#35](https://github.com/LucJosin/on_audio_query/issues/35) + +### Fixes + +#### IOS + +- **Fixed** error with wrong `[MPMediaQuery]` type and wrong value from `[jpegData]`. - [#37](https://github.com/LucJosin/on_audio_query/issues/37) + +#### Documentation + +- Updated broken `README` links. - [#36](https://github.com/LucJosin/on_audio_query/issues/36) + +### Documentation + +- Updated `README` documentation. + +## [2.3.1] - [09.27.2021] + +### Features + +#### Android/Web + +- **Added** `[ignoreCase]` to: + - `[querySongs]`. + - `[queryAlbums]`. + - `[queryArtists]`. + - `[queryPlaylists]`. + - `[queryGenres]`. + +### Fixes + +#### Android + +- **Fixed** `error` when trying to build using `Android`. - [#32](https://github.com/LucJosin/on_audio_query/issues/32) & [#33](https://github.com/LucJosin/on_audio_query/issues/33) +- **Fixed** `error` related to android song projection. - [#31](https://github.com/LucJosin/on_audio_query/issues/31) +- **Fixed** `'bug'` when using `SongSortType.TITLE`. This is now a `'feature'` and can be controlled using `[ignoreCase]`. - [#29](https://github.com/LucJosin/on_audio_query/issues/29) + +### Documentation + +- Updated `README` documentation. + +### Changes + +#### Android + +- Updated `[Kotlin]` and `[Dependencies]` versions. +- Moved from `[JCenter]` to `[MavenCentral]`. + +## [2.3.0] - [09.25.2021] + +### Features + +#### Android/IOS/Web + +- **Added** `[numOfSongs]` to `[PlaylistModel]` and `[GenreModel]`. +- **Added** `Playlist` and `Artist` to `ArtworkType`. + +#### Android/IOS + +- **Added** `quality` to `queryArtwork`. + +#### Android + +- **Added** `[isAudioBook]`, `[Genre]` and `[GenreId]` to `[SongModel]`. +- Re-**Added** to `[SongModel]`: + - `[isAlarm]`. + - `[isMusic]`. + - `[isNotification]`. + - `[isPodcast]`. + - `[isRingtone]`. + +### Fixes + +#### Android + +- **Fixed** wrong value returning from `[id]` when using `[ArtistModel]`. +- **Fixed** wrong value returning from `[id]` when using `[GenreModel]`. +- **Fixed** no value returning from `[queryAudiosFrom]` when using `ARTIST_ID`. + +### Documentation + +- Updated `README` documentation. +- Updated `OnAudioQuery` and `OnAudioQueryExample` documentation to support new `[Flutter 2.5]`. + +### Changes + +- **[Changed]** wrong name `DATA_ADDED` to `DATE_ADDED` for both `[SongSortType]` and `[PlaylistSortType]`. - [#27](https://github.com/LucJosin/on_audio_query/issues/27) + +### ⚠ Important Changes + +#### Dart + +- The parameter `args` from `[queryWithFilters]` is no longer required. + +#### @Deprecated + +- `[DEFAULT]` from `[SongSortType]`. +- `[DEFAULT]` from `[PlaylistSortType]`. +- `[DEFAULT]` from `[ArtistSortType]`. +- `[DEFAULT]` from `[AlbumSortType]`. +- `[DEFAULT]` from `[GenreSortType]`. +- `[ARTIST_KEY]` from `[ArtistSortType]`. +- `[ARTIST_NAME]` from `[ArtistSortType]`. +- `[ALBUM_NAME]` from `[AlbumSortType]`. +- `[GENRE_NAME]` from `[GenreSortType]`. +- `[DATA_ADDED]` from `[SongSortType]`. +- `[DATA_ADDED]` from `[PlaylistSortType]`. + + + + +## [2.2.0] - [08.25.2021] + +### Features + +#### IOS + +- Added a `filter` to avoid cloud audios/songs. + +### Fixes + +#### IOS + +- **Fixed** wrong value returning from `[permissionsStatus]`. - [#24](https://github.com/LucJosin/on_audio_query/issues/24) + +### Documentation + +- Updated `README` documentation. + +## [2.1.2] - [08.24.2021] + +### Fixes + +#### Android + +- **Fixed** duplicate `media` from `[queryWithFilters]`. +- **Fixed** `crash` when calling `[queryWithFilters]`. - [#23](https://github.com/LucJosin/on_audio_query/issues/23) +- **Fixed** `null` artwork returning from `[queryArtwork]` on Android 11/R. - [#21](https://github.com/LucJosin/on_audio_query/issues/21) + +### Documentation + +- Updated `README` documentation. +- Updated `pubspec` documentation. + +## [2.1.1] - [08.23.2021] + +### Fixes + +#### Android + +- **Fixed** error when using `[removeFromPlaylist]`. - [#22](https://github.com/LucJosin/on_audio_query/issues/22) + +### Documentation + +- Updated `README` documentation. +- Updated `[OnAudioQueryExample]` to support `[Web]` platform. + +## [2.1.0] - [08.23.2021] + +### Features + +#### on_audio_query + +- The plugin now supports `[Web]`. +- The plugin now utilize `[Platform interface]` package. + +#### Web + +- Added: + - `[querySongs]`. + - `[queryAlbums]`. + - `[queryArtists]`. + - `[queryGenres]`. + - `[queryAudiosFrom]`. + - `[queryWithFilters]`. + - `[queryArtwork]`. + - `[queryDeviceInfo]`. + +### Documentation + +- Updated `on_audio_query` documentation. +- Updated `README` documentation. +- Updated `PLATFORMS` documentation. +- Added documentation to `Web` platform. + +## [2.0.0] - [08.17.2021] + +### Release + +- `[2.0.0]` release. + +See all development [changes](https://github.com/LucJosin/on_audio_query/blob/main/CHANGELOG.md): + +- [2.0.0-beta.3](#200-beta3---08172021---github-only) +- [2.0.0-beta.2](#200-beta2---08152021) +- [2.0.0-beta.1](#200-beta1---08142021) +- [2.0.0-beta.0](#200-beta0---08132021) +- [2.0.0-alpha.1](#200-alpha1---08082021---github-only) +- [2.0.0-alpha.0](#200-alpha0---08052021---github-only) +- [2.0.0-dev.1](#200-dev1---08052021---internal) +- [2.0.0-dev.0](#200-dev0---08022021---internal) + +## [2.0.0-beta.3] - [08.17.2021] - [GitHub Only] + +### Features + +#### Android + +- Now **ALL** methods will only `"query"` if has permission to `READ`. + +### Fixes + +#### Android + +- **Fixed** no value returning when using `[permissionsRequest]`. + +### Documentation + +- Updated `README` documentation. +- Added more documentation to `Android` platform. + +## [2.0.0-beta.2] - [08.15.2021] + +### Features + +#### IOS + +- Now **ALL** methods will only `"query"` if has permission to `Library`. +- Added `[addToPlaylist]`. + +#### Dart + +- Added `[author]` and `[desc]` arguments to `[createPlaylist]`. **(IOS only)** + +### Fixes + +#### IOS + +- **Fixed** crash when using `[queryArtwork]`. +- **Fixed** wrong `[id]` value returning from `[PlaylistModel]`. + +### Documentation + +- Updated `README` documentation. + +## [2.0.0-beta.1] - [08.14.2021] + +### Features + +#### IOS + +- Added `[queryArtwork]`. + +### Fixes + +#### Android + +- **Fixed** `error` when building to `[Android]`. + +#### IOS + +- **Fixed** wrong `[duration]`, `[dateAdded]` and `[bookmark]` values returning from `[SongModel]`. + +### Documentation + +- Updated `on_audio_query` documentation. +- Updated `README` documentation. +- Updated `DEPRECATED` documentation. +- Added documentation to `IOS` platform. + +### ⚠ Important Changes + +#### @Deprecated + +- `[artwork]` from `[QueryArtworkWidget]`. +- `[deviceSDK]` from `[QueryArtworkWidget]`. +- `[requestPermission]` from `[QueryArtworkWidget]`. + +## [2.0.0-beta.0] - [08.13.2021] + +### Features + +#### on_audio_query + +- Added a [`DART ANALYZER`](https://github.com/axel-op/dart-package-analyzer/) to `PULL_REQUEST` and `PUSH`. + +### Documentation + +- Updated `on_audio_query` documentation. +- Updated `README` documentation. +- Updated `DEPRECATED` documentation. +- Created [`PLATFORMS`](https://github.com/LucJosin/on_audio_query/blob/2.0.0-dev/PLATFORMS.md) file. + +### ⚠ Important Changes + +#### Dart + +- Now **ALL** methods has `Named Optional` arguments. +- Changed `[queryArtworks]` to `[queryArtwork]`. + +#### @Deprecated + +- `[requestPermission]` argument from **ALL** methods. +- `[queryAudios]`. +- `[artwork]` from `[SongModel]`. +- `[path]` from `[querySongs]`. + +## [2.0.0-alpha.1] - [08.08.2021] - [GitHub Only] + +### Features + +#### Dart + +- Added `[artwork]` to `[PlaylistModel]` as `[Uint8List]` +- Added `[numOfTracks]` to `[PlaylistModel]` +- Added `[playlistAuthor]` and `[playlistDesc]` parameter to `[createPlaylist]` (IOS only) +- Added `[OnModelFormatter]` extension. + - Added `[toSongModel]`. + - Added `[toAlbumModel]`. + - Added `[toPlaylistModel]`. + - Added `[toArtistModel]`. + - Added `[toGenreModel]`. + +#### IOS + +- Added `[queryWithFilters]` method. +- Added `[createPlaylist]` method. +- Added `[queryPlaylists]` method. +- Added `[queryAudiosFrom]` method. + +### ⚠ Important Changes + +#### Dart + +- Now `[dateAdded]` from `[PlaylistModel]` return a `[int]`. +- Now `[dateModified]` from `[PlaylistModel]` return a `[int]`. + +#### @Deprecated + +- `[queryAudiosOnly]` +- `[AudiosOnlyType]` +- `[queryAudiosBy]` +- `[AudiosByType]` + +### Dev Changes + +#### Dart + +- ~~Added checker to all `[int]` from `[PlaylistModel]`.~~ + - Temporary + +## [2.0.0-alpha.0] - [08.05.2021] - [GitHub Only] + +### Release + +- `[2.0.0-alpha.0]` release. + +## [2.0.0-dev.1] - [08.05.2021] - [Internal] + +### Features + +#### IOS + +- Added `[queryArtists]` and `[queryGenres]`. + +### ⚠ Important Changes + +#### @Deprecated + +- Removed `[artwork]` from `[ArtistModel]`. +- Removed `[artwork]` from `[GenreModel]`. + +### Dev Changes + +#### Dart + +- ~~Added a checker to all int items from `[ArtistModel]`.~~ + - Temporary +- ~~Added a checker to all int items from `[GenreModel]`.~~ + - Temporary + +## [2.0.0-dev.0] - [08.02.2021] - [Internal] + +### Features + +#### on_audio_query + +- The plugin now supports `[IOS]`. **(Not 100%)** + +#### IOS + +- Added `[querySongs]` and `[queryAlbums]`. + +#### Dart + +- Added `[model]` to `[DeviceModel]`. + +### Changes + +#### Dart + +- Now `[sdk]` are `[version]`. +- Now `[deviceType]` are `[type]`. + +### ⚠ Important Changes + +#### Dart + +- Now `[artwork]` from `[SongModel]` return a `[Uint8list]`. +- ~~Now all `[int]` from `[SongModel]` can be `[null]`.~~ +- Now `[artwork]` from `[AlbumModel]` return a `[Uint8list]`. + +#### @**Deprecated** + +- `[numOfSongsArtists]` from `[AlbumModel]`. +- `[maxyear]` from `[AlbumModel]`. +- `[minyear]` from `[AlbumModel]`. +- `[release]` from `[DeviceModel]`. +- `[code]` from `[DeviceModel]`. +- `[year]` from `[SongModel]`. +- `[is_alarm]` from `[SongModel]`. +- `[is_music]` from `[SongModel]`. +- `[is_notification]` from `[SongModel]`. +- `[is_ringtone]` from `[SongModel]`. +- `[is_podcast]` from `[SongModel]`. +- `[file_parent]` from `[SongModel]`. +- `[firstYear]` from `[AlbumModel]`. +- `[lastYear]` from `[AlbumModel]`. + +### Dev Changes + +#### Dart + +- Now `[queryDeviceInfo]` will return Map instead of List. +- ~~Added a checker to all int items from `[SongModel]`.~~ + - Temporary. +- ~~Added a checker to all int items from `[AlbumModel]`.~~ + - Temporary. + +## [1.2.0] - [07.30.2021] + +### Features + +- Added `[path]` parameter to `[querySongs]` and `[queryAudio]`. +- Added `[getMap]` to: + - `[SongModel]`. + - `[AlbumModel]`. + - `[ArtistModel]`. + - `[GenreModel]`. + - `[PlaylistModel]`. + - `[DeviceModel]`. + +### Documentation + +- Updated `README` documentation. + +## [1.1.3+1] - [07.19.2021] + +### Fixes + +#### Android + +- **Fixed** `[Kotlin]` issue when installing the plugin. + +### Documentation + +- Updated `README` documentation. + +### Changes + +#### Android + +- Downgraded some `[Kotlin]` dependencies. + +## [1.1.3] - [07.18.2021] + +### Fixes + +#### Android + +- **Fixed** `[cursor]` problem when using `[AudiosFromType.GENRE_NAME]` or `[AudiosFromType.GENRE_ID]` on `[queryAudiosFrom]`. - [#16](https://github.com/LucJosin/on_audio_query/issues/16) and [#12](https://github.com/LucJosin/on_audio_query/issues/12) + +### Documentation + +- Updated `README` documentation. + +### Changes + +#### Android + +- Updated some `[Kotlin]` dependencies. + +## [1.1.2] - [07.07.2021] + +### Fixes + +#### Android + +- ~~**Fixed** `[cursor]` problem when using `[AudiosFromType.GENRE_NAME]` or `[AudiosFromType.GENRE_ID]` on `[queryAudiosFrom]`.~~ + +### Documentation + +- Updated `README` documentation. + +## [1.1.1] - [06.23.2021] + +### Features + +#### Dart/Android + +- Added `[uri]` to `[SongModel]`. - [Added #10](https://github.com/LucJosin/on_audio_query/issues/10) + +### Fixes + +#### Android + +- **Fixed** `java.lang.Integer cannot be cast to java.lang.Long` from `[queryArtworks]`. - [#11](https://github.com/LucJosin/on_audio_query/issues/11) + +### Documentation + +- Updated `README` documentation. +- Created `DEPRECATED` file/history. + +### Changes + +#### Dart + +- Changed from `[deviceInfo]` to `[deviceSDK]` on `[QueryArtworkWidget]`. + +### ⚠ Important Changes + +#### Dart + +- Deprecated `[deviceInfo]` from `[QueryArtworkWidget]`. + +## [1.1.0] - [06.03.2021] + +### Features + +#### Dart/Android + +- Added `[queryDeviceInfo]`. +- Added `[dateModified]` to `[SongModel]`. +- Added `[querySongsBy]` and `[SongsByType]`. + +### Fixes + +#### Android + +- **Fixed** incompatibility with `[permission_handler]`. - [#3](https://github.com/LucJosin/on_audio_query/issues/3) - Thanks [@mvanbeusekom](https://github.com/mvanbeusekom) + +#### Dart + +- **Fixed** wrong name. From `[dataAdded]` to `[dateAdded]`. + +### Documentation + +- Updated `README` documentation. +- Updated `[OnAudioQueryExample]` to add new `[queryDeviceInfo]` and `[QueryArtworkWidget]` methods. + +### Changes + +#### Android + +- Updated some `[Kotlin]` dependencies. +- Changed some `[Kotlin]` methods. + +### ⚠ Important Changes + +#### Dart + +- Now `[getDeviceSDK]`, `[getDeviceRelease]` and `[getDeviceCode]` are part of `[queryDeviceInfo]`. +- Now `[QueryArtworkWidget]` support Android above and below 29/Q/10. +- Now `[size]`, `[albumId]`, `[artistId]`, `[dataAdded]`, `[dataModified]`, `[duration]`, `[track]` and `[year]` from `[SongModel]` will return `[int]`. + +## [1.0.8] - [05.19.2021] + +### Features + +#### Dart + +- Added `[artworkClipBehavior]`, `[keepOldArtwork]`, `[repeat]` and `[scale]` to `[QueryArtworkWidget]`. +- Added comments to `[QueryArtworkWidget]`. + +### Fixes + +#### Android + +- **Fixed** Now `[queryArtworks]` will return null. - [#6](https://github.com/LucJosin/on_audio_query/issues/6) + +### Documentation + +- Updated `README` documentation. + +### ⚠ Important Changes + +#### Dart + +- Now `[queryArtworks]` return `[Uint8List?]`. + +## [1.0.7] - [05.18.2021] + +### Features + +#### Dart/Android + +- Added `[queryFromFolder]`. +- Added `[queryAllPath]`. +- Added `[_display_name_wo_ext]` (`[displayName]` without extension) to `[SongModel]`. - [Added #5](https://github.com/LucJosin/on_audio_query/issues/5) +- Added `[file_extension]` (Only file extension) to `[SongModel]`. +- Added `[file_parent]` (All the path before file) to `[SongModel]`. +- Added `[Genre]` to `[queryAudiosFrom]`. +- Added `[ALBUM_ID]`, `[ARTIST_ID]` and `[PLAYLIST_ID]` to `[AudiosFromType]`. - [Added #2](https://github.com/LucJosin/on_audio_query/issues/2) + +### Documentation + +- Updated `README` documentation. + +### Changes + +#### Dart/Android + +- Now `[queryAudiosFrom]` supports `[name]` and `[id]`. +- Now `[albumId]` from `[AlbumModel]` return a `[int]`. + +#### Android + +- Now all `[Kotlin]` checks will throw a `[Exception]` if value don't exist. +- Updated some `[Kotlin]` dependencies. + +### ⚠ Important Changes + +#### Dart/Android + +- Changed `[ALBUM]`, `[ARTIST]` and `[PLAYLIST]` to `[ALBUM_NAME]`, `[ARTIST_NAME]` and `[PLAYLIST_NAME]` in `[AudiosFromType]`. + +## [1.0.6] - [04.08.2021] + +### Fixes + +#### Android + +- **Fixed** `[queryArtwork]` returning null album image in Android 11. - [#1](https://github.com/LucJosin/on_audio_query/issues/1) + +### Documentation + +- Updated `README` documentation. + +### Changes + +#### Android + +- Removed unnecessary code from `[WithFiltersType]`. +- Updated some `[Kotlin]` dependencies. + +## [1.0.5] - [03.31.2021] + +### Features + +#### Dart/Android + +- Added `[queryAudiosOnly]`. +- Added `[queryWithFilters]`. +- Added `[AudiosOnlyType]` and `[WithFiltersType]`. +- Added `[SongsArgs]`, `[AlbumsArgs]`, `[PlaylistsArgs]`, `[ArtistsArgs]`, `[GenresArgs]`. +- Added `[EXTERNAL]` and `[INTERNAL]` parameters for some query methods. + +### Documentation + +- Updated `README` documentation. + +### Changes + +#### Dart/Android + +- Now `[querySongs]`, `[queryAlbums]`, `[queryArtists]`, `[queryPlaylists]` and `[queryGenres]` have `[UriType]` as parameter. + +#### Android + +- Updated some `[Kotlin]` dependencies. + +## [1.0.3] - [03.28.2021] + +### ⚠ Important Changes + +#### Dart + +- Migrate to null safety. + +## [1.0.2] - [03.27.2021] + +### Fixes + +#### Dart + +- **Fixed** flutter example. + +#### Android + +- **Fixed** `[audiosFromPlaylist]` [**Now this method is part of queryAudiosFrom**] +- **Fixed** `"count(*)"` error from `[addToPlaylist]`. [**Permission bug on Android 10 still happening**] + +### Documentation + +- Updated `README` documentation. + +### Changes + +#### Dart + +- Now `[Id]` in models return `[int]` instead `[String]`. + +### ⚠ Important Changes + +#### Dart/Android + +- Removed `[ALBUM_KEY]`, `[ARTIST_KEY]` from all query audio methods. + +#### Android + +- Moved `[audiosFromPlaylist]` to `[queryAudiosFrom]`. + +## [1.0.0] - [03.24.2021] + +### Release + +- `[on_audio_query]` release. + +## [0.5.0] - [03.23.2021] + +### Features + +#### Dart/Android + +- Changed some methods structure. +- Added `[moveItemTo]` method to Playlist. +- Added `[Size]` and `[Format]` parameters to `[queryArtwork]`. +- Added `[getDeviceSDK]`, `[getDeviceRelease]` and `[getDeviceCode]`. +- Added `[retryRequest]` parameter to `[permissionsRequest]`. + +#### Dart + +- Added `[QueryArtworkWidget]`. + +### Fixes + +- Added parameter `[AudioId]` to `[addToPlaylist]` and `[removeFromPlaylist]`. + +### Documentation + +- Updated `README` documentation. +- Added more comments to `[Kotlin]` and `[Dart]` code. + +### Changes + +- Now Playlist methods parameters request `[id]` instead Name. +- Now `[renamePlaylist]` add more information -> `[Date_Modified]`. +- Now when `[requestPermission]` parameter is set to true or `[permissionsRequest]` method is called, both `[READ]` and `[WRITE]` is requested. + +## [0.4.0] - [03.18.2021] + +### Features + +#### Dart/Android + +- Changed some methods structure. +- Added `[renamePlaylist]`. +- Added separate option for sortType order `[ASC_OR_SMALLER]` and `[DESC_OR_GREATER]`. +- Added `[permissionsStatus]` and `[permissionsRequest]`. + +### Documentation + +- Updated `README` documentation. +- Added some comments to `[Kotlin]` and `[Dart]` code. + +### Changes + +- Now `[createPlaylist]`, `[removePlaylist]`, `[addToPlaylist]` and `[removeFromPlaylist]` return bool. + +## [0.3.0] - [03.16.2021] + +### Features + +#### Dart/Android + +- Added `[createPlaylist]`, `[removePlaylist]`, `[addToPlaylist]` and `[removeFromPlaylist]`. + +#### Dart + +- Updated the `[Example]` application. + +### Documentation + +- Updated `README` documentation. + +## [0.2.5] - [03.11.2021] + +### Features + +#### Dart/Android + +- Added `[queryArtworks]` and `[queryAudiosFrom]`. + +### Fixes + +- Added a better performance for query images. + +### Documentation + +- Updated `README` documentation. + +## [0.2.0] - [03.10.2021] + +### Features + +#### Dart/Android + +- Added `[queryArtists]`, `[queryPlaylists]` and `[queryGenres]`. +- Added `[ArtistSortType]`, `[PlaylistsSortType]` and `[GenreSortType]`. + +#### Android + +- Now all methods use `Kotlin Coroutines` for query in background, adding a better performance. + +### Documentation + +- Updated `README` documentation. +- Updated `pubspec.yaml`. +- Created `README` translation section. +- Created `README` translation for `pt-BR` [Portuguese]. + +## [0.1.5] - [03.08.2021] + +### Features + +#### Dart/Android + +- Added `[querySongs]`, `[queryAudio]` and `[queryAlbums]`. +- Added `[AudioSortType]` and `[AlbumSortType]`. + +#### Android + +- Added `[Optional]` and `[Built-in]` Write and Read Storage Permission. + +### Documentation + +- Created a `README` documentation. + +## [0.0.1] - [02.16.2021] + +### Features + +#### Dart/Android + +- Created the base for the plugin. + + + + diff --git a/local_packages/on_audio_query-2.9.0/DEPRECATED.md b/local_packages/on_audio_query-2.9.0/DEPRECATED.md new file mode 100644 index 0000000..df27429 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/DEPRECATED.md @@ -0,0 +1,86 @@ +## [2.5.0] - [10.15.2021] -> [2.6.0] - [02.01.2022] +### Deprecated +- `[albumId]` from `[AlbumModel]`. + - Use `[id]` instead. + +## [2.3.0] - [09.25.2021] -> [2.5.0] - [10.15.2021] +### Deprecated +- `[DEFAULT]` from `[SongSortType]`. + - Use `[TITLE]` instead. +- `[DEFAULT]` from `[PlaylistSortType]`. + - Use `[PLAYLIST]` instead. +- `[DEFAULT]` from `[ArtistSortType]`. + - Use `[ARTIST]` instead. +- `[DEFAULT]` from `[AlbumSortType]`. + - Use `[ALBUM]` instead. +- `[DEFAULT]` from `[GenreSortType]`. + - Use `[GENRE]` instead. +- `[ARTIST_KEY]` from `[ArtistSortType]`. +- `[ARTIST_NAME]` from `[ArtistSortType]`. + - Use `[ARTIST]` instead. +- `[ALBUM_NAME]` from `[AlbumSortType]`. + - Use `[ALBUM]` instead. +- `[GENRE_NAME]` from `[GenreSortType]`. + - Use `[GENRE]` instead. +- `[DATA_ADDED]` from `[SongSortType]`. + - Use `[DATE_ADDED]` instead. +- `[DATA_ADDED]` from `[PlaylistSortType]`. + - Use `[DATE_ADDED]` instead. + +## [2.0.0] - [06.23.2021] -> [2.3.0] - [09.25.2021] +### Deprecated +- `[albumName]` from `[AlbumModel]`. + - Use `[album]` instead. +- `[artistName]` from `[ArtistModel]`. + - Use `[artist]` instead. +- `[genreName]` from `[GenreModel]`. + - Use `[genre]` instead. +- `[playlistName]` from `[PlaylistModel]`. + - Use `[playlist]` instead. +- `[ALBUM_NAME]` FROM `[AudiosFromType]`. + - Use `[ALBUM]` instead. +- `[ARTIST_NAME]` FROM `[AudiosFromType]`. + - Use `[ARTIST]` instead. +- `[GENRE_NAME]` FROM `[AudiosFromType]`. + - Use `[GENRE]` instead. +- `[ALBUM_NAME]` FROM `[AlbumsArgs]`. + - Use `[ALBUM]` instead. +- `[PLAYLIST_NAME]` FROM `[PlaylistsArgs]`. + - Use `[PLAYLIST]` instead. +- `[PLAYLIST_NAME]` FROM `[PlaylistSortType]`. + - Use `[PLAYLIST]` instead. +- `[ARTIST_NAME]` FROM `[ArtistsArgs]`. + - Use `[ARTIST]` instead. +- `[GENRE_NAME]` FROM `[GenresArgs]`. + - Use `[GENRE]` instead. +- `[numOfSongsArtists]` from `[AlbumModel]`. +- `[firstYear]` from `[AlbumModel]`. + - `[FIRST_YEAR]` from `[AlbumSortType]`. +- `[lastYear]` from `[AlbumModel]`. + - `[LAST_YEAR]` from `[AlbumSortType]`. +- `[release]` from `[DeviceModel]`. +- `[code]` from `[DeviceModel]`. +- `[year]` from `[SongModel]`. +- ~~`[is_alarm]` from `[SongModel]`~~. +- ~~`[is_music]` from `[SongModel]`~~. +- ~~`[is_notification]` from `[SongModel]`~~. +- ~~`[is_ringtone]` from `[SongModel]`~~. +- ~~`[is_podcast]` from `[SongModel]`~~. +- `[file_parent]` from `[SongModel]`. +- `[path]` from `[querySongs]`. +- `[artwork]` from `[SongModel]`. + - Use `[queryArtwork]` instead. +- `[artwork]` from `[ArtistModel]`. +- `[artwork]` from `[GenreModel]`. +- `[artwork]` from `[QueryArtworkWidget]`. +- `[deviceSDK]` from `[QueryArtworkWidget]`. +- `[requestPermission]` from `[QueryArtworkWidget]`. +- `[queryAudiosOnly]`. +- `[querySongsBy]`. +- `[AudiosOnlyType]`. +- `[SongsByType]`. +- `[queryAudios]`. + +## [1.1.1] - [06.23.2021] -> [1.2.0] - [07.30.2021] +### Deprecated +- `[deviceInfo]` from `[QueryArtworkWidget]`. \ No newline at end of file diff --git a/local_packages/on_audio_query-2.9.0/LICENSE b/local_packages/on_audio_query-2.9.0/LICENSE new file mode 100644 index 0000000..f313597 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2021, Lucas Josino +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of Lucas Josino nor the names of its contributors may be +used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/local_packages/on_audio_query-2.9.0/PLATFORMS.md b/local_packages/on_audio_query-2.9.0/PLATFORMS.md new file mode 100644 index 0000000..bd3d29d --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/PLATFORMS.md @@ -0,0 +1,272 @@ +# on_audio_query - Platforms support + +Here you'll see a extra information about every method/type etc.. + +## Topics: + +* [Methods](#methods) +* [Sort Types](#sorttypes) + * [SongSortType](#songsorttype) + * [AlbumSortType](#albumsorttype) + * [ArtistSortType](#artistsorttype) + * [PlaylistSortType](#playlistsorttype) + * [GenreSortType](#genresorttype) +* [Order Types](#ordertypes) +* [Uri Types](#uritypes) +* [Artwork Types](#artworktype) +* [Artwork Format Types](#artworkformat) +* [Audios From Types](#audiosfromtype) +* [With Filter Types](#withfilterstype) + * [AudiosArgs](#audiosargs) + * [AlbumsArgs](#albumsargs) + * [PlaylistsArgs](#playlistsargs) + * [ArtistsArgs](#artistsargs) + * [GenresArgs](#genressargs) +* [Models](#models) + * [SongModel](#songmodel) + * [AlbumModel](#albummodel) + * [PlaylistModel](#playlistmodel) + * [ArtistModel](#artistmodel) + * [GenreModel](#genremodel) +* [DeviceModel](#devicemodel) + +✔️ -> Supported
+❌ -> Not Supported
+ +## Methods + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `querySongs` | `✔️` | `✔️` | `✔️` |
+| `queryAlbums` | `✔️` | `✔️` | `✔️` |
+| `queryArtists` | `✔️` | `✔️` | `✔️` |
+| `queryPlaylists` | `✔️` | `✔️` | `❌` |
+| `queryGenres` | `✔️` | `✔️` | `✔️` |
+| `queryAudiosFrom` | `✔️` | `✔️` | `✔️` |
+| `queryWithFilters` | `✔️` | `✔️` | `✔️` |
+| `queryArtwork` | `✔️` | `✔️` | `✔️` |
+| `createPlaylist` | `✔️` | `✔️` | `❌` |
+| `removePlaylist` | `✔️` | `❌` | `❌` |
+| `addToPlaylist` | `✔️` | `✔️` | `❌` |
+| `removeFromPlaylist` | `✔️` | `❌` | `❌` |
+| `renamePlaylist` | `✔️` | `❌` | `❌` |
+| `moveItemTo` | `✔️` | `❌` | `❌` |
+| `permissionsRequest` | `✔️` | `✔️` | `❌` |
+| `permissionsStatus` | `✔️` | `✔️` | `❌` |
+| `queryDeviceInfo` | `✔️` | `✔️` | `✔️` |
+| `scanMedia` | `✔️` | `❌` | `❌` |
+ +## SortTypes + +### SongSortType + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `DEFAULT` | `✔️` | `✔️` | `✔️` |
+| `ARTIST` | `✔️` | `✔️` | `✔️` |
+| `ALBUM` | `✔️` | `✔️` | `✔️` |
+| `DURATION` | `✔️` | `✔️` | `✔️` |
+| `DATA_ADDED` | `✔️` | `✔️` | `❌` |
+| `SIZE` | `✔️` | `✔️` | `✔️` |
+| `DISPLAY_NAME` | `✔️` | `✔️` | `✔️` |
+ +### AlbumSortType + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `DEFAULT` | `✔️` | `✔️` | `✔️` |
+| `ARTIST` | `✔️` | `✔️` | `✔️` |
+| `ALBUM` | `✔️` | `✔️` | `✔️` |
+| `NUM_OF_SONGS` | `✔️` | `✔️` | `✔️` |
+ +### ArtistSortType + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `DEFAULT` | `✔️` | `❌` | `✔️` |
+| `ARTIST_NAME` | `✔️` | `✔️` | `✔️` |
+| `ARTIST_KEY` | `✔️` | `❌` | `❌` |
+| `NUM_OF_TRACKS` | `✔️` | `✔️` | `✔️` |
+| `NUM_OF_ALBUMS` | `✔️` | `✔️` | `✔️` |
+ +### PlaylistSortType + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `DEFAULT` | `✔️` | `❌` | `❌` |
+| `DATA_ADDED` | `✔️` | `❌` | `❌` |
+| `PLAYLIST_NAME` | `✔️` | `❌` | `❌` |
+ +### GenreSortType + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `DEFAULT` | `✔️` | `✔️` | `✔️` |
+ +## OrderTypes + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `ASC` | `✔️` | `✔️` | `✔️` |
+| `DESC` | `✔️` | `✔️` | `✔️` |
+ +## UriTypes + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `EXTERNAL` | `✔️` | `❌` | `❌` |
+| `INTERNAL` | `✔️` | `✔️` | `✔️` |
+ +## ArtworkTypes + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `AUDIO` | `✔️` | `✔️` | `✔️` |
+| `ALBUM` | `✔️` | `✔️` | `✔️` |
+ +## ArtworkFormat + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `JPEG` | `✔️` | `✔️` | `❌` |
+| `PNG` | `✔️` | `✔️` | `❌` |
+ +## AudiosFromType + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `ALBUM_NAME` | `✔️` | `✔️` | `✔️` |
+| `ALBUM_ID` | `✔️` | `✔️` | `❌` |
+| `ARTIST_NAME` | `✔️` | `✔️` | `✔️` |
+| `ARTIST_ID` | `✔️` | `✔️` | `❌` |
+| `GENRE_NAME` | `✔️` | `✔️` | `✔️` |
+| `GENRE_ID` | `✔️` | `✔️` | `❌` |
+| `PLAYLIST` | `✔️` | `✔️` | `❌` |
+ +## WithFiltersType + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `AUDIOS` | `✔️` | `✔️` | `✔️` |
+| `ALBUMS` | `✔️` | `✔️` | `✔️` |
+| `PLAYLISTS` | `✔️` | `✔️` | `❌` |
+| `ARTISTS` | `✔️` | `✔️` | `✔️` |
+| `GENRES` | `✔️` | `✔️` | `✔️` |
+ +### AudiosArgs + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `TITLE` | `✔️` | `✔️` | `✔️` |
+| `DISPLAY_NAME` | `✔️` | `❌` | `✔️` |
+| `ALBUM` | `✔️` | `✔️` | `✔️` |
+| `ARTIST` | `✔️` | `✔️` | `✔️` |
+ +### AlbumsArgs + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `ALBUM_NAME` | `✔️` | `✔️` | `✔️` |
+| `ARTIST` | `✔️` | `✔️` | `✔️` |
+ +### PlaylistsArgs + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `PLAYLIST_NAME` | `✔️` | `✔️` | `❌` |
+ +### ArtistsArgs + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `ARTIST_NAME` | `✔️` | `✔️` | `✔️` |
+ +### GenresArgs + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `GENRE_NAME` | `✔️` | `✔️` | `✔️` |
+ +## Models + +### SongModel + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `id` | `✔️` | `✔️` | `✔️` |
+| `data` | `✔️` | `✔️` | `✔️` |
+| `uri` | `✔️` | `❌` | `❌` |
+| `displayName` | `✔️` | `✔️` | `✔️` |
+| `displayNameWOExt` | `✔️` | `✔️` | `✔️` |
+| `size` | `✔️` | `✔️` | `✔️` |
+| `album` | `✔️` | `✔️` | `✔️` |
+| `albumId` | `✔️` | `✔️` | `✔️` |
+| `artist` | `✔️` | `✔️` | `✔️` |
+| `artistId` | `✔️` | `✔️` | `✔️` |
+| `genre` | `✔️` | `✔️` | `✔️` |
+| `genreId` | `✔️` | `✔️` | `✔️` |
+| `bookmark` | `✔️` | `✔️` | `❌` |
+| `composer` | `✔️` | `✔️` | `❌` |
+| `dateAdded` | `✔️` | `✔️` | `❌` |
+| `dateModified` | `✔️` | `❌` | `✔️` |
+| `duration` | `✔️` | `✔️` | `❌` |
+| `title` | `✔️` | `✔️` | `✔️` |
+| `track` | `✔️` | `✔️` | `✔️` |
+| `fileExtension` | `✔️` | `✔️` | `✔️` |
+| `is_alarm` | `✔️` | `❌` | `❌` |
+| `is_audiobook` | `✔️` | `❌` | `❌` |
+| `is_music` | `✔️` | `❌` | `❌` |
+| `is_notification` | `✔️` | `❌` | `❌` |
+| `is_podcast` | `✔️` | `❌` | `❌` |
+| `is_ringtone` | `✔️` | `❌` | `❌` |
+ +### AlbumModel + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `id` | `✔️` | `✔️` | `✔️` |
+| `album` | `✔️` | `✔️` | `✔️` |
+| `albumId` | `✔️` | `✔️` | `✔️` |
+| `artist` | `✔️` | `✔️` | `✔️` |
+| `artistId` | `✔️` | `✔️` | `✔️` |
+| `numOfSongs` | `✔️` | `✔️` | `✔️` |
+ +### PlaylistModel + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `id` | `✔️` | `✔️` | `❌` |
+| `playlist` | `✔️` | `✔️` | `❌` |
+| `data` | `✔️` | `❌` | `❌` |
+| `dateAdded` | `✔️` | `✔️` | `❌` |
+| `dateModified` | `✔️` | `✔️` | `❌` |
+| `numOfSongs` | `✔️` | `✔️` | `❌` |
+| `artwork` | `❌` | `✔️` | `❌` |
+ +### ArtistModel + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `id` | `✔️` | `✔️` | `✔️` |
+| `artist` | `✔️` | `✔️` | `✔️` |
+| `numberOfAlbums` | `✔️` | `✔️` | `✔️` |
+| `numberOfTracks` | `✔️` | `✔️` | `✔️` |
+ +### GenreModel + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `id` | `✔️` | `✔️` | `✔️` |
+| `genre` | `✔️` | `✔️` | `✔️` |
+| `numOfSongs` | `✔️` | `✔️` | `✔️` |
+ +### DeviceModel + +| Methods | Android | IOS | Web | +|--------------|-----------------|-----------------|-----------------| +| `version` | `✔️` | `✔️` | `✔️` |
+| `type` | `✔️` | `✔️` | `✔️` |
+| `model` | `✔️` | `✔️` | `❌` |
+ + + diff --git a/local_packages/on_audio_query-2.9.0/README.md b/local_packages/on_audio_query-2.9.0/README.md new file mode 100644 index 0000000..19e31dd --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/README.md @@ -0,0 +1,194 @@ +
+ +# on_audio_query +[![Pub.dev](https://img.shields.io/pub/v/on_audio_query?color=9cf&label=Pub.dev&style=flat-square)](https://pub.dev/packages/on_audio_query) +[![Platforms](https://img.shields.io/badge/Platforms-Android%20%7C%20IOS%20%7C%20Web-9cf?&style=flat-square)]() +[![Languages](https://img.shields.io/badge/Languages-Dart%20%7C%20Kotlin%20%7C%20Swift-9cf?&style=flat-square)]() + +[Flutter](https://flutter.dev/) Plugin used to query audios/songs 🎶 infos [title, artist, album, etc..] from device storage.
+ +**Any problem? [Issues](https://github.com/LucJosin/on_audio_query/issues)**
+**Any suggestion? [Pull request](https://github.com/LucJosin/on_audio_query/pulls)** + +
+ +### Topics: + +* [Installation](#installation) +* [Platforms](#platforms) +* [Overview](#overview) +* [Examples](#examples) +* [Gif Examples](#gif-examples) +* [License](#license) + +## Platforms: + + +| Methods | Android | IOS | Web | +|-------|:----------:|:----------:|:----------:| +| `querySongs` | `✔️` | `✔️` | `✔️` |
+| `queryAlbums` | `✔️` | `✔️` | `✔️` |
+| `queryArtists` | `✔️` | `✔️` | `✔️` |
+| `queryPlaylists` | `✔️` | `✔️` | `❌` |
+| `queryGenres` | `✔️` | `✔️` | `✔️` |
+| `queryAudiosFrom` | `✔️` | `✔️` | `✔️` |
+| `queryWithFilters` | `✔️` | `✔️` | `✔️` |
+| `queryArtwork` | `✔️` | `✔️` | `✔️` |
+| `createPlaylist` | `✔️` | `✔️` | `❌` |
+| `removePlaylist` | `✔️` | `❌` | `❌` |
+| `addToPlaylist` | `✔️` | `✔️` | `❌` |
+| `removeFromPlaylist` | `✔️` | `❌` | `❌` |
+| `renamePlaylist` | `✔️` | `❌` | `❌` |
+| `moveItemTo` | `✔️` | `❌` | `❌` |
+| `checkAndRequest` | `✔️` | `✔️` | `❌` |
+| `permissionsRequest` | `✔️` | `✔️` | `❌` |
+| `permissionsStatus` | `✔️` | `✔️` | `❌` |
+| `queryDeviceInfo` | `✔️` | `✔️` | `✔️` |
+| `scanMedia` | `✔️` | `❌` | `❌` |
+ +✔️ -> Supported
+❌ -> Not Supported
+ +**[See all platforms methods support](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/PLATFORMS.md)** + +## Installation: + +Add the following code to your `pubspec.yaml`: +```yaml +dependencies: + on_audio_query: ^2.9.0 +``` + +### Request Permission: + +#### Android: +To use this plugin add the following code to your [AndroidManifest.xml](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/example/android/app/src/main/AndroidManifest.xml) +```xml + + + + + + + + + + + + +``` + +#### IOS: +To use this plugin add the following code to your [Info.plist](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/example/ios/Runner/Info.plist) +``` + + + NSAppleMusicUsageDescription + $(PROJECT_NAME) requires access to media library + + +``` + +## Some Features: + +* Optional and Built-in storage `READ` and `WRITE` permission request +* Get all audios/songs. +* Get all albums and album-specific audios. +* Get all artists and artist-specific audios. +* Get all playlists and playlists-specific audios. +* Get all genres and genres-specific audios. +* Get all query methods with specific `keys` [Search]. +* Create/Delete/Rename playlists. +* Add/Remove/Move specific audios to playlists. +* Specific sort types for all query methods. + +## Overview: + +All types of methods on this plugin: + +### Artwork Widget + +```dart + Widget someOtherName() async { + return QueryArtworkWidget( + id: , + type: ArtworkType.AUDIO, + ); + } +``` + +**See more: [QueryArtworkWidget](https://pub.dev/documentation/on_audio_query/latest/on_audio_query/QueryArtworkWidget-class.html)** + +## Examples: + +#### OnAudioQuery + +```dart +final OnAudioQuery _audioQuery = OnAudioQuery(); +``` + +#### Query methods: + +- queryAudios(); +- queryAlbums(); +- queryArtists(); +- queryPlaylists(); +- queryGenres(). + +```dart + someName() async { + // Query Audios + List audios = await _audioQuery.queryAudios(); + + // Query Albums + List albums = await _audioQuery.queryAlbums(); + } +``` + +#### scanMedia + +You'll use this method when updating a media from storage. This method will update the media 'state' and +Android `MediaStore` will be able to know this 'state'. + +```dart + someName() async { + OnAudioQuery _audioQuery = OnAudioQuery(); + File file = File('path'); + try { + if (file.existsSync()) { + file.deleteSync(); + _audioQuery.scanMedia(file.path); // Scan the media 'path' + } + } catch (e) { + debugPrint('$e'); + } + } +``` + +#### queryArtwork + +```dart + someName() async { + // DEFAULT: ArtworkFormat.JPEG, 200 and false + Uint8List something = await _audioQuery.queryArtwork( + , + ArtworkType.AUDIO, + ..., + ); + } +``` + +Or you can use a basic and custom Widget. +**See example [QueryArtworkWidget](#artwork-widget)** + +## Gif Examples: +| | | | | +|:---:|:---:|:---:|:---:| +| | | | | +| Songs | Albums | Playlists | Artists | + +## LICENSE: + +* [LICENSE](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/LICENSE) + +> * [Back to top](#on_audio_query) diff --git a/local_packages/on_audio_query-2.9.0/analysis_options.yaml b/local_packages/on_audio_query-2.9.0/analysis_options.yaml new file mode 100644 index 0000000..565534d --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options \ No newline at end of file diff --git a/local_packages/on_audio_query-2.9.0/example/README.md b/local_packages/on_audio_query-2.9.0/example/README.md new file mode 100644 index 0000000..23e2086 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/README.md @@ -0,0 +1,16 @@ +# on_audio_query_example + +Demonstrates how to use the on_audio_query plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/local_packages/on_audio_query-2.9.0/example/analysis_options.yaml b/local_packages/on_audio_query-2.9.0/example/analysis_options.yaml new file mode 100644 index 0000000..565534d --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options \ No newline at end of file diff --git a/local_packages/on_audio_query-2.9.0/example/android/.gradle/7.0.2/fileChanges/last-build.bin b/local_packages/on_audio_query-2.9.0/example/android/.gradle/7.0.2/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/android/.gradle/7.0.2/fileChanges/last-build.bin differ diff --git a/local_packages/on_audio_query-2.9.0/example/android/.gradle/7.0.2/fileHashes/fileHashes.lock b/local_packages/on_audio_query-2.9.0/example/android/.gradle/7.0.2/fileHashes/fileHashes.lock new file mode 100644 index 0000000..e61ea83 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/android/.gradle/7.0.2/fileHashes/fileHashes.lock differ diff --git a/local_packages/on_audio_query-2.9.0/example/android/.gradle/7.0.2/gc.properties b/local_packages/on_audio_query-2.9.0/example/android/.gradle/7.0.2/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/on_audio_query-2.9.0/example/android/.gradle/checksums/checksums.lock b/local_packages/on_audio_query-2.9.0/example/android/.gradle/checksums/checksums.lock new file mode 100644 index 0000000..5db7e2f Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/android/.gradle/checksums/checksums.lock differ diff --git a/local_packages/on_audio_query-2.9.0/example/android/.gradle/vcs-1/gc.properties b/local_packages/on_audio_query-2.9.0/example/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/build.gradle b/local_packages/on_audio_query-2.9.0/example/android/app/build.gradle new file mode 100644 index 0000000..64a6dfb --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/build.gradle @@ -0,0 +1,63 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 36 + namespace "com.lucasjosino.on_audio_query_example" + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + 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.lucasjosino.on_audio_query_example" + minSdkVersion 16 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/debug/AndroidManifest.xml b/local_packages/on_audio_query-2.9.0/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..a60c34c --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/AndroidManifest.xml b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d99bd91 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000..e86636e --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,29 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + try { + flutterEngine.getPlugins().add(new dev.flutter.plugins.integration_test.IntegrationTestPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin integration_test, dev.flutter.plugins.integration_test.IntegrationTestPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.lucasjosino.on_audio_query.OnAudioQueryPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin on_audio_query_android, com.lucasjosino.on_audio_query.OnAudioQueryPlugin", e); + } + } +} diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/kotlin/com/lucasjosino/example/MainActivity.kt b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/kotlin/com/lucasjosino/example/MainActivity.kt new file mode 100644 index 0000000..392537f --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/kotlin/com/lucasjosino/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.lucasjosino.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/kotlin/com/lucasjosino/on_audio_query_example/MainActivity.kt b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/kotlin/com/lucasjosino/on_audio_query_example/MainActivity.kt new file mode 100644 index 0000000..aefa8ec --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/kotlin/com/lucasjosino/on_audio_query_example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.lucasjosino.on_audio_query_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/drawable-v21/launch_background.xml b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..1cb7aa2 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/drawable/launch_background.xml b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..8403758 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/values-night/styles.xml b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..827f12f --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/values/styles.xml b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..ccf2854 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/android/app/src/profile/AndroidManifest.xml b/local_packages/on_audio_query-2.9.0/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..a60c34c --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/android/build.gradle b/local_packages/on_audio_query-2.9.0/example/android/build.gradle new file mode 100644 index 0000000..5d13cba --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '2.1.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/local_packages/on_audio_query-2.9.0/example/android/gradle.properties b/local_packages/on_audio_query-2.9.0/example/android/gradle.properties new file mode 100644 index 0000000..46c1f16 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/local_packages/on_audio_query-2.9.0/example/android/gradle/wrapper/gradle-wrapper.properties b/local_packages/on_audio_query-2.9.0/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f9e81bc --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Oct 15 16:26:20 BRT 2021 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/local_packages/on_audio_query-2.9.0/example/android/local.properties b/local_packages/on_audio_query-2.9.0/example/android/local.properties new file mode 100644 index 0000000..28e8fd6 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/local.properties @@ -0,0 +1,2 @@ +sdk.dir=D:\\AndroidSDK +flutter.sdk=D:\\FlutterSDK\\flutter_windows_3.29.3-stable\\flutter \ No newline at end of file diff --git a/local_packages/on_audio_query-2.9.0/example/android/settings.gradle b/local_packages/on_audio_query-2.9.0/example/android/settings.gradle new file mode 100644 index 0000000..33f0745 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/local_packages/on_audio_query-2.9.0/example/android/settings_aar.gradle b/local_packages/on_audio_query-2.9.0/example/android/settings_aar.gradle new file mode 100644 index 0000000..d3db109 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Flutter/AppFrameworkInfo.plist b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..0d9747f --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Debug.xcconfig b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..553cfac --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Generated.xcconfig b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Generated.xcconfig new file mode 100644 index 0000000..ff85a47 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Generated.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=D:\FlutterSDK\flutter_windows_3.29.3-stable\flutter +FLUTTER_APPLICATION_PATH=D:\AndroidProject\yumi\local_packages\on_audio_query-2.9.0\example +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib\main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Release.xcconfig b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..4b6e1f6 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Flutter/flutter_export_environment.sh b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/flutter_export_environment.sh new file mode 100644 index 0000000..87f3c69 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=D:\FlutterSDK\flutter_windows_3.29.3-stable\flutter" +export "FLUTTER_APPLICATION_PATH=D:\AndroidProject\yumi\local_packages\on_audio_query-2.9.0\example" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib\main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.pbxproj b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4463cdf --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,556 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0454AA9BC1C6A74D29F0DC05 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89EDBAA686FD6A0F3044D3BE /* Pods_Runner.framework */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0E792F2A41600ECBFBB0ACDA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B7A1BC6F17ED8BDB08A32DE /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 76E158E1888B20842AFB70C7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 89EDBAA686FD6A0F3044D3BE /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0454AA9BC1C6A74D29F0DC05 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 47B71ED6578BEB388DFE7ED6 /* Pods */ = { + isa = PBXGroup; + children = ( + 0E792F2A41600ECBFBB0ACDA /* Pods-Runner.debug.xcconfig */, + 3B7A1BC6F17ED8BDB08A32DE /* Pods-Runner.release.xcconfig */, + 76E158E1888B20842AFB70C7 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 47B71ED6578BEB388DFE7ED6 /* Pods */, + F3BDBD7B8197D374D9E11050 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + F3BDBD7B8197D374D9E11050 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 89EDBAA686FD6A0F3044D3BE /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9801508057E29DB85D6FE897 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 25DEC95C3A6807E8090BAA98 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 25DEC95C3A6807E8090BAA98 /* [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; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + 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"; + }; + 9801508057E29DB85D6FE897 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.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_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 29Q2WS8ZTA; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExampleIOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.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 = 10.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_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 29Q2WS8ZTA; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExampleIOS; + 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_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 29Q2WS8ZTA; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.lucasjosino.onAudioQueryExampleIOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..c4b79bd --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..fc6bf80 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..af0309c --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..f360727 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..17ccc03 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..fc6bf80 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..af0309c --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/AppDelegate.swift b/local_packages/on_audio_query-2.9.0/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..3763683 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..1950fd8 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..d08a4de --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..65a94b5 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..497371e --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Base.lproj/Main.storyboard b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..bbb83ca --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/GeneratedPluginRegistrant.m b/local_packages/on_audio_query-2.9.0/example/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000..984d48c --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,28 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#else +@import integration_test; +#endif + +#if __has_include() +#import +#else +@import on_audio_query_ios; +#endif + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { + [IntegrationTestPlugin registerWithRegistrar:[registry registrarForPlugin:@"IntegrationTestPlugin"]]; + [OnAudioQueryPlugin registerWithRegistrar:[registry registrarForPlugin:@"OnAudioQueryPlugin"]]; +} + +@end diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Info.plist b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Info.plist new file mode 100644 index 0000000..b69edd7 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + on_audio_query_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSAppleMusicUsageDescription + Please let this application access your music library + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/local_packages/on_audio_query-2.9.0/example/ios/Runner/Runner-Bridging-Header.h b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..fae207f --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/local_packages/on_audio_query-2.9.0/example/lib/main.dart b/local_packages/on_audio_query-2.9.0/example/lib/main.dart new file mode 100644 index 0000000..0144ec1 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/lib/main.dart @@ -0,0 +1,143 @@ +/* +============= +Author: Lucas Josino +Github: https://github.com/LucJosin +Website: https://www.lucasjosino.com/ +============= +Plugin/Id: on_audio_query#0 +Homepage: https://github.com/LucJosin/on_audio_query +Pub: https://pub.dev/packages/on_audio_query +License: https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/LICENSE +Copyright: © 2021, Lucas Josino. All rights reserved. +============= +*/ + +import 'package:flutter/material.dart'; +import 'package:on_audio_query/on_audio_query.dart'; + +void main() { + runApp( + const MaterialApp( + home: Songs(), + ), + ); +} + +class Songs extends StatefulWidget { + const Songs({Key? key}) : super(key: key); + + @override + _SongsState createState() => _SongsState(); +} + +class _SongsState extends State { + // Main method. + final OnAudioQuery _audioQuery = OnAudioQuery(); + + // Indicate if application has permission to the library. + bool _hasPermission = false; + + @override + void initState() { + super.initState(); + // (Optinal) Set logging level. By default will be set to 'WARN'. + // + // Log will appear on: + // * XCode: Debug Console + // * VsCode: Debug Console + // * Android Studio: Debug and Logcat Console + LogConfig logConfig = LogConfig(logType: LogType.DEBUG); + _audioQuery.setLogConfig(logConfig); + + // Check and request for permission. + checkAndRequestPermissions(); + } + + checkAndRequestPermissions({bool retry = false}) async { + // The param 'retryRequest' is false, by default. + _hasPermission = await _audioQuery.checkAndRequest( + retryRequest: retry, + ); + + // Only call update the UI if application has all required permissions. + _hasPermission ? setState(() {}) : null; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("OnAudioQueryExample"), + elevation: 2, + ), + body: Center( + child: !_hasPermission + ? noAccessToLibraryWidget() + : FutureBuilder>( + // Default values: + future: _audioQuery.querySongs( + sortType: null, + orderType: OrderType.ASC_OR_SMALLER, + uriType: UriType.EXTERNAL, + ignoreCase: true, + ), + builder: (context, item) { + // Display error, if any. + if (item.hasError) { + return Text(item.error.toString()); + } + + // Waiting content. + if (item.data == null) { + return const CircularProgressIndicator(); + } + + // 'Library' is empty. + if (item.data!.isEmpty) return const Text("Nothing found!"); + + // You can use [item.data!] direct or you can create a: + // List songs = item.data!; + return ListView.builder( + itemCount: item.data!.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(item.data![index].title), + subtitle: Text(item.data![index].artist ?? "No Artist"), + trailing: const Icon(Icons.arrow_forward_rounded), + // This Widget will query/load image. + // You can use/create your own widget/method using [queryArtwork]. + leading: QueryArtworkWidget( + controller: _audioQuery, + id: item.data![index].id, + type: ArtworkType.AUDIO, + ), + ); + }, + ); + }, + ), + ), + ); + } + + Widget noAccessToLibraryWidget() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.redAccent.withOpacity(0.5), + ), + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("Application doesn't have access to the library"), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () => checkAndRequestPermissions(retry: true), + child: const Text("Allow"), + ), + ], + ), + ); + } +} diff --git a/local_packages/on_audio_query-2.9.0/example/pubspec.lock b/local_packages/on_audio_query-2.9.0/example/pubspec.lock new file mode 100644 index 0000000..c6545dc --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/pubspec.lock @@ -0,0 +1,328 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + id3: + dependency: transitive + description: + name: id3 + sha256: "24176a6e08db6297c8450079e94569cd8387f913c817e5e3d862be7dc191e0b8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + on_audio_query: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "2.9.0" + on_audio_query_android: + dependency: transitive + description: + name: on_audio_query_android + sha256: c0b946011ee7aa8ec76b21837f0cb0007120ac291202ed6c6fb4c498ba2ff6fe + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + on_audio_query_ios: + dependency: transitive + description: + name: on_audio_query_ios + sha256: "9b3efa39a656fa3720980e3c6a1f55b7257d0032a45ffeb3f70eaa2c7f10f929" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + on_audio_query_platform_interface: + dependency: transitive + description: + name: on_audio_query_platform_interface + sha256: c23e019a31bd0774828476e428fd33b0dd1d82c9d4791dba80429358fc65dcd3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.7.0" + on_audio_query_web: + dependency: transitive + description: + name: on_audio_query_web + sha256: "990efa52d879e6caffa97f24b34acd9caa1ce2c4c4cb873fe5a899a9b1af02c7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.4" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.flutter-io.cn" + source: hosted + version: "14.3.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.4" +sdks: + dart: ">=3.7.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/local_packages/on_audio_query-2.9.0/example/pubspec.yaml b/local_packages/on_audio_query-2.9.0/example/pubspec.yaml new file mode 100644 index 0000000..6f0c444 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/pubspec.yaml @@ -0,0 +1,73 @@ +name: on_audio_query_example +description: Demonstrates how to use the on_audio_query plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + flutter: + sdk: flutter + + on_audio_query: + # When depending on this package from a real application you should use: + # on_audio_query: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.1 + +dev_dependencies: + flutter_lints: ^1.0.4 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - assets/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/local_packages/on_audio_query-2.9.0/example/web/favicon.png b/local_packages/on_audio_query-2.9.0/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/web/favicon.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/web/icons/Icon-192.png b/local_packages/on_audio_query-2.9.0/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/web/icons/Icon-192.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/web/icons/Icon-512.png b/local_packages/on_audio_query-2.9.0/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/local_packages/on_audio_query-2.9.0/example/web/icons/Icon-512.png differ diff --git a/local_packages/on_audio_query-2.9.0/example/web/index.html b/local_packages/on_audio_query-2.9.0/example/web/index.html new file mode 100644 index 0000000..a0262fd --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/web/index.html @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + example + + + + + + + diff --git a/local_packages/on_audio_query-2.9.0/example/web/manifest.json b/local_packages/on_audio_query-2.9.0/example/web/manifest.json new file mode 100644 index 0000000..71f58db --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/local_packages/on_audio_query-2.9.0/lib/on_audio_query.dart b/local_packages/on_audio_query-2.9.0/lib/on_audio_query.dart new file mode 100644 index 0000000..c9dc649 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/lib/on_audio_query.dart @@ -0,0 +1,32 @@ +/* +============= +Author: Lucas Josino +Github: https://github.com/LucJosin +Website: https://www.lucasjosino.com/ +============= +Plugin/Id: on_audio_query#0 +Homepage: https://github.com/LucJosin/on_audio_query +Pub: https://pub.dev/packages/on_audio_query +License: https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/LICENSE +Copyright: © 2021, Lucas Josino. All rights reserved. +============= +*/ + +library on_audio_query; + +// Dart +import 'dart:async'; + +// Flutter +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +// Platform Interface +import 'package:on_audio_query_platform_interface/on_audio_query_platform_interface.dart'; +export 'package:on_audio_query_platform_interface/on_audio_query_platform_interface.dart'; + +// Controllers +part 'src/on_audio_query.dart'; + +// Widgets +part 'widget/query_artwork_widget.dart'; diff --git a/local_packages/on_audio_query-2.9.0/lib/src/on_audio_query.dart b/local_packages/on_audio_query-2.9.0/lib/src/on_audio_query.dart new file mode 100644 index 0000000..ac3c95b --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/lib/src/on_audio_query.dart @@ -0,0 +1,667 @@ +/* +============= +Author: Lucas Josino +Github: https://github.com/LucJosin +Website: https://www.lucasjosino.com/ +============= +Plugin/Id: on_audio_query#0 +Homepage: https://github.com/LucJosin/on_audio_query +Pub: https://pub.dev/packages/on_audio_query +License: https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/LICENSE +Copyright: © 2021, Lucas Josino. All rights reserved. +============= +*/ + +part of on_audio_query; + +/// Interface and Main method for use on_audio_query +class OnAudioQuery { + /// The platform interface that drives this plugin + static OnAudioQueryPlatform get platform => OnAudioQueryPlatform.instance; + + dynamic _getArgs( + WithFiltersType withType, + ) { + switch (withType) { + case WithFiltersType.AUDIOS: + return AudiosArgs.TITLE; + case WithFiltersType.ALBUMS: + return AlbumsArgs.ALBUM; + case WithFiltersType.PLAYLISTS: + return PlaylistsArgs.PLAYLIST; + case WithFiltersType.ARTISTS: + return ArtistsArgs.ARTIST; + case WithFiltersType.GENRES: + return GenresArgs.GENRE; + } + } + + /// Simplified version of [permissionsStatus] and [permissionsRequest]. + /// + /// Will check and request, if necessary, all required permissions. + /// + /// **OBS: Will always return true on web platform.** + Future checkAndRequest({bool retryRequest = false}) async { + if (kIsWeb) return true; + + bool hasPermission = await platform.permissionsStatus(); + if (!hasPermission) { + hasPermission = await platform.permissionsRequest( + retryRequest: retryRequest, + ); + } + + return hasPermission; + } + + /// Used to set the logging behavior. + /// + /// Parameters: + /// + /// * [logType] is used to define the logging level. [LogType]. + /// * [detailedLog] is used to define if detailed log will be shown + /// (Disable by default to avoid spam). + /// + /// Important: + /// + /// * If [logType] is null, will be set to [WARN]. + /// * If [detailedLog] is null, will be set to [false]. + Future setLogConfig(LogConfig? logConfig) async { + return platform.setLogConfig(logConfig); + } + + /// Used to return Songs Info based in [SongModel]. + /// + /// Parameters: + /// + /// * [orderType] is used to define if order will be Ascending or Descending. + /// * [sortType] is used to define list sort. + /// * [uriType] is used to define if songs will be catch in [EXTERNAL] or [INTERNAL] storage. + /// * [ignoreCase] is used to define if sort will ignore the lowercase or not. + /// * [path] is used to define where the songs will be 'queried'. + /// + /// Important: + /// + /// * If [orderType] is null, will be set to [ASC_OR_SMALLER]. + /// * If [sortType] is null, will be set to [DEFAULT]. + /// * If [uriType] is null, will be set to [EXTERNAL]. + /// * If [ignoreCase] is null, will be set to [true]. + /// * If [path] is null, will be set to the default platform [path]. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `✔️` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> querySongs({ + SongSortType? sortType, + OrderType? orderType, + UriType? uriType, + bool? ignoreCase, + String? path, + }) async { + return platform.querySongs( + sortType: sortType, + orderType: orderType, + uriType: uriType, + ignoreCase: ignoreCase, + path: path, + ); + } + + /// Used to return Albums Info based in [AlbumModel]. + /// + /// Parameters: + /// + /// * [orderType] is used to define if order will be Ascending or Descending. + /// * [sortType] is used to define list sort. + /// * [uriType] is used to define if songs will be catch in [EXTERNAL] or [INTERNAL] storage. + /// * [ignoreCase] is used to define if sort will ignore the lowercase or not. + /// + /// Important: + /// + /// * If [orderType] is null, will be set to [ASC_OR_SMALLER]. + /// * If [sortType] is null, will be set to [AlbumName]. + /// * If [uriType] is null, will be set to [EXTERNAL]. + /// * If [ignoreCase] is null, will be set to [true]. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `✔️` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> queryAlbums({ + AlbumSortType? sortType, + OrderType? orderType, + UriType? uriType, + bool? ignoreCase, + }) async { + return platform.queryAlbums( + sortType: sortType, + orderType: orderType, + uriType: uriType, + ignoreCase: ignoreCase, + ); + } + + /// Used to return Artists Info based in [ArtistModel]. + /// + /// Parameters: + /// + /// * [orderType] is used to define if order will be Ascending or Descending. + /// * [sortType] is used to define list sort. + /// * [uriType] is used to define if songs will be catch in [EXTERNAL] or [INTERNAL] storage. + /// * [ignoreCase] is used to define if sort will ignore the lowercase or not. + /// + /// Important: + /// + /// * If [orderType] is null, will be set to [ASC_OR_SMALLER]. + /// * If [sortType] is null, will be set to [ArtistName]. + /// * If [uriType] is null, will be set to [EXTERNAL]. + /// * If [ignoreCase] is null, will be set to [true]. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `✔️` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> queryArtists({ + ArtistSortType? sortType, + OrderType? orderType, + UriType? uriType, + bool? ignoreCase, + }) async { + return platform.queryArtists( + sortType: sortType, + orderType: orderType, + uriType: uriType, + ignoreCase: ignoreCase, + ); + } + + /// Used to return Playlists Info based in [PlaylistModel]. + /// + /// Parameters: + /// + /// * [orderType] is used to define if order will be Ascending or Descending. + /// * [sortType] is used to define list sort. + /// * [uriType] is used to define if songs will be catch in [EXTERNAL] or [INTERNAL] storage. + /// * [ignoreCase] is used to define if sort will ignore the lowercase or not. + /// + /// Important: + /// + /// * If [orderType] is null, will be set to [ASC_OR_SMALLER]. + /// * If [sortType] is null, will be set to [PlaylistName]. + /// * If [uriType] is null, will be set to [EXTERNAL]. + /// * If [ignoreCase] is null, will be set to [true]. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> queryPlaylists({ + PlaylistSortType? sortType, + OrderType? orderType, + UriType? uriType, + bool? ignoreCase, + }) async { + return platform.queryPlaylists( + sortType: sortType, + orderType: orderType, + uriType: uriType, + ignoreCase: ignoreCase, + ); + } + + /// Used to return Genres Info based in [GenreModel]. + /// + /// Parameters: + /// + /// * [orderType] is used to define if order will be Ascending or Descending. + /// * [sortType] is used to define list sort. + /// * [uriType] is used to define if songs will be catch in [EXTERNAL] or [INTERNAL] storage. + /// * [ignoreCase] is used to define if sort will ignore the lowercase or not. + /// + /// Important: + /// + /// * If [orderType] is null, will be set to [ASC_OR_SMALLER]. + /// * If [sortType] is null, will be set to [GenreName]. + /// * If [uriType] is null, will be set to [EXTERNAL]. + /// * If [ignoreCase] is null, will be set to [true]. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `✔️` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> queryGenres({ + GenreSortType? sortType, + OrderType? orderType, + UriType? uriType, + bool? ignoreCase, + }) async { + return platform.queryGenres( + sortType: sortType, + orderType: orderType, + uriType: uriType, + ignoreCase: ignoreCase, + ); + } + + /// Used to return Songs/Audios Info from a specific queryType based in [SongModel]. + /// + /// Parameters: + /// + /// * [type] is used to define where audio will be query. + /// * [where] is used to query audios from specific method. + /// * [orderType] is used to define if order will be Ascending or Descending. + /// * [sortType] is used to define list sort. + /// * [ignoreCase] is used to define if sort will ignore the lowercase or not. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `✔️` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> queryAudiosFrom( + AudiosFromType type, + Object where, { + SongSortType? sortType, + OrderType? orderType, + bool? ignoreCase, + }) async { + return platform.queryAudiosFrom( + type, + where, + sortType: sortType, + orderType: orderType, + ignoreCase: ignoreCase, + ); + } + + /// Used to return Songs Info based in Something. Works like a "Search". + /// + /// Parameters: + /// + /// * [withType] The type of search based in [WithFiltersType]. + /// * [args] is used to define what you're looking for. + /// * [argsVal] The "key". + /// + /// Before you use: + /// + /// * [queryWithFilters] implements all types based in [WithFiltersType], this method return always a [dynamic] List. + /// * After call this method you will need to specify the [Model]. See [Example1]. + /// + /// Example1: + /// + /// ```dart + /// //Using [FutureBuilder] + /// //I changed [>] to [-] + /// builder: (context, AsyncSnapshot-List-dynamic-- item) { + /// List-SongModel- = item.data!.map((e) => SongModel(e)).toList(); //Ex1 + /// List-ArtistModel- = item.data!.map((e) => ArtistModel(e)).toList(); //Ex2 + /// ...} + /// ``` + /// + /// Important: + /// + /// * If [args] is null, will be set to [Title] or [Name]. + /// * If Android >= Q/10 [artwork] will return null, in this case, it's necessary use [queryArtwork]. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `✔️` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> queryWithFilters( + String argsVal, + WithFiltersType withType, { + dynamic args, + }) async { + return platform.queryWithFilters( + argsVal, + withType, + args ?? _getArgs(withType), + ); + } + + /// Used to return Songs Artwork. + /// + /// Parameters: + /// + /// * [type] is used to define if artwork is from audios or albums. + /// * [format] is used to define type [PNG] or [JPEG]. + /// * [size] is used to define image quality. + /// + /// Usage and Performance: + /// + /// * Using [PNG] will return a better image quality but a slow performance. + /// * Using [Size] greater than 200 probably won't make difference in quality but will cause a slow performance. + /// + /// Important: + /// + /// * This method is only necessary for API >= 29 [Android Q/10]. + /// * If [queryArtwork] is called in Android below Q/10, will return null. + /// * If [format] is null, will be set to [JPEG] for better performance. + /// * If [size] is null, will be set to [200] for better performance + /// * We need this method separated from [querySongs/queryAudios] because + /// return [Uint8List] and using inside query causes a slow performance. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `✔️` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future queryArtwork( + int id, + ArtworkType type, { + ArtworkFormat? format, + int? size, + int? quality, + }) async { + return platform.queryArtwork( + id, + type, + format: format, + size: size, + quality: quality, + ); + } + + /// Used to return Songs Info from a specific [Folder] based in [SongModel]. + /// + /// Parameters: + /// + /// * [path] is used to define where the plugin will search for audio. + /// * [orderType] is used to define if order will be Ascending or Descending. + /// * [sortType] is used to define list sort. + /// * [uriType] is used to define if songs will be catch in [EXTERNAL] or [INTERNAL] storage. + /// + /// Important: + /// + /// * If [orderType] is null, will be set to [ASC_OR_SMALLER]. + /// * If [sortType] is null, will be set to [title]. + /// * If [uriType] is null, will be set to [EXTERNAL]. + /// * If Android >= Q/10 [artwork] will return null, in this case, it's necessary use [queryArtwork]. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `❌` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> queryFromFolder( + String path, { + SongSortType? sortType, + OrderType? orderType, + UriType? uriType, + }) async { + return platform.queryFromFolder( + path, + sortType: sortType, + orderType: orderType, + uriType: uriType, + ); + } + + /// Used to return Songs path. + /// + /// Important: + /// + /// * Duplicate path will be ignored. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `❌` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future> queryAllPath() async { + return platform.queryAllPath(); + } + + //Playlist methods + + /// Used to create a Playlist + /// + /// Parameters: + /// + /// * [name] the playlist name. + /// * [author] the playlist author. (IOS only) + /// * [desc] the playlist description. (IOS only) + /// + /// Important: + /// + /// * This method create a playlist using [External Storage], all apps will be able to see this playlist + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future createPlaylist( + String name, { + String? author, + String? desc, + }) async { + return platform.createPlaylist( + name, + author: author, + desc: desc, + ); + } + + /// Used to remove/delete a Playlist + /// + /// Parameters: + /// + /// * [playlistId] is used to check if Playlist exist. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `❌` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future removePlaylist(int playlistId) async { + return platform.removePlaylist(playlistId); + } + + /// Used to add a specific song/audio to a specific Playlist + /// + /// Parameters: + /// + /// * [playlistId] is used to check if Playlist exist. + /// * [audioId] is used to add specific audio to Playlist. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future addToPlaylist(int playlistId, int audioId) async { + return platform.addToPlaylist(playlistId, audioId); + } + + /// Used to remove a specific song/audio from a specific Playlist + /// + /// Parameters: + /// + /// * [playlistId] is used to check if Playlist exist. + /// * [audioId] is used to remove specific audio from Playlist. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `❌` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future removeFromPlaylist(int playlistId, int audioId) async { + return platform.removeFromPlaylist(playlistId, audioId); + } + + /// Used to change song/audio position from a specific Playlist + /// + /// Parameters: + /// + /// * [playlistId] is used to check if Playlist exist. + /// * [from] is the old position from a audio/song. + /// * [to] is the new position from a audio/song. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `❌` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future moveItemTo(int playlistId, int from, int to) async { + return platform.moveItemTo(playlistId, from, to); + } + + /// Used to rename a specific Playlist + /// + /// Parameters: + /// + /// * [playlistId] is used to check if Playlist exist. + /// * [newName] is used to add a new name to a Playlist. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `❌` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future renamePlaylist(int playlistId, String newName) async { + return renamePlaylist(playlistId, newName); + } + + // Permissions methods + + /// Used to check Android permissions status + /// + /// Important: + /// + /// * This method will always return a bool. + /// * If return true `[READ]` and `[WRITE]` permissions is Granted, + /// else `[READ]` and `[WRITE]` is Denied. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future permissionsStatus() async { + return platform.permissionsStatus(); + } + + /// Used to request Android permissions. + /// + /// Important: + /// + /// * This method will always return a bool. + /// * If return true `[READ]` and `[WRITE]` permissions is Granted, + /// else `[READ]` and `[WRITE]` is Denied. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future permissionsRequest({bool retryRequest = false}) async { + return platform.permissionsRequest(retryRequest: retryRequest); + } + + // Device Information + + /// Used to return Device Info + /// + /// Will return: + /// + /// * Device SDK. + /// * Device Release. + /// * Device Code. + /// * Device Type. + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `✔️` | `✔️` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/PLSCFORMS.md) + Future queryDeviceInfo() async { + return platform.queryDeviceInfo(); + } + + // Others + + /// Used to scan the given [path] + /// + /// Will return: + /// + /// * A boolean indicating if the path was scanned or not. + /// + /// Usage: + /// + /// * When using the [Android] platform. After deleting a media using the [dart:io], + /// call this method to update the media. If the media was successfully and the path + /// not scanned. Will keep showing on [querySongs]. + /// + /// Example: + /// + /// ```dart + /// OnAudioQuery _audioQuery = OnAudioQuery(); + /// File file = File('path'); + /// + /// try { + /// if (file.existsSync()) { + /// file.deleteSync(); + /// _audioQuery.scanMedia(file.path); // Scan the media 'path' + /// } + /// } catch (e) { + /// debugPrint('$e'); + /// } + /// ``` + /// + /// Platforms: + /// + /// | Android | IOS | Web | + /// |--------------|-----------------|-----------------| + /// | `✔️` | `❌` | `❌` |
+ /// + /// See more about [platforms support](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/PLSCFORMS.md) + Future scanMedia(String path) async { + return await platform.scanMedia(path); + } +} diff --git a/local_packages/on_audio_query-2.9.0/lib/widget/query_artwork_widget.dart b/local_packages/on_audio_query-2.9.0/lib/widget/query_artwork_widget.dart new file mode 100644 index 0000000..011c0c1 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/lib/widget/query_artwork_widget.dart @@ -0,0 +1,334 @@ +/* +============= +Author: Lucas Josino +Github: https://github.com/LucJosin +Website: https://www.lucasjosino.com/ +============= +Plugin/Id: on_audio_query#0 +Homepage: https://github.com/LucJosin/on_audio_query +Pub: https://pub.dev/packages/on_audio_query +License: https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/LICENSE +Copyright: © 2021, Lucas Josino. All rights reserved. +============= +*/ + +part of on_audio_query; + +/// Widget that will help to "query" artwork for song/album. +/// +/// A simple example on how you can use the [queryArtwork]. +/// +/// Important: +/// +/// * If [controller] is null, will be create a new instance. +/// * Log set with [setLogConfig] will only work if [controller] is not null. +/// +/// See more: [QueryArtworkWidget](https://pub.dev/documentation/on_audio_query/latest/on_audio_query/QueryArtworkWidget-class.html) +class QueryArtworkWidget extends StatelessWidget { + /// Used to find and get image. + /// + /// All Audio/Song has a unique [id]. + final int id; + + /// Used to call the platform specific method. + /// + /// Important: + /// + /// * If [controller] is null, will be create a new instance. + /// * Log set with [setLogConfig] will only work if [controller] is not null. + final OnAudioQuery? controller; + + /// Used to define artwork [type]. + /// + /// Opts: [AUDIO] and [ALBUM]. + final ArtworkType type; + + /// Used to define artwork [format]. + /// + /// Opts: [JPEG] and [PNG]. + /// + /// Important: + /// + /// * If [format] is not defined, will be set to [JPEG]. + final ArtworkFormat format; + + /// Used to define artwork [size]. + /// + /// Important: + /// + /// * If [size] is not defined, will be set to [200]. + /// * This value have a directly influence to image quality. + final int size; + + /// Used to define artwork [quality]. + /// + /// Important: + /// + /// * If [quality] is not defined, will be set to [100]. + final int quality; + + /// Used to define the artwork [border radius]. + /// + /// Important: + /// + /// * If [artworkBorder] is null, will be set to [50]. + final BorderRadius? artworkBorder; + + /// Used to define the artwork [quality]. + /// + /// Important: + /// + /// * If [artworkQuality] is not defined, will be set to [low]. + /// * This value doesn't have a directly influence to image quality. + final FilterQuality artworkQuality; + + /// Used to define artwork [width]. + /// + /// Important: + /// + /// * If [artworkWidth] is not defined, will be set to [50]. + final double artworkWidth; + + /// Used to define artwork [height]. + /// + /// Important: + /// + /// * If [artworkHeight] is not defined, will be set to [50]. + final double artworkHeight; + + /// Used to define artwork [fit]. + /// + /// Important: + /// + /// * If [artworkFit] is not defined, will be set to [cover]. + final BoxFit artworkFit; + + /// Used to define artwork [clip]. + /// + /// Important: + /// + /// * If [artworkClipBehavior] is not defined, will be set to [antiAlias]. + final Clip artworkClipBehavior; + + /// Used to define artwork [scale]. + /// + /// Important: + /// + /// * If [artworkScale] is not defined, will be set to [1.0]. + final double artworkScale; + + /// Used to define if artwork should [repeat]. + /// + /// Important: + /// + /// * If [artworkRepeat] is not defined, will be set to [false]. + final ImageRepeat artworkRepeat; + + /// Used to define artwork [color]. + /// + /// Important: + /// + /// * [artworkColor] default value is [null]. + final Color? artworkColor; + + /// Used to define artwork [blend]. + /// + /// Important: + /// + /// * [artworkBlendMode] default value is [null]. + final BlendMode? artworkBlendMode; + + /// Used to define if artwork should [keep] old art even when [Flutter State] change. + /// + /// ## Flutter Docs: + /// + /// ### Why is the default value of [gaplessPlayback] false? + /// + /// Having the default value of [gaplessPlayback] be false helps prevent + /// situations where stale or misleading information might be presented. + /// Consider the following case: + /// + /// We have constructed a 'Person' widget that displays an avatar [Image] of + /// the currently loaded person along with their name. We could request for a + /// new person to be loaded into the widget at any time. Suppose we have a + /// person currently loaded and the widget loads a new person. What happens + /// if the [Image] fails to load? + /// + /// * Option A ([gaplessPlayback] = false): The new person's name is coupled + /// with a blank image. + /// + /// * Option B ([gaplessPlayback] = true): The widget displays the avatar of + /// the previous person and the name of the newly loaded person. + /// + /// Important: + /// + /// * If [keepOldArtwork] is not defined, will be set to [false]. + final bool keepOldArtwork; + + /// Used to define a Widget when audio/song don't return any artwork. + /// + /// Important: + /// + /// * If [nullArtworkWidget] is null, will be set to [image_not_supported] icon. + final Widget? nullArtworkWidget; + + /// A builder function that is called if an error occurs during image loading. + /// + /// + /// If this builder is not provided, any exceptions will be reported to + /// [FlutterError.onError]. If it is provided, the caller should either handle + /// the exception by providing a replacement widget, or rethrow the exception. + /// + /// Important: + /// + /// * If [errorBuilder] is null, will set the [nullArtworkWidget] and if is null + /// will be used a icon(image_not_supported). + /// + /// The following sample uses [errorBuilder] to show a '😢' in place of the + /// image that fails to load, and prints the error to the console. + /// + /// ```dart + /// Widget build(BuildContext context) { + /// return DecoratedBox( + /// decoration: BoxDecoration( + /// color: Colors.white, + /// border: Border.all(), + /// borderRadius: BorderRadius.circular(20), + /// ), + /// child: Image.network( + /// 'https://example.does.not.exist/image.jpg', + /// errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { + /// // Appropriate logging or analytics, e.g. + /// // myAnalytics.recordError( + /// // 'An error occurred loading "https://example.does.not.exist/image.jpg"', + /// // exception, + /// // stackTrace, + /// // ); + /// return const Text('😢'); + /// }, + /// ), + /// ); + /// } + /// ``` + /// + /// `From flutter documentation` + /// + final Widget Function(BuildContext, Object, StackTrace?)? errorBuilder; + + /// A builder function responsible for creating the widget that represents + /// this image. + /// + /// The following sample demonstrates how to use this builder to implement an + /// image that fades in once it's been loaded. + /// + /// This sample contains a limited subset of the functionality that the + /// [FadeInImage] widget provides out of the box. + /// + /// ```dart + /// @override + /// Widget build(BuildContext context) { + /// return DecoratedBox( + /// decoration: BoxDecoration( + /// color: Colors.white, + /// border: Border.all(), + /// borderRadius: BorderRadius.circular(20), + /// ), + /// child: Image.network( + /// 'https://flutter.github.io/assets-for-api-docs/assets/widgets/puffin.jpg', + /// frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) { + /// if (wasSynchronouslyLoaded) { + /// return child; + /// } + /// return AnimatedOpacity( + /// child: child, + /// opacity: frame == null ? 0 : 1, + /// duration: const Duration(seconds: 1), + /// curve: Curves.easeOut, + /// ); + /// }, + /// ), + /// ); + /// } + /// ``` + /// + /// `From flutter documentation` + /// + final Widget Function(BuildContext, Widget, int?, bool)? frameBuilder; + + /// Widget that will help to "query" artwork for song/album. + /// + /// A simple example on how you can use the [queryArtwork]. + /// + /// See more: [QueryArtworkWidget](https://pub.dev/documentation/on_audio_query/latest/on_audio_query/QueryArtworkWidget-class.html) + const QueryArtworkWidget({ + Key? key, + required this.id, + required this.type, + this.quality = 50, + this.controller, + this.format = ArtworkFormat.JPEG, + this.size = 200, + this.artworkQuality = FilterQuality.low, + this.artworkBorder, + this.artworkWidth = 50, + this.artworkHeight = 50, + this.artworkFit = BoxFit.cover, + this.artworkClipBehavior = Clip.antiAlias, + this.artworkScale = 1.0, + this.artworkRepeat = ImageRepeat.noRepeat, + this.artworkColor, + this.artworkBlendMode, + this.keepOldArtwork = false, + this.nullArtworkWidget, + this.errorBuilder, + this.frameBuilder, + }) : assert(quality <= 100), + super(key: key); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: (controller ?? OnAudioQuery()).queryArtwork( + id, + type, + format: format, + size: size, + quality: quality, + ), + builder: (context, item) { + if (item.data != null && item.data!.isNotEmpty) { + return ClipRRect( + borderRadius: artworkBorder ?? BorderRadius.circular(50), + clipBehavior: artworkClipBehavior, + child: Image.memory( + item.data!, + gaplessPlayback: keepOldArtwork, + repeat: artworkRepeat, + scale: artworkScale, + width: artworkWidth, + height: artworkHeight, + fit: artworkFit, + color: artworkColor, + colorBlendMode: artworkBlendMode, + filterQuality: artworkQuality, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder ?? + (context, exception, stackTrace) { + return nullArtworkWidget ?? + const Icon( + Icons.image_not_supported, + size: 50, + ); + }, + ), + ); + } + return nullArtworkWidget ?? + const Icon( + Icons.image_not_supported, + size: 50, + ); + }, + ); + } +} diff --git a/local_packages/on_audio_query-2.9.0/pubspec.lock b/local_packages/on_audio_query-2.9.0/pubspec.lock new file mode 100644 index 0000000..a0cb754 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/pubspec.lock @@ -0,0 +1,258 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + id3: + dependency: transitive + description: + name: id3 + sha256: "24176a6e08db6297c8450079e94569cd8387f913c817e5e3d862be7dc191e0b8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + on_audio_query_android: + dependency: "direct main" + description: + name: on_audio_query_android + sha256: c0b946011ee7aa8ec76b21837f0cb0007120ac291202ed6c6fb4c498ba2ff6fe + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + on_audio_query_ios: + dependency: "direct main" + description: + name: on_audio_query_ios + sha256: "9b3efa39a656fa3720980e3c6a1f55b7257d0032a45ffeb3f70eaa2c7f10f929" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + on_audio_query_platform_interface: + dependency: "direct main" + description: + name: on_audio_query_platform_interface + sha256: c23e019a31bd0774828476e428fd33b0dd1d82c9d4791dba80429358fc65dcd3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.7.0" + on_audio_query_web: + dependency: "direct main" + description: + name: on_audio_query_web + sha256: "990efa52d879e6caffa97f24b34acd9caa1ce2c4c4cb873fe5a899a9b1af02c7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.4" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.flutter-io.cn" + source: hosted + version: "14.3.1" +sdks: + dart: ">=3.7.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/local_packages/on_audio_query-2.9.0/pubspec.yaml b/local_packages/on_audio_query-2.9.0/pubspec.yaml new file mode 100644 index 0000000..44d9c36 --- /dev/null +++ b/local_packages/on_audio_query-2.9.0/pubspec.yaml @@ -0,0 +1,52 @@ +# ======== +# author: Lucas Josino +# github: https://github.com/LucJosin +# website: https://www.lucasjosino.com/ +# ======== +name: on_audio_query +description: Flutter Plugin used to query audios/songs infos [title, artist, album, etc..] from device storage. +version: 2.9.0 +homepage: https://github.com/LucJosin/on_audio_query/tree/main/packages/on_audio_query +issue_tracker: https://github.com/LucJosin/on_audio_query/issues +# pub.dev: https://pub.dev/packages/on_audio_query +topics: + - audio + - song + - audioquery + - on-audio-query + - storage + - mediastore + - mpmediaquery + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=1.20.0" + +dependencies: + # on_audio_query + on_audio_query_platform_interface: ^1.7.0 + on_audio_query_web: ^1.6.0 + on_audio_query_ios: ^1.1.0 + on_audio_query_android: + path: ../on_audio_query_android-1.1.0 + + # Flutter + flutter: + sdk: flutter + +dev_dependencies: + # Flutter + flutter_lints: ^1.0.4 + flutter_test: + sdk: flutter + +# The following section is specific to Flutter. +flutter: + # Material Design + uses-material-design: true + plugin: + platforms: + android: + default_package: on_audio_query_android + ios: + default_package: on_audio_query_ios diff --git a/local_packages/on_audio_query_android-1.1.0/CHANGELOG.md b/local_packages/on_audio_query_android-1.1.0/CHANGELOG.md new file mode 100644 index 0000000..86ce93f --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/CHANGELOG.md @@ -0,0 +1,15 @@ +## 1.1.0 + +- See more [on_audio_query - CHANGELOG](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md). + +## 1.0.1 + +- See more [on_audio_query - CHANGELOG](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md). + +## 1.0.0 + +- See more [on_audio_query - CHANGELOG](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md). + +## 1.0.0-prerelease.0 - [06.20.2022] + +- See more [on_audio_query - CHANGELOG](https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/CHANGELOG.md). diff --git a/local_packages/on_audio_query_android-1.1.0/LICENSE b/local_packages/on_audio_query_android-1.1.0/LICENSE new file mode 100644 index 0000000..c110c94 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, Lucas Josino +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/local_packages/on_audio_query_android-1.1.0/README.md b/local_packages/on_audio_query_android-1.1.0/README.md new file mode 100644 index 0000000..ad66ce0 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/README.md @@ -0,0 +1,11 @@ +# on_audio_query_android + +The Android implementation of [`on_audio_query`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `on_audio_query` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/on_audio_query +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/local_packages/on_audio_query_android-1.1.0/android/.gradle/7.0.2/fileChanges/last-build.bin b/local_packages/on_audio_query_android-1.1.0/android/.gradle/7.0.2/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/local_packages/on_audio_query_android-1.1.0/android/.gradle/7.0.2/fileChanges/last-build.bin differ diff --git a/local_packages/on_audio_query_android-1.1.0/android/.gradle/7.0.2/fileHashes/fileHashes.lock b/local_packages/on_audio_query_android-1.1.0/android/.gradle/7.0.2/fileHashes/fileHashes.lock new file mode 100644 index 0000000..416e12f Binary files /dev/null and b/local_packages/on_audio_query_android-1.1.0/android/.gradle/7.0.2/fileHashes/fileHashes.lock differ diff --git a/local_packages/on_audio_query_android-1.1.0/android/.gradle/7.0.2/gc.properties b/local_packages/on_audio_query_android-1.1.0/android/.gradle/7.0.2/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/on_audio_query_android-1.1.0/android/.gradle/checksums/checksums.lock b/local_packages/on_audio_query_android-1.1.0/android/.gradle/checksums/checksums.lock new file mode 100644 index 0000000..385f3d3 Binary files /dev/null and b/local_packages/on_audio_query_android-1.1.0/android/.gradle/checksums/checksums.lock differ diff --git a/local_packages/on_audio_query_android-1.1.0/android/.gradle/vcs-1/gc.properties b/local_packages/on_audio_query_android-1.1.0/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/on_audio_query_android-1.1.0/android/build.gradle b/local_packages/on_audio_query_android-1.1.0/android/build.gradle new file mode 100644 index 0000000..f46f39b --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/build.gradle @@ -0,0 +1,51 @@ +group 'com.lucasjosino.on_audio_query' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '2.1.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 36 + namespace "com.lucasjosino.on_audio_query" + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 16 + } +} + +dependencies { + def lifecycle_version = "2.3.1" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" +} diff --git a/local_packages/on_audio_query_android-1.1.0/android/gradle.properties b/local_packages/on_audio_query_android-1.1.0/android/gradle.properties new file mode 100644 index 0000000..46c1f16 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/local_packages/on_audio_query_android-1.1.0/android/gradle/wrapper/gradle-wrapper.properties b/local_packages/on_audio_query_android-1.1.0/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cd907d4 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Sep 27 10:16:16 BRT 2021 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/local_packages/on_audio_query_android-1.1.0/android/settings.gradle b/local_packages/on_audio_query_android-1.1.0/android/settings.gradle new file mode 100644 index 0000000..b93f7e9 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'on_audio_query' diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/AndroidManifest.xml b/local_packages/on_audio_query_android-1.1.0/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9e88df7 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/OnAudioQueryPlugin.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/OnAudioQueryPlugin.kt new file mode 100644 index 0000000..768c35b --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/OnAudioQueryPlugin.kt @@ -0,0 +1,188 @@ +/* +============= +Author: Lucas Josino +Github: https://github.com/LucJosin +Website: https://www.lucasjosino.com/ +============= +Plugin/Id: on_audio_query#0 +Homepage: https://github.com/LucJosin/on_audio_query +Pub: https://pub.dev/packages/on_audio_query +License: https://github.com/LucJosin/on_audio_query/blob/main/on_audio_query/LICENSE +Copyright: © 2021, Lucas Josino. All rights reserved. +============= +*/ + +package com.lucasjosino.on_audio_query + +import android.media.MediaScannerConnection +import android.os.Build +import com.lucasjosino.on_audio_query.consts.Method +import com.lucasjosino.on_audio_query.controllers.MethodController +import com.lucasjosino.on_audio_query.controllers.PermissionController +import io.flutter.Log +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +class OnAudioQueryPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { + + init { + // Set default logging level + Log.setLogLevel(Log.WARN) + } + + companion object { + // Get the current class name. + private const val TAG: String = "OnAudioQueryPlugin" + + // Method channel name. + private const val CHANNEL_NAME = "com.lucasjosino.on_audio_query" + } + + private var permissionController = PermissionController() + private var methodController = MethodController() + + private var binding: ActivityPluginBinding? = null + + private lateinit var channel: MethodChannel + + // Dart <-> Kotlin communication + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + Log.i(TAG, "Attached to engine") + + // Setup the method channel communication. + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel.setMethodCallHandler(this) + } + + // Methods will always follow the same route: + // Receive method -> check permission -> controller -> do what's needed -> return to dart + override fun onMethodCall(call: MethodCall, result: Result) { + Log.d(TAG, "Started method call (${call.method})") + + // Init the plugin provider with current 'call' and 'result'. + PluginProvider.setCurrentMethod(call, result) + + // If user deny permission request a pop up will immediately show up + // If [retryRequest] is null, the message will only show when call method again + val retryRequest = call.argument("retryRequest") ?: false + permissionController.retryRequest = retryRequest + + Log.i(TAG, "Method call: ${call.method}") + when (call.method) { + // Permissions + Method.PERMISSION_STATUS -> { + val hasPermission = permissionController.permissionStatus() + result.success(hasPermission) + } + Method.PERMISSION_REQUEST -> { + permissionController.requestPermission() + } + + // Device information + Method.QUERY_DEVICE_INFO -> { + result.success( + hashMapOf( + "device_model" to Build.MODEL, + "device_sys_version" to Build.VERSION.SDK_INT, + "device_sys_type" to "Android" + ) + ) + } + + // This method will scan the given path to update the 'state'. + // When deleting a file using 'dart:io', call this method to update the file 'state'. + Method.SCAN -> { + val sPath: String? = call.argument("path") + val context = PluginProvider.context() + + // Check if the given file is null or empty. + if (sPath == null || sPath.isEmpty()) { + Log.w(TAG, "Method 'scan' was called with null or empty 'path'") + result.success(false) + } + + // Scan and return + MediaScannerConnection.scanFile(context, arrayOf(sPath), null) { _, _ -> + Log.d(TAG, "Scanned file: $sPath") + result.success(true) + } + } + + // Logging + Method.SET_LOG_CONFIG -> { + // Log level + Log.setLogLevel(call.argument("level")!!) + + // Define if 'warn' level will show more detailed logging. + PluginProvider.showDetailedLog = call.argument("showDetailedLog")!! + + result.success(true) + } + + // All others methods + else -> { + Log.d(TAG, "Checking permissions...") + + val hasPermission = permissionController.permissionStatus() + Log.d(TAG, "Application has permissions: $hasPermission") + + if (!hasPermission) { + Log.w(TAG, "The application doesn't have access to the library") + result.error( + "MissingPermissions", + "Application doesn't have access to the library", + "Call the [permissionsRequest] method or install a external plugin to handle the app permission." + ) + } + + methodController.find() + } + } + + Log.d(TAG, "Ended method call (${call.method})\n ") + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + Log.i(TAG, "Detached from engine") + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + Log.i(TAG, "Attached to activity") + + // Init plugin provider with 'activity' and 'context'. + PluginProvider.set(binding.activity) + + // Add to controller the permission to listen to the request result. + this.binding = binding + binding.addRequestPermissionsResultListener(permissionController) + } + + override fun onDetachedFromActivityForConfigChanges() { + Log.i(TAG, "Detached from engine (config changes)") + onDetachedFromActivity() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + Log.i(TAG, "Reattached to activity (config changes)") + onAttachedToActivity(binding) + } + + // Detach all parameters. + override fun onDetachedFromActivity() { + Log.i(TAG, "Detached from activity") + + // Remove the permission listener + if (binding != null) { + binding!!.removeRequestPermissionsResultListener(permissionController) + } + + this.binding = null + Log.i(TAG, "Removed all declared methods") + } +} diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/PluginProvider.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/PluginProvider.kt new file mode 100644 index 0000000..e0fa2c7 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/PluginProvider.kt @@ -0,0 +1,97 @@ +package com.lucasjosino.on_audio_query + +import android.app.Activity +import android.content.Context +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.lang.ref.WeakReference + +/** + * A singleton used to define all variables/methods that will be used on all plugin. + * + * The singleton will provider the ability to 'request' required variables/methods on any moment. + * + * All variables/methods should be defined after plugin initialization (activity/context) and + * dart request (call/result). + */ +object PluginProvider { + private const val ERROR_MESSAGE = + "Tried to get one of the methods but the 'PluginProvider' has not initialized" + + /** + * Define if 'warn' level will show more detailed logging. + * + * Will be used when a query produce some error. + */ + var showDetailedLog: Boolean = false + + private lateinit var context: WeakReference + + private lateinit var activity: WeakReference + + private lateinit var call: WeakReference + + private lateinit var result: WeakReference + + /** + * Used to define the current [Activity] and [Context]. + * + * Should be defined once. + */ + fun set(activity: Activity) { + this.context = WeakReference(activity.applicationContext) + this.activity = WeakReference(activity) + } + + /** + * Used to define the current dart request. + * + * Should be defined/redefined on every [MethodChannel.MethodCallHandler.onMethodCall] request. + */ + fun setCurrentMethod(call: MethodCall, result: MethodChannel.Result) { + this.call = WeakReference(call) + this.result = WeakReference(result) + } + + /** + * The current plugin 'context'. Defined once. + * + * @throws UninitializedPluginProviderException + * @return [Context] + */ + fun context(): Context { + return this.context.get() ?: throw UninitializedPluginProviderException(ERROR_MESSAGE) + } + + /** + * The current plugin 'activity'. Defined once. + * + * @throws UninitializedPluginProviderException + * @return [Activity] + */ + fun activity(): Activity { + return this.activity.get() ?: throw UninitializedPluginProviderException(ERROR_MESSAGE) + } + + /** + * The current plugin 'call'. Will be replace with newest dart request. + * + * @throws UninitializedPluginProviderException + * @return [MethodCall] + */ + fun call(): MethodCall { + return this.call.get() ?: throw UninitializedPluginProviderException(ERROR_MESSAGE) + } + + /** + * The current plugin 'result'. Will be replace with newest dart request. + * + * @throws UninitializedPluginProviderException + * @return [MethodChannel.Result] + */ + fun result(): MethodChannel.Result { + return this.result.get() ?: throw UninitializedPluginProviderException(ERROR_MESSAGE) + } + + class UninitializedPluginProviderException(msg: String) : Exception(msg) +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/consts/Method.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/consts/Method.kt new file mode 100644 index 0000000..dce7b90 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/consts/Method.kt @@ -0,0 +1,29 @@ +package com.lucasjosino.on_audio_query.consts + +object Method { + // General methods + const val PERMISSION_STATUS = "permissionsStatus" + const val PERMISSION_REQUEST = "permissionsRequest" + const val QUERY_DEVICE_INFO = "queryDeviceInfo" + const val SCAN = "scan" + const val SET_LOG_CONFIG = "setLogConfig" + + // Query methods + const val QUERY_AUDIOS = "querySongs" + const val QUERY_ALBUMS = "queryAlbums" + const val QUERY_ARTISTS = "queryArtists" + const val QUERY_GENRES = "queryGenres" + const val QUERY_PLAYLISTS = "queryPlaylists" + const val QUERY_ARTWORK = "queryArtwork" + const val QUERY_AUDIOS_FROM = "queryAudiosFrom" + const val QUERY_WITH_FILTERS = "queryWithFilters" + const val QUERY_ALL_PATHS = "queryAllPath" + + // Playlist methods + const val CREATE_PLAYLIST = "createPlaylist" + const val REMOVE_PLAYLIST = "removePlaylist" + const val ADD_TO_PLAYLIST = "addToPlaylist" + const val REMOVE_FROM_PLAYLIST = "removeFromPlaylist" + const val RENAME_PLAYLIST = "renamePlaylist" + const val MOVE_ITEM_TO = "moveItemTo" +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/MethodController.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/MethodController.kt new file mode 100644 index 0000000..0b15ba2 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/MethodController.kt @@ -0,0 +1,32 @@ +package com.lucasjosino.on_audio_query.controllers + +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.consts.Method +import com.lucasjosino.on_audio_query.queries.* + +class MethodController() { + + // + fun find() { + when (PluginProvider.call().method) { + //Query methods + Method.QUERY_AUDIOS -> AudioQuery().querySongs() + Method.QUERY_ALBUMS -> AlbumQuery().queryAlbums() + Method.QUERY_ARTISTS -> ArtistQuery().queryArtists() + Method.QUERY_PLAYLISTS -> PlaylistQuery().queryPlaylists() + Method.QUERY_GENRES -> GenreQuery().queryGenres() + Method.QUERY_ARTWORK -> ArtworkQuery().queryArtwork() + Method.QUERY_AUDIOS_FROM -> AudioFromQuery().querySongsFrom() + Method.QUERY_WITH_FILTERS -> WithFiltersQuery().queryWithFilters() + Method.QUERY_ALL_PATHS -> AllPathQuery().queryAllPath() + //Playlists methods + Method.CREATE_PLAYLIST -> PlaylistController().createPlaylist() + Method.REMOVE_PLAYLIST -> PlaylistController().removePlaylist() + Method.ADD_TO_PLAYLIST -> PlaylistController().addToPlaylist() + Method.REMOVE_FROM_PLAYLIST -> PlaylistController().removeFromPlaylist() + Method.RENAME_PLAYLIST -> PlaylistController().renamePlaylist() + Method.MOVE_ITEM_TO -> PlaylistController().moveItemTo() + else -> PluginProvider.result().notImplemented() + } + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PermissionController.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PermissionController.kt new file mode 100644 index 0000000..0da869d --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PermissionController.kt @@ -0,0 +1,93 @@ +package com.lucasjosino.on_audio_query.controllers + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.interfaces.PermissionManagerInterface +import io.flutter.Log +import io.flutter.plugin.common.PluginRegistry + +class PermissionController : PermissionManagerInterface, + PluginRegistry.RequestPermissionsResultListener { + + companion object { + private const val TAG: String = "PermissionController" + + private const val REQUEST_CODE: Int = 88560 + } + + var retryRequest: Boolean = false + + private var permissions: Array = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arrayOf( + Manifest.permission.READ_MEDIA_AUDIO, + Manifest.permission.READ_MEDIA_IMAGES + ) + } else { + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + } + + override fun permissionStatus(): Boolean = permissions.all { + // After "leaving" this class, context will be null so, we need this context argument to + // call the [checkSelfPermission]. + return ContextCompat.checkSelfPermission( + PluginProvider.context(), + it + ) == PackageManager.PERMISSION_GRANTED + } + + override fun requestPermission() { + Log.d(TAG, "Requesting permissions.") + Log.d(TAG, "SDK: ${Build.VERSION.SDK_INT}, Should retry request: $retryRequest") + val activity = PluginProvider.activity() + ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) + } + + // Second requestPermission, this one with the option "Never Ask Again". + override fun retryRequestPermission() { + val activity = PluginProvider.activity() + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permissions[0]) + || ActivityCompat.shouldShowRequestPermissionRationale(activity, permissions[1]) + ) { + Log.d(TAG, "Retrying permission request") + retryRequest = false + requestPermission() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + // When the incoming request code doesn't match the request codes defined by the on_audio_query + // plugin return [false] to indicate the [on_audio_query] plugin is not handling the request + // result and Android should continue executing other registered handlers. + if (REQUEST_CODE != requestCode) return false + + // Check permission + val isPermissionGranted = (grantResults.isNotEmpty() + && grantResults[0] == PackageManager.PERMISSION_GRANTED) + + Log.d(TAG, "Permission accepted: $isPermissionGranted") + + // After all checks, we can handle the permission request. + val result = PluginProvider.result() + when { + isPermissionGranted -> result.success(true) + retryRequest -> retryRequestPermission() + else -> result.success(false) + } + + // Return [true] here to indicate that the [on_audio_query] plugin handled the permission request + // result and Android should not continue executing other registered handlers. + return true + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PlaylistController.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PlaylistController.kt new file mode 100644 index 0000000..6161426 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/controllers/PlaylistController.kt @@ -0,0 +1,215 @@ +package com.lucasjosino.on_audio_query.controllers + +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.os.Build +import android.provider.MediaStore +import android.util.Log +import com.lucasjosino.on_audio_query.PluginProvider + +/** OnPlaylistsController */ +class PlaylistController { + + //Main parameters + private val uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI + private val contentValues = ContentValues() + private val channelError = "on_audio_error" + private lateinit var resolver: ContentResolver + + //Query projection + private val columns = arrayOf( + "count(*)" + ) + + private val context = PluginProvider.context() + private val result = PluginProvider.result() + private val call = PluginProvider.call() + + // + fun createPlaylist() { + this.resolver = context.contentResolver + val playlistName = call.argument("playlistName")!! + + //For create we don't check if name already exist + contentValues.put(MediaStore.Audio.Playlists.NAME, playlistName) + contentValues.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis()) + resolver.insert(uri, contentValues) + result.success(true) + } + + // + fun removePlaylist() { + this.resolver = context.contentResolver + val playlistId = call.argument("playlistId")!! + + //Check if Playlist exists based in Id + if (!checkPlaylistId(playlistId)) result.success(false) + else { + val delUri = ContentUris.withAppendedId(uri, playlistId.toLong()) + resolver.delete(delUri, null, null) + result.success(true) + } + } + + //TODO Add option to use a list + //TODO Fix error on Android 10 + fun addToPlaylist() { + this.resolver = context.contentResolver + val playlistId = call.argument("playlistId")!! + val audioId = call.argument("audioId")!! + + + //Check if Playlist exists based in Id + if (!checkPlaylistId(playlistId)) result.success(false) + else { + val uri = + MediaStore.Audio.Playlists.Members.getContentUri("external", playlistId.toLong()) + //If Android is Q/10 or above "count(*)" don't count, so, we use other method. + val columnsBasedOnVersion = if (Build.VERSION.SDK_INT < 29) columns else null + val cursor = resolver.query(uri, columnsBasedOnVersion, null, null, null) + var count = -1 + while (cursor != null && cursor.moveToNext()) { + count += if (Build.VERSION.SDK_INT < 29) cursor.count else cursor.getInt(0) + } + cursor?.close() + // + try { + contentValues.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, count + 1) + contentValues.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId.toLong()) + resolver.insert(uri, contentValues) + result.success(true) + } catch (e: Exception) { + Log.i(channelError, e.toString()) + } + } + } + + //TODO Add option to use a list + fun removeFromPlaylist() { + this.resolver = context.contentResolver + val playlistId = call.argument("playlistId")!! + val audioId = call.argument("audioId")!! + + //Check if Playlist exists based on Id + if (!checkPlaylistId(playlistId)) result.success(false) + else { + try { + val uri = MediaStore.Audio.Playlists.Members.getContentUri( + "external", + playlistId.toLong() + ) + val where = MediaStore.Audio.Playlists.Members._ID + "=?" + resolver.delete(uri, where, arrayOf(audioId.toString())) + result.success(true) + } catch (e: Exception) { + Log.i("on_audio_error: ", e.toString()) + result.success(false) + } + } + } + + //TODO("Need tests") + fun moveItemTo() { + this.resolver = context.contentResolver + val playlistId = call.argument("playlistId")!! + val from = call.argument("from")!! + val to = call.argument("to")!! + + //Check if Playlist exists based in Id + if (!checkPlaylistId(playlistId)) result.success(false) + else { + MediaStore.Audio.Playlists.Members.moveItem(resolver, playlistId.toLong(), from, to) + result.success(true) + } + } + + // + fun renamePlaylist() { + this.resolver = context.contentResolver + val playlistId = call.argument("playlistId")!! + val newPlaylistName = call.argument("newPlName")!! + + //Check if Playlist exists based in Id + if (!checkPlaylistId(playlistId)) result.success(false) + else { + contentValues.put(MediaStore.Audio.Playlists.NAME, newPlaylistName) + contentValues.put(MediaStore.Audio.Playlists.DATE_MODIFIED, System.currentTimeMillis()) + resolver.update(uri, contentValues, "_id=${playlistId.toLong()}", null) + result.success(true) + } + } + + //Return true if playlist already exist, false if don't exist + private fun checkPlaylistId(plId: Int): Boolean { + val cursor = resolver.query( + uri, + arrayOf(MediaStore.Audio.Playlists.NAME, MediaStore.Audio.Playlists._ID), + null, + null, + null + ) + while (cursor != null && cursor.moveToNext()) { + val playListId = cursor.getInt(1) //Id + if (playListId == plId) return true + } + cursor?.close() + return false + } +} + +//Extras: + +//I/PlaylistCursor[All]: [ +// title_key +// instance_id +// playlist_id +// duration +// is_ringtone +// album_artist +// orientation +// artist +// height +// is_drm +// bucket_display_name +// is_audiobook +// owner_package_name +// volume_name +// title_resource_uri +// date_modified +// date_expires +// composer +// _display_name +// datetaken +// mime_type +// is_notification +// _id +// year +// _data +// _hash +// _size +// album +// is_alarm +// title +// track +// width +// is_music +// album_key +// is_trashed +// group_id +// document_id +// artist_id +// artist_key +// is_pending +// date_added +// audio_id +// is_podcast +// album_id +// primary_directory +// secondary_directory +// original_document_id +// bucket_id +// play_order +// bookmark +// relative_path +// ] \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/PermissionManagerInterface.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/PermissionManagerInterface.kt new file mode 100644 index 0000000..447416e --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/interfaces/PermissionManagerInterface.kt @@ -0,0 +1,7 @@ +package com.lucasjosino.on_audio_query.interfaces + +interface PermissionManagerInterface { + fun permissionStatus() : Boolean + fun requestPermission() + fun retryRequestPermission() +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AlbumQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AlbumQuery.kt new file mode 100644 index 0000000..5292181 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AlbumQuery.kt @@ -0,0 +1,92 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.checkAlbumsUriType +import com.lucasjosino.on_audio_query.types.sorttypes.checkAlbumSortType +import io.flutter.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** OnAlbumsQuery */ +class AlbumQuery : ViewModel() { + + companion object { + private const val TAG = "OnAlbumsQuery" + } + + // Main parameters. + private val helper = QueryHelper() + + private lateinit var uri: Uri + private lateinit var sortType: String + private lateinit var resolver: ContentResolver + + /** + * Method to "query" all albums. + */ + fun queryAlbums() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // Sort: Type and Order. + sortType = checkAlbumSortType( + call.argument("sortType"), + call.argument("orderType")!!, + call.argument("ignoreCase")!! + ) + + // Check uri: + // * 0 -> External + // * 1 -> Internal + uri = checkAlbumsUriType(call.argument("uri")!!) + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\turi: $uri") + + // Query everything in background for a better performance. + viewModelScope.launch { + val queryResult = loadAlbums() + result.success(queryResult) + } + } + + // Loading in Background + private suspend fun loadAlbums(): ArrayList> = + withContext(Dispatchers.IO) { + // Setup the cursor with 'uri', 'projection'(null == all items) and 'sortType'. + val cursor = resolver.query(uri, null, null, null, sortType) + + val albumList: ArrayList> = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(album) inside this "cursor", take one and "format" + // into a 'Map'. + while (cursor != null && cursor.moveToNext()) { + val tempData: MutableMap = HashMap() + + for (albumMedia in cursor.columnNames) { + tempData[albumMedia] = helper.loadAlbumItem(albumMedia, cursor) + } + + // Android 10 and above 'album_art' will return null. Use 'queryArtwork' instead. + val art = tempData["album_art"].toString() + if (art.isEmpty()) tempData.remove("album_art") + + albumList.add(tempData) + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return@withContext albumList + } +} diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AllPathQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AllPathQuery.kt new file mode 100644 index 0000000..4f90ab4 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AllPathQuery.kt @@ -0,0 +1,59 @@ +package com.lucasjosino.on_audio_query.queries + +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.net.Uri +import android.provider.MediaStore +import com.lucasjosino.on_audio_query.PluginProvider +import io.flutter.Log +import java.io.File + +/** OnAllPathQuery */ +class AllPathQuery { + + companion object { + private const val TAG = "OnAllPathQuery" + + private val URI: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + + private lateinit var resolver: ContentResolver + + /** + * Method to "query" all paths. + */ + fun queryAllPath() { + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + val resultAllPath = loadAllPath() + result.success(resultAllPath) + } + + // Ignore the '_data' deprecation because this plugin support older versions. + @SuppressLint("Range") + private fun loadAllPath(): ArrayList { + val cursor = resolver.query(URI, null, null, null, null) + + val songPathList: ArrayList = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(path) inside this "cursor", take one and add to the list. + while (cursor != null && cursor.moveToNext()) { + val content = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)) + + val path = File(content).parent + + // Check if path is null or if already exist inside list. + if (path != null && !songPathList.contains(path)) { + songPathList.add(path) + } + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return songPathList + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtistQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtistQuery.kt new file mode 100644 index 0000000..d86afad --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtistQuery.kt @@ -0,0 +1,91 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.controllers.PermissionController +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.checkArtistsUriType +import com.lucasjosino.on_audio_query.types.sorttypes.checkArtistSortType +import com.lucasjosino.on_audio_query.utils.artistProjection +import io.flutter.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** OnArtistsQuery */ +class ArtistQuery : ViewModel() { + + companion object { + private const val TAG = "OnArtistsQuery" + } + + //Main parameters + private val helper = QueryHelper() + + // None of this methods can be null. + private lateinit var uri: Uri + private lateinit var resolver: ContentResolver + private lateinit var sortType: String + + /** + * Method to "query" all artists. + */ + fun queryArtists() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // Sort: Type and Order + sortType = checkArtistSortType( + call.argument("sortType"), + call.argument("orderType")!!, + call.argument("ignoreCase")!! + ) + + // Check uri: + // * 0 -> External. + // * 1 -> Internal. + uri = checkArtistsUriType(call.argument("uri")!!) + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\turi: $uri") + + // Query everything in background for a better performance. + viewModelScope.launch { + val queryResult = loadArtists() + result.success(queryResult) + } + } + + // Loading in Background + private suspend fun loadArtists(): ArrayList> = + withContext(Dispatchers.IO) { + // Setup the cursor with 'uri', 'projection' and 'sortType'. + val cursor = resolver.query(uri, artistProjection, null, null, sortType) + + val artistList: ArrayList> = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(artist) inside this "cursor", take one and "format" + // into a 'Map'. + while (cursor != null && cursor.moveToNext()) { + val tempData: MutableMap = HashMap() + + for (artistMedia in cursor.columnNames) { + tempData[artistMedia] = helper.loadArtistItem(artistMedia, cursor) + } + + artistList.add(tempData) + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return@withContext artistList + } +} diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtworkQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtworkQuery.kt new file mode 100644 index 0000000..31b55c9 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/ArtworkQuery.kt @@ -0,0 +1,187 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.content.ContentUris +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Build +import android.util.Size +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.checkArtworkFormat +import com.lucasjosino.on_audio_query.types.checkArtworkType +import io.flutter.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream +import java.io.FileInputStream + +class ArtworkQuery : ViewModel() { + + companion object { + private const val TAG = "OnArtworksQuery" + } + + private val helper = QueryHelper() + private var type: Int = -1 + private var id: Number = 0 + private var quality: Int = 100 + private var size: Int = 200 + private var showDetailedLog: Boolean = false + + private lateinit var uri: Uri + private lateinit var resolver: ContentResolver + private lateinit var format: Bitmap.CompressFormat + + /** + * Method to "query" artwork. + */ + fun queryArtwork() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + this.showDetailedLog = PluginProvider.showDetailedLog + + id = call.argument("id")!! + + // If the 'size' is null, will be '200'. + size = call.argument("size")!! + + // The 'quality' value cannot be greater than 100 so, we check and if is, set to '50'. + quality = call.argument("quality")!! + if (quality > 100) quality = 50 + + // Check format: + // * 0 -> JPEG + // * 1 -> PNG + format = checkArtworkFormat(call.argument("format")!!) + + // Check uri: + // * 0 -> Song. + // * 1 -> Album. + // * 2 -> Playlist. + // * 3 -> Artist. + // * 4 -> Genre. + uri = checkArtworkType(call.argument("type")!!) + + // This query is 'universal' will work for multiple types (audio, album, artist, etc...). + type = call.argument("type")!! + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tid: $id") + Log.d(TAG, "\tquality: $quality") + Log.d(TAG, "\tformat: $format") + Log.d(TAG, "\turi: $uri") + Log.d(TAG, "\ttype: $type") + + // Query everything in background for a better performance. + viewModelScope.launch { + var resultArtList = loadArt() + + // Sometimes android will extract a 'wrong' or 'empty' artwork. Just set as null. + if (resultArtList != null && resultArtList.isEmpty()) { + Log.i(TAG, "Artwork for '$id' is empty. Returning null") + resultArtList = null + } + + result.success(resultArtList) + } + } + + //Loading in Background + private suspend fun loadArt(): ByteArray? = withContext(Dispatchers.IO) { + var artData: ByteArray? = null + + // If 'Android' >= 29/Q: + // * Limited access to files/folders. Use 'loadThumbnail'. + // If 'Android' < 29/Q: + // * Use the 'embeddedPicture' from 'MediaMetadataRetriever' to get the image. + if (Build.VERSION.SDK_INT >= 29) { + try { + // If 'type' is 2, 3 or 4, Get the first item from playlist or artist. + // Use the first artist song to 'simulate' the artwork. + // + // Type: + // * 2 -> Playlist. + // * 3 -> Artist. + // * 4 -> Genre. + // + // OBS: The 'id' is defined as 'Number'. Convert to 'Long' + val query = if (type == 2 || type == 3 || type == 4) { + val item = helper.loadFirstItem(type, id, resolver) ?: return@withContext null + ContentUris.withAppendedId(uri, item.toLong()) + } else { + ContentUris.withAppendedId(uri, id.toLong()) + } + + val bitmap = resolver.loadThumbnail(query, Size(size, size), null) + artData = convertOrResize(bitmap = bitmap)!! + } catch (e: Exception) { + // This may produce a lot of logging on console so, will required a explicit request + // to show the errors. + if (showDetailedLog) Log.w(TAG, "($id) Message: $e") + } + } else { + // If 'uri == Audio': + // * Load the first 'item' from cursor using the 'id' as filter. + // else: + // * Load the first 'item' from 'album' using the 'id' as filter. + // + // If 'item' return null, no song/album has found, return null. + val item = helper.loadFirstItem(type, id, resolver) ?: return@withContext null + + try { + val file = FileInputStream(item) + val metadata = MediaMetadataRetriever() + + metadata.setDataSource(file.fd) + val image = metadata.embeddedPicture + + // Convert image. If null, return + artData = convertOrResize(byteArray = image) ?: return@withContext null + + // 'close' can only be called using 'Android' >= 29/Q. + if (Build.VERSION.SDK_INT >= 29) metadata.close() + } catch (e: Exception) { + // This may produce a lot of logging on console so, will required a explicit request + // to show the errors. + if (showDetailedLog) Log.w(TAG, "($id) Message: $e") + } + } + + return@withContext artData + } + + // + private fun convertOrResize(bitmap: Bitmap? = null, byteArray: ByteArray? = null): ByteArray? { + val convertedBytes: ByteArray? + val byteArrayBase = ByteArrayOutputStream() + + try { + // If 'bitmap' isn't null: + // * The image(bitmap) is from first method. (Android >= 29/Q). + // else: + // * The image(bytearray) is from second method. (Android < 29/Q). + if (bitmap != null) { + bitmap.compress(format, quality, byteArrayBase) + } else { + val convertedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray!!.size) + convertedBitmap.compress(format, quality, byteArrayBase) + } + } catch (e: Exception) { + // This may produce a lot of logging on console so, will required a explicit request + // to show the errors. + if (showDetailedLog) Log.w(TAG, "($id) Message: $e") + } + + convertedBytes = byteArrayBase.toByteArray() + byteArrayBase.close() + return convertedBytes + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioFromQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioFromQuery.kt new file mode 100644 index 0000000..90f2e44 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioFromQuery.kt @@ -0,0 +1,214 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.controllers.PermissionController +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.checkAudiosFromType +import com.lucasjosino.on_audio_query.types.sorttypes.checkSongSortType +import com.lucasjosino.on_audio_query.utils.songProjection +import io.flutter.Log +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** OnAudiosFromQuery */ +class AudioFromQuery : ViewModel() { + + companion object { + private const val TAG = "OnAudiosFromQuery" + + private val URI: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + + //Main parameters + private val helper = QueryHelper() + private var pId = 0 + private var pUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + + // None of this methods can be null. + private lateinit var where: String + private lateinit var whereVal: String + private lateinit var sortType: String + private lateinit var resolver: ContentResolver + + /** + * Method to "query" all songs from a specific item. + */ + fun querySongsFrom() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // The type of 'item': + // * 0 -> Album + // * 1 -> Album Id + // * 2 -> Artist + // * 3 -> Artist Id + // * 4 -> Genre + // * 5 -> Genre Id + // * 6 -> Playlist + val type = call.argument("type")!! + + // Sort: Type and Order. + sortType = checkSongSortType( + call.argument("sortType"), + call.argument("orderType")!!, + call.argument("ignoreCase")!! + ) + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\ttype: $type") + Log.d(TAG, "\turi: $URI") + + // TODO: Add a better way to handle this query + // This will fix (for now) the problem between Android < 30 && Android > 30 + // The method used to query genres on Android >= 30 don't work properly on Android < 30 so, + // we need separate. + // + // If helper == 6 (Playlist) send to 'querySongsFromPlaylistOrGenre' in any version. + // If helper == 4 (Genre) || helper == 5 (GenreId) and Android < 30 send to + // 'querySongsFromPlaylistOrGenre' else, follow the rest of the "normal" code. + // + // Why? Android 10 and below doesn't have "genre" category and we need use a "workaround". + // 'MediaStore'(https://developer.android.com/reference/android/provider/MediaStore.Audio.AudioColumns#GENRE) + if (type == 6 || ((type == 4 || type == 5) && Build.VERSION.SDK_INT < 30)) { + // Works on Android 10. + querySongsFromPlaylistOrGenre(result, call, type) + } else { + // Works on Android 11. + // 'whereVal' -> Album/Artist/Genre + // 'where' -> uri + whereVal = call.argument("where")!!.toString() + where = checkAudiosFromType(type) + + // Query everything in background for a better performance. + viewModelScope.launch { + val resultSongList = loadSongsFrom() + result.success(resultSongList) + } + } + } + + //Loading in Background + private suspend fun loadSongsFrom(): ArrayList> = + withContext(Dispatchers.IO) { + // Setup the cursor with 'uri', 'projection', 'selection'(where) and 'values'(whereVal). + val cursor = resolver.query(URI, songProjection(), where, arrayOf(whereVal), sortType) + + val songsFromList: ArrayList> = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(song) inside this "cursor", take one and "format" + // into a 'Map'. + while (cursor != null && cursor.moveToNext()) { + val tempData: MutableMap = HashMap() + for (audioMedia in cursor.columnNames) { + tempData[audioMedia] = helper.loadSongItem(audioMedia, cursor) + } + + //Get a extra information from audio, e.g: extension, uri, etc.. + val tempExtraData = helper.loadSongExtraInfo(URI, tempData) + tempData.putAll(tempExtraData) + + songsFromList.add(tempData) + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return@withContext songsFromList + } + + // TODO: Remove unnecessary code. + private fun querySongsFromPlaylistOrGenre( + result: MethodChannel.Result, + call: MethodCall, + type: Int + ) { + val info = call.argument("where")!! + + // Check if playlist exists using the id. + val checkedName = if (type == 4 || type == 5) { + checkName(genreName = info.toString()) + } else { + checkName(plName = info.toString()) + } + + if (!checkedName) pId = info.toString().toInt() + + pUri = if (type == 4 || type == 5) { + MediaStore.Audio.Genres.Members.getContentUri("external", pId.toLong()) + } else { + MediaStore.Audio.Playlists.Members.getContentUri("external", pId.toLong()) + } + + // Query everything in background for a better performance. + viewModelScope.launch { + val resultSongsFrom = loadSongsFromPlaylistOrGenre() + result.success(resultSongsFrom) + } + } + + private suspend fun loadSongsFromPlaylistOrGenre(): ArrayList> = + withContext(Dispatchers.IO) { + val songsFrom: ArrayList> = ArrayList() + + val cursor = resolver.query(pUri, songProjection(), null, null, sortType) + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + while (cursor != null && cursor.moveToNext()) { + val tempData: MutableMap = HashMap() + + for (media in cursor.columnNames) { + tempData[media] = helper.loadSongItem(media, cursor) + } + + //Get a extra information from audio, e.g: extension, uri, etc.. + val tempExtraData = helper.loadSongExtraInfo(URI, tempData) + tempData.putAll(tempExtraData) + + songsFrom.add(tempData) + } + + cursor?.close() + return@withContext songsFrom + } + + // Return true if playlist or genre exists and false, if don't. + private fun checkName(plName: String? = null, genreName: String? = null): Boolean { + val uri: Uri + val projection: Array + + if (plName != null) { + uri = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI + projection = arrayOf(MediaStore.Audio.Playlists.NAME, MediaStore.Audio.Playlists._ID) + } else { + uri = MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI + projection = arrayOf(MediaStore.Audio.Genres.NAME, MediaStore.Audio.Genres._ID) + } + + val cursor = resolver.query(uri, projection, null, null, null) + while (cursor != null && cursor.moveToNext()) { + val name = cursor.getString(0) + + if (name != null && name == plName || name == genreName) { + pId = cursor.getInt(1) + return true + } + } + + cursor?.close() + return false + } +} diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioQuery.kt new file mode 100644 index 0000000..7457ac9 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/AudioQuery.kt @@ -0,0 +1,101 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.checkAudiosUriType +import com.lucasjosino.on_audio_query.types.sorttypes.checkSongSortType +import com.lucasjosino.on_audio_query.utils.songProjection +import io.flutter.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** OnAudiosQuery */ +class AudioQuery : ViewModel() { + + companion object { + private const val TAG = "OnAudiosQuery" + } + + // Main parameters + private val helper = QueryHelper() + private var selection: String? = null + + private lateinit var uri: Uri + private lateinit var sortType: String + private lateinit var resolver: ContentResolver + + /** + * Method to "query" all songs. + */ + fun querySongs() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // Sort: Type and Order. + sortType = checkSongSortType( + call.argument("sortType"), + call.argument("orderType")!!, + call.argument("ignoreCase")!! + ) + + // Check uri: + // * 0 -> External. + // * 1 -> Internal. + uri = checkAudiosUriType(call.argument("uri")!!) + + // Here we provide a custom 'path'. + if (call.argument("path") != null) { + val projection = songProjection() + selection = projection[0] + " like " + "'%" + call.argument("path") + "/%'" + } + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\tselection: $selection") + Log.d(TAG, "\turi: $uri") + + // Query everything in background for a better performance. + viewModelScope.launch { + val queryResult = loadSongs() + result.success(queryResult) + } + } + + //Loading in Background + private suspend fun loadSongs(): ArrayList> = + withContext(Dispatchers.IO) { + // Setup the cursor with 'uri', 'projection' and 'sortType'. + val cursor = resolver.query(uri, songProjection(), selection, null, sortType) + + val songList: ArrayList> = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(song) inside this "cursor", take one and "format" + // into a 'Map'. + while (cursor != null && cursor.moveToNext()) { + val tempData: MutableMap = HashMap() + + for (audioMedia in cursor.columnNames) { + tempData[audioMedia] = helper.loadSongItem(audioMedia, cursor) + } + + //Get a extra information from audio, e.g: extension, uri, etc.. + val tempExtraData = helper.loadSongExtraInfo(uri, tempData) + tempData.putAll(tempExtraData) + + songList.add(tempData) + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return@withContext songList + } +} diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/GenreQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/GenreQuery.kt new file mode 100644 index 0000000..adfb576 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/GenreQuery.kt @@ -0,0 +1,95 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.checkGenresUriType +import com.lucasjosino.on_audio_query.types.sorttypes.checkGenreSortType +import com.lucasjosino.on_audio_query.utils.genreProjection +import io.flutter.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** OnGenresQuery */ +class GenreQuery : ViewModel() { + + companion object { + private const val TAG = "OnGenresQuery" + } + + // Main parameters. + private val helper = QueryHelper() + + private lateinit var uri: Uri + private lateinit var sortType: String + private lateinit var resolver: ContentResolver + + /** + * Method to "query" all genres. + */ + fun queryGenres() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // Sort: Type and Order. + sortType = checkGenreSortType( + call.argument("sortType"), + call.argument("orderType")!!, + call.argument("ignoreCase")!! + ) + + // Check uri: + // * 0 -> External + // * 1 -> Internal + uri = checkGenresUriType(call.argument("uri")!!) + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\turi: $uri") + + // Query everything in background for a better performance. + viewModelScope.launch { + val queryResult = loadGenres() + result.success(queryResult) + } + } + + // Loading in Background + private suspend fun loadGenres(): ArrayList> = + withContext(Dispatchers.IO) { + // Setup the cursor with 'uri', 'projection' and 'sortType'. + val cursor = resolver.query(uri, genreProjection, null, null, sortType) + + val genreList: ArrayList> = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(genre) inside this "cursor", take one and "format" + // into a 'Map'. + while (cursor != null && cursor.moveToNext()) { + val genreData: MutableMap = HashMap() + + for (genreMedia in cursor.columnNames) { + genreData[genreMedia] = helper.loadGenreItem(genreMedia, cursor) + } + + // Count and add the number of songs for every genre. + val mediaCount = helper.getMediaCount(0, genreData["_id"].toString(), resolver) + genreData["num_of_songs"] = mediaCount + + if (genreData["name"] != null && genreData["_id"] != 0) { + genreList.add(genreData) + } + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return@withContext genreList + } +} diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/PlaylistQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/PlaylistQuery.kt new file mode 100644 index 0000000..0243748 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/PlaylistQuery.kt @@ -0,0 +1,93 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.controllers.PermissionController +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.checkPlaylistsUriType +import com.lucasjosino.on_audio_query.types.sorttypes.checkGenreSortType +import com.lucasjosino.on_audio_query.utils.playlistProjection +import io.flutter.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** OnPlaylistQuery */ +class PlaylistQuery : ViewModel() { + + companion object { + private const val TAG = "OnPlaylistQuery" + } + + //Main parameters + private val helper = QueryHelper() + + private lateinit var uri: Uri + private lateinit var resolver: ContentResolver + private lateinit var sortType: String + + /** + * Method to "query" all playlists. + */ + fun queryPlaylists() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // Sort: Type and Order. + sortType = checkGenreSortType( + call.argument("sortType"), + call.argument("orderType")!!, + call.argument("ignoreCase")!! + ) + // Check uri: + // * 0 -> External. + // * 1 -> Internal. + uri = checkPlaylistsUriType(call.argument("uri")!!) + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\tsortType: $sortType") + Log.d(TAG, "\turi: $uri") + + // Query everything in background for a better performance. + viewModelScope.launch { + val queryResult = loadPlaylists() + result.success(queryResult) + } + } + + //Loading in Background + private suspend fun loadPlaylists(): ArrayList> = + withContext(Dispatchers.IO) { + // Setup the cursor with 'uri' and 'projection'. + val cursor = resolver.query(uri, playlistProjection, null, null, null) + + val playlistList: ArrayList> = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item(playlist) inside this "cursor", take one and "format" + // into a 'Map'. + while (cursor != null && cursor.moveToNext()) { + val playlistData: MutableMap = HashMap() + + for (playlistMedia in cursor.columnNames) { + playlistData[playlistMedia] = helper.loadPlaylistItem(playlistMedia, cursor) + } + + // Count and add the number of songs for every playlist. + val mediaCount = helper.getMediaCount(1, playlistData["_id"].toString(), resolver) + playlistData["num_of_songs"] = mediaCount + + playlistList.add(playlistData) + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return@withContext playlistList + } +} diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/WithFiltersQuery.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/WithFiltersQuery.kt new file mode 100644 index 0000000..f589345 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/WithFiltersQuery.kt @@ -0,0 +1,117 @@ +package com.lucasjosino.on_audio_query.queries + +import android.content.ContentResolver +import android.net.Uri +import android.provider.MediaStore +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lucasjosino.on_audio_query.PluginProvider +import com.lucasjosino.on_audio_query.queries.helper.QueryHelper +import com.lucasjosino.on_audio_query.types.* +import io.flutter.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class WithFiltersQuery : ViewModel() { + + companion object { + private const val TAG = "OnWithFiltersQuery" + + private val URI = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + + //Main parameters + private val helper = QueryHelper() + private var projection: Array? = arrayOf() + + private lateinit var resolver: ContentResolver + private lateinit var withType: Uri + private lateinit var argsVal: String + private lateinit var argsKey: String + + // + fun queryWithFilters() { + val call = PluginProvider.call() + val result = PluginProvider.result() + val context = PluginProvider.context() + this.resolver = context.contentResolver + + // Choose the type. + // * 0 -> Audios + // * 1 -> Albums + // * 2 -> Playlists + // * 3 -> Artists + // * 4 -> Genres + withType = checkWithFiltersType(call.argument("withType")!!) + + // The 'args' are converted to 'String' before send to 'MethodChannel'. + argsVal = "%" + call.argument("argsVal")!! + "%" + + // A dynamic 'projection' to every type of "query". + projection = checkProjection(withType) + + // Choose the 'arg'. + argsKey = when (withType) { + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI -> checkSongsArgs(call.argument("args")!!) + MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI -> checkAlbumsArgs(call.argument("args")!!) + MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI -> checkPlaylistsArgs( + call.argument( + "args" + )!! + ) + MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI -> checkArtistsArgs(call.argument("args")!!) + MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI -> checkGenresArgs(call.argument("args")!!) + else -> throw Exception("[argsKey] returned null. Report this issue on [on_audio_query] GitHub.") + } + + Log.d(TAG, "Query config: ") + Log.d(TAG, "\twithType: $withType") + Log.d(TAG, "\targsVal: $argsVal") + Log.d(TAG, "\targsKey: $argsKey") + + // Query everything in background for a better performance. + viewModelScope.launch { + val queryResult = loadWithFilters() + result.success(queryResult) + } + } + + //Loading in Background + private suspend fun loadWithFilters(): ArrayList> = + withContext(Dispatchers.IO) { + // Setup the cursor with 'uri', 'projection', 'argsKey' and 'argsVal'. + val cursor = resolver.query(withType, projection, argsKey, arrayOf(argsVal), null) + + val withFiltersList: ArrayList> = ArrayList() + + Log.d(TAG, "Cursor count: ${cursor?.count}") + + // For each item inside this "cursor", take one and "format" + // into a 'Map'. + while (cursor != null && cursor.moveToNext()) { + val tempData: MutableMap = HashMap() + + for (media in cursor.columnNames) { + tempData[media] = helper.chooseWithFilterType( + withType, + media, + cursor + ) + } + + // If 'withType' is a song media, add the extra information. + if (withType == MediaStore.Audio.Media.EXTERNAL_CONTENT_URI) { + //Get a extra information from audio, e.g: extension, uri, etc.. + val tempExtraData = helper.loadSongExtraInfo(URI, tempData) + tempData.putAll(tempExtraData) + } + + withFiltersList.add(tempData) + } + + // Close cursor to avoid memory leaks. + cursor?.close() + return@withContext withFiltersList + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/helper/QueryHelper.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/helper/QueryHelper.kt new file mode 100644 index 0000000..9c567eb --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/queries/helper/QueryHelper.kt @@ -0,0 +1,237 @@ +package com.lucasjosino.on_audio_query.queries.helper + +import android.content.ContentResolver +import android.content.ContentUris +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.util.Log +import java.io.File + +class QueryHelper { + //This method will load some extra information about audio/song + fun loadSongExtraInfo( + uri: Uri, + songData: MutableMap + ): MutableMap { + val file = File(songData["_data"].toString()) + + //Getting displayName without [Extension]. + songData["_display_name_wo_ext"] = file.nameWithoutExtension + //Adding only the extension + songData["file_extension"] = file.extension + + //A different type of "data" + val tempUri = ContentUris.withAppendedId(uri, songData["_id"].toString().toLong()) + songData["_uri"] = tempUri.toString() + + return songData + } + + //This method will separate [String] from [Int] + fun loadSongItem(itemProperty: String, cursor: Cursor): Any? { + return when (itemProperty) { + // Int + "_id", + "album_id", + "artist_id" -> { + // The [id] from Android >= 30/R is a [Long] instead of [Int]. + if (Build.VERSION.SDK_INT >= 30) { + cursor.getLong(cursor.getColumnIndex(itemProperty)) + } else { + cursor.getInt(cursor.getColumnIndex(itemProperty)) + } + } + "_size", + "bookmark", + "date_added", + "date_modified", + "duration", + "track" -> cursor.getInt(cursor.getColumnIndex(itemProperty)) + // Boolean + "is_alarm", + "is_audiobook", + "is_music", + "is_notification", + "is_podcast", + "is_ringtone" -> { + val value = cursor.getString(cursor.getColumnIndex(itemProperty)) + if (value == "0") return false + return true + } + // String + else -> cursor.getString(cursor.getColumnIndex(itemProperty)) + } + } + + //This method will separate [String] from [Int] + fun loadAlbumItem(itemProperty: String, cursor: Cursor): Any? { + return when (itemProperty) { + "_id", + "artist_id" -> { + // The [album] id from Android >= 30/R is a [Long] instead of [Int]. + if (Build.VERSION.SDK_INT >= 30) { + cursor.getLong(cursor.getColumnIndex(itemProperty)) + } else { + cursor.getInt(cursor.getColumnIndex(itemProperty)) + } + } + "numsongs" -> cursor.getInt(cursor.getColumnIndex(itemProperty)) + else -> cursor.getString(cursor.getColumnIndex(itemProperty)) + } + } + + //This method will separate [String] from [Int] + fun loadPlaylistItem(itemProperty: String, cursor: Cursor): Any? { + return when (itemProperty) { + "_id", + "date_added", + "date_modified" -> cursor.getLong(cursor.getColumnIndex(itemProperty)) + else -> cursor.getString(cursor.getColumnIndex(itemProperty)) + } + } + + //This method will separate [String] from [Int] + fun loadArtistItem(itemProperty: String, cursor: Cursor): Any? { + return when (itemProperty) { + "_id" -> { + // The [artist] id from Android >= 30/R is a [Long] instead of [Int]. + if (Build.VERSION.SDK_INT >= 30) { + cursor.getLong(cursor.getColumnIndex(itemProperty)) + } else { + cursor.getInt(cursor.getColumnIndex(itemProperty)) + } + } + "number_of_albums", + "number_of_tracks" -> cursor.getInt(cursor.getColumnIndex(itemProperty)) + else -> cursor.getString(cursor.getColumnIndex(itemProperty)) + } + } + + //This method will separate [String] from [Int] + fun loadGenreItem(itemProperty: String, cursor: Cursor): Any? { + return when (itemProperty) { + "_id" -> { + // The [genre] id from Android >= 30/R is a [Long] instead of [Int]. + if (Build.VERSION.SDK_INT >= 30) { + cursor.getLong(cursor.getColumnIndex(itemProperty)) + } else { + cursor.getInt(cursor.getColumnIndex(itemProperty)) + } + } + else -> cursor.getString(cursor.getColumnIndex(itemProperty)) + } + } + + fun getMediaCount(type: Int, arg: String, resolver: ContentResolver): Int { + val uri: Uri = if (type == 0) { + MediaStore.Audio.Genres.Members.getContentUri("external", arg.toLong()) + } else { + MediaStore.Audio.Playlists.Members.getContentUri("external", arg.toLong()) + } + val cursor = resolver.query(uri, null, null, null, null) + val count = cursor?.count ?: -1 + cursor?.close() + return count + } + + // Ignore the [Data] deprecation because this plugin support older versions. + @Suppress("DEPRECATION") + fun loadFirstItem(type: Int, id: Number, resolver: ContentResolver): String? { + + // We use almost the same method to 'query' the first item from Song/Album/Artist and we + // need to use a different uri when 'querying' from playlist. + // If [type] is something different, return null. + val selection: String? = when (type) { + 0 -> MediaStore.Audio.Media._ID + "=?" + 1 -> MediaStore.Audio.Media.ALBUM_ID + "=?" + 2 -> null + 3 -> MediaStore.Audio.Media.ARTIST_ID + "=?" + 4 -> null + else -> return null + } + + var dataOrId: String? = null + var cursor: Cursor? = null + try { + // Type 2 or 4 we use a different uri. + // + // Type 2 == Playlist + // Type 4 == Genre + // + // And the others we use the normal uri. + when (true) { + (type == 2 && selection == null) -> { + cursor = resolver.query( + MediaStore.Audio.Playlists.Members.getContentUri("external", id.toLong()), + arrayOf( + MediaStore.Audio.Playlists.Members.DATA, + MediaStore.Audio.Playlists.Members.AUDIO_ID + ), + null, + null, + null + ) + } + (type == 4 && selection == null) -> { + cursor = resolver.query( + MediaStore.Audio.Genres.Members.getContentUri("external", id.toLong()), + arrayOf( + MediaStore.Audio.Genres.Members.DATA, + MediaStore.Audio.Genres.Members.AUDIO_ID + ), + null, + null, + null + ) + } + else -> { + cursor = resolver.query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + arrayOf(MediaStore.Audio.Media.DATA, MediaStore.Audio.Media._ID), + selection, + arrayOf(id.toString()), + null + ) + } + } + } catch (e: Exception) { +// Log.i("on_audio_error", e.toString()) + } + + // + if (cursor != null) { + cursor.moveToFirst() + // Try / Catch to avoid problems. Everytime someone request the first song from a playlist and + // this playlist is empty will crash the app, so we just 'print' the error. + try { + dataOrId = + if (Build.VERSION.SDK_INT >= 29 && (type == 2 || type == 3 || type == 4)) { + cursor.getString(1) + } else { + cursor.getString(0) + } + } catch (e: Exception) { + Log.i("on_audio_error", e.toString()) + } + } + cursor?.close() + + return dataOrId + } + + fun chooseWithFilterType(uri: Uri, itemProperty: String, cursor: Cursor): Any? { + return when (uri) { + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI -> loadSongItem(itemProperty, cursor) + MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI -> loadAlbumItem(itemProperty, cursor) + MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI -> loadPlaylistItem( + itemProperty, + cursor + ) + MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI -> loadArtistItem(itemProperty, cursor) + MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI -> loadGenreItem(itemProperty, cursor) + else -> null + } + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/ArtworkType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/ArtworkType.kt new file mode 100644 index 0000000..b0d8bba --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/ArtworkType.kt @@ -0,0 +1,21 @@ +package com.lucasjosino.on_audio_query.types + +import android.graphics.Bitmap +import android.net.Uri +import android.provider.MediaStore + +fun checkArtworkType(sortType: Int): Uri { + return when (sortType) { + 0, 2, 3, 4 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + 1 -> MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI + else -> throw Exception("[checkArtworkType] value don't exist!") + } +} + +fun checkArtworkFormat(format: Int) : Bitmap.CompressFormat { + return when (format) { + 0 -> Bitmap.CompressFormat.JPEG + 1 -> Bitmap.CompressFormat.PNG + else -> throw Exception("[checkArtworkFormat] value don't exist!") + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/AudiosFromType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/AudiosFromType.kt new file mode 100644 index 0000000..54806bc --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/AudiosFromType.kt @@ -0,0 +1,18 @@ +package com.lucasjosino.on_audio_query.types + +import android.annotation.SuppressLint +import android.provider.MediaStore + +@SuppressLint("InlinedApi") +fun checkAudiosFromType(sortType: Int): String { + return when (sortType) { + 0 -> MediaStore.Audio.Media.ALBUM + " like ?" + 1 -> MediaStore.Audio.Media.ALBUM_ID + " like ?" + 2 -> MediaStore.Audio.Media.ARTIST + " like ?" + 3 -> MediaStore.Audio.Media.ARTIST_ID + " like ?" + // Use the [TRIM] to remove all empty space from genre name. + 4 -> "TRIM(\"" + MediaStore.Audio.Media.GENRE + "\")" + " like ?" + 5 -> MediaStore.Audio.Media.GENRE_ID + " like ?" + else -> throw Exception("[checkAudiosFromType] value don't exist!") + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/UriType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/UriType.kt new file mode 100644 index 0000000..1af5d27 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/UriType.kt @@ -0,0 +1,44 @@ +package com.lucasjosino.on_audio_query.types + +import android.net.Uri +import android.provider.MediaStore + +fun checkAudiosUriType(uriType: Int): Uri { + return when (uriType) { + 0 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + 1 -> MediaStore.Audio.Media.INTERNAL_CONTENT_URI + else -> throw Exception("[checkAudiosUriType] value don't exist!") + } +} + +fun checkAlbumsUriType(uriType: Int): Uri { + return when (uriType) { + 0 -> MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI + 1 -> MediaStore.Audio.Albums.INTERNAL_CONTENT_URI + else -> throw Exception("[checkAlbumsUriType] value don't exist!") + } +} + +fun checkPlaylistsUriType(uriType: Int): Uri { + return when (uriType) { + 0 -> MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI + 1 -> MediaStore.Audio.Playlists.INTERNAL_CONTENT_URI + else -> throw Exception("[checkPlaylistsUriType] value don't exist!") + } +} + +fun checkArtistsUriType(uriType: Int): Uri { + return when (uriType) { + 0 -> MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI + 1 -> MediaStore.Audio.Artists.INTERNAL_CONTENT_URI + else -> throw Exception("[checkArtistsUriType] value don't exist!") + } +} + +fun checkGenresUriType(uriType: Int): Uri { + return when (uriType) { + 0 -> MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI + 1 -> MediaStore.Audio.Genres.INTERNAL_CONTENT_URI + else -> throw Exception("[checkGenresUriType] value don't exist!") + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/WithFiltersType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/WithFiltersType.kt new file mode 100644 index 0000000..6ca65b9 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/WithFiltersType.kt @@ -0,0 +1,73 @@ +package com.lucasjosino.on_audio_query.types + +import android.annotation.SuppressLint +import android.net.Uri +import android.provider.MediaStore +import com.lucasjosino.on_audio_query.utils.artistProjection +import com.lucasjosino.on_audio_query.utils.genreProjection +import com.lucasjosino.on_audio_query.utils.playlistProjection +import com.lucasjosino.on_audio_query.utils.songProjection + +fun checkWithFiltersType(sortType: Int): Uri { + return when (sortType) { + 0 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + 1 -> MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI + 2 -> MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI + 3 -> MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI + 4 -> MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI + else -> throw Exception("[checkWithFiltersType] value don't exist!") + } +} + +fun checkProjection(withType: Uri): Array? { + return when (withType) { + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI -> songProjection() + // [Album] projection is null because we need all items. + MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI -> null + MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI -> playlistProjection + MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI -> artistProjection + MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI -> genreProjection + else -> songProjection() + } +} + +@SuppressLint("InlinedApi") +fun checkSongsArgs(args: Int): String { + return when (args) { + 0 -> MediaStore.Audio.Media.TITLE + " like ?" + 1 -> MediaStore.Audio.Media.DISPLAY_NAME + " like ?" + 2 -> MediaStore.Audio.Media.ALBUM + " like ?" + 3 -> MediaStore.Audio.Media.ARTIST + " like ?" + else -> throw Exception("[checkSongsArgs] value don't exist!") + } +} + +fun checkAlbumsArgs(args: Int): String { + return when (args) { + 0 -> MediaStore.Audio.Albums.ALBUM + " like ?" + 1 -> MediaStore.Audio.Albums.ARTIST + " like ?" + else -> throw Exception("[checkAlbumsArgs] value don't exist!") + } +} + +fun checkPlaylistsArgs(args: Int): String { + return when (args) { + 0 -> MediaStore.Audio.Playlists.NAME + " like ?" + else -> throw Exception("[checkPlaylistsArgs] value don't exist!") + } +} + +fun checkArtistsArgs(args: Int): String { + return when (args) { + 0 -> MediaStore.Audio.Artists.ARTIST + " like ?" + else -> throw Exception("[checkArtistsArgs] value don't exist!") + } +} + +fun checkGenresArgs(args: Int): String { + return when (args) { + 0 -> MediaStore.Audio.Genres.NAME + " like ?" + else -> throw Exception("[checkGenresArgs] value don't exist!") + } +} + diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/AlbumSortType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/AlbumSortType.kt new file mode 100644 index 0000000..98703ca --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/AlbumSortType.kt @@ -0,0 +1,20 @@ +package com.lucasjosino.on_audio_query.types.sorttypes + +import android.provider.MediaStore + +fun checkAlbumSortType(sortType: Int?, order: Int, ignoreCase: Boolean): String { + //[ASC] = Ascending Order + //[DESC] = Descending Order + //TODO: **Review this code later** + val orderAndCase: String = if (ignoreCase) { + if (order == 0) " COLLATE NOCASE ASC" else " COLLATE NOCASE DESC" + } else { + if (order == 0) " ASC" else " DESC" + } + return when (sortType) { + 0 -> MediaStore.Audio.Albums.ALBUM + orderAndCase + 1 -> MediaStore.Audio.Albums.ARTIST + orderAndCase + 2 -> MediaStore.Audio.Albums.NUMBER_OF_SONGS + orderAndCase + else -> MediaStore.Audio.Albums.DEFAULT_SORT_ORDER + orderAndCase + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/ArtistSortType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/ArtistSortType.kt new file mode 100644 index 0000000..464e6b1 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/ArtistSortType.kt @@ -0,0 +1,20 @@ +package com.lucasjosino.on_audio_query.types.sorttypes + +import android.provider.MediaStore + +fun checkArtistSortType(sortType: Int?, order: Int, ignoreCase: Boolean): String { + //[ASC] = Ascending Order + //[DESC] = Descending Order + //TODO: **Review this code later** + val orderAndCase: String = if (ignoreCase) { + if (order == 0) " COLLATE NOCASE ASC" else " COLLATE NOCASE DESC" + } else { + if (order == 0) " ASC" else " DESC" + } + return when (sortType) { + 0 -> MediaStore.Audio.Artists.ARTIST + orderAndCase + 1 -> MediaStore.Audio.Artists.NUMBER_OF_TRACKS + orderAndCase + 2 -> MediaStore.Audio.Artists.NUMBER_OF_ALBUMS + orderAndCase + else -> MediaStore.Audio.Artists.DEFAULT_SORT_ORDER + orderAndCase + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/GenreSortType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/GenreSortType.kt new file mode 100644 index 0000000..b8c414e --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/GenreSortType.kt @@ -0,0 +1,18 @@ +package com.lucasjosino.on_audio_query.types.sorttypes + +import android.provider.MediaStore + +fun checkGenreSortType(sortType: Int?, order: Int, ignoreCase: Boolean): String { + //[ASC] = Ascending Order + //[DESC] = Descending Order + //TODO: **Review this code later** + val orderAndCase: String = if (ignoreCase) { + if (order == 0) " COLLATE NOCASE ASC" else " COLLATE NOCASE DESC" + } else { + if (order == 0) " ASC" else " DESC" + } + return when (sortType) { + 0 -> MediaStore.Audio.Genres.NAME + orderAndCase + else -> MediaStore.Audio.Genres.DEFAULT_SORT_ORDER + orderAndCase + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/PlaylistSortType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/PlaylistSortType.kt new file mode 100644 index 0000000..3eaea85 --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/PlaylistSortType.kt @@ -0,0 +1,19 @@ +package com.lucasjosino.on_audio_query.types.sorttypes + +import android.provider.MediaStore + +fun checkPlaylistSortType(sortType: Int?, order: Int, ignoreCase: Boolean): String { + //[ASC] = Ascending Order + //[DESC] = Descending Order + //TODO: **Review this code later** + val orderAndCase: String = if (ignoreCase) { + if (order == 0) " COLLATE NOCASE ASC" else " COLLATE NOCASE DESC" + } else { + if (order == 0) " ASC" else " DESC" + } + return when (sortType) { + 0 -> MediaStore.Audio.Playlists.NAME + orderAndCase + 1 -> MediaStore.Audio.Playlists.DATE_ADDED + orderAndCase + else -> MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER + orderAndCase + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/SongSortType.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/SongSortType.kt new file mode 100644 index 0000000..77308cb --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/types/sorttypes/SongSortType.kt @@ -0,0 +1,26 @@ +package com.lucasjosino.on_audio_query.types.sorttypes + +import android.annotation.SuppressLint +import android.provider.MediaStore + +@SuppressLint("InlinedApi") +fun checkSongSortType(sortType: Int?, order: Int, ignoreCase: Boolean): String { + //[ASC] = Ascending Order + //[DESC] = Descending Order + //TODO: **Review this code later** + val orderAndCase: String = if (ignoreCase) { + if (order == 0) " COLLATE NOCASE ASC" else " COLLATE NOCASE DESC" + } else { + if (order == 0) " ASC" else " DESC" + } + return when (sortType) { + 0 -> MediaStore.Audio.Media.TITLE + orderAndCase + 1 -> MediaStore.Audio.Media.ARTIST + orderAndCase + 2 -> MediaStore.Audio.Media.ALBUM + orderAndCase + 3 -> MediaStore.Audio.Media.DURATION + orderAndCase + 4 -> MediaStore.Audio.Media.DATE_ADDED + orderAndCase + 5 -> MediaStore.Audio.Media.SIZE + orderAndCase + 6 -> MediaStore.Audio.Media.DISPLAY_NAME + orderAndCase + else -> MediaStore.Audio.Media.DEFAULT_SORT_ORDER + orderAndCase + } +} \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/CursorProjection.kt b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/CursorProjection.kt new file mode 100644 index 0000000..be0bd2a --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/android/src/main/kotlin/com/lucasjosino/on_audio_query/utils/CursorProjection.kt @@ -0,0 +1,73 @@ +package com.lucasjosino.on_audio_query.utils + +import android.annotation.SuppressLint +import android.os.Build +import android.provider.MediaStore + +// Query songs projection +@SuppressLint("InlinedApi") +// Ignore the [Data] deprecation because this plugin support older versions. +@Suppress("DEPRECATION") +fun songProjection(): Array { + val tmpProjection = arrayListOf( + MediaStore.Audio.Media.DATA, // TODO: Deprecated + MediaStore.Audio.Media.DISPLAY_NAME, + MediaStore.Audio.Media._ID, + MediaStore.Audio.Media.SIZE, + MediaStore.Audio.Media.ALBUM, + MediaStore.Audio.Media.ALBUM_ARTIST, + MediaStore.Audio.Media.ALBUM_ID, + MediaStore.Audio.Media.ARTIST, + MediaStore.Audio.Media.ARTIST_ID, + MediaStore.Audio.Media.BOOKMARK, + MediaStore.Audio.Media.COMPOSER, + MediaStore.Audio.Media.DATE_ADDED, + MediaStore.Audio.Media.DATE_MODIFIED, + MediaStore.Audio.Media.DURATION, + MediaStore.Audio.Media.TITLE, + MediaStore.Audio.Media.TRACK, + MediaStore.Audio.Media.YEAR, + MediaStore.Audio.Media.IS_ALARM, + MediaStore.Audio.Media.IS_MUSIC, + MediaStore.Audio.Media.IS_NOTIFICATION, + MediaStore.Audio.Media.IS_PODCAST, + MediaStore.Audio.Media.IS_RINGTONE, + ) + + if (Build.VERSION.SDK_INT >= 29) { + tmpProjection.add(MediaStore.Audio.Media.IS_AUDIOBOOK) // Only Api >= 29 + } + + if (Build.VERSION.SDK_INT >= 30) { + tmpProjection.add(MediaStore.Audio.Media.GENRE) // Only Api >= 30 + tmpProjection.add(MediaStore.Audio.Media.GENRE_ID) // Only Api >= 30 + } + + return tmpProjection.toTypedArray() +} + + +// Query playlists projection +// Ignore the [Data] deprecation because this plugin support older versions. +@Suppress("DEPRECATION") +val playlistProjection = arrayOf( + MediaStore.Audio.Playlists.DATA, + MediaStore.Audio.Playlists._ID, + MediaStore.Audio.Playlists.DATE_ADDED, + MediaStore.Audio.Playlists.DATE_MODIFIED, + MediaStore.Audio.Playlists.NAME +) + +//Query artists projection +val artistProjection = arrayOf( + MediaStore.Audio.Artists._ID, + MediaStore.Audio.Artists.ARTIST, + MediaStore.Audio.Artists.NUMBER_OF_ALBUMS, + MediaStore.Audio.Artists.NUMBER_OF_TRACKS +) + +//Query genres projection +val genreProjection = arrayOf( + MediaStore.Audio.Genres._ID, + MediaStore.Audio.Genres.NAME +) \ No newline at end of file diff --git a/local_packages/on_audio_query_android-1.1.0/pubspec.yaml b/local_packages/on_audio_query_android-1.1.0/pubspec.yaml new file mode 100644 index 0000000..1cfe39f --- /dev/null +++ b/local_packages/on_audio_query_android-1.1.0/pubspec.yaml @@ -0,0 +1,37 @@ +# ======== +# author: Lucas Josino +# github: https://github.com/LucJosin +# website: https://www.lucasjosino.com/ +# ======== +name: on_audio_query_android +description: Android implementation of the on_audio_query plugin. +version: 1.1.0 +homepage: https://github.com/LucJosin/on_audio_query/tree/main/packages/on_audio_query_android +# pub.dev: https://pub.dev/packages/on_audio_query +# pub.dev (Android): https://pub.dev/packages/on_audio_query_android + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=2.5.0" + +dependencies: + # on_audio_query + on_audio_query_platform_interface: ^1.7.0 + + # Flutter + flutter: + sdk: flutter + +dev_dependencies: + # Flutter + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + +flutter: + plugin: + implements: on_audio_query + platforms: + android: + package: com.lucasjosino.on_audio_query + pluginClass: OnAudioQueryPlugin diff --git a/local_packages/tancent_vap-1.0.0+1/CHANGELOG.md b/local_packages/tancent_vap-1.0.0+1/CHANGELOG.md new file mode 100644 index 0000000..eb0f495 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0+1 + +* VAPView with Android & iOS Support +* VAPView dynamic content support +* Can play with multiple widget diff --git a/local_packages/tancent_vap-1.0.0+1/LICENSE b/local_packages/tancent_vap-1.0.0+1/LICENSE new file mode 100644 index 0000000..64d0337 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mochamad Nizwar Syafuan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/README.md b/local_packages/tancent_vap-1.0.0+1/README.md new file mode 100644 index 0000000..b7254a9 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/README.md @@ -0,0 +1,156 @@ +# Tencent VAP - Flutter + +A Flutter plugin that integrates Tencent's Video Animation Player (VAP) technology for high-performance video animations with dynamic content replacement. Supports multiple simultaneous animations through individual VAPView widgets. + +## Features + +- **High-Performance Playback**: Hardware-accelerated video animation rendering +- **Flexible Controls**: Loop options, scale types, audio control, lifecycle management +- **Dynamic Content**: Text and image injection (Base64, files, assets, URLs) +- **Event System**: Complete lifecycle and error handling callbacks +- **Multiple Instances**: Run concurrent animations with independent widgets + +### Platform Setup + +**Android** (`android/app/build.gradle`): +```gradle +android { + compileSdkVersion 34 + defaultConfig { + minSdkVersion 21 + } +} +``` + +**iOS** (`ios/Podfile`): +```ruby +platform :ios, '11.0' +``` + +## Quick Start + +```dart +// Basic VAP animation +VapView( + scaleType: ScaleType.fitCenter, + repeat: -1, + onViewCreated: (controller) { + controller.setAnimListener( + onVideoStart: () => print('Started'), + onVideoComplete: () => print('Completed'), + onFailed: (code, type, msg) => print('Error: $code'), + ); + controller.playAsset('animations/sample.mp4'); + }, +) + +// With dynamic content +VapView( + vapTagContents: { + 'username': TextContent('John Doe'), + 'avatar': ImageURLContent('https://example.com/avatar.jpg'), + 'badge': ImageAssetContent('assets/images/badge.png'), + }, + onViewCreated: (controller) => controller.playAsset('animations/profile.mp4'), +) +``` +## API Reference + +### VapView Properties +- `scaleType`: How video scales (`fitCenter`, `centerCrop`, `fitXY`) +- `repeat`: Loop count (0=once, -1=infinite, 1=2times, 2=3times and so on) +- `mute`: Audio enabled/disabled +- `vapTagContents`: Initial dynamic content +- `onViewCreated`: Controller callback + +### VapController Methods +```dart +// Playback +await controller.playFile('/path/to/file.mp4'); +await controller.playAsset('assets/anim.mp4'); +await controller.stop(); +await controller.setLoop(-1); + +// Content Management +await controller.setVapTagContent('tag', TextContent('text')); +await controller.setVapTagContents({'tag1': content1, 'tag2': content2}); +VAPContent? content = await controller.getVapTagContent('tag'); +await controller.clearVapTagContents(); + +// Events +controller.setAnimListener( + onVideoStart: () {}, + onVideoComplete: () {}, + onFailed: (code, type, msg) {}, +); +``` + +### VAPContent Types +- `TextContent('text')` - Dynamic text +- `ImageBase64Content('base64...')` - Base64 images +- `ImageFileContent('/path/file.jpg')` - Local files +- `ImageAssetContent('assets/img.png')` - Flutter assets +- `ImageURLContent('https://url.jpg')` - Remote images + +## Error Handling + +Common error codes: +- `-1`: File not found +- `-2`: Playback error +- `-1001`: Invalid video data +- `-1002`: Decoder malfunction +- `-1006`: File too large + +```dart +controller.setAnimListener( + onFailed: (code, type, message) { + switch (code) { + case -1: /* Handle file not found */; break; + case -1001: /* Handle corrupted data */; break; + default: print('Error $code: $message'); + } + }, +); +``` + +## Best Practices + +- **Memory Management**: Always dispose controllers in `dispose()` +- **Batch Updates**: Use `setVapTagContents()` for multiple content updates +- **Error Handling**: Wrap playback calls in try-catch blocks +- **Performance**: Use appropriate scale types and avoid unnecessary infinite loops + +## Troubleshooting + +1. **Animation not playing**: Check VAP file exists, verify asset registration +2. **Memory issues**: Dispose controllers, limit concurrent animations +3. **Content not updating**: Verify tag names, set content before playback +4. **iOS build issues**: Ensure iOS 11.0+, run `pod install` + +## Contributing + +We welcome contributions to improve this plugin! Please feel free to: + +- Report bugs and issues +- Suggest new features +- Submit pull requests +- Improve documentation + +## Credits + +This Flutter plugin is built upon the excellent work of Tencent's VAP project: + +- **Original VAP Project**: [https://github.com/Tencent/vap](https://github.com/Tencent/vap) +- **License**: MIT License +- **Tencent VAP Team**: For creating the foundational VAP technology + +We extend our gratitude to the Tencent team for developing and open-sourcing the VAP framework, which makes high-performance video animations accessible to developers worldwide. + +## [Instruction to create the Animation Video](https://github.com/Tencent/vap/blob/master/tool/README_en.md) + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +The original Tencent VAP project is also licensed under the MIT License. + \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/analysis_options.yaml b/local_packages/tancent_vap-1.0.0+1/analysis_options.yaml new file mode 100644 index 0000000..a66c323 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/checksums.lock b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/checksums.lock new file mode 100644 index 0000000..37ad2cd Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/checksums.lock differ diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/md5-checksums.bin b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/md5-checksums.bin new file mode 100644 index 0000000..9d8a58e Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/md5-checksums.bin differ diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/sha1-checksums.bin b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/sha1-checksums.bin new file mode 100644 index 0000000..3c45ea4 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/checksums/sha1-checksums.bin differ diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/dependencies-accessors/gc.properties b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/dependencies-accessors/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/executionHistory/executionHistory.lock b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/executionHistory/executionHistory.lock new file mode 100644 index 0000000..e979bc3 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/executionHistory/executionHistory.lock differ diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/fileChanges/last-build.bin b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/fileChanges/last-build.bin differ diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/fileHashes/fileHashes.lock b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/fileHashes/fileHashes.lock new file mode 100644 index 0000000..3674ea1 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/fileHashes/fileHashes.lock differ diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/gc.properties b/local_packages/tancent_vap-1.0.0+1/android/.gradle/8.9/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_packages/tancent_vap-1.0.0+1/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..4693942 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/buildOutputCleanup/cache.properties b/local_packages/tancent_vap-1.0.0+1/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..28c20e5 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Thu Apr 09 19:25:07 CST 2026 +gradle.version=8.9 diff --git a/local_packages/tancent_vap-1.0.0+1/android/.gradle/vcs-1/gc.properties b/local_packages/tancent_vap-1.0.0+1/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/tancent_vap-1.0.0+1/android/app/build.gradle.kts b/local_packages/tancent_vap-1.0.0+1/android/app/build.gradle.kts new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/tancent_vap-1.0.0+1/android/build.gradle b/local_packages/tancent_vap-1.0.0+1/android/build.gradle new file mode 100644 index 0000000..42bf0a2 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/build.gradle @@ -0,0 +1,71 @@ +group = "com.laskarmedia.vap" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.1.0" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.7.3") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "com.laskarmedia.vap" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 21 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { +// implementation("io.github.tencent:vap:2.0.28") + implementation 'com.google.android.gms:play-services-basement:18.7.1' +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/settings.gradle b/local_packages/tancent_vap-1.0.0+1/android/settings.gradle new file mode 100644 index 0000000..966dd89 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'vap' diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/AndroidManifest.xml b/local_packages/tancent_vap-1.0.0+1/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c7f11c5 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapPlugin.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapPlugin.kt new file mode 100644 index 0000000..9beec56 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapPlugin.kt @@ -0,0 +1,41 @@ +package com.laskarmedia.vap + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.plugin.platform.PlatformViewRegistry + +/** VapPlugin */ +class VapPlugin : FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "vap") + channel.setMethodCallHandler(this) + // Register the platform view + flutterPluginBinding.platformViewRegistry.registerViewFactory( + "vap_view", + VapViewFactory(flutterPluginBinding.binaryMessenger) + ) + } + + override fun onMethodCall(call: MethodCall, result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapView.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapView.kt new file mode 100644 index 0000000..93a3928 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/VapView.kt @@ -0,0 +1,529 @@ +package com.laskarmedia.vap + +import android.content.Context +import android.graphics.Bitmap +import android.view.View +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory +import io.flutter.plugin.common.StandardMessageCodec +import android.os.Handler +import android.os.Looper +import android.widget.FrameLayout +import android.util.Log +import androidx.annotation.IntegerRes +import com.tencent.qgame.animplayer.AnimConfig +import com.tencent.qgame.animplayer.AnimView +import com.tencent.qgame.animplayer.inter.IAnimListener +import com.tencent.qgame.animplayer.inter.IFetchResource +import com.tencent.qgame.animplayer.mix.Resource +import com.tencent.qgame.animplayer.util.ScaleType +import org.json.JSONObject + +// import com.tencent.vap.player.AnimView // Uncomment when VAP is available + +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class VapViewFactory(private val messenger: BinaryMessenger) : + PlatformViewFactory(StandardMessageCodec.INSTANCE) { + override fun create(context: Context, id: Int, args: Any?): PlatformView { + val params = args as? Map + return VapView(context, params, messenger, id) + } +} + +@OptIn(DelicateCoroutinesApi::class) +class VapView( + context: Context, params: Map?, messenger: BinaryMessenger, id: Int +) : PlatformView, MethodChannel.MethodCallHandler { + private val frameLayout = FrameLayout(context) + private val animView = AnimView(context) + private val channel = MethodChannel(messenger, "vap_view_$id") + private var playResult: MethodChannel.Result? = null + private val vapTagContents = mutableMapOf>() + + init { + // Set scaleType from params + when (params?.get("scaleType") as? String) { + "fitCenter" -> animView.setScaleType(ScaleType.FIT_CENTER) + "centerCrop" -> animView.setScaleType(ScaleType.CENTER_CROP) + "fitXY" -> animView.setScaleType(ScaleType.FIT_XY) + else -> animView.setScaleType(ScaleType.FIT_CENTER) + } + + channel.setMethodCallHandler(this) + frameLayout.addView(animView) + + // Initial playback if filePath or assetName is provided + (params?.get("filePath") as? String)?.let { playFile(it, null) } + (params?.get("assetName") as? String)?.let { playAsset(it, context, null) } + (params?.get("loop") as? Int)?.let { animView.setLoop(it) } + (params?.get("mute") as? Boolean)?.let { animView.setMute(it) } + + // Set initial VAP tag contents from params + (params?.get("vapTagContents") as? Map>)?.let { contents -> + for ((tag, contentMap) in contents) { + vapTagContents[tag] = contentMap + Log.d("VapView", "Stored tag content for tag: $tag, contentType: ${contentMap["contentType"]} contentValue: ${contentMap["contentValue"]}") + } + } + + // Set the fetch resource callback to provide VAP tag contents + animView.setFetchResource(object : IFetchResource { + override fun fetchImage(resource: Resource, result: (Bitmap?) -> Unit) { + val contentMap = vapTagContents[resource.tag] + val contentValue = contentMap?.get("contentValue") + val contentType = contentMap?.get("contentType") + + if (!contentValue.isNullOrEmpty() && !contentType.isNullOrEmpty()) { + handleVapTagContent(contentValue, contentType, resource.tag) { bitmap -> + result(bitmap) + } + } else { + Log.w("VapView", "No content found for tag: ${resource.tag}") + result(null) + } + } + + override fun fetchText(resource: Resource, result: (String?) -> Unit) { + val contentMap = vapTagContents.get(resource.tag) + val contentValue = contentMap?.get("contentValue") + result(contentValue) + } + + override fun releaseResource(resources: List) { + // Don't clear vapTagContents here as they might be reused + // vapTagContents.clear() + } + }) + animView.setAnimListener(object : IAnimListener { + override fun onVideoConfigReady(config: AnimConfig): Boolean { + GlobalScope.launch(Dispatchers.Main) { + val animConfigMap = mutableMapOf() + config.let { + animConfigMap["width"] = it.width + animConfigMap["height"] = it.height + animConfigMap["fps"] = it.fps + animConfigMap["totalFrames"] = it.totalFrames + animConfigMap["videoHeight"] = it.videoHeight + animConfigMap["videoWidth"] = it.videoWidth + animConfigMap["isMix"] = it.isMix + animConfigMap["orien"] = it.orien + animConfigMap["alphaPointRect"] = mapOf( + "x" to it.alphaPointRect.x, + "y" to it.alphaPointRect.y, + "w" to it.alphaPointRect.w, + "h" to it.alphaPointRect.h + ) + animConfigMap["rgbPointRect"] = mapOf( + "x" to it.rgbPointRect.x, + "y" to it.rgbPointRect.y, + "w" to it.rgbPointRect.w, + "h" to it.rgbPointRect.h + ) + animConfigMap["version"] = it.version + } + val args = mapOf( + "config" to animConfigMap + ) + channel.invokeMethod("onVideoConfigReady", args) + } + return super.onVideoConfigReady(config) + } + + override fun onFailed(errorType: Int, errorMsg: String?) { + GlobalScope.launch(Dispatchers.Main) { + playError(errorType, errorMsg) + val args = mapOf("errorCode" to errorType, "errorMsg" to errorMsg, "errorType" to "VIDEO_PLAYBACK_ERROR") + channel.invokeMethod("onFailed", args) + } + } + + override fun onVideoComplete() { + GlobalScope.launch(Dispatchers.Main) { + playSuccess() + channel.invokeMethod("onVideoComplete", null) + } + } + + override fun onVideoDestroy() { + GlobalScope.launch(Dispatchers.Main) { + channel.invokeMethod("onVideoDestroy", null) + } + } + + override fun onVideoRender(frameIndex: Int, config: AnimConfig?) { + GlobalScope.launch(Dispatchers.Main) { + val animConfigMap = mutableMapOf() + config?.let { + animConfigMap["width"] = it.width + animConfigMap["height"] = it.height + animConfigMap["fps"] = it.fps + animConfigMap["totalFrames"] = it.totalFrames + animConfigMap["videoHeight"] = it.videoHeight + animConfigMap["videoWidth"] = it.videoWidth + animConfigMap["isMix"] = it.isMix + animConfigMap["orien"] = it.orien + animConfigMap["alphaPointRect"] = mapOf( + "x" to it.alphaPointRect.x, + "y" to it.alphaPointRect.y, + "w" to it.alphaPointRect.w, + "h" to it.alphaPointRect.h + ) + animConfigMap["rgbPointRect"] = mapOf( + "x" to it.rgbPointRect.x, + "y" to it.rgbPointRect.y, + "w" to it.rgbPointRect.w, + "h" to it.rgbPointRect.h + ) + animConfigMap["version"] = it.version + } + val args = mapOf( + "frameIndex" to frameIndex, "config" to animConfigMap + ) + channel.invokeMethod("onVideoRender", args) + } + } + + override fun onVideoStart() { + GlobalScope.launch(Dispatchers.Main) { + channel.invokeMethod("onVideoStart", null) + } + } + }) + } + + override fun getView(): View = frameLayout + + private fun playError(errorType: Int, errorMsg: String?) { + if (playResult != null) { + playResult?.error(errorType.toString(), errorMsg, "$errorType: VIDEO_PLAYBACK_ERROR") + playResult = null + } + } + + private fun playSuccess() { + if (playResult != null) { + playResult?.success(null) + playResult = null + } + } + + private fun reset() { + animView.stopPlay() + frameLayout.removeAllViews() + clearVapTagContents() + } + + override fun dispose() { + this.reset() + channel.setMethodCallHandler(null) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "dispose" -> { + dispose() + } + + "playFile" -> { + val path = call.argument("filePath") + playFile(path, result) + } + + "playAsset" -> { + val asset = call.argument("assetName") + playAsset(asset, frameLayout.context, result) + } + + "stop" -> { + animView.stopPlay() + result.success(null) + } + + "setLoop" -> { + val loop = call.argument("loop") ?: 1 + animView.setLoop(loop) + result.success(null) + } + + "setMute" -> { + val mute = call.argument("mute") ?: false + animView.setMute(mute) + result.success(null) + } + + "setScaleType" -> { + val type = call.argument("scaleType") + setScaleType(type) + result.success(null) + } + + "setVapTagContent" -> { + val tag = call.argument("tag") + val contentMap = call.argument>("content") + if (tag != null && contentMap != null) { + setVapTagContent(tag, contentMap) + } + result.success(null) + } + + "setVapTagContents" -> { + val contents = call.argument>>("contents") + if (contents != null) { + setVapTagContents(contents) + } + result.success(null) + } + + "getVapTagContent" -> { + val tag = call.argument("tag") + val content = if (tag != null) getVapTagContent(tag) else null + result.success(content) + } + + "getAllVapTagContents" -> { + result.success(vapTagContents.toMap()) + } + + "clearVapTagContents"->{ + clearVapTagContents() + result.success(null) + } + + else -> result.notImplemented() + } + } + + private fun playFile(path: String?, result: MethodChannel.Result?) { + if (path != null) { + playResult = result + animView.startPlay(java.io.File(path)) + } + } + + private fun playAsset(asset: String?, context: Context, result: MethodChannel.Result?) { + if (asset != null) { + playResult = result + animView.startPlay(context.assets, "flutter_assets/$asset") + } + } + + private fun setScaleType(type: String?) { + when (type) { + "fitCenter" -> animView.setScaleType(ScaleType.FIT_CENTER) + "centerCrop" -> animView.setScaleType(ScaleType.CENTER_CROP) + "fitXY" -> animView.setScaleType(ScaleType.FIT_XY) + else -> animView.setScaleType(ScaleType.FIT_CENTER) + } + } + + // VAP Tag Content Management + private fun setVapTagContent(tag: String, contentMap: Map) { + vapTagContents[tag] = contentMap + } + + private fun setVapTagContents(contents: Map>) { + vapTagContents.putAll(contents) + } + + private fun getVapTagContent(tag: String): String? { + return vapTagContents[tag]?.get("contentValue") + } + + private fun clearVapTagContents() { + vapTagContents.clear() + } + + // Handle VAP tag content - supports different content types + private fun handleVapTagContent(content: String, contentType: String, tag: String, callback: (Bitmap?) -> Unit) { + Log.d("VapView", "Processing tag: $tag, contentType: $contentType") + + try { + when (contentType) { + "image_base64" -> { + handleBase64Image(content, tag, callback) + } + "image_file" -> { + handleFileImage(content, tag, callback) + } + "image_asset" -> { + handleAssetImage(content, tag, callback) + } + "image_url" -> { + handleUrlImage(content, tag, callback) + } + "text" -> { + // Text content is not handled by image loading, skip + Log.d("VapView", "Text content type for tag: $tag, skipping image loading") + callback(null) + } + else -> { + Log.e("VapView", "Unsupported content type: $contentType for tag: $tag") + callback(null) + } + } + } catch (e: Exception) { + Log.e("VapView", "Error handling VAP tag content for tag '$tag': ${e.message}") + callback(null) + } + } + + private fun handleBase64Image(content: String, tag: String, callback: (Bitmap?) -> Unit) { + GlobalScope.launch(Dispatchers.IO) { + try { + var base64String = content + + // Remove data URL prefix if present (e.g., "data:image/png;base64,") + when { + content.startsWith("data:image/") -> { + val commaIndex = content.indexOf(",") + if (commaIndex != -1) { + base64String = content.substring(commaIndex + 1) + } + } + content.startsWith("base64:") -> { + base64String = content.substring(7) // Remove "base64:" prefix + } + } + + val byteArray = android.util.Base64.decode(base64String, android.util.Base64.DEFAULT) + val bitmap = android.graphics.BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + + GlobalScope.launch(Dispatchers.Main) { + if (bitmap != null) { + Log.d("VapView", "Successfully decoded base64 image for tag: $tag") + } else { + Log.e("VapView", "Failed to decode base64 image for tag: $tag") + } + callback(bitmap) + } + } catch (e: Exception) { + Log.e("VapView", "Error decoding base64 image for tag '$tag': ${e.message}") + GlobalScope.launch(Dispatchers.Main) { + callback(null) + } + } + } + } + + private fun handleFileImage(filePath: String, tag: String, callback: (Bitmap?) -> Unit) { + GlobalScope.launch(Dispatchers.IO) { + try { + var fullPath = filePath + + // Handle different file path types + when { + filePath.startsWith("/") -> { + // Absolute path + fullPath = filePath + } + filePath.startsWith("file://") -> { + // File URL + fullPath = filePath.substring(7) + } + else -> { + // Relative path - try in different locations + val context = frameLayout.context + + // Try in external files dir + val externalFile = java.io.File(context.getExternalFilesDir(null), filePath) + if (externalFile.exists()) { + fullPath = externalFile.absolutePath + } else { + // Try in internal files dir + val internalFile = java.io.File(context.filesDir, filePath) + if (internalFile.exists()) { + fullPath = internalFile.absolutePath + } else { + // Use as-is and let it fail if invalid + fullPath = filePath + } + } + } + } + + val file = java.io.File(fullPath) + if (!file.exists()) { + Log.e("VapView", "File not found: $fullPath for tag: $tag") + GlobalScope.launch(Dispatchers.Main) { + callback(null) + } + return@launch + } + + val bitmap = android.graphics.BitmapFactory.decodeFile(fullPath) + GlobalScope.launch(Dispatchers.Main) { + if (bitmap != null) { + Log.d("VapView", "Successfully loaded file image for tag: $tag from: $fullPath") + } else { + Log.e("VapView", "Failed to decode image file: $fullPath for tag: $tag") + } + callback(bitmap) + } + } catch (e: Exception) { + Log.e("VapView", "Error loading file image for tag '$tag': ${e.message}") + GlobalScope.launch(Dispatchers.Main) { + callback(null) + } + } + } + } + + private fun handleAssetImage(assetPath: String, tag: String, callback: (Bitmap?) -> Unit) { + GlobalScope.launch(Dispatchers.IO) { + try { + val context = frameLayout.context + val inputStream = context.assets.open("flutter_assets/$assetPath") + val bitmap = android.graphics.BitmapFactory.decodeStream(inputStream) + inputStream.close() + + GlobalScope.launch(Dispatchers.Main) { + if (bitmap != null) { + Log.d("VapView", "Successfully loaded asset image for tag: $tag from: $assetPath") + } else { + Log.e("VapView", "Failed to decode asset image: $assetPath for tag: $tag") + } + callback(bitmap) + } + } catch (e: Exception) { + Log.e("VapView", "Error loading asset image for tag '$tag': ${e.message}") + GlobalScope.launch(Dispatchers.Main) { + callback(null) + } + } + } + } + + private fun handleUrlImage(url: String, tag: String, callback: (Bitmap?) -> Unit) { + GlobalScope.launch(Dispatchers.IO) { + try { + val connection = java.net.URL(url).openConnection() + connection.doInput = true + connection.connect() + val inputStream = connection.getInputStream() + val bitmap = android.graphics.BitmapFactory.decodeStream(inputStream) + inputStream.close() + + GlobalScope.launch(Dispatchers.Main) { + if (bitmap != null) { + Log.d("VapView", "Successfully loaded URL image for tag: $tag from: $url") + } else { + Log.e("VapView", "Failed to decode URL image: $url for tag: $tag") + } + callback(bitmap) + } + } catch (e: Exception) { + Log.e("VapView", "Error loading URL image for tag '$tag': ${e.message}") + GlobalScope.launch(Dispatchers.Main) { + callback(null) + } + } + } + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimConfig.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimConfig.kt new file mode 100644 index 0000000..5d59e3d --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimConfig.kt @@ -0,0 +1,92 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.graphics.Bitmap +import android.graphics.MaskFilter +import com.tencent.qgame.animplayer.mask.MaskConfig +import com.tencent.qgame.animplayer.util.ALog +import org.json.JSONException +import org.json.JSONObject + +/** + * vapc里读取出来的基础配置 + */ +class AnimConfig { + + companion object { + private const val TAG = "${Constant.TAG}.AnimConfig" + } + + val version = 2 // 不同版本号不兼容 + var totalFrames = 0 // 总帧数 + var width = 0 // 需要显示视频的真实宽高 + var height = 0 + var videoWidth = 0 // 视频实际宽高 + var videoHeight = 0 + var orien = Constant.ORIEN_DEFAULT // 0-兼容模式 1-竖屏 2-横屏 + var fps = 0 + var isMix = false // 是否为融合动画 + var alphaPointRect = PointRect(0, 0 ,0 ,0) // alpha区域 + var rgbPointRect = PointRect(0, 0, 0, 0) // rgb区域 + var isDefaultConfig = false // 没有vapc配置时默认逻辑 + var defaultVideoMode = Constant.VIDEO_MODE_SPLIT_HORIZONTAL + + var maskConfig: MaskConfig ?= null + var jsonConfig: JSONObject? = null + + + /** + * @return 解析是否成功,失败按默认配置走 + */ + fun parse(json: JSONObject): Boolean { + return try { + json.getJSONObject("info").apply { + val v = getInt("v") + if (version != v) { + ALog.e(TAG, "current version=$version target=$v") + return false + } + totalFrames = getInt("f") + width = getInt("w") + height = getInt("h") + videoWidth = getInt("videoW") + videoHeight = getInt("videoH") + orien = getInt("orien") + fps = getInt("fps") + isMix = getInt("isVapx") == 1 + val a = getJSONArray("aFrame") ?: return false + alphaPointRect = PointRect(a.getInt(0), a.getInt(1), a.getInt(2), a.getInt(3)) + val c = getJSONArray("rgbFrame") ?: return false + rgbPointRect = PointRect(c.getInt(0), c.getInt(1), c.getInt(2), c.getInt(3)) + } + true + } catch (e : JSONException) { + ALog.e(TAG, "json parse fail $e", e) + false + } + } + + override fun toString(): String { + return "AnimConfig(version=$version, totalFrames=$totalFrames, width=$width, height=$height, videoWidth=$videoWidth, videoHeight=$videoHeight, orien=$orien, fps=$fps, isMix=$isMix, alphaPointRect=$alphaPointRect, rgbPointRect=$rgbPointRect, isDefaultConfig=$isDefaultConfig)" + } + + +} + + +data class PointRect(val x: Int, val y: Int, val w: Int, val h: Int) +data class RefVec2(val w: Int, val h: Int) //参考宽&高 \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimConfigManager.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimConfigManager.kt new file mode 100644 index 0000000..9d48366 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimConfigManager.kt @@ -0,0 +1,188 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.os.SystemClock +import com.tencent.qgame.animplayer.file.IFileContainer +import com.tencent.qgame.animplayer.util.ALog +import org.json.JSONObject +import java.nio.charset.Charset + +/** + * 配置管理 + */ +class AnimConfigManager(val player: AnimPlayer) { + + companion object { + private const val TAG = "${Constant.TAG}.AnimConfigManager" + } + + var config: AnimConfig? = null + var isParsingConfig = false // 是否正在读取配置 + + /** + * 解析配置 + * @return true 解析成功 false 解析失败 + */ + fun parseConfig(fileContainer: IFileContainer, enableVersion1: Boolean, defaultVideoMode: Int, defaultFps: Int): Int { + try { + isParsingConfig = true + // 解析vapc + val time = SystemClock.elapsedRealtime() + val result = parse(fileContainer, defaultVideoMode, defaultFps) + ALog.i(TAG, "parseConfig cost=${SystemClock.elapsedRealtime() - time}ms enableVersion1=$enableVersion1 result=$result") + if (!result) { + isParsingConfig = false + return Constant.REPORT_ERROR_TYPE_PARSE_CONFIG + } + if (config?.isDefaultConfig == true && !enableVersion1) { + isParsingConfig = false + return Constant.REPORT_ERROR_TYPE_PARSE_CONFIG + } + // 插件解析配置 + val resultCode = config?.let { + player.pluginManager.onConfigCreate(it) + } ?: Constant.OK + isParsingConfig = false + return resultCode + } catch (e : Throwable) { + ALog.e(TAG, "parseConfig error $e", e) + isParsingConfig = false + return Constant.REPORT_ERROR_TYPE_PARSE_CONFIG + } + } + + /** + * 默认配置解析(兼容老视频格式) + */ + fun defaultConfig(_videoWidth: Int, _videoHeight: Int) { + if (config?.isDefaultConfig == false) return + config?.apply { + videoWidth = _videoWidth + videoHeight = _videoHeight + when (defaultVideoMode) { + Constant.VIDEO_MODE_SPLIT_HORIZONTAL -> { + // 视频左右对齐(alpha左\rgb右) + width = _videoWidth / 2 + height = _videoHeight + alphaPointRect = PointRect(0, 0, width, height) + rgbPointRect = PointRect(width, 0, width, height) + } + Constant.VIDEO_MODE_SPLIT_VERTICAL -> { + // 视频上下对齐(alpha上\rgb下) + width = _videoWidth + height = _videoHeight / 2 + alphaPointRect = PointRect(0, 0, width, height) + rgbPointRect = PointRect(0, height, width, height) + } + Constant.VIDEO_MODE_SPLIT_HORIZONTAL_REVERSE -> { + // 视频左右对齐(rgb左\alpha右) + width = _videoWidth / 2 + height = _videoHeight + rgbPointRect = PointRect(0, 0, width, height) + alphaPointRect = PointRect(width, 0, width, height) + } + Constant.VIDEO_MODE_SPLIT_VERTICAL_REVERSE -> { + // 视频上下对齐(rgb上\alpha下) + width = _videoWidth + height = _videoHeight / 2 + rgbPointRect = PointRect(0, 0, width, height) + alphaPointRect = PointRect(0, height, width, height) + } + else -> { + // 默认视频左右对齐(alpha左\rgb右) + width = _videoWidth / 2 + height = _videoHeight + alphaPointRect = PointRect(0, 0, width, height) + rgbPointRect = PointRect(width, 0, width, height) + } + } + } + } + + + private fun parse(fileContainer: IFileContainer, defaultVideoMode: Int, defaultFps: Int): Boolean { + + val config = AnimConfig() + this.config = config + + + // 查找vapc box + fileContainer.startRandomRead() + val boxHead = ByteArray(8) + var head: BoxHead? = null + var vapcStartIndex: Long = 0 + while (fileContainer.read(boxHead, 0, boxHead.size) == 8) { + val h = parseBoxHead(boxHead) ?: break + if ("vapc" == h.type) { + h.startIndex = vapcStartIndex + head = h + break + } + vapcStartIndex += h.length + fileContainer.skip(h.length - 8L) + } + + if (head == null) { + ALog.e(TAG, "vapc box head not found") + // 按照默认配置生成config + config.apply { + isDefaultConfig = true + this.defaultVideoMode = defaultVideoMode + fps = defaultFps + } + player.fps = config.fps + return true + } + + // 读取vapc box + val vapcBuf = ByteArray(head.length - 8) // ps: OOM exception + fileContainer.read(vapcBuf, 0 , vapcBuf.size) + fileContainer.closeRandomRead() + + val json = String(vapcBuf, 0, vapcBuf.size, Charset.forName("UTF-8")) + val jsonObj = JSONObject(json) + config.jsonConfig = jsonObj + val result = config.parse(jsonObj) + if (defaultFps > 0) { + config.fps = defaultFps + } + player.fps = config.fps + return result + } + + private fun parseBoxHead(boxHead: ByteArray): BoxHead? { + if (boxHead.size != 8) return null + val head = BoxHead() + var length: Int = 0 + length = length or (boxHead[0].toInt() and 0xff shl 24) + length = length or (boxHead[1].toInt() and 0xff shl 16) + length = length or (boxHead[2].toInt() and 0xff shl 8) + length = length or (boxHead[3].toInt() and 0xff) + head.length = length + head.type = String(boxHead, 4, 4, Charset.forName("US-ASCII")) + return head + } + + private class BoxHead { + var startIndex: Long = 0 + var length: Int = 0 + var type: String? = null + } + + + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimPlayer.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimPlayer.kt new file mode 100644 index 0000000..898d85a --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimPlayer.kt @@ -0,0 +1,157 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import com.tencent.qgame.animplayer.file.IFileContainer +import com.tencent.qgame.animplayer.inter.IAnimListener +import com.tencent.qgame.animplayer.mask.MaskConfig +import com.tencent.qgame.animplayer.plugin.AnimPluginManager +import com.tencent.qgame.animplayer.util.ALog + +class AnimPlayer(val animView: IAnimView) { + + companion object { + private const val TAG = "${Constant.TAG}.AnimPlayer" + } + + var animListener: IAnimListener? = null + var decoder: Decoder? = null + var audioPlayer: AudioPlayer? = null + var fps: Int = 0 + set(value) { + decoder?.fps = value + field = value + } + // 设置默认的fps <= 0 表示以vapc配置为准 > 0 表示以此设置为准 + var defaultFps: Int = 0 + var playLoop: Int = 0 + set(value) { + decoder?.playLoop = value + audioPlayer?.playLoop = value + field = value + } + var supportMaskBoolean : Boolean = false + var maskEdgeBlurBoolean : Boolean = false + // 是否兼容老版本 默认不兼容 + var enableVersion1 : Boolean = false + // 视频模式 + var videoMode: Int = Constant.VIDEO_MODE_SPLIT_HORIZONTAL + var isDetachedFromWindow = false + var isSurfaceAvailable = false + var startRunnable: Runnable? = null + var isStartRunning = false // 启动时运行状态 + var isMute = false // 是否静音 + + val configManager = AnimConfigManager(this) + val pluginManager = AnimPluginManager(this) + + fun onSurfaceTextureDestroyed() { + isSurfaceAvailable = false + isStartRunning = false + decoder?.destroy() + audioPlayer?.destroy() + } + + fun onSurfaceTextureAvailable(width: Int, height: Int) { + isSurfaceAvailable = true + startRunnable?.run() + startRunnable = null + } + + + fun onSurfaceTextureSizeChanged(width: Int, height: Int) { + decoder?.onSurfaceSizeChanged(width, height) + } + + fun startPlay(fileContainer: IFileContainer) { + isStartRunning = true + prepareDecoder() + if (decoder?.prepareThread() == false) { + isStartRunning = false + decoder?.onFailed(Constant.REPORT_ERROR_TYPE_CREATE_THREAD, Constant.ERROR_MSG_CREATE_THREAD) + decoder?.onVideoComplete() + return + } + // 在线程中解析配置 + decoder?.renderThread?.handler?.post { + val result = configManager.parseConfig(fileContainer, enableVersion1, videoMode, defaultFps) + if (result != Constant.OK) { + isStartRunning = false + decoder?.onFailed(result, Constant.getErrorMsg(result)) + decoder?.onVideoComplete() + return@post + } + ALog.i(TAG, "parse ${configManager.config}") + val config = configManager.config + // 如果是默认配置,因为信息不完整onVideoConfigReady不会被调用 + if (config != null && (config.isDefaultConfig || animListener?.onVideoConfigReady(config) == true)) { + innerStartPlay(fileContainer) + } else { + ALog.i(TAG, "onVideoConfigReady return false") + } + } + } + + private fun innerStartPlay(fileContainer: IFileContainer) { + synchronized(AnimPlayer::class.java) { + if (isSurfaceAvailable) { + isStartRunning = false + decoder?.start(fileContainer) + if (!isMute) { + audioPlayer?.start(fileContainer) + } + } else { + startRunnable = Runnable { + innerStartPlay(fileContainer) + } + animView.prepareTextureView() + } + } + } + + fun stopPlay() { + decoder?.stop() + audioPlayer?.stop() + } + + fun isRunning(): Boolean { + return isStartRunning // 启动过程运行状态 + || (decoder?.isRunning ?: false) // 解码过程运行状态 + + } + + private fun prepareDecoder() { + if (decoder == null) { + decoder = HardDecoder(this).apply { + playLoop = this@AnimPlayer.playLoop + fps = this@AnimPlayer.fps + } + } + if (audioPlayer == null) { + audioPlayer = AudioPlayer(this).apply { + playLoop = this@AnimPlayer.playLoop + } + } + } + + fun updateMaskConfig(maskConfig: MaskConfig?) { + configManager.config?.maskConfig = configManager.config?.maskConfig ?: MaskConfig() + configManager.config?.maskConfig?.safeSetMaskBitmapAndReleasePre(maskConfig?.alphaMaskBitmap) + configManager.config?.maskConfig?.maskPositionPair = maskConfig?.maskPositionPair + configManager.config?.maskConfig?.maskTexPair = maskConfig?.maskTexPair + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimView.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimView.kt new file mode 100644 index 0000000..4ea674d --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AnimView.kt @@ -0,0 +1,311 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.content.Context +import android.content.res.AssetManager +import android.graphics.SurfaceTexture +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.TextureView +import android.view.View +import android.widget.FrameLayout +import com.tencent.qgame.animplayer.file.AssetsFileContainer +import com.tencent.qgame.animplayer.file.FileContainer +import com.tencent.qgame.animplayer.file.IFileContainer +import com.tencent.qgame.animplayer.inter.IAnimListener +import com.tencent.qgame.animplayer.inter.IFetchResource +import com.tencent.qgame.animplayer.inter.OnResourceClickListener +import com.tencent.qgame.animplayer.mask.MaskConfig +import com.tencent.qgame.animplayer.textureview.InnerTextureView +import com.tencent.qgame.animplayer.util.ALog +import com.tencent.qgame.animplayer.util.IScaleType +import com.tencent.qgame.animplayer.util.ScaleType +import com.tencent.qgame.animplayer.util.ScaleTypeUtil +import java.io.File + +open class AnimView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0): + IAnimView, + FrameLayout(context, attrs, defStyleAttr), + TextureView.SurfaceTextureListener { + + companion object { + private const val TAG = "${Constant.TAG}.AnimView" + } + private lateinit var player: AnimPlayer + + private val uiHandler by lazy { Handler(Looper.getMainLooper()) } + private var surface: SurfaceTexture? = null + private var animListener: IAnimListener? = null + private var innerTextureView: InnerTextureView? = null + private var lastFile: IFileContainer? = null + private val scaleTypeUtil = ScaleTypeUtil() + + // 代理监听 + private val animProxyListener by lazy { + object : IAnimListener { + + override fun onVideoConfigReady(config: AnimConfig): Boolean { + scaleTypeUtil.setVideoSize(config.width, config.height) + return animListener?.onVideoConfigReady(config) ?: super.onVideoConfigReady(config) + } + + override fun onVideoStart() { + animListener?.onVideoStart() + } + + override fun onVideoRender(frameIndex: Int, config: AnimConfig?) { + animListener?.onVideoRender(frameIndex, config) + } + + override fun onVideoComplete() { + hide() + animListener?.onVideoComplete() + } + + override fun onVideoDestroy() { + hide() + animListener?.onVideoDestroy() + } + + override fun onFailed(errorType: Int, errorMsg: String?) { + animListener?.onFailed(errorType, errorMsg) + } + + } + } + + // 保证AnimView已经布局完成才加入TextureView + private var onSizeChangedCalled = false + private var needPrepareTextureView = false + private val prepareTextureViewRunnable = Runnable { + removeAllViews() + innerTextureView = InnerTextureView(context).apply { + player = this@AnimView.player + isOpaque = false + surfaceTextureListener = this@AnimView + layoutParams = scaleTypeUtil.getLayoutParam(this) + } + addView(innerTextureView) + } + + + init { + hide() + player = AnimPlayer(this) + player.animListener = animProxyListener + } + + + override fun prepareTextureView() { + if (onSizeChangedCalled) { + uiHandler.post(prepareTextureViewRunnable) + } else { + ALog.e(TAG, "onSizeChanged not called") + needPrepareTextureView = true + } + } + + override fun getSurfaceTexture(): SurfaceTexture? { + return innerTextureView?.surfaceTexture ?: surface + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { + ALog.i(TAG, "onSurfaceTextureSizeChanged $width x $height") + player.onSurfaceTextureSizeChanged(width, height) + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { + } + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + ALog.i(TAG, "onSurfaceTextureDestroyed") + this.surface = null + player.onSurfaceTextureDestroyed() + uiHandler.post { + innerTextureView?.surfaceTextureListener = null + innerTextureView = null + removeAllViews() + } + return true + } + + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + ALog.i(TAG, "onSurfaceTextureAvailable width=$width height=$height") + this.surface = surface + player.onSurfaceTextureAvailable(width, height) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + ALog.i(TAG, "onSizeChanged w=$w, h=$h") + scaleTypeUtil.setLayoutSize(w, h) + onSizeChangedCalled = true + // 需要保证onSizeChanged被调用 + if (needPrepareTextureView) { + needPrepareTextureView = false + prepareTextureView() + } + } + + override fun onAttachedToWindow() { + ALog.i(TAG, "onAttachedToWindow") + super.onAttachedToWindow() + player.isDetachedFromWindow = false + // 自动恢复播放 + if (player.playLoop > 0) { + lastFile?.apply { + startPlay(this) + } + } + } + + override fun onDetachedFromWindow() { + ALog.i(TAG, "onDetachedFromWindow") + super.onDetachedFromWindow() + player.isDetachedFromWindow = true + player.onSurfaceTextureDestroyed() + } + + + override fun setAnimListener(animListener: IAnimListener?) { + this.animListener = animListener + } + + override fun setFetchResource(fetchResource: IFetchResource?) { + player.pluginManager.getMixAnimPlugin()?.resourceRequest = fetchResource + } + + override fun setOnResourceClickListener(resourceClickListener: OnResourceClickListener?) { + player.pluginManager.getMixAnimPlugin()?.resourceClickListener = resourceClickListener + } + + /** + * 兼容方案,优先保证表情显示 + */ + open fun enableAutoTxtColorFill(enable: Boolean) { + player.pluginManager.getMixAnimPlugin()?.autoTxtColorFill = enable + } + + override fun setLoop(playLoop: Int) { + player.playLoop = playLoop + } + + override fun supportMask(isSupport : Boolean, isEdgeBlur : Boolean) { + player.supportMaskBoolean = isSupport + player.maskEdgeBlurBoolean = isEdgeBlur + } + + override fun updateMaskConfig(maskConfig: MaskConfig?) { + player.updateMaskConfig(maskConfig) + } + + + @Deprecated("Compatible older version mp4, default false") + fun enableVersion1(enable: Boolean) { + player.enableVersion1 = enable + } + + // 兼容老版本视频模式 + @Deprecated("Compatible older version mp4") + fun setVideoMode(mode: Int) { + player.videoMode = mode + } + + override fun setFps(fps: Int) { + ALog.i(TAG, "setFps=$fps") + player.defaultFps = fps + } + + override fun setScaleType(type : ScaleType) { + scaleTypeUtil.currentScaleType = type + } + + override fun setScaleType(scaleType: IScaleType) { + scaleTypeUtil.scaleTypeImpl = scaleType + } + + /** + * @param isMute true 静音 + */ + override fun setMute(isMute: Boolean) { + ALog.e(TAG, "set mute=$isMute") + player.isMute = isMute + } + + override fun startPlay(file: File) { + try { + val fileContainer = FileContainer(file) + startPlay(fileContainer) + } catch (e: Throwable) { + animProxyListener.onFailed(Constant.REPORT_ERROR_TYPE_FILE_ERROR, Constant.ERROR_MSG_FILE_ERROR) + animProxyListener.onVideoComplete() + } + } + + override fun startPlay(assetManager: AssetManager, assetsPath: String) { + try { + val fileContainer = AssetsFileContainer(assetManager, assetsPath) + startPlay(fileContainer) + } catch (e: Throwable) { + animProxyListener.onFailed(Constant.REPORT_ERROR_TYPE_FILE_ERROR, Constant.ERROR_MSG_FILE_ERROR) + animProxyListener.onVideoComplete() + } + } + + + override fun startPlay(fileContainer: IFileContainer) { + ui { + if (visibility != View.VISIBLE) { + ALog.e(TAG, "AnimView is GONE, can't play") + return@ui + } + if (!player.isRunning()) { + lastFile = fileContainer + player.startPlay(fileContainer) + } else { + ALog.e(TAG, "is running can not start") + } + } + } + + + override fun stopPlay() { + player.stopPlay() + } + + override fun isRunning(): Boolean { + return player.isRunning() + } + + override fun getRealSize(): Pair { + return scaleTypeUtil.getRealSize() + } + + private fun hide() { + lastFile?.close() + ui { + removeAllViews() + } + } + + private fun ui(f:()->Unit) { + if (Looper.myLooper() == Looper.getMainLooper()) f() else uiHandler.post { f() } + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AudioPlayer.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AudioPlayer.kt new file mode 100644 index 0000000..9276674 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/AudioPlayer.kt @@ -0,0 +1,211 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.media.* +import com.tencent.qgame.animplayer.file.IFileContainer +import com.tencent.qgame.animplayer.util.ALog +import com.tencent.qgame.animplayer.util.MediaUtil +import java.lang.RuntimeException + +class AudioPlayer(val player: AnimPlayer) { + + companion object { + private const val TAG = "${Constant.TAG}.AudioPlayer" + } + + var extractor: MediaExtractor? = null + var decoder: MediaCodec? = null + var audioTrack: AudioTrack? = null + val decodeThread = HandlerHolder(null, null) + var isRunning = false + var playLoop = 0 + var isStopReq = false + var needDestroy = false + + + + private fun prepareThread(): Boolean { + return Decoder.createThread(decodeThread, "anim_audio_thread") + } + + fun start(fileContainer: IFileContainer) { + isStopReq = false + needDestroy = false + if (!prepareThread()) return + if (isRunning) { + stop() + } + isRunning = true + decodeThread.handler?.post { + try { + startPlay(fileContainer) + } catch (e: Throwable) { + ALog.e(TAG, "Audio exception=$e", e) + release() + } + } + } + + fun stop() { + isStopReq = true + } + + private fun startPlay(fileContainer: IFileContainer) { + val extractor = MediaUtil.getExtractor(fileContainer) + this.extractor = extractor + val audioIndex = MediaUtil.selectAudioTrack(extractor) + if (audioIndex < 0) { + ALog.e(TAG, "cannot find audio track") + release() + return + } + extractor.selectTrack(audioIndex) + val format = extractor.getTrackFormat(audioIndex) + val mime = format.getString(MediaFormat.KEY_MIME) ?: "" + ALog.i(TAG, "audio mime=$mime") + if (!MediaUtil.checkSupportCodec(mime)) { + ALog.e(TAG, "mime=$mime not support") + release() + return + } + + val decoder = MediaCodec.createDecoderByType(mime).apply { + configure(format, null, null, 0) + start() + } + this.decoder = decoder + + val decodeInputBuffers = decoder.inputBuffers + var decodeOutputBuffers = decoder.outputBuffers + + val bufferInfo = MediaCodec.BufferInfo() + val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + val channelConfig = getChannelConfig(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)) + + val bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT) + val audioTrack = AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM) + this.audioTrack = audioTrack + val state = audioTrack.state + if (state != AudioTrack.STATE_INITIALIZED) { + release() + ALog.e(TAG, "init audio track failure") + return + } + audioTrack.play() + val timeOutUs = 1000L + var isEOS = false + while (!isStopReq) { + if (!isEOS) { + val inputIndex = decoder.dequeueInputBuffer(timeOutUs) + if (inputIndex >= 0) { + val inputBuffer = decodeInputBuffers[inputIndex] + inputBuffer.clear() + val sampleSize = extractor.readSampleData(inputBuffer, 0) + if (sampleSize < 0) { + isEOS = true + decoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM) + } else { + decoder.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0) + extractor.advance() + } + } + } + val outputIndex = decoder.dequeueOutputBuffer(bufferInfo, 0) + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + decodeOutputBuffers = decoder.outputBuffers + } + if (outputIndex >= 0) { + val outputBuffer = decodeOutputBuffers[outputIndex] + val chunkPCM = ByteArray(bufferInfo.size) + outputBuffer.get(chunkPCM) + outputBuffer.clear() + audioTrack.write(chunkPCM, 0, bufferInfo.size) + decoder.releaseOutputBuffer(outputIndex, false) + } + + if (isEOS && bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + if (--playLoop > 0) { + ALog.d(TAG, "Reached EOS, looping -> playLoop") + extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) + decoder.flush() + isEOS = false + } else { + ALog.i(TAG, "decode finish") + release() + break + } + } + } + release() + } + + + private fun release() { + try { + decoder?.apply { + stop() + release() + } + decoder = null + extractor?.release() + extractor = null + audioTrack?.apply { + pause() + flush() + stop() + release() + } + audioTrack = null + } catch (e: Throwable) { + ALog.e(TAG, "release exception=$e", e) + } + isRunning = false + if (needDestroy) { + destroyInner() + } + } + + fun destroy() { + if (isRunning) { + needDestroy = true + stop() + } else { + destroyInner() + } + } + + private fun destroyInner() { + if (player.isDetachedFromWindow) { + ALog.i(TAG, "destroyThread") + decodeThread.handler?.removeCallbacksAndMessages(null) + decodeThread.thread = Decoder.quitSafely(decodeThread.thread) + } + } + + private fun getChannelConfig(channelCount: Int): Int { + return when (channelCount) { + 1 -> AudioFormat.CHANNEL_CONFIGURATION_MONO + 2 -> AudioFormat.CHANNEL_OUT_STEREO + 3 -> AudioFormat.CHANNEL_OUT_STEREO or AudioFormat.CHANNEL_OUT_FRONT_CENTER + 4 -> AudioFormat.CHANNEL_OUT_QUAD + 5 -> AudioFormat.CHANNEL_OUT_QUAD or AudioFormat.CHANNEL_OUT_FRONT_CENTER + 6 -> AudioFormat.CHANNEL_OUT_5POINT1 + 7 -> AudioFormat.CHANNEL_OUT_5POINT1 or AudioFormat.CHANNEL_OUT_BACK_CENTER + else -> throw RuntimeException("Unsupported channel count: $channelCount") + } + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Constant.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Constant.kt new file mode 100644 index 0000000..19210e7 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Constant.kt @@ -0,0 +1,70 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +object Constant { + const val TAG = "AnimPlayer" + + // 视频适配的屏幕方向 + const val ORIEN_DEFAULT = 0 // 兼容模式 + const val ORIEN_PORTRAIT = 1 // 适配竖屏的视频 + const val ORIEN_LANDSCAPE = 2 // 适配横屏的视频 + + // 视频对齐方式 (兼容老版本视频模式) + @Deprecated("Compatible older version mp4") + const val VIDEO_MODE_SPLIT_HORIZONTAL = 1 // 视频左右对齐(alpha左\rgb右) + @Deprecated("Compatible older version mp4") + const val VIDEO_MODE_SPLIT_VERTICAL = 2 // 视频上下对齐(alpha上\rgb下) + @Deprecated("Compatible older version mp4") + const val VIDEO_MODE_SPLIT_HORIZONTAL_REVERSE = 3 // 视频左右对齐(rgb左\alpha右) + @Deprecated("Compatible older version mp4") + const val VIDEO_MODE_SPLIT_VERTICAL_REVERSE = 4 // 视频上下对齐(rgb上\alpha下) + + + const val OK = 0 // 成功 + + const val REPORT_ERROR_TYPE_EXTRACTOR_EXC = 10001 // MediaExtractor exception + const val REPORT_ERROR_TYPE_DECODE_EXC = 10002 // MediaCodec exception + const val REPORT_ERROR_TYPE_CREATE_THREAD = 10003 // 线程创建失败 + const val REPORT_ERROR_TYPE_CREATE_RENDER = 10004 // render创建失败 + const val REPORT_ERROR_TYPE_PARSE_CONFIG = 10005 // 配置解析失败 + const val REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX = 10006 // vapx融合动画资源获取失败 + const val REPORT_ERROR_TYPE_FILE_ERROR = 10007 // 文件无法读取 + const val REPORT_ERROR_TYPE_HEVC_NOT_SUPPORT = 10008 // 不支持h265 + + const val ERROR_MSG_EXTRACTOR_EXC = "0x1 MediaExtractor exception" // MediaExtractor exception + const val ERROR_MSG_DECODE_EXC = "0x2 MediaCodec exception" // MediaCodec exception + const val ERROR_MSG_CREATE_THREAD = "0x3 thread create fail" // 线程创建失败 + const val ERROR_MSG_CREATE_RENDER = "0x4 render create fail" // render创建失败 + const val ERROR_MSG_PARSE_CONFIG = "0x5 parse config fail" // 配置解析失败 + const val ERROR_MSG_CONFIG_PLUGIN_MIX = "0x6 vapx fail" // vapx融合动画资源获取失败 + const val ERROR_MSG_FILE_ERROR = "0x7 file can't read" // 文件无法读取 + const val ERROR_MSG_HEVC_NOT_SUPPORT = "0x8 hevc not support" // 不支持h265 + + + fun getErrorMsg(errorType: Int, errorMsg: String? = null): String { + return when(errorType) { + REPORT_ERROR_TYPE_EXTRACTOR_EXC -> ERROR_MSG_EXTRACTOR_EXC + REPORT_ERROR_TYPE_DECODE_EXC -> ERROR_MSG_DECODE_EXC + REPORT_ERROR_TYPE_CREATE_THREAD -> ERROR_MSG_CREATE_THREAD + REPORT_ERROR_TYPE_CREATE_RENDER -> ERROR_MSG_CREATE_RENDER + REPORT_ERROR_TYPE_PARSE_CONFIG -> ERROR_MSG_PARSE_CONFIG + REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX -> ERROR_MSG_CONFIG_PLUGIN_MIX + else -> "unknown" + } + " ${errorMsg ?: ""}" + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Decoder.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Decoder.kt new file mode 100644 index 0000000..814a8c4 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Decoder.kt @@ -0,0 +1,171 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.os.Build +import android.os.HandlerThread +import android.os.Handler +import com.tencent.qgame.animplayer.file.IFileContainer +import com.tencent.qgame.animplayer.inter.IAnimListener +import com.tencent.qgame.animplayer.util.ALog +import com.tencent.qgame.animplayer.util.SpeedControlUtil + + +abstract class Decoder(val player: AnimPlayer) : IAnimListener { + + companion object { + private const val TAG = "${Constant.TAG}.Decoder" + + fun createThread(handlerHolder: HandlerHolder, name: String): Boolean { + try { + if (handlerHolder.thread == null || handlerHolder.thread?.isAlive == false) { + handlerHolder.thread = HandlerThread(name).apply { + start() + handlerHolder.handler = Handler(looper) + } + } + return true + } catch (e: OutOfMemoryError) { + ALog.e(TAG, "createThread OOM", e) + } + return false + } + + fun quitSafely(thread: HandlerThread?): HandlerThread? { + thread?.apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + thread.quitSafely() + } else { + thread.quit() + } + } + return null + } + } + + var render: IRenderListener? = null + val renderThread = HandlerHolder(null, null) + val decodeThread = HandlerHolder(null, null) + private var surfaceWidth = 0 + private var surfaceHeight = 0 + var fps: Int = 0 + set(value) { + speedControlUtil.setFixedPlaybackRate(value) + field = value + } + var playLoop = 0 // 循环播放次数 + var isRunning = false // 是否正在运行 + var isStopReq = false // 是否需要停止 + val speedControlUtil by lazy { SpeedControlUtil() } + + abstract fun start(fileContainer: IFileContainer) + + fun stop() { + isStopReq = true + } + + abstract fun destroy() + + fun prepareThread(): Boolean { + return createThread(renderThread, "anim_render_thread") && createThread(decodeThread, "anim_decode_thread") + } + + fun prepareRender(needYUV: Boolean): Boolean { + if (render == null) { + ALog.i(TAG, "prepareRender") + player.animView.getSurfaceTexture()?.apply { + if (needYUV) { + ALog.i(TAG, "use yuv render") + render = YUVRender(this) + } else { + render = Render(this).apply { + updateViewPort(surfaceWidth, surfaceHeight) + } + } + } + } + return render != null + } + + fun preparePlay(videoWidth: Int, videoHeight: Int) { + player.configManager.defaultConfig(videoWidth, videoHeight) + player.configManager.config?.apply { + render?.setAnimConfig(this) + } + player.pluginManager.onRenderCreate() + } + + /** + * decode过程中视频尺寸变化 + * 主要是没有16进制对齐的老视频 + */ + fun videoSizeChange(newWidth: Int, newHeight: Int) { + if (newWidth <= 0 || newHeight <= 0) return + val config = player.configManager.config ?: return + if (config.videoWidth != newWidth || config.videoHeight != newHeight) { + ALog.i(TAG, "videoSizeChange old=(${config.videoWidth},${config.videoHeight}), new=($newWidth,$newHeight)") + config.videoWidth = newWidth + config.videoHeight = newHeight + render?.setAnimConfig(config) + } + } + + + fun destroyThread() { + if (player.isDetachedFromWindow) { + ALog.i(TAG, "destroyThread") + renderThread.handler?.removeCallbacksAndMessages(null) + decodeThread.handler?.removeCallbacksAndMessages(null) + renderThread.thread = quitSafely(renderThread.thread) + decodeThread.thread = quitSafely(decodeThread.thread) + renderThread.handler = null + decodeThread.handler = null + } + } + + fun onSurfaceSizeChanged(width: Int, height: Int) { + surfaceWidth = width + surfaceHeight = height + render?.updateViewPort(width, height) + } + + override fun onVideoStart() { + ALog.i(TAG, "onVideoStart") + player.animListener?.onVideoStart() + } + + override fun onVideoRender(frameIndex: Int, config: AnimConfig?) { + ALog.d(TAG, "onVideoRender") + player.animListener?.onVideoRender(frameIndex, config) + } + + override fun onVideoComplete() { + ALog.i(TAG, "onVideoComplete") + player.animListener?.onVideoComplete() + } + + override fun onVideoDestroy() { + ALog.i(TAG, "onVideoDestroy") + player.animListener?.onVideoDestroy() + } + + override fun onFailed(errorType: Int, errorMsg: String?) { + ALog.e(TAG, "onFailed errorType=$errorType, errorMsg=$errorMsg") + player.animListener?.onFailed(errorType, errorMsg) + } +} + +data class HandlerHolder(var thread: HandlerThread?, var handler: Handler?) \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/EGLUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/EGLUtil.kt new file mode 100644 index 0000000..f302424 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/EGLUtil.kt @@ -0,0 +1,115 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.graphics.SurfaceTexture +import android.opengl.EGL14 +import android.view.Surface +import com.tencent.qgame.animplayer.util.ALog +import javax.microedition.khronos.egl.* + +class EGLUtil { + + companion object { + private const val TAG = "${Constant.TAG}.EGLUtil" + } + + private var egl: EGL10? = null + private var eglDisplay: EGLDisplay? = null + private var eglSurface: EGLSurface? = null + private var eglContext: EGLContext? = null + private var eglConfig: EGLConfig? = null + private var surface: Surface? = null + + init { + eglDisplay = EGL10.EGL_NO_DISPLAY + eglSurface = EGL10.EGL_NO_SURFACE + eglContext = EGL10.EGL_NO_CONTEXT + } + + fun start(surfaceTexture: SurfaceTexture) { + try { + egl = EGLContext.getEGL() as EGL10 + eglDisplay = egl?.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) + val version = IntArray(2) + egl?.eglInitialize(eglDisplay, version) + eglConfig = chooseConfig() + surface = Surface(surfaceTexture) + eglSurface = egl?.eglCreateWindowSurface(eglDisplay, eglConfig, surface, null) + eglContext = createContext(egl, eglDisplay, eglConfig) + if (eglSurface == null || eglSurface == EGL10.EGL_NO_SURFACE) { + ALog.e(TAG, "error:${Integer.toHexString(egl?.eglGetError() ?: 0)}") + return + } + + if (egl?.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) == false) { + ALog.e(TAG, "make current error:${Integer.toHexString(egl?.eglGetError() ?: 0)}") + } + } catch (e: Throwable) { + ALog.e(TAG, "error:$e", e) + } + } + + + private fun chooseConfig(): EGLConfig? { + val configsCount = IntArray(1) + val configs = arrayOfNulls(1) + val attributes =getAttributes() + val confSize = 1 + if (egl?.eglChooseConfig(eglDisplay, attributes, configs, confSize, configsCount) == true) { + return configs[0] + } + return null + } + + private fun getAttributes(): IntArray { + return intArrayOf( + EGL10.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, //指定渲染api类别 + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 8, + EGL10.EGL_DEPTH_SIZE, 0, + EGL10.EGL_STENCIL_SIZE, 0, + EGL10.EGL_NONE + ) + } + + private fun createContext(egl: EGL10?, eglDisplay: EGLDisplay?, eglConfig: EGLConfig?): EGLContext? { + val attrs = intArrayOf( + EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, + EGL10.EGL_NONE + ) + return egl?.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrs) + } + + fun swapBuffers() { + if (eglDisplay == null || eglSurface == null) return + egl?.eglSwapBuffers(eglDisplay, eglSurface) + } + + fun release() { + egl?.apply { + eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT) + eglDestroySurface(eglDisplay, eglSurface) + eglDestroyContext(eglDisplay, eglContext) + eglTerminate(eglDisplay) + surface?.release() + surface = null + } + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/HardDecoder.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/HardDecoder.kt new file mode 100644 index 0000000..d3344a0 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/HardDecoder.kt @@ -0,0 +1,397 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.graphics.SurfaceTexture +import android.media.MediaCodec +import android.media.MediaCodecInfo +import android.media.MediaExtractor +import android.media.MediaFormat +import android.os.Build +import android.view.Surface +import com.tencent.qgame.animplayer.file.IFileContainer +import com.tencent.qgame.animplayer.util.ALog +import com.tencent.qgame.animplayer.util.MediaUtil + +class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameAvailableListener { + + + companion object { + private const val TAG = "${Constant.TAG}.HardDecoder" + } + + private var surface: Surface? = null + private var glTexture: SurfaceTexture? = null + private val bufferInfo by lazy { MediaCodec.BufferInfo() } + private var needDestroy = false + + // 动画的原始尺寸 + private var videoWidth = 0 + private var videoHeight = 0 + + // 动画对齐后的尺寸 + private var alignWidth = 0 + private var alignHeight = 0 + + // 动画是否需要走YUV渲染逻辑的标志位 + private var needYUV = false + private var outputFormat: MediaFormat? = null + + override fun start(fileContainer: IFileContainer) { + isStopReq = false + needDestroy = false + isRunning = true + renderThread.handler?.post { + startPlay(fileContainer) + } + } + + override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) { + if (isStopReq) return + ALog.d(TAG, "onFrameAvailable") + renderData() + } + + private fun renderData() { + renderThread.handler?.post { + try { + glTexture?.apply { + updateTexImage() + render?.renderFrame() + player.pluginManager.onRendering() + render?.swapBuffers() + } + } catch (e: Throwable) { + ALog.e(TAG, "render exception=$e", e) + } + } + } + + private fun startPlay(fileContainer: IFileContainer) { + + var extractor: MediaExtractor? = null + var decoder: MediaCodec? = null + var format: MediaFormat? = null + var trackIndex = 0 + + try { + extractor = MediaUtil.getExtractor(fileContainer) + trackIndex = MediaUtil.selectVideoTrack(extractor) + if (trackIndex < 0) { + throw RuntimeException("No video track found") + } + extractor.selectTrack(trackIndex) + format = extractor.getTrackFormat(trackIndex) + if (format == null) throw RuntimeException("format is null") + + // 是否支持h265 + if (MediaUtil.checkIsHevc(format)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP + || !MediaUtil.checkSupportCodec(MediaUtil.MIME_HEVC)) { + + onFailed(Constant.REPORT_ERROR_TYPE_HEVC_NOT_SUPPORT, + "${Constant.ERROR_MSG_HEVC_NOT_SUPPORT} " + + "sdk:${Build.VERSION.SDK_INT}" + + ",support hevc:" + MediaUtil.checkSupportCodec(MediaUtil.MIME_HEVC)) + release(null, null) + return + } + } + + videoWidth = format.getInteger(MediaFormat.KEY_WIDTH) + videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT) + // 防止没有INFO_OUTPUT_FORMAT_CHANGED时导致alignWidth和alignHeight不会被赋值一直是0 + alignWidth = videoWidth + alignHeight = videoHeight + ALog.i(TAG, "Video size is $videoWidth x $videoHeight") + + // 由于使用mediacodec解码老版本素材时对宽度1500尺寸的视频进行数据对齐,解码后的宽度变成1504,导致采样点出现偏差播放异常 + // 所以当开启兼容老版本视频模式并且老版本视频的宽度不能被16整除时要走YUV渲染逻辑 + // 但是这样直接判断有风险,后期想办法改 + needYUV = videoWidth % 16 != 0 && player.enableVersion1 + + try { + if (!prepareRender(needYUV)) { + throw RuntimeException("render create fail") + } + } catch (t: Throwable) { + onFailed(Constant.REPORT_ERROR_TYPE_CREATE_RENDER, "${Constant.ERROR_MSG_CREATE_RENDER} e=$t") + release(null, null) + return + } + + preparePlay(videoWidth, videoHeight) + + render?.apply { + glTexture = SurfaceTexture(getExternalTexture()).apply { + setOnFrameAvailableListener(this@HardDecoder) + setDefaultBufferSize(videoWidth, videoHeight) + } + clearFrame() + } + + } catch (e: Throwable) { + ALog.e(TAG, "MediaExtractor exception e=$e", e) + onFailed(Constant.REPORT_ERROR_TYPE_EXTRACTOR_EXC, "${Constant.ERROR_MSG_EXTRACTOR_EXC} e=$e") + release(decoder, extractor) + return + } + + try { + val mime = format.getString(MediaFormat.KEY_MIME) ?: "" + ALog.i(TAG, "Video MIME is $mime") + decoder = MediaCodec.createDecoderByType(mime).apply { + if (needYUV) { + format.setInteger( + MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar + ) + configure(format, null, null, 0) + } else { + surface = Surface(glTexture) + configure(format, surface, null, 0) + } + + start() + decodeThread.handler?.post { + try { + startDecode(extractor, this) + } catch (e: Throwable) { + ALog.e(TAG, "MediaCodec exception e=$e", e) + onFailed(Constant.REPORT_ERROR_TYPE_DECODE_EXC, "${Constant.ERROR_MSG_DECODE_EXC} e=$e") + release(decoder, extractor) + } + } + } + } catch (e: Throwable) { + ALog.e(TAG, "MediaCodec configure exception e=$e", e) + onFailed(Constant.REPORT_ERROR_TYPE_DECODE_EXC, "${Constant.ERROR_MSG_DECODE_EXC} e=$e") + release(decoder, extractor) + return + } + } + + private fun startDecode(extractor: MediaExtractor ,decoder: MediaCodec) { + val TIMEOUT_USEC = 10000L + var inputChunk = 0 + var outputDone = false + var inputDone = false + var frameIndex = 0 + var isLoop = false + + val decoderInputBuffers = decoder.inputBuffers + + while (!outputDone) { + if (isStopReq) { + ALog.i(TAG, "stop decode") + release(decoder, extractor) + return + } + + if (!inputDone) { + val inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC) + if (inputBufIndex >= 0) { + val inputBuf = decoderInputBuffers[inputBufIndex] + val chunkSize = extractor.readSampleData(inputBuf, 0) + if (chunkSize < 0) { + decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM) + inputDone = true + ALog.d(TAG, "decode EOS") + } else { + val presentationTimeUs = extractor.sampleTime + decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, presentationTimeUs, 0) + ALog.d(TAG, "submitted frame $inputChunk to dec, size=$chunkSize") + inputChunk++ + extractor.advance() + } + } else { + ALog.d(TAG, "input buffer not available") + } + } + + if (!outputDone) { + val decoderStatus = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC) + when { + decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> ALog.d(TAG, "no output from decoder available") + decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> ALog.d(TAG, "decoder output buffers changed") + decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { + outputFormat = decoder.outputFormat + outputFormat?.apply { + try { + // 有可能取到空值,做一层保护 + val stride = getInteger("stride") + val sliceHeight = getInteger("slice-height") + if (stride > 0 && sliceHeight > 0) { + alignWidth = stride + alignHeight = sliceHeight + } + } catch (t: Throwable) { + ALog.e(TAG, "$t", t) + } + } + ALog.i(TAG, "decoder output format changed: $outputFormat") + } + decoderStatus < 0 -> { + throw RuntimeException("unexpected result from decoder.dequeueOutputBuffer: $decoderStatus") + } + else -> { + var loop = 0 + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + loop = --playLoop + player.playLoop = playLoop // 消耗loop次数 自动恢复后能有正确的loop次数 + outputDone = playLoop <= 0 + } + val doRender = !outputDone + if (doRender) { + speedControlUtil.preRender(bufferInfo.presentationTimeUs) + } + + if (needYUV && doRender) { + yuvProcess(decoder, decoderStatus) + } + + // release & render + decoder.releaseOutputBuffer(decoderStatus, doRender && !needYUV) + + if (frameIndex == 0 && !isLoop) { + onVideoStart() + } + player.pluginManager.onDecoding(frameIndex) + onVideoRender(frameIndex, player.configManager.config) + + frameIndex++ + ALog.d(TAG, "decode frameIndex=$frameIndex") + if (loop > 0) { + ALog.d(TAG, "Reached EOD, looping") + player.pluginManager.onLoopStart() + extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) + inputDone = false + decoder.flush() + speedControlUtil.reset() + frameIndex = 0 + isLoop = true + } + if (outputDone) { + release(decoder, extractor) + } + } + } + } + } + + } + + /** + * 获取到解码后每一帧的YUV数据,裁剪出正确的尺寸 + */ + private fun yuvProcess(decoder: MediaCodec, outputIndex: Int) { + val outputBuffer = decoder.outputBuffers[outputIndex] + outputBuffer?.let { + it.position(0) + it.limit(bufferInfo.offset + bufferInfo.size) + var yuvData = ByteArray(outputBuffer.remaining()) + outputBuffer.get(yuvData) + + if (yuvData.isNotEmpty()) { + var yData = ByteArray(videoWidth * videoHeight) + var uData = ByteArray(videoWidth * videoHeight / 4) + var vData = ByteArray(videoWidth * videoHeight / 4) + + if (outputFormat?.getInteger(MediaFormat.KEY_COLOR_FORMAT) == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) { + yuvData = yuv420spTop(yuvData) + } + + yuvCopy(yuvData, 0, alignWidth, alignHeight, yData, videoWidth, videoHeight) + yuvCopy(yuvData, alignWidth * alignHeight, alignWidth / 2, alignHeight / 2, uData, videoWidth / 2, videoHeight / 2) + yuvCopy(yuvData, alignWidth * alignHeight * 5 / 4, alignWidth / 2, alignHeight / 2, vData, videoWidth / 2, videoHeight / 2) + + render?.setYUVData(videoWidth, videoHeight, yData, uData, vData) + renderData() + } + } + } + + private fun yuv420spTop(yuv420sp: ByteArray): ByteArray { + val yuv420p = ByteArray(yuv420sp.size) + val ySize = alignWidth * alignHeight + System.arraycopy(yuv420sp, 0, yuv420p, 0, alignWidth * alignHeight) + var i = ySize + var j = ySize + while (i < ySize * 3 / 2) { + yuv420p[j] = yuv420sp[i] + yuv420p[j + ySize / 4] = yuv420sp[i + 1] + i += 2 + j++ + } + return yuv420p + } + + private fun yuvCopy(src: ByteArray, srcOffset: Int, inWidth: Int, inHeight: Int, dest: ByteArray, outWidth: Int, outHeight: Int) { + for (h in 0 until inHeight) { + if (h < outHeight) { + System.arraycopy(src, srcOffset + h * inWidth, dest, h * outWidth, outWidth) + } + } + } + + private fun release(decoder: MediaCodec?, extractor: MediaExtractor?) { + renderThread.handler?.post { + render?.clearFrame() + try { + ALog.i(TAG, "release") + decoder?.apply { + stop() + release() + } + extractor?.release() + glTexture?.release() + glTexture = null + speedControlUtil.reset() + player.pluginManager.onRelease() + render?.releaseTexture() + surface?.release() + surface = null + } catch (e: Throwable) { + ALog.e(TAG, "release e=$e", e) + } + isRunning = false + onVideoComplete() + if (needDestroy) { + destroyInner() + } + } + } + + override fun destroy() { + if (isRunning) { + needDestroy = true + stop() + } else { + destroyInner() + } + } + + private fun destroyInner() { + ALog.i(TAG, "destroyInner") + renderThread.handler?.post { + player.pluginManager.onDestroy() + render?.destroyRender() + render = null + onVideoDestroy() + destroyThread() + } + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/IAnimView.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/IAnimView.kt new file mode 100644 index 0000000..37e68e9 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/IAnimView.kt @@ -0,0 +1,66 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.content.res.AssetManager +import android.graphics.SurfaceTexture +import com.tencent.qgame.animplayer.file.IFileContainer +import com.tencent.qgame.animplayer.inter.IAnimListener +import com.tencent.qgame.animplayer.inter.IFetchResource +import com.tencent.qgame.animplayer.inter.OnResourceClickListener +import com.tencent.qgame.animplayer.mask.MaskConfig +import com.tencent.qgame.animplayer.util.IScaleType +import com.tencent.qgame.animplayer.util.ScaleType +import java.io.File + +interface IAnimView { + + fun prepareTextureView() + + fun getSurfaceTexture(): SurfaceTexture? + + fun setAnimListener(animListener: IAnimListener?) + + fun setFetchResource(fetchResource: IFetchResource?) + + fun setOnResourceClickListener(resourceClickListener: OnResourceClickListener?) + + fun setLoop(playLoop: Int) + + fun supportMask(isSupport: Boolean, isEdgeBlur: Boolean) + + fun updateMaskConfig(maskConfig: MaskConfig?) + + fun setFps(fps: Int) + + fun setScaleType(type: ScaleType) + + fun setScaleType(scaleType: IScaleType) + + fun setMute(isMute: Boolean) + + fun startPlay(file: File) + + fun startPlay(assetManager: AssetManager, assetsPath: String) + + fun startPlay(fileContainer: IFileContainer) + + fun stopPlay() + + fun isRunning(): Boolean + + fun getRealSize(): Pair +} diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/IRenderListener.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/IRenderListener.kt new file mode 100644 index 0000000..edf38bb --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/IRenderListener.kt @@ -0,0 +1,54 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +interface IRenderListener { + + /** + * 初始化渲染环境,获取shader字段,创建绑定纹理 + */ + fun initRender() + + /** + * 渲染上屏 + */ + fun renderFrame() + + fun clearFrame() + + /** + * 释放纹理 + */ + fun destroyRender() + + /** + * 设置视频配置 + */ + fun setAnimConfig(config: AnimConfig) + + /** + * 显示区域大小变化 + */ + fun updateViewPort(width: Int, height: Int) {} + + fun getExternalTexture(): Int + + fun releaseTexture() + + fun swapBuffers() + + fun setYUVData(width: Int, height: Int, y: ByteArray?, u: ByteArray?, v: ByteArray?) {} +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Render.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Render.kt new file mode 100644 index 0000000..558de06 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/Render.kt @@ -0,0 +1,153 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.graphics.SurfaceTexture +import android.opengl.GLES11Ext +import android.opengl.GLES20 +import com.tencent.qgame.animplayer.util.GlFloatArray +import com.tencent.qgame.animplayer.util.ShaderUtil +import com.tencent.qgame.animplayer.util.TexCoordsUtil +import com.tencent.qgame.animplayer.util.VertexUtil +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.ShortBuffer + +class Render(surfaceTexture: SurfaceTexture): IRenderListener { + + companion object { + private const val TAG = "${Constant.TAG}.Render" + } + + private val vertexArray = GlFloatArray() + private val alphaArray = GlFloatArray() + private val rgbArray = GlFloatArray() + private var surfaceSizeChanged = false + private var surfaceWidth = 0 + private var surfaceHeight = 0 + private val eglUtil: EGLUtil = EGLUtil() + private var shaderProgram = 0 + private var genTexture = IntArray(1) + private var uTextureLocation: Int = 0 + private var aPositionLocation: Int = 0 + private var aTextureAlphaLocation: Int = 0 + private var aTextureRgbLocation: Int = 0 + + init { + eglUtil.start(surfaceTexture) + initRender() + } + + private fun setVertexBuf(config: AnimConfig) { + vertexArray.setArray(VertexUtil.create(config.width, config.height, PointRect(0, 0, config.width, config.height), vertexArray.array)) + } + + private fun setTexCoords(config: AnimConfig) { + val alpha = TexCoordsUtil.create(config.videoWidth, config.videoHeight, config.alphaPointRect, alphaArray.array) + val rgb = TexCoordsUtil.create(config.videoWidth, config.videoHeight, config.rgbPointRect, rgbArray.array) + alphaArray.setArray(alpha) + rgbArray.setArray(rgb) + } + + override fun initRender() { + shaderProgram = ShaderUtil.createProgram(RenderConstant.VERTEX_SHADER, RenderConstant.FRAGMENT_SHADER) + uTextureLocation = GLES20.glGetUniformLocation(shaderProgram, "texture") + aPositionLocation = GLES20.glGetAttribLocation(shaderProgram, "vPosition") + aTextureAlphaLocation = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateAlpha") + aTextureRgbLocation = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateRgb") + + GLES20.glGenTextures(genTexture.size, genTexture, 0) + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, genTexture[0]) + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat()) + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat()) + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) + } + + override fun renderFrame() { + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f) + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) + if (surfaceSizeChanged && surfaceWidth>0 && surfaceHeight>0) { + surfaceSizeChanged = false + GLES20.glViewport(0,0, surfaceWidth, surfaceHeight) + } + draw() + } + + override fun clearFrame() { + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f) + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) + eglUtil.swapBuffers() + } + + override fun destroyRender() { + releaseTexture() + eglUtil.release() + } + + override fun releaseTexture() { + GLES20.glDeleteTextures(genTexture.size, genTexture, 0) + } + + /** + * 设置视频配置 + */ + override fun setAnimConfig(config: AnimConfig) { + setVertexBuf(config) + setTexCoords(config) + } + + /** + * 显示区域大小变化 + */ + override fun updateViewPort(width: Int, height: Int) { + if (width <=0 || height <=0) return + surfaceSizeChanged = true + surfaceWidth = width + surfaceHeight = height + } + + override fun swapBuffers() { + eglUtil.swapBuffers() + } + + /** + * mediaCodec渲染使用的 + */ + override fun getExternalTexture(): Int { + return genTexture[0] + } + + private fun draw() { + GLES20.glUseProgram(shaderProgram) + // 设置顶点坐标 + vertexArray.setVertexAttribPointer(aPositionLocation) + // 绑定纹理 + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, genTexture[0]) + GLES20.glUniform1i(uTextureLocation, 0) + + // 设置纹理坐标 + // alpha 通道坐标 + alphaArray.setVertexAttribPointer(aTextureAlphaLocation) + // rgb 通道坐标 + rgbArray.setVertexAttribPointer(aTextureRgbLocation) + + // draw + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/RenderConstant.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/RenderConstant.kt new file mode 100644 index 0000000..4b2bc01 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/RenderConstant.kt @@ -0,0 +1,43 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + + +object RenderConstant { + + const val VERTEX_SHADER = "attribute vec4 vPosition;\n" + + "attribute vec4 vTexCoordinateAlpha;\n" + + "attribute vec4 vTexCoordinateRgb;\n" + + "varying vec2 v_TexCoordinateAlpha;\n" + + "varying vec2 v_TexCoordinateRgb;\n" + + "\n" + + "void main() {\n" + + " v_TexCoordinateAlpha = vec2(vTexCoordinateAlpha.x, vTexCoordinateAlpha.y);\n" + + " v_TexCoordinateRgb = vec2(vTexCoordinateRgb.x, vTexCoordinateRgb.y);\n" + + " gl_Position = vPosition;\n" + + "}" + const val FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES texture;\n" + + "varying vec2 v_TexCoordinateAlpha;\n" + + "varying vec2 v_TexCoordinateRgb;\n" + + "\n" + + "void main () {\n" + + " vec4 alphaColor = texture2D(texture, v_TexCoordinateAlpha);\n" + + " vec4 rgbColor = texture2D(texture, v_TexCoordinateRgb);\n" + + " gl_FragColor = vec4(rgbColor.r, rgbColor.g, rgbColor.b, alphaColor.r);\n" + + "}" +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/YUVRender.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/YUVRender.kt new file mode 100644 index 0000000..bd6b439 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/YUVRender.kt @@ -0,0 +1,207 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +import android.graphics.SurfaceTexture +import android.opengl.GLES20 +import com.tencent.qgame.animplayer.util.GlFloatArray +import com.tencent.qgame.animplayer.util.ShaderUtil.createProgram +import com.tencent.qgame.animplayer.util.TexCoordsUtil +import com.tencent.qgame.animplayer.util.VertexUtil +import java.nio.ByteBuffer +import java.nio.FloatBuffer + +class YUVRender (surfaceTexture: SurfaceTexture): IRenderListener { + + companion object { + private const val TAG = "${Constant.TAG}.YUVRender" + } + + private val vertexArray = GlFloatArray() + private val alphaArray = GlFloatArray() + private val rgbArray = GlFloatArray() + + private var shaderProgram = 0 + + //顶点位置 + private var avPosition = 0 + + //rgb纹理位置 + private var rgbPosition = 0 + + //alpha纹理位置 + private var alphaPosition = 0 + + //shader yuv变量 + private var samplerY = 0 + private var samplerU = 0 + private var samplerV = 0 + private var textureId = IntArray(3) + private var convertMatrixUniform = 0 + private var convertOffsetUniform = 0 + + //YUV数据 + private var widthYUV = 0 + private var heightYUV = 0 + private var y: ByteBuffer? = null + private var u: ByteBuffer? = null + private var v: ByteBuffer? = null + + private val eglUtil: EGLUtil = EGLUtil() + + // 像素数据向GPU传输时默认以4字节对齐 + private var unpackAlign = 4 + + // YUV offset + private val YUV_OFFSET = floatArrayOf( + 0f, -0.501960814f, -0.501960814f + ) + + // RGB coefficients + private val YUV_MATRIX = floatArrayOf( + 1f, 1f, 1f, + 0f, -0.3441f, 1.772f, + 1.402f, -0.7141f, 0f + ) + + init { + eglUtil.start(surfaceTexture) + initRender() + } + + override fun initRender() { + shaderProgram = createProgram(YUVShader.VERTEX_SHADER, YUVShader.FRAGMENT_SHADER) + //获取顶点坐标字段 + avPosition = GLES20.glGetAttribLocation(shaderProgram, "v_Position") + //获取纹理坐标字段 + rgbPosition = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateRgb") + alphaPosition = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateAlpha") + + //获取yuv字段 + samplerY = GLES20.glGetUniformLocation(shaderProgram, "sampler_y") + samplerU = GLES20.glGetUniformLocation(shaderProgram, "sampler_u") + samplerV = GLES20.glGetUniformLocation(shaderProgram, "sampler_v") + convertMatrixUniform = GLES20.glGetUniformLocation(shaderProgram, "convertMatrix") + convertOffsetUniform = GLES20.glGetUniformLocation(shaderProgram, "offset") + //创建3个纹理 + GLES20.glGenTextures(textureId.size, textureId, 0) + + //绑定纹理 + for (id in textureId) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, id) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR) + } + } + + override fun renderFrame() { + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f) + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) + draw() + } + + override fun clearFrame() { + GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f) + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) + eglUtil.swapBuffers() + } + + override fun destroyRender() { + releaseTexture() + eglUtil.release() + } + + override fun setAnimConfig(config: AnimConfig) { + vertexArray.setArray(VertexUtil.create(config.width, config.height, PointRect(0, 0, config.width, config.height), vertexArray.array)) + val alpha = TexCoordsUtil.create(config.videoWidth, config.videoHeight, config.alphaPointRect, alphaArray.array) + val rgb = TexCoordsUtil.create(config.videoWidth, config.videoHeight, config.rgbPointRect, rgbArray.array) + alphaArray.setArray(alpha) + rgbArray.setArray(rgb) + } + + override fun getExternalTexture(): Int { + return textureId[0] + } + + override fun releaseTexture() { + GLES20.glDeleteTextures(textureId.size, textureId, 0) + } + + override fun swapBuffers() { + eglUtil.swapBuffers() + } + + override fun setYUVData(width: Int, height: Int, y: ByteArray?, u: ByteArray?, v: ByteArray?) { + widthYUV = width + heightYUV = height + this.y = ByteBuffer.wrap(y) + this.u = ByteBuffer.wrap(u) + this.v = ByteBuffer.wrap(v) + + // 当视频帧的u或者v分量的宽度不能被4整除时,用默认的4字节对齐会导致存取最后一行时越界,所以在向GPU传输数据前指定对齐方式 + if ((widthYUV / 2) % 4 != 0) { + this.unpackAlign = if ((widthYUV / 2) % 2 == 0) 2 else 1 + } + } + + private fun draw() { + if (widthYUV > 0 && heightYUV > 0 && y != null && u != null && v != null) { + GLES20.glUseProgram(shaderProgram) + vertexArray.setVertexAttribPointer(avPosition) + alphaArray.setVertexAttribPointer(alphaPosition) + rgbArray.setVertexAttribPointer(rgbPosition) + + GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, unpackAlign) + + //激活纹理0来绑定y数据 + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]) + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, widthYUV, heightYUV, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, y) + + //激活纹理1来绑定u数据 + GLES20.glActiveTexture(GLES20.GL_TEXTURE1) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[1]) + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, widthYUV / 2, heightYUV / 2, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, u) + + //激活纹理2来绑定v数据 + GLES20.glActiveTexture(GLES20.GL_TEXTURE2) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[2]) + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, widthYUV / 2, heightYUV / 2, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, v) + + //给fragment_shader里面yuv变量设置值 0 1 标识纹理x + GLES20.glUniform1i(samplerY, 0) + GLES20.glUniform1i(samplerU, 1) + GLES20.glUniform1i(samplerV, 2) + + GLES20.glUniform3fv(convertOffsetUniform, 1, FloatBuffer.wrap(YUV_OFFSET)) + GLES20.glUniformMatrix3fv(convertMatrixUniform, 1, false, YUV_MATRIX, 0) + + //绘制 + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) + y?.clear() + u?.clear() + v?.clear() + y = null + u = null + v = null + GLES20.glDisableVertexAttribArray(avPosition) + GLES20.glDisableVertexAttribArray(rgbPosition) + GLES20.glDisableVertexAttribArray(alphaPosition) + } + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/YUVShader.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/YUVShader.kt new file mode 100644 index 0000000..25f0ed6 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/YUVShader.kt @@ -0,0 +1,60 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer + +object YUVShader { + + const val VERTEX_SHADER = "attribute vec4 v_Position;\n" + + "attribute vec2 vTexCoordinateAlpha;\n" + + "attribute vec2 vTexCoordinateRgb;\n" + + "varying vec2 v_TexCoordinateAlpha;\n" + + "varying vec2 v_TexCoordinateRgb;\n" + + "\n" + + "void main() {\n" + + " v_TexCoordinateAlpha = vTexCoordinateAlpha;\n" + + " v_TexCoordinateRgb = vTexCoordinateRgb;\n" + + " gl_Position = v_Position;\n" + + "}" + + const val FRAGMENT_SHADER = "precision mediump float;\n" + + "uniform sampler2D sampler_y;\n" + + "uniform sampler2D sampler_u;\n" + + "uniform sampler2D sampler_v;\n" + + "varying vec2 v_TexCoordinateAlpha;\n" + + "varying vec2 v_TexCoordinateRgb;\n" + + "uniform mat3 convertMatrix;\n" + + "uniform vec3 offset;\n" + + "\n" + + "void main() {\n" + + " highp vec3 yuvColorAlpha;\n" + + " highp vec3 yuvColorRGB;\n" + + " highp vec3 rgbColorAlpha;\n" + + " highp vec3 rgbColorRGB;\n" + + " yuvColorAlpha.x = texture2D(sampler_y,v_TexCoordinateAlpha).r;\n" + + " yuvColorRGB.x = texture2D(sampler_y,v_TexCoordinateRgb).r;\n" + + " yuvColorAlpha.y = texture2D(sampler_u,v_TexCoordinateAlpha).r;\n" + + " yuvColorAlpha.z = texture2D(sampler_v,v_TexCoordinateAlpha).r;\n" + + " yuvColorRGB.y = texture2D(sampler_u,v_TexCoordinateRgb).r;\n" + + " yuvColorRGB.z = texture2D(sampler_v,v_TexCoordinateRgb).r;\n" + + " yuvColorAlpha += offset;\n" + + " yuvColorRGB += offset;\n" + + " rgbColorAlpha = convertMatrix * yuvColorAlpha; \n" + + " rgbColorRGB = convertMatrix * yuvColorRGB; \n" + + " gl_FragColor=vec4(rgbColorRGB, rgbColorAlpha.r);\n" + + "}" + + // RGB2*Alpha+RGB1*(1-Alpha) +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/AssetsFileContainer.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/AssetsFileContainer.kt new file mode 100644 index 0000000..b3d0d20 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/AssetsFileContainer.kt @@ -0,0 +1,65 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.file + +import android.content.res.AssetFileDescriptor +import android.content.res.AssetManager +import android.media.MediaExtractor +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.util.ALog + +class AssetsFileContainer(assetManager: AssetManager, assetsPath: String): IFileContainer { + + companion object { + private const val TAG = "${Constant.TAG}.FileContainer" + } + + private val assetFd: AssetFileDescriptor = assetManager.openFd(assetsPath) + private val assetsInputStream: AssetManager.AssetInputStream = + assetManager.open(assetsPath, AssetManager.ACCESS_STREAMING) as AssetManager.AssetInputStream + + init { + ALog.i(TAG, "AssetsFileContainer init") + } + + override fun setDataSource(extractor: MediaExtractor) { + if (assetFd.declaredLength < 0) { + extractor.setDataSource(assetFd.fileDescriptor) + } else { + extractor.setDataSource(assetFd.fileDescriptor, assetFd.startOffset, assetFd.declaredLength) + } + } + + override fun startRandomRead() { + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + return assetsInputStream.read(b, off, len) + } + + override fun skip(pos: Long) { + assetsInputStream.skip(pos) + } + + override fun closeRandomRead() { + assetsInputStream.close() + } + + override fun close() { + assetFd.close() + assetsInputStream.close() + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/FileContainer.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/FileContainer.kt new file mode 100644 index 0000000..a4551b9 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/FileContainer.kt @@ -0,0 +1,60 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.file + +import android.media.MediaExtractor +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.util.ALog +import java.io.File +import java.io.FileNotFoundException +import java.io.RandomAccessFile + +class FileContainer(private val file: File) : IFileContainer { + + companion object { + private const val TAG = "${Constant.TAG}.FileContainer" + } + + private var randomAccessFile: RandomAccessFile? = null + + init { + ALog.i(TAG, "FileContainer init") + if (!(file.exists() && file.isFile && file.canRead())) throw FileNotFoundException("Unable to read $file") + } + + override fun setDataSource(extractor: MediaExtractor) { + extractor.setDataSource(file.toString()) + } + + override fun startRandomRead() { + randomAccessFile = RandomAccessFile(file, "r") + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + return randomAccessFile?.read(b, off, len) ?: -1 + } + + override fun skip(pos: Long) { + randomAccessFile?.skipBytes(pos.toInt()) + } + + override fun closeRandomRead() { + randomAccessFile?.close() + } + + override fun close() { + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/IFileContainer.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/IFileContainer.kt new file mode 100644 index 0000000..0ed01db --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/IFileContainer.kt @@ -0,0 +1,34 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.file + +import android.media.MediaExtractor + +interface IFileContainer { + + fun setDataSource(extractor: MediaExtractor) + + fun startRandomRead() + + fun read(b: ByteArray, off: Int, len: Int): Int + + fun skip(pos: Long) + + fun closeRandomRead() + + fun close() + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/StreamContainer.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/StreamContainer.kt new file mode 100644 index 0000000..770e472 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/StreamContainer.kt @@ -0,0 +1,50 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.file + +import android.annotation.TargetApi +import android.media.MediaExtractor +import android.os.Build +import java.io.ByteArrayInputStream + +@TargetApi(Build.VERSION_CODES.M) +class StreamContainer(private val bytes: ByteArray) : IFileContainer { + + private var stream: ByteArrayInputStream = ByteArrayInputStream(bytes) + + override fun setDataSource(extractor: MediaExtractor) { + val dataSource = StreamMediaDataSource(bytes) + extractor.setDataSource(dataSource) + } + + override fun startRandomRead() { + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + return stream.read(b, off, len) + } + + override fun skip(pos: Long) { + stream.skip(pos) + } + + override fun closeRandomRead() { + } + + override fun close() { + stream.close() + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/StreamMediaDataSource.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/StreamMediaDataSource.kt new file mode 100644 index 0000000..7566d71 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/file/StreamMediaDataSource.kt @@ -0,0 +1,49 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.file + +import android.annotation.TargetApi +import android.media.MediaDataSource +import android.os.Build + +@TargetApi(Build.VERSION_CODES.M) +class StreamMediaDataSource(val bytes: ByteArray) : MediaDataSource() { + + override fun close() { + } + + override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { + var newSize = size + synchronized(StreamMediaDataSource::class) { + val length = bytes.size + if (position >= length) { + return -1 + } + if (position + newSize > length) { + newSize -= (position + newSize).toInt() - length + } + System.arraycopy(bytes, position.toInt(), buffer, offset, newSize) + return newSize + } + + } + + override fun getSize(): Long { + synchronized(StreamMediaDataSource::class) { + return bytes.size.toLong() + } + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/IAnimListener.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/IAnimListener.kt new file mode 100644 index 0000000..337a7bf --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/IAnimListener.kt @@ -0,0 +1,74 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.inter + +import com.tencent.qgame.animplayer.AnimConfig + +interface IAnimListener { + + /** + * 配置准备好后回调 + * ps:如果是默认配置(没有发现vapc配置),因为信息不完整onVideoConfigReady不会被调用,默认播放 + * @return true 继续播放 false 结束播放 + */ + fun onVideoConfigReady(config: AnimConfig): Boolean { + return true // 默认继续播放 + } + + /** + * 开始播放 + */ + fun onVideoStart() + + + /** + * 视频渲染每一帧时的回调 + * @param frameIndex 帧索引 + */ + fun onVideoRender(frameIndex: Int, config: AnimConfig?) + + /** + * 视频播放结束 + */ + fun onVideoComplete() + + /** + * 视频被销毁 + */ + fun onVideoDestroy() + + /** + * 失败回调 + * @param errorType 错误类型 + * @param errorMsg 错误消息 + */ + fun onFailed(errorType: Int, errorMsg: String?) +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/IFetchResource.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/IFetchResource.kt new file mode 100644 index 0000000..57d3627 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/IFetchResource.kt @@ -0,0 +1,33 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.inter + +import android.graphics.Bitmap +import com.tencent.qgame.animplayer.mix.Resource + +/** + * 获取资源 + */ +interface IFetchResource { + // 获取图片 (暂时不支持Bitmap.Config.ALPHA_8 主要是因为一些机型opengl兼容问题) + fun fetchImage(resource: Resource, result:(Bitmap?) -> Unit) + + // 获取文字 + fun fetchText(resource: Resource, result:(String?) -> Unit) + + // 资源释放通知 + fun releaseResource(resources: List) +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/OnResourceClickListener.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/OnResourceClickListener.kt new file mode 100644 index 0000000..89de924 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/inter/OnResourceClickListener.kt @@ -0,0 +1,24 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.inter + +import com.tencent.qgame.animplayer.mix.Resource + +interface OnResourceClickListener { + + // 返回被点击的资源 + fun onClick(resource: Resource) +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskAnimPlugin.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskAnimPlugin.kt new file mode 100644 index 0000000..4fe01fb --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskAnimPlugin.kt @@ -0,0 +1,66 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mask + +import com.tencent.qgame.animplayer.AnimConfig +import com.tencent.qgame.animplayer.AnimPlayer +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.plugin.IAnimPlugin +import com.tencent.qgame.animplayer.util.ALog + +class MaskAnimPlugin(val player: AnimPlayer): IAnimPlugin { + + companion object { + private const val TAG = "${Constant.TAG}.MaskAnimPlugin" + } + + private var maskRender: MaskRender? = null + private var animConfig : AnimConfig ?= null + + + override fun onConfigCreate(config: AnimConfig): Int { + return Constant.OK + } + + override fun onRenderCreate() { + ALog.i(TAG, "mask render init") + maskRender = if(player.supportMaskBoolean) MaskRender(this) else return + maskRender?.initMaskShader(player.maskEdgeBlurBoolean) + } + + override fun onRendering(frameIndex: Int) { + animConfig = if(player.supportMaskBoolean && player.configManager.config is AnimConfig) player.configManager.config else return + animConfig?.let {config -> + maskRender?.renderFrame(config) + } + } + + + override fun onRelease() { + destroy() + } + + override fun onDestroy() { + destroy() + } + + + private fun destroy() { + // 强制结束等待 + animConfig?.maskConfig?.release() + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskConfig.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskConfig.kt new file mode 100644 index 0000000..eb1ef4a --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskConfig.kt @@ -0,0 +1,74 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mask + +import android.graphics.Bitmap +import com.tencent.qgame.animplayer.PointRect +import com.tencent.qgame.animplayer.RefVec2 +import com.tencent.qgame.animplayer.util.TextureLoadUtil + +class MaskConfig() { + var maskTexPair: Pair? = null //遮罩坐标矩形 + var maskPositionPair: Pair? = null //内容坐标矩形 + + constructor(bitmap: Bitmap?, positionPair :Pair?, texPair: Pair?) : this() { + maskPositionPair = positionPair + maskTexPair = texPair + alphaMaskBitmap = bitmap + } + + private var maskTexId = 0 + fun getMaskTexId() : Int { + return maskTexId + } + fun updateMaskTex() : Int { + maskTexId = TextureLoadUtil.loadTexture(alphaMaskBitmap) + return maskTexId + } + + var alphaMaskBitmap: Bitmap? = null //遮罩 + private set(value) { + field = value + } + + fun safeSetMaskBitmapAndReleasePre(bitmap: Bitmap?) { + if (maskTexId > 0) { //释放 + TextureLoadUtil.releaseTexure(maskTexId) + maskTexId = 0 + } + alphaMaskBitmap = bitmap + } + + + fun release() { + alphaMaskBitmap = null + maskTexPair = null + maskPositionPair = null + } + + override fun equals(other: Any?): Boolean { + return other is MaskConfig && this.alphaMaskBitmap != other.alphaMaskBitmap + && this.maskTexPair?.first != other.maskTexPair?.first && this.maskTexPair?.second != other.maskTexPair?.second + && this.maskPositionPair?.first != other.maskPositionPair?.first && this.maskPositionPair?.second != other.maskPositionPair?.second + } + + override fun hashCode(): Int { + var result = alphaMaskBitmap?.hashCode() ?: 0 + result = 31 * result + (maskTexPair?.hashCode() ?: 0) + result = 31 * result + (maskPositionPair?.hashCode() ?: 0) + return result + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskRender.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskRender.kt new file mode 100644 index 0000000..1a76209 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskRender.kt @@ -0,0 +1,130 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mask + +import android.opengl.GLES11Ext +import android.opengl.GLES20 +import com.tencent.qgame.animplayer.AnimConfig +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.PointRect +import com.tencent.qgame.animplayer.RefVec2 +import com.tencent.qgame.animplayer.util.GlFloatArray +import com.tencent.qgame.animplayer.util.TexCoordsUtil +import com.tencent.qgame.animplayer.util.VertexUtil + +/** + * vapx 渲染 + */ +class MaskRender(private val maskAnimPlugin: MaskAnimPlugin) { + companion object { + private const val TAG = "${Constant.TAG}.MaskRender" + } + + var maskShader: MaskShader? = null + var vertexArray = GlFloatArray() + private var maskArray = GlFloatArray() + /** + * shader 与 texture初始化 + */ + fun initMaskShader(edgeBlur: Boolean) { + // shader 初始化 + maskShader = MaskShader(edgeBlur) + GLES20.glDisable(GLES20.GL_DEPTH_TEST) // 关闭深度测试 + } + + fun renderFrame(config: AnimConfig) { + val videoTextureId = maskAnimPlugin.player.decoder?.render?.getExternalTexture() ?: return + if (videoTextureId <= 0) return + val shader = this.maskShader ?: return + var maskTexId: Int = config.maskConfig?.getMaskTexId() ?: return + val maskBitmap = config.maskConfig?.alphaMaskBitmap ?: return + val maskTexRect = config.maskConfig?.maskTexPair?.first ?: return + val maskTexRefVec2 = config.maskConfig?.maskTexPair?.second ?: return + val maskPositionRect = config.maskConfig?.maskPositionPair?.first ?: PointRect( + 0, + 0, + config.width, + config.height + ) + val maskPositionRefVec2 = + config.maskConfig?.maskPositionPair?.second ?: RefVec2(config.width, config.height) + shader.useProgram() + // 顶点坐标 + vertexArray.setArray( + VertexUtil.create( + maskPositionRefVec2.w, + maskPositionRefVec2.h, + maskPositionRect, + vertexArray.array + ) + ) + vertexArray.setVertexAttribPointer(shader.aPositionLocation) + + if (maskTexId <= 0 && !maskBitmap.isRecycled) { + maskTexId = config.maskConfig?.updateMaskTex() ?: 0 + } + if (maskTexId > 0) { + maskArray.setArray( + TexCoordsUtil.create( + maskTexRefVec2.w, + maskTexRefVec2.h, + maskTexRect, + maskArray.array + ) + ) + maskArray.setVertexAttribPointer(shader.aTextureMaskCoordinatesLocation) + // 绑定alpha纹理 + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, maskTexId) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR.toFloat() + ) + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE + ) + GLES20.glTexParameteri( + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE + ) + GLES20.glUniform1i(shader.uTextureMaskUnitLocation, 0) + + GLES20.glEnable(GLES20.GL_BLEND) + // 基于源象素alpha通道值的半透明混合函数 + GLES20.glBlendFuncSeparate( + GLES20.GL_ONE, + GLES20.GL_SRC_ALPHA, + GLES20.GL_ZERO, + GLES20.GL_SRC_ALPHA + ) + // draw + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) + + GLES20.glDisable(GLES20.GL_BLEND) + } + + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskShader.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskShader.kt new file mode 100644 index 0000000..2812cae --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mask/MaskShader.kt @@ -0,0 +1,114 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mask + +import android.opengl.GLES20 +import android.util.Log +import com.tencent.qgame.animplayer.util.ShaderUtil + +class MaskShader(edgeBlurBoolean : Boolean) { + companion object { + + private const val VERTEX = + "attribute vec4 vPosition;\n" + + "attribute vec4 vTexCoordinateAlphaMask;\n" + + "varying vec2 v_TexCoordinateAlphaMask;\n" + + "\n" + + "void main() {\n" + + " v_TexCoordinateAlphaMask = vec2(vTexCoordinateAlphaMask.x, vTexCoordinateAlphaMask.y);\n" + + " gl_Position = vPosition;\n" + + "}" + //边缘做高斯模糊 + private const val FRAGMENT_BLUR_EDGE = + "precision mediump float;\n" + + "uniform sampler2D uTextureAlphaMask;\n" + + "varying vec2 v_TexCoordinateAlphaMask;\n" + + "mat3 weight = mat3(0.0625,0.125,0.0625,0.125,0.25,0.125,0.0625,0.125,0.0625);\n " + + "int coreSize=3;\n" + + "float texelOffset = .01;\n" + + "\n" + + "void main() {\n" + + " float alphaResult = 0.;\n" + + " for(int y = 0; y < coreSize; y++) {\n" + + " for(int x = 0;x < coreSize; x++) {\n" + + " alphaResult += texture2D(uTextureAlphaMask, vec2(v_TexCoordinateAlphaMask.x + (-1.0 + float(x)) * texelOffset,v_TexCoordinateAlphaMask.y + (-1.0 + float(y)) * texelOffset)).a * weight[x][y];\n" + + " }\n" + + " }\n" + + " gl_FragColor = vec4(0, 0, 0, alphaResult);\n" + + "}" + + private const val FRAGMENT_NO_BLUR_EDGE = + "precision mediump float;\n" + + "uniform sampler2D uTextureAlphaMask;\n" + + "varying vec2 v_TexCoordinateAlphaMask;\n" + + "\n" + + "void main () {\n" + + " vec4 alphaMaskColor = texture2D(uTextureAlphaMask, v_TexCoordinateAlphaMask);\n" + + " gl_FragColor = vec4(0, 0, 0, alphaMaskColor.a);\n" + + "}" + + private const val FRAGMENT_ROW = + "precision mediump float;\n" + + "uniform sampler2D uTextureAlphaMask;\n" + + "varying vec2 v_TexCoordinateAlphaMask;\n" + + "vec3 weight = vec3(0.4026,0.2442,0.0545);\n " + + "\n" + + "void main() {\n" + + " float texelOffset = .01;\n" + + " vec2 uv[5];\n" + + " uv[0]= v_TexCoordinateAlphaMask;\n" + + " uv[1]=vec2(uv[0].x+texelOffset*1.0, uv[0].y);\n" + + " uv[2]=vec2(uv[0].x-texelOffset*1.0, uv[0].y);\n" + + " uv[3]=vec2(uv[0].x+texelOffset*2.0, uv[0].y);\n" + + " uv[4]=vec2(uv[0].x-texelOffset*2.0, uv[0].y);\n" + + " float alphaResult = texture2D(uTextureAlphaMask, uv[0]).a * weight[0];\n" + + " for(int i = 1; i < 3; ++i) {\n" + + " alphaResult += texture2D(uTextureAlphaMask, uv[2*i-1]).a * weight[i];\n" + + " alphaResult += texture2D(uTextureAlphaMask, uv[2*i]).a * weight[i];\n" + + " }\n" + + " gl_FragColor = vec4(0, 0, 0, alphaResult);\n" + + "}" + + // Uniform constants + private const val U_TEXTURE_ALPHA_MASK_UNIT = "uTextureAlphaMask" + + // Attribute constants + private const val A_POSITION = "vPosition" + private const val A_TEXTURE_MASK_COORDINATES = "vTexCoordinateAlphaMask" + } + + // Shader program + private val program: Int + + // Uniform locations + val uTextureMaskUnitLocation: Int + // Attribute locations + val aPositionLocation: Int + val aTextureMaskCoordinatesLocation: Int + + init { + program = if(edgeBlurBoolean) ShaderUtil.createProgram(VERTEX, FRAGMENT_BLUR_EDGE) else ShaderUtil.createProgram(VERTEX, FRAGMENT_NO_BLUR_EDGE) + uTextureMaskUnitLocation = GLES20.glGetUniformLocation(program, U_TEXTURE_ALPHA_MASK_UNIT) + + aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION) + aTextureMaskCoordinatesLocation = GLES20.glGetAttribLocation(program, A_TEXTURE_MASK_COORDINATES) + } + + fun useProgram() { + GLES20.glUseProgram(program) + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Frame.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Frame.kt new file mode 100644 index 0000000..af0e916 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Frame.kt @@ -0,0 +1,83 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mix + +import android.util.SparseArray +import com.tencent.qgame.animplayer.PointRect +import org.json.JSONObject + +/** + * 单帧 + */ +class Frame(val index: Int, json: JSONObject) { + var srcId = "" + var z = 0 + var frame: PointRect + var mFrame: PointRect + var mt = 0 // 遮罩旋转角度v2 版本只支持 0 与 90度 + + init { + srcId = json.getString("srcId") + z = json.getInt("z") + + val f = json.getJSONArray("frame") + frame = PointRect(f.getInt(0), f.getInt(1), f.getInt(2), f.getInt(3)) + + val m = json.getJSONArray("mFrame") + mFrame = PointRect(m.getInt(0), m.getInt(1), m.getInt(2), m.getInt(3)) + + mt = json.getInt("mt") + } +} + + +/** + * 一帧的集合 + */ +class FrameSet(json: JSONObject) { + var index = 0 // 哪一帧 + val list = ArrayList() + init { + index = json.getInt("i") + val objJsonArray = json.getJSONArray("obj") + val objLen = objJsonArray?.length() ?: 0 + for (i in 0 until objLen) { + val frameJson = objJsonArray?.getJSONObject(i) ?: continue + val frame = Frame(index, frameJson) + list.add(frame) + } + // 绘制顺序排序 + list.sortBy {it.z} + } +} + +/** + * 所有帧集合 + */ +class FrameAll(json: JSONObject) { + // 每一帧的集合 + val map = SparseArray() + + init { + val frameJsonArray = json.getJSONArray("frame") + val frameLen = frameJsonArray?.length() ?: 0 + for (i in 0 until frameLen) { + val frameSetJson = frameJsonArray?.getJSONObject(i) ?: continue + val frameSet = FrameSet(frameSetJson) + map.put(frameSet.index, frameSet) + } + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixAnimPlugin.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixAnimPlugin.kt new file mode 100644 index 0000000..5ee676d --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixAnimPlugin.kt @@ -0,0 +1,229 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mix + +import android.graphics.Bitmap +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.view.MotionEvent +import com.tencent.qgame.animplayer.AnimConfig +import com.tencent.qgame.animplayer.AnimPlayer +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.inter.IFetchResource +import com.tencent.qgame.animplayer.inter.OnResourceClickListener +import com.tencent.qgame.animplayer.plugin.IAnimPlugin +import com.tencent.qgame.animplayer.util.ALog +import com.tencent.qgame.animplayer.util.BitmapUtil + +class MixAnimPlugin(val player: AnimPlayer): IAnimPlugin { + + companion object { + private const val TAG = "${Constant.TAG}.MixAnimPlugin" + } + var resourceRequest: IFetchResource? = null + var resourceClickListener: OnResourceClickListener? = null + var srcMap: SrcMap? = null + var frameAll: FrameAll? = null + var curFrameIndex = -1 // 当前帧 + private var resultCbCount = 0 // 回调次数 + private var mixRender:MixRender? = null + private val mixTouch by lazy { MixTouch(this) } + var autoTxtColorFill = true // 是否启动自动文字填充 默认开启 + + // 同步锁 + private val lock = Object() + private var forceStopLock = false + + + override fun onConfigCreate(config: AnimConfig): Int { + if (!config.isMix) return Constant.OK + if (resourceRequest == null) { + ALog.e(TAG, "IFetchResource is empty") + // 没有设置IFetchResource 当成普通视频播放 + return Constant.OK + } + // step 1 parse src + parseSrc(config) + + // step 2 parse frame + parseFrame(config) + + // step 3 fetch resource + fetchResourceSync() + + // step 4 生成文字bitmap + val result = createBitmap() + if (!result) { + return Constant.REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX + } + + // step 5 check resource + ALog.i(TAG, "load resource $resultCbCount") + srcMap?.map?.values?.forEach { + if (it.bitmap == null) { + ALog.e(TAG, "missing src $it") + return Constant.REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX + } else if (it.bitmap?.config == Bitmap.Config.ALPHA_8) { + ALog.e(TAG, "src $it bitmap must not be ALPHA_8") + return Constant.REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX + } + } + return Constant.OK + } + + override fun onRenderCreate() { + if (player.configManager.config?.isMix == false) return + ALog.i(TAG, "mix render init") + mixRender = MixRender(this) + mixRender?.init() + } + + override fun onRendering(frameIndex: Int) { + val config = player.configManager.config ?: return + if (!config.isMix) return + curFrameIndex = frameIndex + val list = frameAll?.map?.get(frameIndex)?.list ?: return + list.forEach {frame -> + val src = srcMap?.map?.get(frame.srcId) ?: return@forEach + mixRender?.renderFrame(config, frame, src) + } + } + + + override fun onRelease() { + destroy() + } + + override fun onDestroy() { + destroy() + } + + override fun onDispatchTouchEvent(ev: MotionEvent): Boolean { + if (player.configManager.config?.isMix == false || resourceClickListener == null) { + return super.onDispatchTouchEvent(ev) + } + mixTouch.onTouchEvent(ev)?.let {resource -> + Handler(Looper.getMainLooper()).post { + resourceClickListener?.onClick(resource) + } + } + // 只要注册监听则拦截所有事件 + return true + } + + private fun destroy() { + // 强制结束等待 + forceStopLockThread() + if (player.configManager.config?.isMix == false) return + val resources = ArrayList() + srcMap?.map?.values?.forEach {src -> + mixRender?.release(src.srcTextureId) + when(src.srcType) { + Src.SrcType.IMG -> resources.add(Resource(src)) + Src.SrcType.TXT -> src.bitmap?.recycle() + else -> {} + } + } + resourceRequest?.releaseResource(resources) + + // 清理 + curFrameIndex = -1 + srcMap?.map?.clear() + frameAll?.map?.clear() + } + + private fun parseSrc(config: AnimConfig) { + config.jsonConfig?.apply { + srcMap = SrcMap(this) + } + } + + + private fun parseFrame(config: AnimConfig) { + config.jsonConfig?.apply { + frameAll = FrameAll(this) + } + } + + + private fun fetchResourceSync() { + synchronized(lock) { + forceStopLock = false // 开始时不会强制关闭 + } + val time = SystemClock.elapsedRealtime() + val totalSrc = srcMap?.map?.size ?: 0 + ALog.i(TAG, "load resource totalSrc = $totalSrc") + + resultCbCount = 0 + srcMap?.map?.values?.forEach {src -> + if (src.srcType == Src.SrcType.IMG) { + ALog.i(TAG, "fetch image ${src.srcId}") + resourceRequest?.fetchImage(Resource(src)) { + src.bitmap = if (it == null) { + ALog.e(TAG, "fetch image ${src.srcId} bitmap return null") + BitmapUtil.createEmptyBitmap() + } else it + ALog.i(TAG, "fetch image ${src.srcId} finish bitmap is ${it?.hashCode()}") + resultCall() + } + } else if (src.srcType == Src.SrcType.TXT) { + ALog.i(TAG, "fetch txt ${src.srcId}") + resourceRequest?.fetchText(Resource(src)) { + src.txt = it ?: "" + ALog.i(TAG, "fetch text ${src.srcId} finish txt is $it") + resultCall() + } + } + } + + // 同步等待所有资源完成 + synchronized(lock) { + while (resultCbCount < totalSrc && !forceStopLock) { + lock.wait() + } + } + ALog.i(TAG, "fetchResourceSync cost=${SystemClock.elapsedRealtime() - time}ms") + } + + private fun forceStopLockThread() { + synchronized(lock) { + forceStopLock = true + lock.notifyAll() + } + } + + private fun resultCall() { + synchronized(lock) { + resultCbCount++ + lock.notifyAll() + } + } + + private fun createBitmap(): Boolean { + return try { + srcMap?.map?.values?.forEach { src -> + if (src.srcType == Src.SrcType.TXT) { + src.bitmap = BitmapUtil.createTxtBitmap(src) + } + } + true + } catch (e: OutOfMemoryError) { + ALog.e(TAG, "draw text OOM $e", e) + false + } + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixRender.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixRender.kt new file mode 100644 index 0000000..014fedb --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixRender.kt @@ -0,0 +1,151 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mix + +import android.opengl.GLES11Ext +import android.opengl.GLES20 +import com.tencent.qgame.animplayer.AnimConfig +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.PointRect +import com.tencent.qgame.animplayer.util.* + +/** + * vapx 渲染 + */ +class MixRender(private val mixAnimPlugin: MixAnimPlugin) { + companion object { + private const val TAG = "${Constant.TAG}.MixRender" + } + var shader: MixShader? = null + var vertexArray = GlFloatArray() + var srcArray = GlFloatArray() + var maskArray = GlFloatArray() + + /** + * shader 与 texture初始化 + */ + fun init() { + // shader 初始化 + shader = MixShader() + GLES20.glDisable(GLES20.GL_DEPTH_TEST) // 关闭深度测试 + + mixAnimPlugin.srcMap?.map?.values?.forEach {src-> + ALog.i(TAG, "init srcId=${src.srcId}") + src.srcTextureId = TextureLoadUtil.loadTexture(src.bitmap) + ALog.i(TAG, "textureProgram=${shader?.program},textureId=${src.srcTextureId}") + } + + } + + fun renderFrame(config:AnimConfig, frame:Frame, src: Src) { + val videoTextureId = mixAnimPlugin.player.decoder?.render?.getExternalTexture() ?: return + if (videoTextureId <= 0) return + val shader = this.shader ?: return + shader.useProgram() + // 定点坐标 + vertexArray.setArray(VertexUtil.create(config.width, config.height, frame.frame, vertexArray.array)) + vertexArray.setVertexAttribPointer(shader.aPositionLocation) + + // src 纹理坐标 + srcArray.setArray(genSrcCoordsArray(srcArray.array, frame.frame.w, frame.frame.h, src.drawWidth, src.drawHeight, src.fitType)) + srcArray.setVertexAttribPointer(shader.aTextureSrcCoordinatesLocation) + // 绑定 src纹理 + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, src.srcTextureId) + GLES20.glUniform1i(shader.uTextureSrcUnitLocation, 0) + + // mask 纹理 + maskArray.setArray(TexCoordsUtil.create(config.videoWidth, config.videoHeight, frame.mFrame, maskArray.array)) + if (frame.mt == 90) { + maskArray.setArray(TexCoordsUtil.rotate90(maskArray.array)) + } + maskArray.setVertexAttribPointer(shader.aTextureMaskCoordinatesLocation) + // 绑定 mask纹理 + GLES20.glActiveTexture(GLES20.GL_TEXTURE1) + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureId) + GLES20.glUniform1i(shader.uTextureMaskUnitLocation, 1) + + // 属性处理 + if (src.srcType == Src.SrcType.TXT && mixAnimPlugin.autoTxtColorFill) { // // 文字需要颜色填充 + GLES20.glUniform1i(shader.uIsFillLocation, 1) + val argb = transColor(src.color) + GLES20.glUniform4f(shader.uColorLocation, argb[1], argb[2], argb[3], argb[0]) + } else { + GLES20.glUniform1i(shader.uIsFillLocation, 0) + GLES20.glUniform4f(shader.uColorLocation, 0f, 0f, 0f, 0f) + } + + GLES20.glEnable(GLES20.GL_BLEND) + // 基于源象素alpha通道值的半透明混合函数 + GLES20.glBlendFuncSeparate(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA, GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA) + // draw + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) + + GLES20.glDisable(GLES20.GL_BLEND) + + } + + fun release(textureId: Int) { + if (textureId != 0) { + GLES20.glDeleteTextures(1, intArrayOf(textureId), 0) + } + } + + /** + * CENTER_FULL 并不是严格的centerCrop(centerCrop已经前置处理),此处主要是为防抖动做处理,复杂遮罩情况下需要固定src大小进行绘制防止抖动 + */ + private fun genSrcCoordsArray(array: FloatArray, fw: Int, fh: Int, sw: Int, sh: Int, fitType: Src.FitType): FloatArray { + return if (fitType == Src.FitType.CENTER_FULL) { + if (fw <= sw && fh <= sh) { + // 中心对齐,不拉伸 + val gw = (sw - fw) / 2 + val gh = (sh - fh) / 2 + TexCoordsUtil.create(sw, sh, PointRect(gw, gh, fw, fh), array) + } else { // centerCrop + val fScale = fw * 1.0f / fh + val sScale = sw * 1.0f / sh + val srcRect = if (fScale > sScale) { + val w = sw + val x = 0 + val h = (sw / fScale).toInt() + val y = (sh - h) / 2 + + PointRect(x, y, w, h) + } else { + val h = sh + val y = 0 + val w = (sh * fScale).toInt() + val x = (sw - w) / 2 + PointRect(x, y, w, h) + } + TexCoordsUtil.create(sw, sh, srcRect, array) + } + } else { // 默认 fitXY + TexCoordsUtil.create(fw, fh, PointRect(0, 0, fw, fh), array) + } + } + + private fun transColor(color: Int): FloatArray { + val argb = FloatArray(4) + argb[0] = (color.ushr(24) and 0x000000ff) / 255f + argb[1] = (color.ushr(16) and 0x000000ff) / 255f + argb[2] = (color.ushr(8) and 0x000000ff) / 255f + argb[3] = (color and 0x000000ff) / 255f + return argb + } + + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixShader.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixShader.kt new file mode 100644 index 0000000..f252aad --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixShader.kt @@ -0,0 +1,94 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mix + +import android.opengl.GLES20 +import com.tencent.qgame.animplayer.util.ShaderUtil + +class MixShader { + companion object { + private const val VERTEX = "attribute vec4 a_Position; \n" + + "attribute vec2 a_TextureSrcCoordinates;\n" + + "attribute vec2 a_TextureMaskCoordinates;\n" + + "varying vec2 v_TextureSrcCoordinates;\n" + + "varying vec2 v_TextureMaskCoordinates;\n" + + "void main()\n" + + "{\n" + + " v_TextureSrcCoordinates = a_TextureSrcCoordinates;\n" + + " v_TextureMaskCoordinates = a_TextureMaskCoordinates;\n" + + " gl_Position = a_Position;\n" + + "}" + + private const val FRAGMENT = "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float; \n" + + "uniform sampler2D u_TextureSrcUnit;\n" + + "uniform samplerExternalOES u_TextureMaskUnit;\n" + + "uniform int u_isFill;\n" + + "uniform vec4 u_Color;\n" + + "varying vec2 v_TextureSrcCoordinates;\n" + + "varying vec2 v_TextureMaskCoordinates;\n" + + "void main()\n" + + "{\n" + + " vec4 srcRgba = texture2D(u_TextureSrcUnit, v_TextureSrcCoordinates);\n" + + " vec4 maskRgba = texture2D(u_TextureMaskUnit, v_TextureMaskCoordinates);\n" + + " float isFill = step(0.5, float(u_isFill));\n" + + " vec4 srcRgbaCal = isFill * vec4(u_Color.r, u_Color.g, u_Color.b, srcRgba.a) + (1.0 - isFill) * srcRgba;\n" + + " gl_FragColor = vec4(srcRgbaCal.r, srcRgbaCal.g, srcRgbaCal.b, srcRgba.a * maskRgba.r);\n" + + "}" + + // Uniform constants + private const val U_TEXTURE_SRC_UNIT = "u_TextureSrcUnit" + private const val U_TEXTURE_MASK_UNIT = "u_TextureMaskUnit" + private const val U_IS_FILL = "u_isFill" + private const val U_COLOR = "u_Color" + + // Attribute constants + private const val A_POSITION = "a_Position" + private const val A_TEXTURE_SRC_COORDINATES = "a_TextureSrcCoordinates" + private const val A_TEXTURE_MASK_COORDINATES = "a_TextureMaskCoordinates" + } + + // Shader program + val program: Int + + // Uniform locations + val uTextureSrcUnitLocation: Int + val uTextureMaskUnitLocation: Int + val uIsFillLocation: Int + val uColorLocation: Int + + // Attribute locations + val aPositionLocation: Int + val aTextureSrcCoordinatesLocation: Int + val aTextureMaskCoordinatesLocation: Int + + init { + program = ShaderUtil.createProgram(VERTEX, FRAGMENT) + uTextureSrcUnitLocation = GLES20.glGetUniformLocation(program, U_TEXTURE_SRC_UNIT) + uTextureMaskUnitLocation = GLES20.glGetUniformLocation(program, U_TEXTURE_MASK_UNIT) + uIsFillLocation = GLES20.glGetUniformLocation(program, U_IS_FILL) + uColorLocation = GLES20.glGetUniformLocation(program, U_COLOR) + + aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION) + aTextureSrcCoordinatesLocation = GLES20.glGetAttribLocation(program, A_TEXTURE_SRC_COORDINATES) + aTextureMaskCoordinatesLocation = GLES20.glGetAttribLocation(program, A_TEXTURE_MASK_COORDINATES) + } + + fun useProgram() { + GLES20.glUseProgram(program) + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixTouch.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixTouch.kt new file mode 100644 index 0000000..3c09f48 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/MixTouch.kt @@ -0,0 +1,56 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mix + +import android.view.MotionEvent +import com.tencent.qgame.animplayer.PointRect + +/** + * 触摸事件 + */ +class MixTouch(private val mixAnimPlugin: MixAnimPlugin) { + + fun onTouchEvent(ev: MotionEvent): Resource? { + val (viewWith, viewHeight) = mixAnimPlugin.player.animView.getRealSize() + val videoWith = mixAnimPlugin.player.configManager.config?.width ?: return null + val videoHeight = mixAnimPlugin.player.configManager.config?.height ?: return null + + if (viewWith == 0 || viewHeight == 0) return null + + when(ev.action) { + MotionEvent.ACTION_UP -> { + val x = ev.x * videoWith / viewWith.toFloat() + val y = ev.y * videoHeight / viewHeight.toFloat() + val list = mixAnimPlugin.frameAll?.map?.get(mixAnimPlugin.curFrameIndex)?.list + list?.forEach {frame -> + val src = mixAnimPlugin.srcMap?.map?.get(frame.srcId) ?: return@forEach + if (calClick(x.toInt(), y.toInt(), frame.frame)) { + return Resource(src).apply { + curPoint = frame.frame + } + } + } + } + } + return null + } + + + private fun calClick(x: Int, y: Int, frame: PointRect): Boolean { + return x >= frame.x && x <= (frame.x + frame.w) + && y >= frame.y && y <= (frame.y + frame.h) + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Resource.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Resource.kt new file mode 100644 index 0000000..54ae8d9 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Resource.kt @@ -0,0 +1,39 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mix + +import android.graphics.Bitmap +import com.tencent.qgame.animplayer.PointRect + +/** + * 资源描述 + */ +class Resource(src: Src) { + var id = "" + var type = Src.SrcType.UNKNOWN + var loadType = Src.LoadType.UNKNOWN + var tag = "" + var bitmap: Bitmap? = null + var curPoint: PointRect? = null // src在当前帧的位置信息 + + init { + id = src.srcId + type = src.srcType + loadType = src.loadType + tag = src.srcTag + bitmap = src.bitmap + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Src.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Src.kt new file mode 100644 index 0000000..5260c7a --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/mix/Src.kt @@ -0,0 +1,152 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.mix + +import android.graphics.Bitmap +import android.graphics.Color +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.util.ALog +import org.json.JSONObject + +class Src { + companion object { + private const val TAG = "${Constant.TAG}.Src" + } + + enum class SrcType(val type: String) { + UNKNOWN("unknown"), + IMG("img"), + TXT("txt"), + } + + enum class LoadType(val type: String) { + UNKNOWN("unknown"), + NET("net"), // 网络加载的图片 + LOCAL("local"), // 本地加载的图片 + } + + enum class FitType(val type: String) { + FIT_XY("fitXY"), // 按原始大小填充纹理 + CENTER_FULL("centerFull"), // 以纹理中心点放置 + } + + enum class Style(val style: String) { + DEFAULT("default"), + BOLD("b"), // 文字粗体 + } + + var srcId = "" + var w = 0 + var h = 0 + var drawWidth = 0 + var drawHeight = 0 + var srcType = SrcType.UNKNOWN + var loadType = LoadType.UNKNOWN + var srcTag = "" + var txt = "" + var style = Style.DEFAULT + var color: Int = 0 + var fitType = FitType.FIT_XY + var srcTextureId = 0 + var bitmap: Bitmap? = null + set(value) { + field = value + genDrawSize(value) + } + + constructor(json: JSONObject) { + srcId = json.getString("srcId") + w = json.getInt("w") + h = json.getInt("h") + // 可选 + var colorStr = json.optString("color", "#000000") + if (colorStr.isEmpty()) { + colorStr = "#000000" + } + color = Color.parseColor(colorStr) + srcTag = json.getString("srcTag") + txt = srcTag + + srcType = when(json.getString("srcType")) { + SrcType.IMG.type -> SrcType.IMG + SrcType.TXT.type -> SrcType.TXT + else -> SrcType.UNKNOWN + } + loadType = when(json.getString("loadType")) { + LoadType.NET.type -> LoadType.NET + LoadType.LOCAL.type -> LoadType.LOCAL + else -> LoadType.UNKNOWN + } + fitType = when(json.getString("fitType")) { + FitType.CENTER_FULL.type -> FitType.CENTER_FULL + else -> FitType.FIT_XY + } + + // 可选 + style = when(json.optString("style", "")) { + Style.BOLD.style -> Style.BOLD + else -> Style.DEFAULT + } + ALog.i(TAG, "${toString()} color=$colorStr") + } + + + private fun genDrawSize(bitmap: Bitmap?) { + val bw = bitmap?.width?: w + val bh = bitmap?.height?: h + drawWidth = bw + drawHeight = bh + if (fitType == FitType.CENTER_FULL) { + if (w == 0 || h == 0) { + return + } + // 按src w h进行centerCrop处理 + val srcRate = w.toFloat() / h.toFloat() + val bitmapRate = bw.toFloat() / bh.toFloat() + + if (bitmapRate >= srcRate) { + drawHeight = h + drawWidth = (h * bitmapRate).toInt() + } else { + drawWidth = w + drawHeight = (w / bitmapRate).toInt() + } + } + } + + + override fun toString(): String { + return "Src(srcId='$srcId', srcType=$srcType, loadType=$loadType, srcTag='$srcTag', bitmap=$bitmap, txt='$txt')" + } + +} + + +class SrcMap(json: JSONObject) { + val map = HashMap() + + init { + val srcJsonArray = json.getJSONArray("src") + val srcLen = srcJsonArray?.length() ?: 0 + for (i in 0 until srcLen) { + val srcJson = srcJsonArray?.getJSONObject(i) ?: continue + val src = Src(srcJson) + if (src.srcType != Src.SrcType.UNKNOWN) { // 不认识的srcType丢弃 + map[src.srcId] = src + } + } + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/plugin/AnimPluginManager.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/plugin/AnimPluginManager.kt new file mode 100644 index 0000000..e3fb697 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/plugin/AnimPluginManager.kt @@ -0,0 +1,127 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.plugin + +import android.view.MotionEvent +import com.tencent.qgame.animplayer.AnimConfig +import com.tencent.qgame.animplayer.AnimPlayer +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.mask.MaskAnimPlugin +import com.tencent.qgame.animplayer.mix.MixAnimPlugin +import com.tencent.qgame.animplayer.util.ALog + +/** + * 动画插件管理 + */ +class AnimPluginManager(val player: AnimPlayer) { + companion object { + private const val TAG = "${Constant.TAG}.AnimPluginManager" + private const val DIFF_TIMES = 4 + } + + private val mixAnimPlugin = MixAnimPlugin(player) + private val maskAnimPlugin = MaskAnimPlugin(player) + + private val plugins = listOf(mixAnimPlugin, maskAnimPlugin) + + // 当前渲染的帧 + private var frameIndex = 0 + // 当前解码的帧 + private var decodeIndex = 0 + // 帧不相同的次数, 连续多次不同则直接使用decodeIndex + private var frameDiffTimes = 0 + + fun getMixAnimPlugin(): MixAnimPlugin? { + return mixAnimPlugin + } + + fun getMaskAnimPlugin() : MaskAnimPlugin? { + return maskAnimPlugin + } + + fun onConfigCreate(config: AnimConfig): Int { + ALog.i(TAG, "onConfigCreate") + plugins.forEach { + val res = it.onConfigCreate(config) + if (res != Constant.OK) { + return res + } + } + return Constant.OK + } + + fun onRenderCreate() { + ALog.i(TAG, "onRenderCreate") + frameIndex = 0 + decodeIndex = 0 + plugins.forEach { + it.onRenderCreate() + } + } + + fun onDecoding(decodeIndex: Int) { + ALog.d(TAG, "onDecoding decodeIndex=$decodeIndex") + this.decodeIndex = decodeIndex + plugins.forEach { + it.onDecoding(decodeIndex) + } + } + + // 开始循环调用 + fun onLoopStart() { + ALog.i(TAG, "onLoopStart") + frameIndex = 0 + decodeIndex = 0 + } + + fun onRendering() { + if (decodeIndex > frameIndex + 1 || frameDiffTimes >= DIFF_TIMES) { + ALog.i(TAG, "jump frameIndex= $frameIndex,decodeIndex=$decodeIndex,frameDiffTimes=$frameDiffTimes") + frameIndex = decodeIndex + } + if (decodeIndex != frameIndex) { + frameDiffTimes++ + } else { + frameDiffTimes = 0 + } + ALog.d(TAG, "onRendering frameIndex=$frameIndex") + plugins.forEach { + it.onRendering(frameIndex) // 第一帧 0 + } + frameIndex++ + } + + fun onRelease() { + ALog.i(TAG, "onRelease") + plugins.forEach { + it.onRelease() + } + } + + fun onDestroy() { + ALog.i(TAG, "onDestroy") + plugins.forEach { + it.onDestroy() + } + } + + fun onDispatchTouchEvent(ev: MotionEvent): Boolean { + plugins.forEach { + if (it.onDispatchTouchEvent(ev)) return true + } + return false + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/plugin/IAnimPlugin.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/plugin/IAnimPlugin.kt new file mode 100644 index 0000000..3837ae2 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/plugin/IAnimPlugin.kt @@ -0,0 +1,48 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.plugin + +import android.view.MotionEvent +import com.tencent.qgame.animplayer.AnimConfig +import com.tencent.qgame.animplayer.Constant + +interface IAnimPlugin { + + // 配置生成 + fun onConfigCreate(config: AnimConfig): Int { + return Constant.OK + } + + // 渲染初始化 + fun onRenderCreate() {} + + // 解码通知 + fun onDecoding(decodeIndex: Int) {} + + // 每一帧渲染 + fun onRendering(frameIndex: Int) {} + + // 每次播放完毕 + fun onRelease() {} + + // 销毁 + fun onDestroy() {} + + /** 触摸事件 + * @return false 不拦截 true 拦截 + */ + fun onDispatchTouchEvent(ev: MotionEvent): Boolean {return false} +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/textureview/InnerTextureView.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/textureview/InnerTextureView.kt new file mode 100644 index 0000000..b572354 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/textureview/InnerTextureView.kt @@ -0,0 +1,21 @@ +package com.tencent.qgame.animplayer.textureview + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.TextureView +import com.tencent.qgame.animplayer.AnimPlayer + +class InnerTextureView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : TextureView(context, attrs, defStyleAttr) { + + var player: AnimPlayer? = null + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + val res = player?.isRunning() == true + && ev != null + && player?.pluginManager?.onDispatchTouchEvent(ev) == true + return if (!res) super.dispatchTouchEvent(ev) else true + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ALog.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ALog.kt new file mode 100644 index 0000000..897c107 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ALog.kt @@ -0,0 +1,54 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +object ALog { + + var isDebug = false + + var log: IALog? = null + + fun i(tag: String, msg: String) { + log?.i(tag, msg) + } + + fun d(tag: String, msg: String) { + if (isDebug) { + log?.d(tag, msg) + } + } + + fun e(tag: String, msg: String) { + log?.e(tag, msg) + } + + fun e(tag: String, msg: String, tr: Throwable) { + log?.e(tag, msg, tr) + } +} + + +interface IALog { + fun i(tag: String, msg: String) {} + + fun d(tag: String, msg: String) {} + + fun e(tag: String, msg: String) {} + + fun e(tag: String, msg: String, tr: Throwable) {} +} + + diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/BitmapUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/BitmapUtil.kt new file mode 100644 index 0000000..f92ba75 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/BitmapUtil.kt @@ -0,0 +1,68 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import android.graphics.* +import android.text.TextPaint +import com.tencent.qgame.animplayer.mix.Src + +object BitmapUtil { + + fun createEmptyBitmap() : Bitmap { + return Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888).apply { + eraseColor(Color.TRANSPARENT) + } + } + + fun createTxtBitmap(src: Src): Bitmap { + val w = src.w + val h = src.h + // 这里使用ALPHA_8 在opengl渲染的时候图像出现错位 + val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + val rect = Rect(0, 0, w, h) + val bounds = Rect() + var sizeR = 0.8f + val paint = TextPaint().apply { + textSize = h * sizeR + textAlign = Paint.Align.CENTER + style = Paint.Style.FILL + isAntiAlias = true + if (src.style == Src.Style.BOLD) { + typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) + } + color = src.color + } + val text = src.txt + while (sizeR > 0.1f) { + paint.getTextBounds(text, 0, text.length, bounds) + if (bounds.width() <= rect.width()) { + break + } + sizeR -= 0.1f + paint.textSize = h * sizeR + } + val fontMetrics = paint.fontMetricsInt + val top = fontMetrics.top + val bottom = fontMetrics.bottom + val baseline = rect.centerY() - top/2 - bottom/2 + + canvas.drawText(text, rect.centerX().toFloat(), baseline.toFloat(), paint) + + return bitmap + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/GlFloatArray.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/GlFloatArray.kt new file mode 100644 index 0000000..c560863 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/GlFloatArray.kt @@ -0,0 +1,50 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import android.opengl.GLES20 +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.FloatBuffer +import kotlin.FloatArray + +class GlFloatArray { + + + val array = FloatArray(8) + + private var floatBuffer: FloatBuffer + + init { + floatBuffer = ByteBuffer + .allocateDirect(array.size * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer() + .put(array) + } + + fun setArray(array: FloatArray) { + floatBuffer.position(0) + floatBuffer.put(array) + } + + + fun setVertexAttribPointer(attributeLocation: Int) { + floatBuffer.position(0) + GLES20.glVertexAttribPointer(attributeLocation, 2, GLES20.GL_FLOAT, false, 0, floatBuffer) + GLES20.glEnableVertexAttribArray(attributeLocation) + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/MediaUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/MediaUtil.kt new file mode 100644 index 0000000..8adb6d9 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/MediaUtil.kt @@ -0,0 +1,107 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import android.media.MediaCodecList +import android.media.MediaExtractor +import android.media.MediaFormat +import com.tencent.qgame.animplayer.Constant +import com.tencent.qgame.animplayer.file.IFileContainer +import kotlin.collections.HashMap + + +object MediaUtil { + + private const val TAG = "${Constant.TAG}.MediaUtil" + + private var isTypeMapInit = false + private val supportTypeMap = HashMap() + + const val MIME_HEVC = "video/hevc" + + fun getExtractor(file: IFileContainer): MediaExtractor { + val extractor = MediaExtractor() + file.setDataSource(extractor) + return extractor + } + + /** + * 是否为h265的视频 + */ + fun checkIsHevc(videoFormat: MediaFormat):Boolean { + val mime = videoFormat.getString(MediaFormat.KEY_MIME) ?: "" + return mime.contains("hevc") + } + + fun selectVideoTrack(extractor: MediaExtractor): Int { + val numTracks = extractor.trackCount + for (i in 0 until numTracks) { + val format = extractor.getTrackFormat(i) + val mime = format.getString(MediaFormat.KEY_MIME) ?: "" + if (mime.startsWith("video/")) { + ALog.i(TAG, "Extractor selected track $i ($mime): $format") + return i + } + } + return -1 + } + + fun selectAudioTrack(extractor: MediaExtractor): Int { + val numTracks = extractor.trackCount + for (i in 0 until numTracks) { + val format = extractor.getTrackFormat(i) + val mime = format.getString(MediaFormat.KEY_MIME) ?: "" + if (mime.startsWith("audio/")) { + ALog.i(TAG, "Extractor selected track $i ($mime): $format") + return i + } + } + return -1 + } + + /** + * 检查设备解码支持类型 + */ + @Synchronized + fun checkSupportCodec(mimeType: String): Boolean { + if (!isTypeMapInit) { + isTypeMapInit = true + getSupportType() + } + return supportTypeMap.containsKey(mimeType.lowercase()) + } + + + private fun getSupportType() { + try { + val numCodecs = MediaCodecList.getCodecCount() + for (i in 0 until numCodecs) { + val codecInfo = MediaCodecList.getCodecInfoAt(i) + if (codecInfo.isEncoder) { + continue + } + val types = codecInfo.supportedTypes + for (j in types.indices) { + supportTypeMap[types[j].lowercase()] = true + } + } + ALog.i(TAG, "supportType=${supportTypeMap.keys}") + } catch (t: Throwable) { + ALog.e(TAG, "getSupportType $t") + } + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ScaleTypeUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ScaleTypeUtil.kt new file mode 100644 index 0000000..93d749e --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ScaleTypeUtil.kt @@ -0,0 +1,252 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import com.tencent.qgame.animplayer.Constant + + +enum class ScaleType { + FIT_XY, // 完整填充整个布局 default + FIT_CENTER, // 按视频比例在布局中间完整显示 + CENTER_CROP, // 按视频比例完整填充布局(多余部分不显示) +} + +interface IScaleType { + + fun getLayoutParam( + layoutWidth: Int, + layoutHeight: Int, + videoWidth: Int, + videoHeight: Int, + layoutParams: FrameLayout.LayoutParams + ): FrameLayout.LayoutParams + + fun getRealSize(): Pair +} + +class ScaleTypeFitXY : IScaleType { + + private var realWidth = 0 + private var realHeight = 0 + + override fun getLayoutParam( + layoutWidth: Int, + layoutHeight: Int, + videoWidth: Int, + videoHeight: Int, + layoutParams: FrameLayout.LayoutParams + ): FrameLayout.LayoutParams { + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + realWidth = layoutWidth + realHeight = layoutHeight + return layoutParams + } + + override fun getRealSize(): Pair { + return Pair(realWidth, realHeight) + } +} + +class ScaleTypeFitCenter : IScaleType { + + private var realWidth = 0 + private var realHeight = 0 + + override fun getLayoutParam( + layoutWidth: Int, + layoutHeight: Int, + videoWidth: Int, + videoHeight: Int, + layoutParams: FrameLayout.LayoutParams + ): FrameLayout.LayoutParams { + val (w, h) = getFitCenterSize(layoutWidth, layoutHeight, videoWidth, videoHeight) + if (w <= 0 && h <= 0) return layoutParams + realWidth = w + realHeight = h + layoutParams.width = w + layoutParams.height = h + layoutParams.gravity = Gravity.CENTER + return layoutParams + } + + override fun getRealSize(): Pair { + return Pair(realWidth, realHeight) + } + + private fun getFitCenterSize( + layoutWidth: Int, + layoutHeight: Int, + videoWidth: Int, + videoHeight: Int + ): Pair { + + val layoutRatio = layoutWidth.toFloat() / layoutHeight + val videoRatio = videoWidth.toFloat() / videoHeight + + val realWidth: Int + val realHeight: Int + if (layoutRatio > videoRatio) { + realHeight = layoutHeight + realWidth = (videoRatio * realHeight).toInt() + } else { + realWidth = layoutWidth + realHeight = (realWidth / videoRatio).toInt() + } + + return Pair(realWidth, realHeight) + } +} + +class ScaleTypeCenterCrop : IScaleType { + + private var realWidth = 0 + private var realHeight = 0 + + override fun getLayoutParam( + layoutWidth: Int, + layoutHeight: Int, + videoWidth: Int, + videoHeight: Int, + layoutParams: FrameLayout.LayoutParams + ): FrameLayout.LayoutParams { + val (w, h) = getCenterCropSize(layoutWidth, layoutHeight, videoWidth, videoHeight) + if (w <= 0 && h <= 0) return layoutParams + realWidth = w + realHeight = h + layoutParams.width = w + layoutParams.height = h + layoutParams.gravity = Gravity.CENTER + return layoutParams + } + + override fun getRealSize(): Pair { + return Pair(realWidth, realHeight) + } + + private fun getCenterCropSize( + layoutWidth: Int, + layoutHeight: Int, + videoWidth: Int, + videoHeight: Int + ): Pair { + + val layoutRatio = layoutWidth.toFloat() / layoutHeight + val videoRatio = videoWidth.toFloat() / videoHeight + + val realWidth: Int + val realHeight: Int + if (layoutRatio > videoRatio) { + realWidth = layoutWidth + realHeight = (realWidth / videoRatio).toInt() + } else { + realHeight = layoutHeight + realWidth = (videoRatio * realHeight).toInt() + } + + return Pair(realWidth, realHeight) + } +} + + +class ScaleTypeUtil { + + companion object { + private const val TAG = "${Constant.TAG}.ScaleTypeUtil" + } + + private val scaleTypeFitXY by lazy { ScaleTypeFitXY() } + private val scaleTypeFitCenter by lazy { ScaleTypeFitCenter() } + private val scaleTypeCenterCrop by lazy { ScaleTypeCenterCrop() } + private var layoutWidth = 0 + private var layoutHeight = 0 + private var videoWidth = 0 + private var videoHeight = 0 + + var currentScaleType = ScaleType.FIT_XY + var scaleTypeImpl: IScaleType? = null + + fun setLayoutSize(w: Int, h: Int) { + layoutWidth = w + layoutHeight = h + } + + fun setVideoSize(w: Int, h: Int) { + videoWidth = w + videoHeight = h + } + + /** + * 获取实际视频容器宽高 + * @return w h + */ + fun getRealSize(): Pair { + val size = getCurrentScaleType().getRealSize() + ALog.i(TAG, "get real size (${size.first}, ${size.second})") + return size + } + + fun getLayoutParam(view: View?): FrameLayout.LayoutParams { + val layoutParams = (view?.layoutParams as? FrameLayout.LayoutParams) + ?: FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + if (!checkParams()) { + ALog.e( + TAG, + "params error: layoutWidth=$layoutWidth, layoutHeight=$layoutHeight, videoWidth=$videoWidth, videoHeight=$videoHeight" + ) + return layoutParams + } + + return getCurrentScaleType().getLayoutParam( + layoutWidth, + layoutHeight, + videoWidth, + videoHeight, + layoutParams + ) + } + + private fun getCurrentScaleType(): IScaleType { + val tmpScaleType = scaleTypeImpl + return if (tmpScaleType != null) { + ALog.i(TAG, "custom scaleType") + tmpScaleType + } else { + ALog.i(TAG, "scaleType=$currentScaleType") + when (currentScaleType) { + ScaleType.FIT_XY -> scaleTypeFitXY + ScaleType.FIT_CENTER -> scaleTypeFitCenter + ScaleType.CENTER_CROP -> scaleTypeCenterCrop + } + } + } + + + private fun checkParams(): Boolean { + return layoutWidth > 0 + && layoutHeight > 0 + && videoWidth > 0 + && videoHeight > 0 + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ShaderUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ShaderUtil.kt new file mode 100644 index 0000000..20e0118 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/ShaderUtil.kt @@ -0,0 +1,74 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import android.opengl.GLES20 +import com.tencent.qgame.animplayer.Constant + +object ShaderUtil { + private const val TAG = "${Constant.TAG}.ShaderUtil" + + + fun createProgram(vertexSource: String, fragmentSource: String): Int { + val vertexShaderHandle = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource) + val fragmentShaderHandle = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource) + return createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle) + } + + + private fun compileShader(shaderType: Int, shaderSource: String): Int { + var shaderHandle = GLES20.glCreateShader(shaderType) + + if (shaderHandle != 0) { + GLES20.glShaderSource(shaderHandle, shaderSource) + GLES20.glCompileShader(shaderHandle) + val compileStatus = IntArray(1) + GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0) + if (compileStatus[0] == 0) { + ALog.e(TAG, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shaderHandle)) + GLES20.glDeleteShader(shaderHandle) + shaderHandle = 0 + } + } + if (shaderHandle == 0) { + throw RuntimeException("Error creating shader.") + } + return shaderHandle + } + + + private fun createAndLinkProgram(vertexShaderHandle: Int, fragmentShaderHandle: Int): Int { + var programHandle = GLES20.glCreateProgram() + + if (programHandle != 0) { + GLES20.glAttachShader(programHandle, vertexShaderHandle) + GLES20.glAttachShader(programHandle, fragmentShaderHandle) + GLES20.glLinkProgram(programHandle) + val linkStatus = IntArray(1) + GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0) + if (linkStatus[0] == 0) { + ALog.e(TAG, "Error compiling program: " + GLES20.glGetProgramInfoLog(programHandle)) + GLES20.glDeleteProgram(programHandle) + programHandle = 0 + } + } + if (programHandle == 0) { + throw RuntimeException("Error creating program.") + } + return programHandle + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/SpeedControlUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/SpeedControlUtil.kt new file mode 100644 index 0000000..c5912e9 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/SpeedControlUtil.kt @@ -0,0 +1,81 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import com.tencent.qgame.animplayer.Constant + +class SpeedControlUtil { + + companion object { + private const val TAG = "${Constant.TAG}.SpeedControlUtil" + } + private val ONE_MILLION = 1000000L + + private var prevPresentUsec: Long = 0 + private var prevMonoUsec: Long = 0 + private var fixedFrameDurationUsec: Long = 0 + private var loopReset = true + + fun setFixedPlaybackRate(fps: Int) { + if (fps <=0) return + fixedFrameDurationUsec = ONE_MILLION / fps + } + + fun preRender(presentationTimeUsec: Long) { + if (prevMonoUsec == 0L) { + prevMonoUsec = System.nanoTime() / 1000 + prevPresentUsec = presentationTimeUsec + } else { + var frameDelta: Long + if (loopReset) { + prevPresentUsec = presentationTimeUsec - ONE_MILLION / 30 + loopReset = false + } + frameDelta = if (fixedFrameDurationUsec != 0L) { + fixedFrameDurationUsec + } else { + presentationTimeUsec - prevPresentUsec + } + when { + frameDelta < 0 -> frameDelta = 0 + frameDelta > 10 * ONE_MILLION -> frameDelta = 5 * ONE_MILLION + } + + val desiredUsec = prevMonoUsec + frameDelta + var nowUsec = System.nanoTime() / 1000 + while (nowUsec < desiredUsec - 100 ) { + var sleepTimeUsec = desiredUsec - nowUsec + if (sleepTimeUsec > 500000) { + sleepTimeUsec = 500000 + } + try { + Thread.sleep(sleepTimeUsec / 1000, (sleepTimeUsec % 1000).toInt() * 1000) + } catch (e: InterruptedException) { + ALog.e(TAG, "e=$e", e) + } + nowUsec = System.nanoTime() / 1000 + } + + prevMonoUsec += frameDelta + prevPresentUsec += frameDelta + } + } + + fun reset() { + prevPresentUsec = 0 + prevMonoUsec = 0 + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/TexCoordsUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/TexCoordsUtil.kt new file mode 100644 index 0000000..ee015e7 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/TexCoordsUtil.kt @@ -0,0 +1,84 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import com.tencent.qgame.animplayer.PointRect + +/** + * 纹理坐标工具 + * 坐标顺序是倒N + */ +object TexCoordsUtil { + + /** + * @param width 纹理的宽高 + * @param height + */ + fun create(width: Int, height: Int, rect: PointRect, array: FloatArray): FloatArray { + + // x0 + array[0] = rect.x.toFloat() / width + // y0 + array[1] = rect.y.toFloat() / height + + // x1 + array[2] = rect.x.toFloat() / width + // y1 + array[3] = (rect.y.toFloat() + rect.h) / height + + // x2 + array[4] = (rect.x.toFloat() + rect.w) / width + // y2 + array[5] = rect.y.toFloat() / height + + // x3 + array[6] = (rect.x.toFloat() + rect.w) / width + // y3 + array[7] = (rect.y.toFloat() + rect.h) / height + + return array + } + + + + + /** + * 顺时针90度 + */ + fun rotate90(array: FloatArray): FloatArray { + // 0->2 1->0 3->1 2->3 + val tx = array[0] + val ty = array[1] + + // 1->0 + array[0] = array[2] + array[1] = array[3] + + // 3->1 + array[2] = array[6] + array[3] = array[7] + + // 2->3 + array[6] = array[4] + array[7] = array[5] + + // 0->2 + array[4] = tx + array[5] = ty + return array + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/TextureLoadUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/TextureLoadUtil.kt new file mode 100644 index 0000000..1918498 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/TextureLoadUtil.kt @@ -0,0 +1,58 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import android.graphics.Bitmap +import android.opengl.GLES20 +import android.opengl.GLUtils + +object TextureLoadUtil { + private const val TAG = "TextureUtil" + fun loadTexture(bitmap: Bitmap?): Int { + val textureObjectIds = IntArray(1) + GLES20.glGenTextures(1, textureObjectIds, 0) + + if (textureObjectIds[0] == 0) { + return 0 + } + + if (bitmap == null) { + GLES20.glDeleteTextures(1, textureObjectIds, 0) + return 0 + } + + if (bitmap.isRecycled) { + ALog.e(TAG, "bitmap isRecycled") + return 0 + } + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR) + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0) + GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) + + return textureObjectIds[0] + } + + + fun releaseTexure(textureId: Int) { + if (textureId != 0) { + GLES20.glDeleteTextures(1, intArrayOf(textureId), 0) + } + } +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/VertexUtil.kt b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/VertexUtil.kt new file mode 100644 index 0000000..80e8304 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/android/src/main/kotlin/com/laskarmedia/vap/animplayer/util/VertexUtil.kt @@ -0,0 +1,64 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 Tencent. All rights reserved. + * + * Licensed under the MIT License (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://opensource.org/licenses/MIT + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tencent.qgame.animplayer.util + +import com.tencent.qgame.animplayer.PointRect + +/** + * 顶点坐标工具 + * 坐标顺序是倒N + */ +object VertexUtil { + + /** + * @param width 画布大大小 + * @param height + */ + fun create(width: Int, height: Int, rect: PointRect, array: FloatArray): FloatArray { + + // x0 + array[0] = switchX(rect.x.toFloat() / width) + // y0 + array[1] = switchY(rect.y.toFloat() / height) + + // x1 + array[2] = switchX(rect.x.toFloat() / width) + // y1 + array[3] = switchY((rect.y.toFloat() + rect.h) / height) + + // x2 + array[4] = switchX((rect.x.toFloat() + rect.w) / width) + // y2 + array[5] = switchY(rect.y.toFloat() / height) + + // x3 + array[6] = switchX((rect.x.toFloat() + rect.w) / width) + // y3 + array[7] = switchY((rect.y.toFloat() + rect.h) / height) + + return array + } + + + private fun switchX(x: Float): Float { + return x * 2f -1f + } + + private fun switchY(y: Float): Float { + return ((y * 2f - 2f) * -1f) - 1f + } + +} \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/example/README.md b/local_packages/tancent_vap-1.0.0+1/example/README.md new file mode 100644 index 0000000..4158a11 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/README.md @@ -0,0 +1,16 @@ +# vap_example + +Demonstrates how to use the vap plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/local_packages/tancent_vap-1.0.0+1/example/analysis_options.yaml b/local_packages/tancent_vap-1.0.0+1/example/analysis_options.yaml new file mode 100644 index 0000000..d4e0f0c --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/checksums/checksums.lock b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/checksums/checksums.lock new file mode 100644 index 0000000..898cb45 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/checksums/checksums.lock differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/fileChanges/last-build.bin b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/fileChanges/last-build.bin differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/fileHashes/fileHashes.lock b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/fileHashes/fileHashes.lock new file mode 100644 index 0000000..71fe201 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/fileHashes/fileHashes.lock differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/gc.properties b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/8.12/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..0c16e7e Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/buildOutputCleanup/cache.properties b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..f4ff9a5 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Thu Apr 09 19:24:30 CST 2026 +gradle.version=8.12 diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/vcs-1/gc.properties b/local_packages/tancent_vap-1.0.0+1/example/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/build.gradle.kts b/local_packages/tancent_vap-1.0.0+1/example/android/app/build.gradle.kts new file mode 100644 index 0000000..29a2b8e --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.laskarmedia.vap_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.laskarmedia.vap_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/debug/AndroidManifest.xml b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/AndroidManifest.xml b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..945b021 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/kotlin/com/laskarmedia/vap_example/MainActivity.kt b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/kotlin/com/laskarmedia/vap_example/MainActivity.kt new file mode 100644 index 0000000..5528d6a --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/kotlin/com/laskarmedia/vap_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.laskarmedia.vap_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/drawable-v21/launch_background.xml b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..1cb7aa2 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/drawable/launch_background.xml b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..8403758 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/values-night/styles.xml b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..360a160 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/values/styles.xml b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5fac679 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/app/src/profile/AndroidManifest.xml b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/build.gradle.kts b/local_packages/tancent_vap-1.0.0+1/example/android/build.gradle.kts new file mode 100644 index 0000000..2f2f3f0 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/gradle.properties b/local_packages/tancent_vap-1.0.0+1/example/android/gradle.properties new file mode 100644 index 0000000..b7cda7b --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/gradle/wrapper/gradle-wrapper.properties b/local_packages/tancent_vap-1.0.0+1/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..dcc7e10 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/local_packages/tancent_vap-1.0.0+1/example/android/settings.gradle.kts b/local_packages/tancent_vap-1.0.0+1/example/android/settings.gradle.kts new file mode 100644 index 0000000..8ddb35d --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/local_packages/tancent_vap-1.0.0+1/example/assets/user.png b/local_packages/tancent_vap-1.0.0+1/example/assets/user.png new file mode 100644 index 0000000..ce1761e Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/assets/user.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/assets/vapx.mp4 b/local_packages/tancent_vap-1.0.0+1/example/assets/vapx.mp4 new file mode 100644 index 0000000..76f3923 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/assets/vapx.mp4 differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/AppFrameworkInfo.plist b/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..e041d38 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/Debug.xcconfig b/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..553cfac --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/Release.xcconfig b/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..4b6e1f6 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Podfile b/local_packages/tancent_vap-1.0.0+1/example/ios/Podfile new file mode 100644 index 0000000..cce7e8b --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + 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/local_packages/tancent_vap-1.0.0+1/example/ios/Podfile.lock b/local_packages/tancent_vap-1.0.0+1/example/ios/Podfile.lock new file mode 100644 index 0000000..a3f3d6e --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - QGVAPlayer (1.0.19) + - tancent_vap (0.0.1): + - Flutter + - QGVAPlayer (= 1.0.19) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - tancent_vap (from `.symlinks/plugins/tancent_vap/ios`) + +SPEC REPOS: + trunk: + - QGVAPlayer + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + tancent_vap: + :path: ".symlinks/plugins/tancent_vap/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + QGVAPlayer: a0bca68c9bd6f1c8de5ac2d10ddf98be6038cce9 + tancent_vap: 4917210cc7e916023fd112655170d8e3d074f482 + +PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 + +COCOAPODS: 1.16.2 diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.pbxproj b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a343921 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 6F8ADB862F8E60C4AB97DA28 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 566311037EC0CAAFB24687F5 /* Pods_Runner.framework */; }; + 744984D6F56DEB89DB72B30F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C4817119AE4459FC02DD87C /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* 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 */ + 062EE43FDE538D0CC0FD8B46 /* 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 = ""; }; + 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 = ""; }; + 3C4817119AE4459FC02DD87C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 464B48BA098671256B7AABFD /* 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 = ""; }; + 566311037EC0CAAFB24687F5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 58C4B2D487313B2935432E1C /* 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 = ""; }; + 59B6F0786928D4354B29DB32 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 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 = ""; }; + D11162BBEA7C33570402D418 /* 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 = ""; }; + D55AA79CB14BDC498FC7B2EE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6F8ADB862F8E60C4AB97DA28 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D9E91B65B0EC3E54A4F18446 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 744984D6F56DEB89DB72B30F /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 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 */, + E3430BFEF77AD2B92D3746DF /* Pods */, + A56E98C5D62987F53044077A /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + A56E98C5D62987F53044077A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 566311037EC0CAAFB24687F5 /* Pods_Runner.framework */, + 3C4817119AE4459FC02DD87C /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + E3430BFEF77AD2B92D3746DF /* Pods */ = { + isa = PBXGroup; + children = ( + D11162BBEA7C33570402D418 /* Pods-Runner.debug.xcconfig */, + 062EE43FDE538D0CC0FD8B46 /* Pods-Runner.release.xcconfig */, + D55AA79CB14BDC498FC7B2EE /* Pods-Runner.profile.xcconfig */, + 58C4B2D487313B2935432E1C /* Pods-RunnerTests.debug.xcconfig */, + 464B48BA098671256B7AABFD /* Pods-RunnerTests.release.xcconfig */, + 59B6F0786928D4354B29DB32 /* 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 = ( + DFC471707750343963518BE1 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + D9E91B65B0EC3E54A4F18446 /* 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 = ( + 2F8FE29D194725917C994C4B /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 59EDB7A442327DC29F0223C4 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2F8FE29D194725917C994C4B /* [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"; + }; + 59EDB7A442327DC29F0223C4 /* [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; + }; + 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"; + }; + DFC471707750343963518BE1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = NAFNLUB99G; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.laskarmedia.vapExample; + 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 = 58C4B2D487313B2935432E1C /* 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.laskarmedia.vapExample.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 = 464B48BA098671256B7AABFD /* 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.laskarmedia.vapExample.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 = 59B6F0786928D4354B29DB32 /* 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.laskarmedia.vapExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = NAFNLUB99G; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.laskarmedia.vapExample; + 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_APPat_icon_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = NAFNLUB99G; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.laskarmedia.vapExample; + 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/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..c4b79bd --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..fc6bf80 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..af0309c --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..bbabc4e --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..17ccc03 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..fc6bf80 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..af0309c --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/AppDelegate.swift b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..8be1cec --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..1950fd8 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..d08a4de --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..65a94b5 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..497371e --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Base.lproj/Main.storyboard b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..bbb83ca --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Info.plist b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Info.plist new file mode 100644 index 0000000..479413b --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Vap + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + vap_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Runner-Bridging-Header.h b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..fae207f --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/local_packages/tancent_vap-1.0.0+1/example/ios/RunnerTests/RunnerTests.swift b/local_packages/tancent_vap-1.0.0+1/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..ffb4fa9 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,27 @@ +import Flutter +import UIKit +import XCTest + + +@testable import vap + +// This demonstrates a simple unit test of the Swift portion of this plugin's implementation. +// +// See https://developer.apple.com/documentation/xctest for more information about using XCTest. + +class RunnerTests: XCTestCase { + + func testGetPlatformVersion() { + let plugin = VapPlugin() + + let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) + + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) + resultExpectation.fulfill() + } + waitForExpectations(timeout: 1) + } + +} diff --git a/local_packages/tancent_vap-1.0.0+1/example/lib/main.dart b/local_packages/tancent_vap-1.0.0+1/example/lib/main.dart new file mode 100644 index 0000000..e209a65 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/lib/main.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:tancent_vap/tancent_vap.dart'; + +main() { + runApp(MaterialApp(home: Application())); +} + +class Application extends StatefulWidget { + const Application({super.key}); + + @override + State createState() => _ApplicationState(); +} + +class _ApplicationState extends State { + late VapController controller; + final TextEditingController textController = TextEditingController(); + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + spacing: 20, + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: textController, + decoration: InputDecoration( + labelText: "Enter text to replace [sTxt1]", + border: OutlineInputBorder(), + ), + ), + ElevatedButton( + onPressed: () { + controller.setVapTagContent( + "[sTxt1]", TextContent(textController.text)); + controller.playAsset("assets/vapx.mp4"); + }, + child: Text("Play Animation"), + ), + ], + ), + ), + ), + + // IgnorePointer to prevent user interaction with the VapView + // This is useful if you want to display the animation without allowing user interaction. + // You can remove this IgnorePointer if you want the VapView to be interactive. + IgnorePointer( + child: VapView( + repeat: 0, // 0 means play once, -1 means loop infinitely + mute: true, // Mute the audio + scaleType: ScaleType.centerCrop, // Scale type for the video + vapTagContents: { + "[sTxt1]": TextContent("Nizwar win the game!"), + "[sImg1]": ImageAssetContent("assets/user.png"), + }, + onViewCreated: (ctl) async { + controller = ctl; + }, + ), + ), + ], + ), + ); + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/example/pubspec.yaml b/local_packages/tancent_vap-1.0.0+1/example/pubspec.yaml new file mode 100644 index 0000000..9ff8516 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: vap_example +description: "Demonstrates how to use the vap plugin." + +publish_to: 'none' + +environment: + sdk: ^3.6.0 + +dependencies: + flutter: + sdk: flutter + + tancent_vap: + path: ../ + + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true + assets: + - assets/ diff --git a/local_packages/tancent_vap-1.0.0+1/ios/Classes/Header.h b/local_packages/tancent_vap-1.0.0+1/ios/Classes/Header.h new file mode 100644 index 0000000..56f3b36 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/ios/Classes/Header.h @@ -0,0 +1 @@ + diff --git a/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapPlugin.swift b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapPlugin.swift new file mode 100644 index 0000000..2e3cb8c --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapPlugin.swift @@ -0,0 +1,18 @@ +import Flutter +import UIKit + +/** VapPlugin */ +public class VapPlugin: NSObject, FlutterPlugin { + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = VapPlugin() + instance.onAttachedToEngine(registrar: registrar) + } + + private func onAttachedToEngine(registrar: FlutterPluginRegistrar) { + // Register the platform view factory for VapView + let factory = VapViewFactory(messenger: registrar.messenger()) + registrar.register(factory, withId: "vap_view") + } + +} diff --git a/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift new file mode 100644 index 0000000..bdf2b8d --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapView.swift @@ -0,0 +1,723 @@ +import Flutter +import UIKit +import QGVAPlayer +import Darwin.Mach + +public class VapFlutterView: NSObject, FlutterPlatformView { + private let _view: UIView + private let channel: FlutterMethodChannel + private var vapView: QGVAPWrapView? + private var repeatCount: Int = 0 + private var playResult: FlutterResult? + private var vapTagContents: [String: [String: Any]] = [:] + + + init( + context: CGRect, + params: [String: Any]?, + messenger: FlutterBinaryMessenger, + id: Int64 + ) { + _view = UIView(frame: context) + channel = FlutterMethodChannel(name: "vap_view_\(id)", binaryMessenger: messenger) + + super.init() + + // Initialize VAP view with proper configuration + vapView = QGVAPWrapView(frame: context) + + vapView?.center = _view.center + // Set scaleType from params (matching Kotlin logic) + if let scaleType = params?["scaleType"] as? String { + switch scaleType { + case "fitCenter": + vapView?.contentMode = .aspectFit + break + case "centerCrop": + vapView?.contentMode = .aspectFill + break + case "fitXY": + vapView?.contentMode = .scaleToFill + break + default: + vapView?.contentMode = .aspectFit + break + } + } else { + // Default scale type (matching Kotlin's default) + vapView?.contentMode = .aspectFit + } + + // Configure background color to prevent visual glitches + vapView?.backgroundColor = UIColor.clear + + _view.backgroundColor = UIColor.clear + + channel.setMethodCallHandler(onMethodCall) + + // Add vapView to container + if let vapView = vapView { + vapView.translatesAutoresizingMaskIntoConstraints = false + _view.addSubview(vapView) + + // Set up auto layout constraints + NSLayoutConstraint.activate([ + vapView.topAnchor.constraint(equalTo: _view.topAnchor), + vapView.leadingAnchor.constraint(equalTo: _view.leadingAnchor), + vapView.trailingAnchor.constraint(equalTo: _view.trailingAnchor), + vapView.bottomAnchor.constraint(equalTo: _view.bottomAnchor) + ]) + } + + // Initial playback if filePath or assetName is provided (matching Kotlin) + if let filePath = params?["filePath"] as? String { + playFile(filePath) + } + if let assetName = params?["assetName"] as? String { + playAsset(assetName) + } + if let loop = params?["loop"] as? Int { + setLoop(loop) + } + if let mute = params?["mute"] as? Bool { + setMute(mute) + } + + // Store tag contents as Maps + if let tagContents = params?["vapTagContents"] as? [String: [String: Any]] { + for (tag, content) in tagContents { + vapTagContents[tag] = content + NSLog("Stored tag content for tag: \(tag), contentType: \(content["contentType"] ?? "unknown")") + } + } + } + + public func view() -> UIView { + return _view + } + + private func reset() { + // Ensure cleanup happens on main thread + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + clearVapTagContents() + + // Stop VAP playback and clean up resources + self.vapView?.stopHWDMP4() + + // Give a moment for VideoToolbox to clean up + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + // Remove all subviews + self._view.subviews.forEach { $0.removeFromSuperview() } + + // Clear the VAP view reference + self.vapView = nil + + // Force garbage collection hint + if #available(iOS 13.0, *) { + // Modern iOS versions handle this automatically + } else { + // For older iOS versions, hint at memory cleanup + DispatchQueue.global(qos: .background).async { + // Trigger background cleanup + } + } + } + } + } + + private func dispose() { + reset() + channel.setMethodCallHandler(nil) + } + + private func onMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "dispose": + dispose() + result(nil) + + case "playFile": + if let args = call.arguments as? [String: Any], + let filePath = args["filePath"] as? String { + playFile(filePath, result) + } + + case "playAsset": + if let args = call.arguments as? [String: Any], + let assetName = args["assetName"] as? String { + playAsset(assetName, result) + } + + case "stop": + stop() + result(nil) + + case "setLoop": + if let args = call.arguments as? [String: Any], + let loop = args["loop"] as? Int { + setLoop(loop) + } else { + setLoop(0) + } + result(nil) + + case "setMute": + if let args = call.arguments as? [String: Any], + let mute = args["mute"] as? Bool { + setMute(mute) + } else { + setMute(false) + } + result(nil) + + case "setScaleType": + if let args = call.arguments as? [String: Any], + let scaleType = args["scaleType"] as? String { + setScaleType(scaleType) + } + result(nil) + + case "setVapTagContent": + if let args = call.arguments as? [String: Any], + let tag = args["tag"] as? String, + let contentMap = args["content"] as? [String: Any] { + setVapTagContent(tag: tag, contentMap: contentMap) + } + result(nil) + + case "setVapTagContents": + if let args = call.arguments as? [String: Any], + let contents = args["contents"] as? [String: [String: Any]] { + setVapTagContents(contents) + } + result(nil) + + case "getVapTagContent": + if let args = call.arguments as? [String: Any], + let tag = args["tag"] as? String { + result(getVapTagContent(tag: tag)) + } else { + result(nil) + } + + case "getAllVapTagContents": + result(vapTagContents) + + case "clearVapTagContents": + clearVapTagContents() + result(nil) + + default: + result(FlutterMethodNotImplemented) + } + } + + private func playFile(_ filePath: String, _ result: FlutterResult? = nil) { + // Ensure we're on the main thread for UI operations + self.playResult = result + DispatchQueue.main.async { [weak self] in + guard let self = self, let vapView = self.vapView else { return } + + // Stop any existing playback first + vapView.stopHWDMP4() + + // Configure VAP player with better error handling + do { + guard FileManager.default.fileExists(atPath: filePath) else { + self.sendFailedEvent(errorCode: -1, errorType: "FILE_NOT_FOUND", errorMsg: "VAP file not found: \(filePath)") + return + } + + // Check file size (VAP files shouldn't be too large for memory) + let fileAttributes = try FileManager.default.attributesOfItem(atPath: filePath) + if let fileSize = fileAttributes[.size] as? NSNumber { + let fileSizeInMB = fileSize.doubleValue / (1024 * 1024) + // Reject extremely large files that will definitely cause issues + if fileSizeInMB > 100 { + self.sendFailedEvent(errorCode: -1006, errorType: "FILE_TOO_LARGE", errorMsg: "VAP file too large (\(fileSizeInMB) MB), maximum size is 100MB") + playError(-1006, "FILE_TOO_LARGE", "VAP file too large (\(fileSizeInMB) MB), maximum size is 100MB") + return + } + } + // Start playback with delegate + vapView.playHWDMP4(filePath, repeatCount: self.repeatCount, delegate: self) + + + } catch { + self.sendFailedEvent(errorCode: -2, errorType: "FILE_PLAYBACK_ERROR", errorMsg: "Failed to play VAP file: \(error.localizedDescription)") + } + } + } + + private func playAsset(_ assetName: String, _ result: FlutterResult? = nil) { + let key = FlutterDartProject.lookupKey(forAsset: assetName) + if let bundlePath = Bundle.main.path(forResource: key, ofType: nil) { + playFile(bundlePath) + } else { + sendFailedEvent(errorCode: -1, errorType: "FILE_NOT_FOUND", errorMsg: "Asset not found: \(assetName)") + } + } + + private func stop() { + vapView?.stopHWDMP4() + DispatchQueue.main.async { [weak self] in + self?.channel.invokeMethod("onVideoDestroy", arguments: nil) + } + } + + private func setLoop(_ loop: Int) { + repeatCount = loop + } + + private func setMute(_ mute: Bool) { + vapView?.setMute(mute) + } + + private func setScaleType(_ scaleType: String) { + switch scaleType { + case "fitCenter": + vapView?.contentMode = .aspectFit + break + case "centerCrop": + vapView?.contentMode = .aspectFill + break + case "fitXY": + vapView?.contentMode = .scaleToFill + break + default: + vapView?.contentMode = .aspectFit + break + } + } + + // MARK: - VAP Tag Content Management + private func setVapTagContent(tag: String, contentMap: [String: Any]) { + vapTagContents[tag] = contentMap + } + + private func setVapTagContents(_ contents: [String: [String: Any]]) { + vapTagContents.merge(contents) { (_, new) in new } + } + + private func getVapTagContent(tag: String) -> String? { + guard let contentMap = vapTagContents[tag], + let contentValue = contentMap["contentValue"] as? String else { + return nil + } + return contentValue + } + + private func clearVapTagContents() { + vapTagContents.removeAll() + } + + // MARK: - Event Handling + private func sendFailedEvent(errorCode: Int, errorType: String, errorMsg: String?) { + let args: [String: Any?] = [ + "errorCode": errorCode, + "errorType": errorType, + "errorMsg": errorMsg + ] + DispatchQueue.main.async { [weak self] in + self?.channel.invokeMethod("onFailed", arguments: args) + } + } + + private func sendVideoCompleteEvent() { + DispatchQueue.main.async { [weak self] in + self?.channel.invokeMethod("onVideoComplete", arguments: nil) + } + } + + private func sendVideoRenderEvent(frameIndex: Int, config: [String: Any]?) { + let args: [String: Any?] = [ + "frameIndex": frameIndex, + "config": config + ] + DispatchQueue.main.async { [weak self] in + self?.channel.invokeMethod("onVideoRender", arguments: args) + } + } + + private func sendVideoConfigReadyEvent(config: [String: Any]) { + let args = ["config": config] + DispatchQueue.main.async { [weak self] in + self?.channel.invokeMethod("onVideoConfigReady", arguments: args) + } + } + + // MARK: - Objective-C 兼容性桥接方法 + @objc public func vapWrap_viewDidFinishPlayMP4(_ totalFrameCount: Int, view: UIView) { + // 注意:参数 `view` 是普通的 UIView,不是 QGVAPWrapView + // 我们直接调用已有方法,并使用 self.vapView 作为参数 + if let vapView = self.vapView { + self.viewDidFinishPlayMP4(totalFrameCount, view: vapView) + } + } + + @objc public func vapWrap_viewDidStartPlayMP4(_ totalFrameCount: Int, view: UIView) { + // 你原来的 viewDidStartPlayMP4 方法只接受一个 QGVAPWrapView 参数 + // 忽略 totalFrameCount 参数,只传递 vapView + if let vapView = self.vapView { + self.viewDidStartPlayMP4(vapView) + } + } + + @objc public func vapWrap_viewDidStopPlayMP4(_ frameIndex: Int, view: UIView) { + if let vapView = self.vapView { + self.viewDidStopPlayMP4(frameIndex, view: vapView) + } + } + + @objc public func vapWrap_onVAPStopWithError(_ error: NSError?, view: UIView) { + // viewDidFailPlayMP4 方法只需要 error 参数 + self.viewDidFailPlayMP4(error ?? NSError(domain: "VAP", code: -1, userInfo: nil)) + } +} + +// MARK: - HWDMP4PlayDelegate +extension VapFlutterView: VAPWrapViewDelegate { + + func shouldStartPlayMP4(_ container: QGVAPWrapView, config: QGVAPConfigModel) -> Bool { + var animConfigMap: [String: Any] = [:] + animConfigMap["width"] = config.info.size.width + animConfigMap["height"] = config.info.size.height + animConfigMap["fps"] = config.info.fps + animConfigMap["totalFrames"] = config.info.framesCount + animConfigMap["videoHeight"] = config.info.videoSize.height + animConfigMap["videoWidth"] = config.info.videoSize.width + animConfigMap["isMix"] = config.info.isMerged + animConfigMap["orien"] = config.info.targetOrientaion.rawValue + animConfigMap["alphaPointRect"] = [ + "x": config.info.alphaAreaRect.minX, + "y": config.info.alphaAreaRect.minY, + "w": config.info.alphaAreaRect.maxX, + "h": config.info.alphaAreaRect.maxY + ] + animConfigMap["rgbPointRect"] = [ + "x": config.info.rgbAreaRect.minX, + "y": config.info.rgbAreaRect.minY, + "w": config.info.rgbAreaRect.maxX, + "h": config.info.rgbAreaRect.maxY + ] + animConfigMap["version"] = config.info.version + sendVideoConfigReadyEvent(config: animConfigMap) + return true + } + + func viewDidStartPlayMP4(_ container: QGVAPWrapView) { + + DispatchQueue.main.async { [weak self] in + self?.channel.invokeMethod("onVideoStart", arguments: nil) + } + } + + func viewDidPlayMP4AtFrame(_ frame: QGMP4AnimatedImageFrame, view container: QGVAPWrapView) { + let args: [String: Any] = ["frameIndex": frame.index] + DispatchQueue.main.async { [weak self] in + self?.channel.invokeMethod("onVideoRender", arguments: args) + } + } + + func viewDidStopPlayMP4(_ lastFrameIndex: Int, view container: QGVAPWrapView) { + playSuccess() + DispatchQueue.main.async { [weak self] in + self?.channel.invokeMethod("onVideoDestroy", arguments: nil) + } + } + + func viewDidFinishPlayMP4(_ totalFrameCount: Int, view container: QGVAPWrapView) { + playSuccess() + sendVideoCompleteEvent() + } + + func viewDidFailPlayMP4(_ error: NSError) { + // Handle specific VideoToolbox errors + var errorMsg = error.localizedDescription + var errorCode = error.code + + // Check for common VideoToolbox errors + if error.domain == "com.apple.videotoolbox" || error.domain.contains("VT") { + switch error.code { + case -12909: // kVTVideoDecoderBadDataErr + errorMsg = "Invalid or corrupted video data. Please check your VAP file encoding." + errorCode = -1001 + case -12911: // kVTVideoDecoderMalfunctionErr + errorMsg = "Video decoder malfunction. Try restarting the app." + errorCode = -1002 + case -12912: // kVTVideoDecoderNotAvailableNowErr + errorMsg = "Video decoder not available. Device may be under memory pressure." + errorCode = -1003 + case -12913: // kVTInvalidSessionErr + errorMsg = "Invalid video session. Please try playing the file again." + errorCode = -1004 + default: + errorMsg = "VideoToolbox error: \(error.localizedDescription)" + errorCode = -1000 + } + } + playError(errorCode, "VIDEO_PLAYBACK_ERROR", errorMsg) + sendFailedEvent(errorCode: errorCode, errorType:"VIDEO_PLAYBACK_ERROR", errorMsg: errorMsg) + } + + func playError(_ code:Int, _ errorType:String, _ errorMsg:String){ + if(playResult != nil){ + playResult?(FlutterError(code: String(code),message: errorMsg,details: ["errorType": errorType])) + playResult = nil + } + } + + func playSuccess(){ + if(playResult != nil){ + playResult?(nil) + playResult = nil + } + } + // MARK: - Resource Management Delegate Methods + public func vapWrapview_content(forVapTag tag: String, resource info: QGVAPSourceInfo) -> String { + guard let contentMap = vapTagContents[tag], + let contentValue = contentMap["contentValue"] as? String else { + return tag + } + + // If the resource type is text, return the content value + if info.type == .text || info.type == .textStr { + return contentValue + } + + // For other resource types (images, etc.), return the tag + return tag + } + + public func vapWrapView_loadVapImage(withURL urlStr: String, context: [AnyHashable : Any], completion completionBlock: @escaping VAPImageCompletionBlock) { + print("URLNYA : " + urlStr + " INI VAPIMAGECONTENT : " + String(describing: vapTagContents) + "\n") + // First check if we have content for this tag in vapTagContents + if let contentMap = vapTagContents[urlStr], + let contentValue = contentMap["contentValue"] as? String, + let contentType = contentMap["contentType"] as? String { + handleVapTagContent(content: contentValue, contentType: contentType, tag: urlStr, completion: completionBlock) + return + } + + // Fallback to original URL loading if no tag content is found + guard let url = URL(string: urlStr) else { + completionBlock(nil, NSError(domain: "VAPImageLoader", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL and no tag content found"]), urlStr) + return + } + + // Simple URLSession implementation - replace with your preferred image loading library + URLSession.shared.dataTask(with: url) { data, response, error in + DispatchQueue.main.async { + if let error = error { + completionBlock(nil, error as NSError, urlStr) + return + } + + guard let data = data, let image = UIImage(data: data) else { + completionBlock(nil, NSError(domain: "VAPImageLoader", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to create image from data"]), urlStr) + return + } + + completionBlock(image, nil, urlStr) + } + }.resume() + } + + func loadVapImageWithURL(_ urlStr: String, context: [String: Any], completion: @escaping VAPImageCompletionBlock) { + // First check if we have content for this tag in vapTagContents + if let contentMap = vapTagContents[urlStr], + let contentValue = contentMap["contentValue"] as? String, + let contentType = contentMap["contentType"] as? String { + handleVapTagContent(content: contentValue, contentType: contentType, tag: urlStr, completion: completion) + return + } + + // Fallback to original URL loading if no tag content is found + guard let url = URL(string: urlStr) else { + completion(nil, NSError(domain: "VAPImageLoader", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL and no tag content found"]), urlStr) + return + } + + // Simple URLSession implementation - replace with your preferred image loading library + URLSession.shared.dataTask(with: url) { data, response, error in + DispatchQueue.main.async { + if let error = error { + completion(nil, error as NSError, urlStr) + return + } + + guard let data = data, let image = UIImage(data: data) else { + completion(nil, NSError(domain: "VAPImageLoader", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to create image from data"]), urlStr) + return + } + + completion(image, nil, urlStr) + } + }.resume() + } + + private func handleVapTagContent(content: String, contentType: String, tag: String, completion: @escaping VAPImageCompletionBlock) { + NSLog("Processing tag: \(tag), contentType: \(contentType)") + + switch contentType { + case "text": + handleTextContent(content: content, tag: tag, completion: completion) + case "image_base64": + handleImageBase64Content(content: content, tag: tag, completion: completion) + case "image_file": + handleImageFileContent(content: content, tag: tag, completion: completion) + case "image_asset": + handleImageAssetContent(content: content, tag: tag, completion: completion) + case "image_url": + handleImageUrlContent(content: content, tag: tag, completion: completion) + default: + NSLog("Unsupported content type: \(contentType) for tag: \(tag)") + completion(nil, NSError(domain: "VAPImageLoader", code: -4, userInfo: [NSLocalizedDescriptionKey: "Unsupported content type: \(contentType)"]), tag) + } + } + + // MARK: - Content Type Handlers + + private func handleTextContent(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) { + // Text content is not handled by image loading, skip + NSLog("Text content type for tag: \(tag), skipping image loading") + completion(nil, nil, tag) + } + + private func handleImageBase64Content(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) { + DispatchQueue.global(qos: .userInitiated).async { + var base64String = content + + // Remove data URL prefix if present (e.g., "data:image/png;base64,") + if content.hasPrefix("data:image/") { + if let commaRange = content.range(of: ",") { + base64String = String(content[commaRange.upperBound...]) + } + } else if content.hasPrefix("base64:") { + base64String = String(content.dropFirst(7)) // Remove "base64:" prefix + } + + guard let imageData = Data(base64Encoded: base64String), + let image = UIImage(data: imageData) else { + DispatchQueue.main.async { + completion(nil, NSError(domain: "VAPImageLoader", code: -3, userInfo: [NSLocalizedDescriptionKey: "Failed to decode base64 image for tag: \(tag)"]), tag) + } + return + } + + DispatchQueue.main.async { + NSLog("Successfully decoded base64 image for tag: \(tag)") + completion(image, nil, tag) + } + } + } + + private func handleImageFileContent(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) { + DispatchQueue.global(qos: .userInitiated).async { + var fullPath = content + + // Handle different file path types + if content.hasPrefix("/") { + // Absolute path + fullPath = content + } else if content.hasPrefix("file://") { + // File URL + fullPath = String(content.dropFirst(7)) + } else { + // Relative path - check in Documents directory + let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + let documentFilePath = documentsPath + "/" + content + + if FileManager.default.fileExists(atPath: documentFilePath) { + fullPath = documentFilePath + } else { + // Use as-is and let it fail if invalid + fullPath = content + } + } + + guard FileManager.default.fileExists(atPath: fullPath) else { + DispatchQueue.main.async { + completion(nil, NSError(domain: "VAPImageLoader", code: -4, userInfo: [NSLocalizedDescriptionKey: "File not found: \(fullPath) for tag: \(tag)"]), tag) + } + return + } + + guard let image = UIImage(contentsOfFile: fullPath) else { + DispatchQueue.main.async { + completion(nil, NSError(domain: "VAPImageLoader", code: -5, userInfo: [NSLocalizedDescriptionKey: "Failed to load image from file: \(fullPath) for tag: \(tag)"]), tag) + } + return + } + + DispatchQueue.main.async { + NSLog("Successfully loaded file image for tag: \(tag) from: \(fullPath)") + completion(image, nil, tag) + } + } + } + + private func handleImageAssetContent(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) { + DispatchQueue.global(qos: .userInitiated).async { + // Try as Flutter asset first + let assetKey = FlutterDartProject.lookupKey(forAsset: content) + var fullPath: String? + + if let assetPath = Bundle.main.path(forResource: assetKey, ofType: nil) { + fullPath = assetPath + } else if let bundlePath = Bundle.main.path(forResource: content, ofType: nil) { + // Try as direct bundle resource + fullPath = bundlePath + } + + guard let validPath = fullPath, FileManager.default.fileExists(atPath: validPath) else { + DispatchQueue.main.async { + completion(nil, NSError(domain: "VAPImageLoader", code: -4, userInfo: [NSLocalizedDescriptionKey: "Asset not found: \(content) for tag: \(tag)"]), tag) + } + return + } + + guard let image = UIImage(contentsOfFile: validPath) else { + DispatchQueue.main.async { + completion(nil, NSError(domain: "VAPImageLoader", code: -5, userInfo: [NSLocalizedDescriptionKey: "Failed to load asset image: \(content) for tag: \(tag)"]), tag) + } + return + } + + DispatchQueue.main.async { + NSLog("Successfully loaded asset image for tag: \(tag) from: \(validPath)") + completion(image, nil, tag) + } + } + } + + private func handleImageUrlContent(content: String, tag: String, completion: @escaping VAPImageCompletionBlock) { + guard let url = URL(string: content) else { + completion(nil, NSError(domain: "VAPImageLoader", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(content) for tag: \(tag)"]), tag) + return + } + + NSLog("Loading image from URL: \(content) for tag: \(tag)") + + URLSession.shared.dataTask(with: url) { data, response, error in + DispatchQueue.main.async { + if let error = error { + NSLog("Failed to load image from URL: \(content) for tag: \(tag), error: \(error.localizedDescription)") + completion(nil, error as NSError, tag) + return + } + + guard let data = data, let image = UIImage(data: data) else { + completion(nil, NSError(domain: "VAPImageLoader", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to create image from URL data: \(content) for tag: \(tag)"]), tag) + return + } + + NSLog("Successfully loaded image from URL: \(content) for tag: \(tag)") + completion(image, nil, tag) + } + }.resume() + } + +} diff --git a/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapViewFactory.swift b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapViewFactory.swift new file mode 100644 index 0000000..1c4035d --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/ios/Classes/VapViewFactory.swift @@ -0,0 +1,24 @@ +import Flutter +import UIKit + +public class VapViewFactory: NSObject, FlutterPlatformViewFactory { + private let messenger: FlutterBinaryMessenger + + public init(messenger: FlutterBinaryMessenger) { + self.messenger = messenger + super.init() + } + + public func create( + withFrame frame: CGRect, + viewIdentifier viewId: Int64, + arguments args: Any? + ) -> FlutterPlatformView { + let params = args as? [String: Any] + return VapFlutterView(context: frame, params: params, messenger: messenger, id: viewId) + } + + public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return FlutterStandardMessageCodec.sharedInstance() + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/ios/Resources/PrivacyInfo.xcprivacy b/local_packages/tancent_vap-1.0.0+1/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..f8d5a6f --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/local_packages/tancent_vap-1.0.0+1/ios/tancent_vap.podspec b/local_packages/tancent_vap-1.0.0+1/ios/tancent_vap.podspec new file mode 100644 index 0000000..da46c6e --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/ios/tancent_vap.podspec @@ -0,0 +1,35 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint vap.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'tancent_vap' + s.version = '0.0.1' + s.summary = 'A Flutter plugin for VAP (Video Animation Player) integration.' + s.description = <<-DESC +A Flutter plugin that provides VAP (Video Animation Player) integration for iOS and Android. +VAP is a high-performance animation framework developed by Tencent for playing transparent video animations. + DESC + s.homepage = 'https://laskarmedia.id' + s.license = { :file => '../LICENSE' } + s.author = { 'Laskarmedia' => 'support@laskarmedia.id' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '12.0' + s.dependency 'QGVAPlayer', '= 1.0.19' + + + # VAP iOS SDK dependency (users need to add the git source in their Podfile) + # s.dependency 'QGVAPlayer' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + # If your plugin requires a privacy manifest, for example if it uses any + # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your + # plugin's privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'vap_privacy' => ['Resources/PrivacyInfo.xcprivacy']} +end diff --git a/local_packages/tancent_vap-1.0.0+1/lib/tancent_vap.dart b/local_packages/tancent_vap-1.0.0+1/lib/tancent_vap.dart new file mode 100644 index 0000000..02906f1 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/lib/tancent_vap.dart @@ -0,0 +1,27 @@ +/// Tencent VAP Flutter Plugin +/// +/// A Flutter plugin for playing VAP (Video Animation Player) animations from Tencent. +/// VAP is a high-performance video animation format that supports transparent backgrounds +/// and dynamic content injection. +/// +/// This library provides: +/// - [VapView] widget for displaying VAP animations +/// - [VapController] for programmatic control of animations +/// - [VAPContent] classes for dynamic content injection +/// - [VAPConfigs] for animation configuration data +/// +/// Example usage: +/// ```dart +/// VapView( +/// scaleType: ScaleType.fitCenter, +/// repeat: -1, +/// onViewCreated: (controller) { +/// controller.playAsset('animations/sample.mp4'); +/// }, +/// ) +/// ``` +library; + +export 'widgets/vap_view.dart'; +export 'utils/anim_configs.dart'; +export 'utils/constant.dart'; diff --git a/local_packages/tancent_vap-1.0.0+1/lib/utils/anim_configs.dart b/local_packages/tancent_vap-1.0.0+1/lib/utils/anim_configs.dart new file mode 100644 index 0000000..487be10 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/lib/utils/anim_configs.dart @@ -0,0 +1,136 @@ +import 'package:flutter/painting.dart'; +import 'package:tancent_vap/utils/constant.dart'; + +/// Configuration data for VAP animations. +/// +/// VAPConfigs contains all the technical details about a VAP animation file, +/// including dimensions, timing, orientation, and channel information. +/// This data is typically provided by the native VAP player when an animation +/// is loaded and ready to play. +/// +/// The configuration is automatically parsed from native data and provided +/// through the [OnVideoConfigReady] callback. +/// +/// Example usage: +/// ```dart +/// controller.setAnimListener( +/// onVideoConfigReady: (config) { +/// print('Animation size: ${config.width}x${config.height}'); +/// print('FPS: ${config.fps}'); +/// print('Total frames: ${config.totalFrames}'); +/// }, +/// ); +/// ``` +class VAPConfigs { + /// The logical width of the animation in pixels. + final int width; + + /// The logical height of the animation in pixels. + final int height; + + /// Frames per second of the animation. + final int fps; + + /// Total number of frames in the animation. + final int totalFrames; + + /// Whether this is a mixed content animation (has both video and alpha channels). + final bool isMix; + + /// The orientation of the video content. + final VideoOrientation orien; + + /// The actual video height in pixels. + final int videoHeight; + + /// The actual video width in pixels. + final int videoWidth; + + /// Rectangle defining the alpha channel area in the video. + /// Used for transparency information in mixed content animations. + final Rect alphaPointRect; + + /// Rectangle defining the RGB channel area in the video. + /// Contains the actual video content in mixed content animations. + final Rect rgbPointRect; + + /// VAP format version number. + final int version; + + /// Creates a VAPConfigs instance. + /// + /// All parameters are required as they represent essential animation properties. + /// This constructor is typically not used directly - instead, use [VAPConfigs.fromMap] + /// to create instances from native data. + VAPConfigs({ + required this.width, + required this.height, + required this.fps, + required this.totalFrames, + required this.isMix, + required this.orien, + required this.videoHeight, + required this.videoWidth, + required this.alphaPointRect, + required this.rgbPointRect, + required this.version, + }); + + /// Creates a VAPConfigs instance from a map of native data. + /// + /// This factory constructor is used internally to parse configuration data + /// received from the native VAP player. The map should contain all required + /// fields with appropriate types. + /// + /// Parameters: + /// - [json]: Map containing configuration data from native code + /// + /// Throws [TypeError] if required fields are missing or have wrong types. + factory VAPConfigs.fromMap(Map json) { + return VAPConfigs( + width: json['width'] as int, + height: json['height'] as int, + fps: json['fps'] as int, + totalFrames: json['totalFrames'] as int, + videoHeight: json['videoHeight'] as int, + videoWidth: json['videoWidth'] as int, + isMix: json['isMix'] as bool, + orien: VideoOrientation.fromValue(json['orien'] as int), + alphaPointRect: Rect.fromLTRB( + (json['alphaPointRect']["x"] as int).toDouble(), + (json['alphaPointRect']["y"] as int).toDouble(), + (json['alphaPointRect']["w"] as int).toDouble(), + (json['alphaPointRect']["h"] as int).toDouble(), + ), + rgbPointRect: Rect.fromLTRB( + (json['rgbPointRect']["x"] as int).toDouble(), + (json['rgbPointRect']["y"] as int).toDouble(), + (json['rgbPointRect']["w"] as int).toDouble(), + (json['rgbPointRect']["h"] as int).toDouble(), + ), + version: json['version'] as int, + ); + } + + /// Converts this VAPConfigs instance to a map. + /// + /// This method is primarily used for debugging and serialization purposes. + /// The returned map contains all configuration properties with their current values. + /// + /// Returns a map with string keys and dynamic values representing the configuration. + Map toMap() { + return { + 'width': width, + 'height': height, + 'fps': fps, + 'totalFrames': totalFrames, + 'isMix': isMix, + 'orien': orien, + 'videoHeight': videoHeight, + 'videoWidth': videoWidth, + 'alphaPointRect': alphaPointRect, + 'rgbPointRect': rgbPointRect, + 'version': version, + }; + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/lib/utils/constant.dart b/local_packages/tancent_vap-1.0.0+1/lib/utils/constant.dart new file mode 100644 index 0000000..d46c5c4 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/lib/utils/constant.dart @@ -0,0 +1,319 @@ +import 'package:tancent_vap/utils/anim_configs.dart'; + +/// Defines how video content should be scaled within its container. +/// +/// These scale types determine how the VAP animation is displayed when the +/// animation dimensions don't exactly match the widget size. +/// +/// Example usage: +/// ```dart +/// VapView( +/// scaleType: ScaleType.fitCenter, // Maintain aspect ratio, fit within bounds +/// ) +/// ``` +enum ScaleType { + /// Scales the video to fit within the container while maintaining aspect ratio. + /// The entire video will be visible, but there may be empty space if the + /// aspect ratios don't match. + fitCenter("fitCenter"), + + /// Scales the video to fill the container while maintaining aspect ratio. + /// The video may be cropped if aspect ratios don't match, but no empty space + /// will be visible. + centerCrop("centerCrop"), + + /// Scales the video to exactly fill the container bounds. + /// This may distort the video if aspect ratios don't match, but ensures + /// the entire container is filled. + fitXY("fitXY"); + + /// The string key used for native platform communication. + final String key; + + const ScaleType(this.key); +} + +/// Defines the orientation of video content. +/// +/// This enum represents the orientation metadata of the VAP animation, +/// which can be used to determine how the content should be displayed +/// or processed. +enum VideoOrientation { + /// No specific orientation or orientation is unknown. + none(0), + + /// Video content is in portrait orientation (height > width). + portrait(1), + + /// Video content is in landscape orientation (width > height). + landscape(2); + + /// The integer value used for native platform communication. + final int value; + + const VideoOrientation(this.value); + + /// Creates a VideoOrientation from an integer value. + /// + /// This method is used when parsing orientation data from native platforms. + /// If the provided [value] doesn't match any known orientation, returns [none]. + /// + /// Parameters: + /// - [value]: Integer value representing the orientation (0=none, 1=portrait, 2=landscape) + static VideoOrientation fromValue(int value) { + return VideoOrientation.values + .firstWhere((e) => e.value == value, orElse: () => none); + } +} + +/// Callback function for animation failure events. +/// +/// Called when an animation fails to load or play. +/// +/// Parameters: +/// - [code]: Error code indicating the type of failure +/// - [type]: String description of the error type +/// - [message]: Optional detailed error message +typedef OnFailed = void Function(int code, String type, String? message); + +/// Callback function for animation completion events. +/// +/// Called when an animation finishes playing completely. +typedef OnVideoComplete = void Function(); + +/// Callback function for animation destruction events. +/// +/// Called when an animation is destroyed and resources are cleaned up. +typedef OnVideoDestroy = void Function(); + +/// Callback function for frame rendering events. +/// +/// Called for each frame that is rendered during animation playback. +/// This callback is only available on Android. +/// +/// Parameters: +/// - [frameInfo]: Index of the current frame being rendered +/// - [configs]: Animation configuration data (may be null) +typedef OnVideoRender = void Function(int frameInfo, VAPConfigs? configs); + +/// Callback function for animation configuration ready events. +/// +/// Called when the animation file has been loaded and configuration +/// data is available. +/// +/// Parameters: +/// - [configs]: Complete animation configuration data +typedef OnVideoConfigReady = void Function(VAPConfigs configs); + +/// Callback function for animation start events. +/// +/// Called when an animation begins playing. +typedef OnVideoStart = void Function(); + +/// Abstract base class for dynamic content that can be injected into VAP animations. +/// +/// VAPContent represents different types of content (text, images) that can be +/// dynamically inserted into VAP animations at runtime. This allows for +/// personalized animations with user-specific data. +/// +/// Subclasses include: +/// - [TextContent]: For dynamic text content +/// - [ImageBase64Content]: For base64-encoded images +/// - [ImageFileContent]: For local file images +/// - [ImageAssetContent]: For Flutter asset images +/// - [ImageURLContent]: For remote images +/// +/// Example usage: +/// ```dart +/// Map contents = { +/// 'username': TextContent('John Doe'), +/// 'avatar': ImageURLContent('https://example.com/avatar.jpg'), +/// 'logo': ImageAssetContent('assets/images/logo.png'), +/// }; +/// ``` +abstract class VAPContent { + /// The type of content (e.g., 'text', 'image_url', 'image_file'). + String get contentType; + + /// The actual content value (text string, file path, URL, etc.). + String get contentValue; + + /// Converts this content to a map for native platform communication. + /// + /// Returns a map containing the content type and value that can be + /// serialized and sent to native platforms. + Map get toMap { + return {'contentType': contentType, 'contentValue': contentValue}; + } + + /// Creates a VAPContent instance from a map received from native platforms. + /// + /// This factory method automatically determines the appropriate subclass + /// based on the content type and value in the map. + /// + /// Parameters: + /// - [value]: Map containing 'contentType' and 'contentValue' keys + /// + /// Returns the appropriate VAPContent subclass instance. + /// Throws [ArgumentError] if the content type is unknown. + static VAPContent fromMap(Map value) { + final contentType = value['contentType'] as String; + final contentValue = value['contentValue'] as String; + + switch (contentType) { + case 'text': + return TextContent(contentValue); + case 'image': + if (contentValue.startsWith('http://') || + contentValue.startsWith('https://')) { + return ImageURLContent(contentValue); + } else if (contentValue.startsWith('data:image/')) { + return ImageBase64Content(contentValue); + } else if (contentValue.startsWith('assets/')) { + return ImageAssetContent(contentValue); + } else { + return ImageFileContent(contentValue); + } + default: + throw ArgumentError('Unknown content type: $contentType'); + } + } +} + +/// Content type for dynamic text injection into VAP animations. +/// +/// TextContent allows you to inject dynamic text into VAP animations at runtime. +/// The text will be rendered using the styling defined in the VAP file. +/// +/// Example usage: +/// ```dart +/// TextContent username = TextContent('John Doe'); +/// await controller.setVapTagContent('username', username); +/// ``` +class TextContent extends VAPContent { + /// The text content to display in the animation. + final String text; + + /// Creates a TextContent with the specified text. + /// + /// Parameters: + /// - [text]: The text string to inject into the animation + TextContent(this.text); + + @override + String get contentType => 'text'; + + @override + String get contentValue => text; +} + +/// Content type for base64-encoded image injection into VAP animations. +/// +/// ImageBase64Content allows you to inject images that are encoded as base64 +/// strings directly into VAP animations. This is useful when you have image +/// data in memory or received from an API. +/// +/// Example usage: +/// ```dart +/// String base64Data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='; +/// ImageBase64Content image = ImageBase64Content(base64Data); +/// await controller.setVapTagContent('avatar', image); +/// ``` +class ImageBase64Content extends VAPContent { + /// The base64-encoded image data. + final String base64; + + /// Creates an ImageBase64Content with the specified base64 data. + /// + /// Parameters: + /// - [base64]: Base64-encoded image string + ImageBase64Content(this.base64); + + @override + String get contentType => 'image_base64'; + + @override + String get contentValue => base64; +} + +/// Content type for local file image injection into VAP animations. +/// +/// ImageFileContent allows you to inject images from the device's file system +/// into VAP animations. The file path should point to a valid image file +/// accessible to the app. +/// +/// Example usage: +/// ```dart +/// ImageFileContent image = ImageFileContent('/storage/emulated/0/Download/avatar.jpg'); +/// await controller.setVapTagContent('avatar', image); +/// ``` +class ImageFileContent extends VAPContent { + /// The file system path to the image file. + final String filePath; + + /// Creates an ImageFileContent with the specified file path. + /// + /// Parameters: + /// - [filePath]: Absolute path to the image file on the device + ImageFileContent(this.filePath); + + @override + String get contentType => 'image_file'; + + @override + String get contentValue => filePath; +} + +/// Content type for Flutter asset image injection into VAP animations. +/// +/// ImageAssetContent allows you to inject images from your app's assets +/// into VAP animations. The asset must be declared in your pubspec.yaml file. +/// +/// Example usage: +/// ```dart +/// ImageAssetContent logo = ImageAssetContent('assets/images/logo.png'); +/// await controller.setVapTagContent('logo', logo); +/// ``` +class ImageAssetContent extends VAPContent { + /// The asset path as declared in pubspec.yaml. + final String assetPath; + + /// Creates an ImageAssetContent with the specified asset path. + /// + /// Parameters: + /// - [assetPath]: Path to the asset image (e.g., 'assets/images/logo.png') + ImageAssetContent(this.assetPath); + + @override + String get contentType => 'image_asset'; + + @override + String get contentValue => assetPath; +} + +/// Content type for remote URL image injection into VAP animations. +/// +/// ImageURLContent allows you to inject images from remote URLs into VAP +/// animations. The image will be downloaded and cached automatically. +/// +/// Example usage: +/// ```dart +/// ImageURLContent avatar = ImageURLContent('https://example.com/avatar.jpg'); +/// await controller.setVapTagContent('avatar', avatar); +/// ``` +class ImageURLContent extends VAPContent { + /// The URL of the remote image. + final String url; + + /// Creates an ImageURLContent with the specified URL. + /// + /// Parameters: + /// - [url]: HTTP/HTTPS URL pointing to an image file + ImageURLContent(this.url); + + @override + String get contentType => 'image_url'; + + @override + String get contentValue => url; +} diff --git a/local_packages/tancent_vap-1.0.0+1/lib/widgets/vap_view.dart b/local_packages/tancent_vap-1.0.0+1/lib/widgets/vap_view.dart new file mode 100644 index 0000000..41afc68 --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/lib/widgets/vap_view.dart @@ -0,0 +1,594 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tancent_vap/utils/constant.dart'; + +import '../utils/anim_configs.dart'; + +/// A widget that displays Tencent VAP (Video Animation Player) animations. +/// +/// VapView is a Flutter widget that renders VAP animations with support for: +/// - Multiple scale types (fitCenter, centerCrop, fitXY) +/// - Loop control (no repeat, finite repeats, infinite loop) +/// - Audio control (mute/unmute) +/// - Dynamic content injection via VAP tags +/// - Cross-platform support (iOS and Android) +/// +/// The widget provides a [VapController] through the [onViewCreated] callback +/// for programmatic control of animation playback and content management. +/// +/// Example usage: +/// ```dart +/// VapView( +/// scaleType: ScaleType.fitCenter, +/// repeat: -1, // Infinite loop +/// mute: false, +/// vapTagContents: { +/// 'username': TextContent('John Doe'), +/// 'avatar': ImageURLContent('https://example.com/avatar.jpg'), +/// }, +/// onViewCreated: (controller) { +/// controller.setAnimListener( +/// onVideoStart: () => print('Animation started'), +/// onVideoComplete: () => print('Animation completed'), +/// ); +/// controller.playAsset('animations/sample.mp4'); +/// }, +/// ) +/// ``` +class VapView extends StatefulWidget { + /// How the video should be scaled within the view bounds. + /// + /// Defaults to [ScaleType.fitCenter]. + final ScaleType scaleType; + + /// Number of times the animation should repeat. + /// + /// - `0`: Play once (no repeat) + /// - Positive number: Repeat that many times + /// - `-1`: Infinite loop + /// + /// Defaults to `0`. + final int repeat; + + /// Whether the animation should be muted during playback. + /// + /// Defaults to `false`. + final bool mute; + + /// Initial dynamic content for VAP tags. + /// + /// This map allows you to provide content that will be injected into + /// the VAP animation at runtime. Keys are tag names defined in the VAP file, + /// values are [VAPContent] objects containing the actual content. + /// + /// Can be `null` if no initial content is needed. + final Map? vapTagContents; + + /// Callback invoked when the platform view is created. + /// + /// Provides a [VapController] instance that can be used to control + /// animation playback, set content, and listen to events. + /// + /// This is the recommended way to obtain a controller reference. + final Function(VapController)? onViewCreated; + + /// Creates a VapView widget. + /// + /// All parameters are optional and have sensible defaults: + /// - [scaleType]: How to scale the video (defaults to [ScaleType.fitCenter]) + /// - [repeat]: Number of repetitions (defaults to 0 for no repeat) + /// - [mute]: Whether to mute audio (defaults to false) + /// - [vapTagContents]: Initial content for VAP tags (optional) + /// - [onViewCreated]: Callback to receive the controller (optional) + const VapView( + {super.key, + this.scaleType = ScaleType.fitCenter, + this.repeat = 0, + this.mute = false, + this.vapTagContents, + this.onViewCreated}); + + @override + State createState() => _VapViewState(); +} + +class _VapViewState extends State { + final VapController _controller = VapController(); + + @override + void dispose() { + // Dispose the controller when the widget is disposed + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final creationParams = { + 'scaleType': widget.scaleType.key, + 'loop': widget.repeat, + 'mute': widget.mute + }; + + // Add VAP tag contents to creation params if provided + if (widget.vapTagContents != null && widget.vapTagContents!.isNotEmpty) { + creationParams['vapTagContents'] = widget.vapTagContents! + .map((key, value) => MapEntry(key, value.toMap)); + } + + if (defaultTargetPlatform == TargetPlatform.iOS) { + return UiKitView( + viewType: 'vap_view', + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onPlatformViewCreated: _onPlatformViewCreated); + } else { + return AndroidView( + viewType: 'vap_view', + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onPlatformViewCreated: _onPlatformViewCreated); + } + } + + void _onPlatformViewCreated(int id) { + final channel = MethodChannel('vap_view_$id'); + _controller._setChannel(channel); + widget.onViewCreated?.call(_controller); + } +} + +/// Controller for programmatic management of VAP animations. +/// +/// VapController provides methods to: +/// - Control playback (play, stop, loop settings) +/// - Manage dynamic content (set, get, clear VAP tag contents) +/// - Listen to animation events (start, complete, error, etc.) +/// - Configure playback settings (mute, scale type) +/// +/// The controller is obtained through [VapView.onViewCreated] callback +/// and should be disposed when no longer needed to prevent memory leaks. +/// +/// Example usage: +/// ```dart +/// VapController controller; +/// +/// // Set event listeners +/// controller.setAnimListener( +/// onVideoStart: () => print('Started'), +/// onVideoComplete: () => print('Completed'), +/// onFailed: (code, type, msg) => print('Error: $code'), +/// ); +/// +/// // Play animation +/// await controller.playAsset('animations/sample.mp4'); +/// +/// // Set dynamic content +/// await controller.setVapTagContent('username', TextContent('John')); +/// +/// // Clean up +/// controller.dispose(); +/// ``` +class VapController { + /// Internal method channel for communication with native platforms. + /// + /// This channel is set when the platform view is created and is used + /// for all communication between Dart and native code. + MethodChannel? _channel; + + /// Gets the method channel, throwing an exception if the controller is disposed. + /// + /// Throws [Exception] if the controller has been disposed. + MethodChannel get __channel { + if (_channel == null) { + throw Exception("Controller already disposed"); + } + return _channel!; + } + + /// Callback for animation failure events. + OnFailed? _onFailed; + + /// Callback for animation completion events. + OnVideoComplete? _onVideoComplete; + + /// Callback for animation destruction events. + OnVideoDestroy? _onVideoDestroy; + + /// Callback for frame rendering events. + OnVideoRender? _onVideoRender; + + /// Callback for animation start events. + OnVideoStart? _onVideoStart; + + /// Callback for animation configuration ready events. + OnVideoConfigReady? _onVideoConfigReady; + + /// Internal method to set the method channel. + /// + /// Called automatically when the platform view is created. + /// Sets up the method call handler for receiving events from native code. + void _setChannel(MethodChannel channel) { + _channel = channel; + _channel + ?.setMethodCallHandler((MethodCall call) => _handleNativeEvent(call)); + } + + /// Sets event listeners for various animation events. + /// + /// All parameters are optional - only set listeners for events you need to handle. + /// This method should typically be called before starting animation playback + /// to ensure all events are captured. + /// + /// Parameters: + /// - [onFailed]: Called when animation fails with error code, type, and message + /// - [onVideoComplete]: Called when animation playback completes + /// - [onVideoDestroy]: Called when animation is destroyed/cleaned up + /// - [onVideoRender]: Called for each frame render with frame index and config + /// - [onVideoStart]: Called when animation starts playing + /// - [onVideoConfigReady]: Called when animation configuration is loaded + /// + /// Example: + /// ```dart + /// controller.setAnimListener( + /// onFailed: (code, type, message) { + /// print("Animation failed with code $code: $message"); + /// }, + /// onVideoComplete: () { + /// print("Video playback completed"); + /// }, + /// onVideoStart: () { + /// print("Video started"); + /// }, + /// ); + /// ``` + void setAnimListener({ + OnFailed? onFailed, + OnVideoComplete? onVideoComplete, + OnVideoDestroy? onVideoDestroy, + OnVideoRender? onVideoRender, + OnVideoStart? onVideoStart, + OnVideoConfigReady? onVideoConfigReady, + }) { + _onFailed = onFailed; + _onVideoComplete = onVideoComplete; + _onVideoDestroy = onVideoDestroy; + _onVideoRender = onVideoRender; + _onVideoStart = onVideoStart; + _onVideoConfigReady = onVideoConfigReady; + } + + /// Internal method to handle events from native platforms. + /// + /// Automatically routes native events to the appropriate Dart callbacks + /// that were set via [setAnimListener]. + Future _handleNativeEvent(MethodCall call) async { + switch (call.method) { + case 'onFailed': + if (_onFailed != null) { + final args = call.arguments as Map; + final code = args['errorCode'] as int; + final type = args['errorType'] as String; + final msg = args['errorMsg'] as String?; + _onFailed!(code, type, msg); + } + break; + case 'onVideoComplete': + _onVideoComplete?.call(); + break; + case 'onVideoDestroy': + _onVideoDestroy?.call(); + break; + case 'onVideoRender': + if (_onVideoRender != null) { + final args = call.arguments as Map; + final frameIndex = args['frameIndex'] as int; + final config = args['config'] as Map?; + _onVideoRender!( + frameIndex, config != null ? VAPConfigs.fromMap(config) : null); + } + break; + case 'onVideoStart': + _onVideoStart?.call(); + break; + case 'onVideoConfigReady': + if (_onVideoConfigReady != null) { + final args = Map.from(call.arguments as Map); + final config = Map.from(args['config'] as Map); + _onVideoConfigReady!(VAPConfigs.fromMap(config)); + } + break; + default: + throw UnimplementedError('Unhandled method: ${call.method}'); + } + } + + /// Plays a VAP animation from a file path. + /// + /// The [filePath] should point to a valid VAP file accessible to the app. + /// This is typically used for files downloaded to the device or copied + /// to the app's documents directory. + /// + /// Throws an exception if the file cannot be found or played. + /// + /// Example: + /// ```dart + /// await controller.playFile('/path/to/animation.mp4'); + /// ``` + Future playFile(String filePath) async { + await __channel.invokeMethod('playFile', {'filePath': filePath}); + } + + /// Plays a VAP animation from a Flutter asset. + /// + /// The [assetName] should correspond to an asset declared in `pubspec.yaml`. + /// This is the recommended way to bundle VAP animations with your app. + /// + /// Throws an exception if the asset cannot be found or played. + /// + /// Example: + /// ```dart + /// await controller.playAsset('assets/animations/sample.mp4'); + /// ``` + Future playAsset(String assetName) async { + await __channel.invokeMethod('playAsset', {'assetName': assetName}); + } + + /// Stops the current animation playback. + /// + /// This will immediately stop the animation and reset the playback state. + /// The animation can be restarted by calling [playFile] or [playAsset] again. + /// + /// Example: + /// ```dart + /// await controller.stop(); + /// ``` + Future stop() async { + await __channel.invokeMethod('stop'); + } + + /// Sets the loop count for animation playback. + /// + /// Parameters: + /// - [loop]: Number of times to repeat the animation + /// - `0`: Play once (no repeat) + /// - Positive number: Repeat that many times + /// - `-1`: Infinite loop + /// + /// Example: + /// ```dart + /// await controller.setLoop(-1); // Infinite loop + /// await controller.setLoop(3); // Play 3 times + /// ``` + Future setLoop(int loop) async { + await __channel.invokeMethod('setLoop', {'loop': loop}); + } + + /// Sets the mute state for animation playback. + /// + /// Parameters: + /// - [mute]: `true` to mute audio, `false` to enable audio + /// + /// Example: + /// ```dart + /// await controller.setMute(true); // Mute audio + /// await controller.setMute(false); // Enable audio + /// ``` + Future setMute(bool mute) async { + await __channel.invokeMethod('setMute', {'mute': mute}); + } + + /// Sets the scale type for animation display. + /// + /// Parameters: + /// - [scaleType]: How the animation should be scaled within its container + /// - `'fitCenter'`: Scale to fit within bounds, maintaining aspect ratio + /// - `'centerCrop'`: Scale to fill bounds, cropping excess, maintaining aspect ratio + /// - `'fitXY'`: Scale to exactly fill bounds, potentially distorting aspect ratio + /// + /// Example: + /// ```dart + /// await controller.setScaleType('centerCrop'); + /// ``` + Future setScaleType(String scaleType) async { + await __channel.invokeMethod('setScaleType', {'scaleType': scaleType}); + } + + /// Sets content for a specific VAP tag. + /// + /// VAP tags are placeholders in the animation file that can be replaced + /// with dynamic content at runtime. This allows for personalized animations + /// with user-specific data. + /// + /// Parameters: + /// - [tag]: The tag name defined in the VAP file (cannot be empty) + /// - [content]: The content to inject ([VAPContent] subclass) + /// + /// Throws [ArgumentError] if [tag] is empty. + /// + /// Example: + /// ```dart + /// await controller.setVapTagContent('username', TextContent('John Doe')); + /// await controller.setVapTagContent('avatar', ImageURLContent('https://...')); + /// ``` + Future setVapTagContent(String tag, VAPContent content) async { + if (tag.isEmpty) { + throw ArgumentError('Tag cannot be empty'); + } + await __channel.invokeMethod( + 'setVapTagContent', {'tag': tag, 'content': content.toMap}); + } + + /// Sets multiple VAP tag contents at once. + /// + /// This method is more efficient than calling [setVapTagContent] multiple times + /// as it batches all updates in a single native call. + /// + /// Parameters: + /// - [contents]: Map of tag names to content objects + /// + /// Throws [ArgumentError] if any tag name is empty. + /// Returns immediately if [contents] is empty. + /// + /// Example: + /// ```dart + /// await controller.setVapTagContents({ + /// 'username': TextContent('John Doe'), + /// 'avatar': ImageURLContent('https://example.com/avatar.jpg'), + /// 'message': TextContent('Hello World!') + /// }); + /// ``` + Future setVapTagContents(Map contents) async { + if (contents.isEmpty) { + return; // Nothing to set + } + + // Validate that no tags are empty + for (final tag in contents.keys) { + if (tag.isEmpty) { + throw ArgumentError('Tag cannot be empty'); + } + } + + await __channel.invokeMethod('setVapTagContents', + {'contents': contents.map((key, value) => MapEntry(key, value.toMap))}); + } + + /// Gets the content for a specific VAP tag. + /// + /// Parameters: + /// - [tag]: The tag name to retrieve content for (cannot be empty) + /// + /// Returns the [VAPContent] associated with the tag, or `null` if no content is set. + /// Throws [ArgumentError] if [tag] is empty. + /// + /// Example: + /// ```dart + /// VAPContent? content = await controller.getVapTagContent('username'); + /// if (content is TextContent) { + /// print('Username: ${content.text}'); + /// } + /// ``` + Future getVapTagContent(String tag) async { + if (tag.isEmpty) { + throw ArgumentError('Tag cannot be empty'); + } + return await __channel + .invokeMethod('getVapTagContent', {'tag': tag}).then((result) { + if (result == null) return null; + return VAPContent.fromMap(Map.from(result as Map)); + }); + } + + /// Gets all currently set VAP tag contents. + /// + /// Returns a map containing all tag names and their associated content. + /// The map will be empty if no content has been set. + /// + /// Example: + /// ```dart + /// Map allContents = await controller.getAllVapTagContents(); + /// for (final entry in allContents.entries) { + /// print('Tag ${entry.key}: ${entry.value.contentValue}'); + /// } + /// ``` + Future> getAllVapTagContents() async { + final result = await __channel.invokeMethod('getAllVapTagContents'); + return Map.from((result ?? {}) as Map) + .map((key, value) { + return MapEntry(key, VAPContent.fromMap(value as Map)); + }); + } + + /// Clears all VAP tag contents. + /// + /// This removes all previously set tag content mappings. + /// The animation will continue to play but without any dynamic content. + /// + /// Example: + /// ```dart + /// await controller.clearVapTagContents(); + /// ``` + Future clearVapTagContents() async { + await __channel.invokeMethod('clearVapTagContents'); + } + + /// Checks if a specific VAP tag has content set. + /// + /// Parameters: + /// - [tag]: The tag name to check + /// + /// Returns `true` if content is set for the tag, `false` otherwise. + /// Returns `false` if [tag] is empty. + /// + /// Example: + /// ```dart + /// if (await controller.hasVapTagContent('username')) { + /// print('Username content is set'); + /// } + /// ``` + Future hasVapTagContent(String tag) async { + if (tag.isEmpty) { + return false; + } + final content = await getVapTagContent(tag); + return content != null; + } + + /// Removes content for a specific VAP tag. + /// + /// Parameters: + /// - [tag]: The tag name to remove content for (cannot be empty) + /// + /// Throws [ArgumentError] if [tag] is empty. + /// Has no effect if the tag doesn't have content set. + /// + /// Example: + /// ```dart + /// await controller.removeVapTagContent('username'); + /// ``` + Future removeVapTagContent(String tag) async { + if (tag.isEmpty) { + throw ArgumentError('Tag cannot be empty'); + } + + final allContents = await getAllVapTagContents(); + allContents.remove(tag); + await setVapTagContents(allContents); + } + + /// Checks if the controller has been disposed. + /// + /// Returns `true` if [dispose] has been called, `false` otherwise. + /// A disposed controller cannot be used for any operations. + bool get isDisposed => _channel == null; + + /// Disposes the controller and releases associated resources. + /// + /// This method should be called when the controller is no longer needed + /// to prevent memory leaks. After calling this method, the controller + /// cannot be used for any operations. + /// + /// It's safe to call this method multiple times. + /// + /// Example: + /// ```dart + /// @override + /// void dispose() { + /// controller.dispose(); + /// super.dispose(); + /// } + /// ``` + Future dispose() async { + try { + await _channel?.invokeMethod('dispose'); + } catch (e) { + // Ignore disposal errors + } finally { + _channel?.setMethodCallHandler(null); + _channel = null; + } + } +} diff --git a/local_packages/tancent_vap-1.0.0+1/pubspec.yaml b/local_packages/tancent_vap-1.0.0+1/pubspec.yaml new file mode 100644 index 0000000..5c9947b --- /dev/null +++ b/local_packages/tancent_vap-1.0.0+1/pubspec.yaml @@ -0,0 +1,29 @@ +name: tancent_vap +description: "Video Animation Player from Tencent for Flutter, supports both Android and iOS platforms." +version: 1.0.0+1 +homepage: https://github.com/nizwar/tancent_vap_flutter.git + +environment: + sdk: ^3.6.0 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + + plugin: + platforms: + android: + package: com.laskarmedia.vap + pluginClass: VapPlugin + ios: + package: com.laskarmedia.vap + pluginClass: VapPlugin diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..a83b54f --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1835 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 + url: "https://pub.dev" + source: hosted + version: "1.3.59" + agora_rtc_engine: + dependency: "direct main" + description: + name: agora_rtc_engine + sha256: "6559294d18ce4445420e19dbdba10fb58cac955cd8f22dbceae26716e194d70e" + url: "https://pub.dev" + source: hosted + version: "6.5.3" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + audioplayers: + dependency: transitive + description: + name: audioplayers + sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70 + url: "https://pub.dev" + source: hosted + version: "6.6.0" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 + url: "https://pub.dev" + source: hosted + version: "6.4.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9 + url: "https://pub.dev" + source: hosted + version: "5.2.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + back_button_interceptor: + dependency: "direct main" + description: + name: back_button_interceptor + sha256: b85977faabf4aeb95164b3b8bf81784bed4c54ea1aef90a036ab6927ecf80c5a + url: "https://pub.dev" + source: hosted + version: "8.0.4" + badges: + dependency: "direct main" + description: + name: badges + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: febf4b0163e0242adc13d7a863b04965351f59e7dfea56675c7c2caa7bcd7476 + url: "https://pub.dev" + source: hosted + version: "5.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cookie_jar: + dependency: "direct main" + description: + name: cookie_jar + sha256: "963da02c1ef64cb5ac20de948c9e5940aa351f1e34a12b1d327c83d85b7e8fff" + url: "https://pub.dev" + source: hosted + version: "4.0.9" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 + url: "https://pub.dev" + source: hosted + version: "10.1.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + event_bus: + dependency: "direct main" + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + extended_image: + dependency: "direct main" + description: + name: extended_image + sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 + url: "https://pub.dev" + source: hosted + version: "10.0.1" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + extended_nested_scroll_view: + dependency: "direct main" + description: + name: extended_nested_scroll_view + sha256: "835580d40c2c62b448bd14adecd316acba469ba61f1510ef559d17668a85e777" + url: "https://pub.dev" + source: hosted + version: "6.2.1" + extended_text: + dependency: "direct main" + description: + name: extended_text + sha256: d8f4a6e2676505b54dc0d5f5e8de9020667b402e9c1b3a8b030a83e568c99654 + url: "https://pub.dev" + source: hosted + version: "15.0.2" + extended_text_field: + dependency: "direct main" + description: + name: extended_text_field + sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" + url: "https://pub.dev" + source: hosted + version: "16.0.2" + extended_text_library: + dependency: transitive + description: + name: extended_text_library + sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + fading_edge_scrollview: + dependency: transitive + description: + name: fading_edge_scrollview + sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" + url: "https://pub.dev" + source: hosted + version: "7.7.3" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e + url: "https://pub.dev" + source: hosted + version: "5.15.3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" + url: "https://pub.dev" + source: hosted + version: "3.15.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + url: "https://pub.dev" + source: hosted + version: "2.24.1" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1" + url: "https://pub.dev" + source: hosted + version: "4.3.10" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb" + url: "https://pub.dev" + source: hosted + version: "3.8.10" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fluro: + dependency: "direct main" + description: + name: fluro + sha256: "24d07d0b285b213ec2045b83e85d076185fa5c23651e44dae0ac6755784b97d0" + url: "https://pub.dev" + source: hosted + version: "2.0.5" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_debouncer: + dependency: "direct main" + description: + name: flutter_debouncer + sha256: "89f98f874e6abbb212f3027a7a27d5ce42c5b6544c8f5967d91140c0ae06ae22" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_foreground_task: + dependency: "direct main" + description: + path: "local_packages/flutter_foreground_task-9.1.0" + relative: true + source: path + version: "9.1.0" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb + url: "https://pub.dev" + source: hosted + version: "1.0.6" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.dev" + source: hosted + version: "0.1.5" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" + flutter_screenutil: + dependency: "direct main" + description: + name: flutter_screenutil + sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" + url: "https://pub.dev" + source: hosted + version: "5.9.3" + flutter_smart_dialog: + dependency: "direct main" + description: + name: flutter_smart_dialog + sha256: "72762b20c25f8e12d490332004c9cca327f39dfc1fcba66124c6b7c108b68850" + url: "https://pub.dev" + source: hosted + version: "4.9.8+10" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_svga: + dependency: "direct main" + description: + name: flutter_svga + sha256: c914ba2aee5e2d53775b64c7ff6530b4cdc3c27fd9348debb0d86fd68b869c39 + url: "https://pub.dev" + source: hosted + version: "0.0.13" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" + url: "https://pub.dev" + source: hosted + version: "8.2.14" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + google_sign_in: + dependency: "direct main" + description: + name: google_sign_in + sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a + url: "https://pub.dev" + source: hosted + version: "6.3.0" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96" + url: "https://pub.dev" + source: hosted + version: "5.9.0" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" + url: "https://pub.dev" + source: hosted + version: "0.12.4+4" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + id3: + dependency: transitive + description: + name: id3 + sha256: "24176a6e08db6297c8450079e94569cd8387f913c817e5e3d862be7dc191e0b8" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + image_cropper: + dependency: "direct main" + description: + name: image_cropper + sha256: f4bad5ed2dfff5a7ce0dfbad545b46a945c702bb6182a921488ef01ba7693111 + url: "https://pub.dev" + source: hosted + version: "5.0.1" + image_cropper_for_web: + dependency: transitive + description: + name: image_cropper_for_web + sha256: "865d798b5c9d826f1185b32e5d0018c4183ddb77b7b82a931e1a06aa3b74974e" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + image_cropper_platform_interface: + dependency: transitive + description: + name: image_cropper_platform_interface + sha256: ee160d686422272aa306125f3b6fb1c1894d9b87a5e20ed33fa008e7285da11e + url: "https://pub.dev" + source: hosted + version: "5.0.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" + url: "https://pub.dev" + source: hosted + version: "0.8.13+16" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + in_app_purchase: + dependency: "direct main" + description: + name: in_app_purchase + sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c" + url: "https://pub.dev" + source: hosted + version: "3.2.3" + in_app_purchase_android: + dependency: transitive + description: + name: in_app_purchase_android + sha256: "634bee4734b17fe55f370f0ac07a22431a9666e0f3a870c6d20350856e8bbf71" + url: "https://pub.dev" + source: hosted + version: "0.4.0+10" + 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: "1d512809edd9f12ff88fce4596a13a18134e2499013f4d6a8894b04699363c93" + url: "https://pub.dev" + source: hosted + version: "0.4.8+1" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + iris_method_channel: + dependency: transitive + description: + name: iris_method_channel + sha256: bfb5cfc6c6eae42da8cd1b35977a72d8b8881848a5dfc3d672e4760a907d11a0 + url: "https://pub.dev" + source: hosted + version: "2.2.4" + isolate_easy_pool: + dependency: "direct main" + description: + name: isolate_easy_pool + sha256: f0204cfdecbb84d61c46240a603bb21c3b2ac925475faf3f4afe18526fcb8f64 + url: "https://pub.dev" + source: hosted + version: "0.0.8" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.dev" + source: hosted + version: "4.11.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + loading_indicator_view_plus: + dependency: "direct main" + description: + name: loading_indicator_view_plus + sha256: "23ad380dd677e8e0182f679f29e1221269ad66b9a1a2d70b19e716ee4723c99e" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + marquee: + dependency: "direct main" + description: + name: marquee + sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mime_type: + dependency: transitive + description: + name: mime_type + sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb + url: "https://pub.dev" + source: hosted + version: "1.0.1" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + on_audio_query: + dependency: "direct main" + description: + path: "local_packages/on_audio_query-2.9.0" + relative: true + source: path + version: "2.9.0" + on_audio_query_android: + dependency: transitive + description: + path: "local_packages/on_audio_query_android-1.1.0" + relative: true + source: path + version: "1.1.0" + on_audio_query_ios: + dependency: transitive + description: + name: on_audio_query_ios + sha256: "9b3efa39a656fa3720980e3c6a1f55b7257d0032a45ffeb3f70eaa2c7f10f929" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + on_audio_query_platform_interface: + dependency: transitive + description: + name: on_audio_query_platform_interface + sha256: c23e019a31bd0774828476e428fd33b0dd1d82c9d4791dba80429358fc65dcd3 + url: "https://pub.dev" + source: hosted + version: "1.7.0" + on_audio_query_web: + dependency: transitive + description: + name: on_audio_query_web + sha256: "990efa52d879e6caffa97f24b34acd9caa1ce2c4c4cb873fe5a899a9b1af02c7" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_drawing: + dependency: transitive + description: + name: path_drawing + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.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: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.dev" + source: hosted + version: "0.15.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pull_to_refresh: + dependency: "direct main" + description: + name: pull_to_refresh + sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + readmore: + dependency: "direct main" + description: + name: readmore + sha256: e8fca2bd397b86342483b409e2ec26f06560a5963aceaa39b27f30722b506187 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sign_in_with_apple: + dependency: "direct main" + description: + name: sign_in_with_apple + sha256: "8bd875c8e8748272749eb6d25b896f768e7e9d60988446d543fe85a37a2392b8" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + sign_in_with_apple_platform_interface: + dependency: transitive + description: + name: sign_in_with_apple_platform_interface + sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sign_in_with_apple_web: + dependency: transitive + description: + name: sign_in_with_apple_web + sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + social_sharing_plus: + dependency: "direct main" + description: + name: social_sharing_plus + sha256: d0bd9dc3358cf66a3aac7cce549902360f30bccf44cf6090a708d97cc54ca854 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" + url: "https://pub.dev" + source: hosted + version: "2.4.2+3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + tancent_vap: + dependency: "direct main" + description: + path: "local_packages/tancent_vap-1.0.0+1" + relative: true + source: path + version: "1.0.0+1" + tencent_cloud_chat_sdk: + dependency: "direct main" + description: + name: tencent_cloud_chat_sdk + sha256: fb930c86017fa4a546f3d0c15bc5a54197f07f003934737e80cf2f9a70cab1ba + url: "https://pub.dev" + source: hosted + version: "8.3.6498+3" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + url: "https://pub.dev" + source: hosted + version: "6.3.29" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + 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: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" + url: "https://pub.dev" + source: hosted + version: "2.9.5" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e + url: "https://pub.dev" + source: hosted + version: "2.9.4" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" + url: "https://pub.dev" + source: hosted + version: "6.6.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + video_thumbnail: + dependency: "direct main" + description: + name: video_thumbnail + sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "24b84143787220a403491c2e5de0877fbbb87baf3f0b18a2a988973863db4b03" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" + url: "https://pub.dev" + source: hosted + version: "4.4.2" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" + url: "https://pub.dev" + source: hosted + version: "3.16.9" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: d7219cfabc6f5fc2032e0fa980ec36d71f308a35a823395af1abc34d9a2ede83 + url: "https://pub.dev" + source: hosted + version: "3.24.2" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + url: "https://pub.dev" + source: hosted + version: "1.1.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.7 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..648bd3a --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,175 @@ +name: yumi +description: "Yumi" +# 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 followinFg defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + + +environment: + sdk: ^3.7.2 + +# 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 + flutter_localizations: + sdk: flutter + + # The following adds the Cupertino Icons fonts to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + #状态管理 + provider: ^6.1.5 + #屏幕适配 + flutter_screenutil: 5.9.3 + #网络请求工具 + dio: ^5.8.0+1 + #网络请求日志打印 + pretty_dio_logger: ^1.1.1 + #下拉刷新 + pull_to_refresh: 2.0.0 + #声网 + agora_rtc_engine: ^6.5.2 + # agora_rtm: 1.6.0 + #腾讯IM + tencent_cloud_chat_sdk: 8.3.6498+3 + #缓存 + flutter_cache_manager: ^3.0.0 + #路由 + fluro: 2.0.5 + #权限请求 + permission_handler: ^12.0.0+1 + webview_flutter: 4.4.2 + #跳转外链 + url_launcher: ^6.3.1 + #键值对存储 + # mmkv: ^2.2.2 + shared_preferences: ^2.5.3 + #cookie + cookie_jar: ^4.0.8 + #toast + fluttertoast: ^8.2.12 + #isolate池 + isolate_easy_pool: ^0.0.8 + #图片加载 + cached_network_image: ^3.4.1 + extended_image: ^10.0.1 + #视频播放 + video_player: ^2.10.0 + #谷歌登录 + google_sign_in: ^6.3.0 + firebase_auth: ^5.7.0 + firebase_core: ^3.15.2 + firebase_crashlytics: ^4.3.9 + #苹果登录 + sign_in_with_apple: ^7.0.1 + #相册 + # wechat_assets_picker: ^9.5.0 + # wechat_camera_picker: ^4.2.0 + image_picker: ^1.2.0 + #图片裁剪 + image_cropper: ^5.0.0 + loading_indicator_view_plus: ^2.0.0 + back_button_interceptor: ^8.0.4 + #vap特效 + # flutter_vap_plus: ^1.2.10 + # tancent_vap: ^1.0.0+1 + tancent_vap: + path: ./local_packages/tancent_vap-1.0.0+1 + #svga特效 + flutter_svga: ^0.0.5 + #banner + carousel_slider: ^5.1.1 + #谷歌支付 + in_app_purchase: ^3.2.3 + + device_info_plus: ^10.0.0 + package_info_plus: ^8.0.0 + uuid: ^4.5.1 + wakelock_plus: ^1.1.0 + #防抖 + flutter_debouncer: ^2.0.0 + #弹框 + flutter_smart_dialog: ^4.9.8+9 + extended_text_field: 16.0.2 + # flutter_sound: 9.28.0 + #图片压缩 + flutter_image_compress: ^2.4.0 + extended_text: ^15.0.2 + #缩略图 + video_thumbnail: ^0.5.6 + badges: ^3.1.2 + #本地路径 + path_provider: ^2.1.4 + photo_view: ^0.15.0 + flutter_staggered_grid_view: ^0.7.0 + marquee: ^2.3.0 + extended_nested_scroll_view: ^6.2.1 + event_bus: ^2.0.1 + readmore: ^3.0.0 + #分享 + social_sharing_plus: ^1.2.3 + #外链跳转 + app_links: ^6.4.1 + flutter_foreground_task: + path: ./local_packages/flutter_foreground_task-9.1.0 + #本地音乐检索 + on_audio_query: + path: ./local_packages/on_audio_query-2.9.0 + +dev_dependencies: + flutter_launcher_icons: ^0.14.4 + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons fonts is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true +# fonts: +# - family: MyCustomFont +# fonts: +# - asset: fonts/AAA-bold.ttf + assets: + # 共享资源 + # 应用资源路径 + - assets/ + - assets/l10n/ + - sc_images/splash/ + - sc_images/login/ + - sc_images/general/ + - sc_images/index/ + - sc_images/room/ + - sc_images/room/entrance/ + - sc_images/room/anim/ + - sc_images/person/ + - sc_images/store/ + - sc_images/msg/ + - sc_images/level/ + - sc_images/coupon/ + - fonts/ \ No newline at end of file diff --git a/sc_images/coupon/sc_icon_coupon_headdress_item_bg.png b/sc_images/coupon/sc_icon_coupon_headdress_item_bg.png new file mode 100644 index 0000000..4118bf2 Binary files /dev/null and b/sc_images/coupon/sc_icon_coupon_headdress_item_bg.png differ diff --git a/sc_images/coupon/sc_icon_coupon_mountains_item_bg.png b/sc_images/coupon/sc_icon_coupon_mountains_item_bg.png new file mode 100644 index 0000000..4685be1 Binary files /dev/null and b/sc_images/coupon/sc_icon_coupon_mountains_item_bg.png differ diff --git a/sc_images/coupon/sc_icon_coupon_vip_item_bg.png b/sc_images/coupon/sc_icon_coupon_vip_item_bg.png new file mode 100644 index 0000000..560f7c7 Binary files /dev/null and b/sc_images/coupon/sc_icon_coupon_vip_item_bg.png differ diff --git a/sc_images/general/sc_icon_add_pic.png b/sc_images/general/sc_icon_add_pic.png new file mode 100644 index 0000000..9e926a2 Binary files /dev/null and b/sc_images/general/sc_icon_add_pic.png differ diff --git a/sc_images/general/sc_icon_app_update_bg.png b/sc_images/general/sc_icon_app_update_bg.png new file mode 100644 index 0000000..78ab6b2 Binary files /dev/null and b/sc_images/general/sc_icon_app_update_bg.png differ diff --git a/sc_images/general/sc_icon_avar_defalt.png b/sc_images/general/sc_icon_avar_defalt.png new file mode 100644 index 0000000..adf3d2f Binary files /dev/null and b/sc_images/general/sc_icon_avar_defalt.png differ diff --git a/sc_images/general/sc_icon_back.png b/sc_images/general/sc_icon_back.png new file mode 100644 index 0000000..01b56e7 Binary files /dev/null and b/sc_images/general/sc_icon_back.png differ diff --git a/sc_images/general/sc_icon_checked.png b/sc_images/general/sc_icon_checked.png new file mode 100644 index 0000000..c9e25d3 Binary files /dev/null and b/sc_images/general/sc_icon_checked.png differ diff --git a/sc_images/general/sc_icon_clear_c.png b/sc_images/general/sc_icon_clear_c.png new file mode 100644 index 0000000..b96c783 Binary files /dev/null and b/sc_images/general/sc_icon_clear_c.png differ diff --git a/sc_images/general/sc_icon_create_dynamic_add_pic.png b/sc_images/general/sc_icon_create_dynamic_add_pic.png new file mode 100644 index 0000000..ec3e0d1 Binary files /dev/null and b/sc_images/general/sc_icon_create_dynamic_add_pic.png differ diff --git a/sc_images/general/sc_icon_delete.png b/sc_images/general/sc_icon_delete.png new file mode 100644 index 0000000..b21e946 Binary files /dev/null and b/sc_images/general/sc_icon_delete.png differ diff --git a/sc_images/general/sc_icon_edit_head_camera_alt.png b/sc_images/general/sc_icon_edit_head_camera_alt.png new file mode 100644 index 0000000..79e9bd0 Binary files /dev/null and b/sc_images/general/sc_icon_edit_head_camera_alt.png differ diff --git a/sc_images/general/sc_icon_edit_user_info_add_pic.png b/sc_images/general/sc_icon_edit_user_info_add_pic.png new file mode 100644 index 0000000..5f20c84 Binary files /dev/null and b/sc_images/general/sc_icon_edit_user_info_add_pic.png differ diff --git a/sc_images/general/sc_icon_game_num0.png b/sc_images/general/sc_icon_game_num0.png new file mode 100644 index 0000000..996bacb Binary files /dev/null and b/sc_images/general/sc_icon_game_num0.png differ diff --git a/sc_images/general/sc_icon_game_num1.png b/sc_images/general/sc_icon_game_num1.png new file mode 100644 index 0000000..6d1f4c1 Binary files /dev/null and b/sc_images/general/sc_icon_game_num1.png differ diff --git a/sc_images/general/sc_icon_game_num2.png b/sc_images/general/sc_icon_game_num2.png new file mode 100644 index 0000000..704988e Binary files /dev/null and b/sc_images/general/sc_icon_game_num2.png differ diff --git a/sc_images/general/sc_icon_game_num3.png b/sc_images/general/sc_icon_game_num3.png new file mode 100644 index 0000000..0ef536f Binary files /dev/null and b/sc_images/general/sc_icon_game_num3.png differ diff --git a/sc_images/general/sc_icon_game_num4.png b/sc_images/general/sc_icon_game_num4.png new file mode 100644 index 0000000..b313a6e Binary files /dev/null and b/sc_images/general/sc_icon_game_num4.png differ diff --git a/sc_images/general/sc_icon_game_num5.png b/sc_images/general/sc_icon_game_num5.png new file mode 100644 index 0000000..73c5ef2 Binary files /dev/null and b/sc_images/general/sc_icon_game_num5.png differ diff --git a/sc_images/general/sc_icon_game_num6.png b/sc_images/general/sc_icon_game_num6.png new file mode 100644 index 0000000..f1efd42 Binary files /dev/null and b/sc_images/general/sc_icon_game_num6.png differ diff --git a/sc_images/general/sc_icon_game_num7.png b/sc_images/general/sc_icon_game_num7.png new file mode 100644 index 0000000..1617cfa Binary files /dev/null and b/sc_images/general/sc_icon_game_num7.png differ diff --git a/sc_images/general/sc_icon_game_num8.png b/sc_images/general/sc_icon_game_num8.png new file mode 100644 index 0000000..87af444 Binary files /dev/null and b/sc_images/general/sc_icon_game_num8.png differ diff --git a/sc_images/general/sc_icon_game_num9.png b/sc_images/general/sc_icon_game_num9.png new file mode 100644 index 0000000..1d8b359 Binary files /dev/null and b/sc_images/general/sc_icon_game_num9.png differ diff --git a/sc_images/general/sc_icon_game_numk.png b/sc_images/general/sc_icon_game_numk.png new file mode 100644 index 0000000..55d02ea Binary files /dev/null and b/sc_images/general/sc_icon_game_numk.png differ diff --git a/sc_images/general/sc_icon_game_numxx.png b/sc_images/general/sc_icon_game_numxx.png new file mode 100644 index 0000000..afd1b75 Binary files /dev/null and b/sc_images/general/sc_icon_game_numxx.png differ diff --git a/sc_images/general/sc_icon_jb.png b/sc_images/general/sc_icon_jb.png new file mode 100644 index 0000000..8f67a7d Binary files /dev/null and b/sc_images/general/sc_icon_jb.png differ diff --git a/sc_images/general/sc_icon_jb2.png b/sc_images/general/sc_icon_jb2.png new file mode 100644 index 0000000..61ebaa2 Binary files /dev/null and b/sc_images/general/sc_icon_jb2.png differ diff --git a/sc_images/general/sc_icon_jb3.png b/sc_images/general/sc_icon_jb3.png new file mode 100644 index 0000000..e846949 Binary files /dev/null and b/sc_images/general/sc_icon_jb3.png differ diff --git a/sc_images/general/sc_icon_loading.png b/sc_images/general/sc_icon_loading.png new file mode 100644 index 0000000..6b06815 Binary files /dev/null and b/sc_images/general/sc_icon_loading.png differ diff --git a/sc_images/general/sc_icon_loading.webp b/sc_images/general/sc_icon_loading.webp new file mode 100644 index 0000000..31ef7b0 Binary files /dev/null and b/sc_images/general/sc_icon_loading.webp differ diff --git a/sc_images/general/sc_icon_logo.png b/sc_images/general/sc_icon_logo.png new file mode 100644 index 0000000..f534190 Binary files /dev/null and b/sc_images/general/sc_icon_logo.png differ diff --git a/sc_images/general/sc_icon_msg_tips_close.png b/sc_images/general/sc_icon_msg_tips_close.png new file mode 100644 index 0000000..70c3368 Binary files /dev/null and b/sc_images/general/sc_icon_msg_tips_close.png differ diff --git a/sc_images/general/sc_icon_no_data_icon.png b/sc_images/general/sc_icon_no_data_icon.png new file mode 100644 index 0000000..6b06815 Binary files /dev/null and b/sc_images/general/sc_icon_no_data_icon.png differ diff --git a/sc_images/general/sc_icon_no_data_icon2.png b/sc_images/general/sc_icon_no_data_icon2.png new file mode 100644 index 0000000..6b06815 Binary files /dev/null and b/sc_images/general/sc_icon_no_data_icon2.png differ diff --git a/sc_images/general/sc_icon_online_user.png b/sc_images/general/sc_icon_online_user.png new file mode 100644 index 0000000..cd99473 Binary files /dev/null and b/sc_images/general/sc_icon_online_user.png differ diff --git a/sc_images/general/sc_icon_pic_close.png b/sc_images/general/sc_icon_pic_close.png new file mode 100644 index 0000000..9aaedbb Binary files /dev/null and b/sc_images/general/sc_icon_pic_close.png differ diff --git a/sc_images/general/sc_icon_reort_add_pic.png b/sc_images/general/sc_icon_reort_add_pic.png new file mode 100644 index 0000000..221ac67 Binary files /dev/null and b/sc_images/general/sc_icon_reort_add_pic.png differ diff --git a/sc_images/general/sc_icon_search.png b/sc_images/general/sc_icon_search.png new file mode 100644 index 0000000..824e3f2 Binary files /dev/null and b/sc_images/general/sc_icon_search.png differ diff --git a/sc_images/general/sc_icon_select.png b/sc_images/general/sc_icon_select.png new file mode 100644 index 0000000..7985c81 Binary files /dev/null and b/sc_images/general/sc_icon_select.png differ diff --git a/sc_images/general/sc_icon_select_ok.png b/sc_images/general/sc_icon_select_ok.png new file mode 100644 index 0000000..bed001f Binary files /dev/null and b/sc_images/general/sc_icon_select_ok.png differ diff --git a/sc_images/general/sc_icon_select_un_ok.png b/sc_images/general/sc_icon_select_un_ok.png new file mode 100644 index 0000000..f93b0a7 Binary files /dev/null and b/sc_images/general/sc_icon_select_un_ok.png differ diff --git a/sc_images/general/sc_icon_setting_language.png b/sc_images/general/sc_icon_setting_language.png new file mode 100644 index 0000000..74703f4 Binary files /dev/null and b/sc_images/general/sc_icon_setting_language.png differ diff --git a/sc_images/general/sc_icon_share_facebook.png b/sc_images/general/sc_icon_share_facebook.png new file mode 100644 index 0000000..eafff71 Binary files /dev/null and b/sc_images/general/sc_icon_share_facebook.png differ diff --git a/sc_images/general/sc_icon_share_link.png b/sc_images/general/sc_icon_share_link.png new file mode 100644 index 0000000..20fac8c Binary files /dev/null and b/sc_images/general/sc_icon_share_link.png differ diff --git a/sc_images/general/sc_icon_share_snapchat.png b/sc_images/general/sc_icon_share_snapchat.png new file mode 100644 index 0000000..19673c4 Binary files /dev/null and b/sc_images/general/sc_icon_share_snapchat.png differ diff --git a/sc_images/general/sc_icon_share_whatsapp.png b/sc_images/general/sc_icon_share_whatsapp.png new file mode 100644 index 0000000..8e6b3f0 Binary files /dev/null and b/sc_images/general/sc_icon_share_whatsapp.png differ diff --git a/sc_images/general/sc_icon_social_privilege_close.png b/sc_images/general/sc_icon_social_privilege_close.png new file mode 100644 index 0000000..2886f80 Binary files /dev/null and b/sc_images/general/sc_icon_social_privilege_close.png differ diff --git a/sc_images/general/sc_icon_social_privilege_open.png b/sc_images/general/sc_icon_social_privilege_open.png new file mode 100644 index 0000000..7a4200d Binary files /dev/null and b/sc_images/general/sc_icon_social_privilege_open.png differ diff --git a/sc_images/general/sc_icon_social_privilege_select.png b/sc_images/general/sc_icon_social_privilege_select.png new file mode 100644 index 0000000..9848483 Binary files /dev/null and b/sc_images/general/sc_icon_social_privilege_select.png differ diff --git a/sc_images/general/sc_icon_social_privilege_unselect.png b/sc_images/general/sc_icon_social_privilege_unselect.png new file mode 100644 index 0000000..d4e431e Binary files /dev/null and b/sc_images/general/sc_icon_social_privilege_unselect.png differ diff --git a/sc_images/general/sc_icon_switch_off.png b/sc_images/general/sc_icon_switch_off.png new file mode 100644 index 0000000..5fe2575 Binary files /dev/null and b/sc_images/general/sc_icon_switch_off.png differ diff --git a/sc_images/general/sc_icon_switch_on.png b/sc_images/general/sc_icon_switch_on.png new file mode 100644 index 0000000..0513ca7 Binary files /dev/null and b/sc_images/general/sc_icon_switch_on.png differ diff --git a/sc_images/general/sc_icon_unselect.png b/sc_images/general/sc_icon_unselect.png new file mode 100644 index 0000000..1997b4d Binary files /dev/null and b/sc_images/general/sc_icon_unselect.png differ diff --git a/sc_images/index/sc_icon_add_room.png b/sc_images/index/sc_icon_add_room.png new file mode 100644 index 0000000..dcdb6b8 Binary files /dev/null and b/sc_images/index/sc_icon_add_room.png differ diff --git a/sc_images/index/sc_icon_admin_center.png b/sc_images/index/sc_icon_admin_center.png new file mode 100644 index 0000000..1257f4d Binary files /dev/null and b/sc_images/index/sc_icon_admin_center.png differ diff --git a/sc_images/index/sc_icon_agen_center.png b/sc_images/index/sc_icon_agen_center.png new file mode 100644 index 0000000..9629926 Binary files /dev/null and b/sc_images/index/sc_icon_agen_center.png differ diff --git a/sc_images/index/sc_icon_agent_center.png b/sc_images/index/sc_icon_agent_center.png new file mode 100644 index 0000000..42208a9 Binary files /dev/null and b/sc_images/index/sc_icon_agent_center.png differ diff --git a/sc_images/index/sc_icon_bag.png b/sc_images/index/sc_icon_bag.png new file mode 100644 index 0000000..9ced62c Binary files /dev/null and b/sc_images/index/sc_icon_bag.png differ diff --git a/sc_images/index/sc_icon_bd_center.png b/sc_images/index/sc_icon_bd_center.png new file mode 100644 index 0000000..4ef72f9 Binary files /dev/null and b/sc_images/index/sc_icon_bd_center.png differ diff --git a/sc_images/index/sc_icon_bd_leader.png b/sc_images/index/sc_icon_bd_leader.png new file mode 100644 index 0000000..1506731 Binary files /dev/null and b/sc_images/index/sc_icon_bd_leader.png differ diff --git a/sc_images/index/sc_icon_become_host_center.png b/sc_images/index/sc_icon_become_host_center.png new file mode 100644 index 0000000..d85d22c Binary files /dev/null and b/sc_images/index/sc_icon_become_host_center.png differ diff --git a/sc_images/index/sc_icon_claimed_text.png b/sc_images/index/sc_icon_claimed_text.png new file mode 100644 index 0000000..20d3d68 Binary files /dev/null and b/sc_images/index/sc_icon_claimed_text.png differ diff --git a/sc_images/index/sc_icon_coupon.png b/sc_images/index/sc_icon_coupon.png new file mode 100644 index 0000000..2fa32b6 Binary files /dev/null and b/sc_images/index/sc_icon_coupon.png differ diff --git a/sc_images/index/sc_icon_coupon_head_bg.png b/sc_images/index/sc_icon_coupon_head_bg.png new file mode 100644 index 0000000..92ac8af Binary files /dev/null and b/sc_images/index/sc_icon_coupon_head_bg.png differ diff --git a/sc_images/index/sc_icon_coupon_recod.png b/sc_images/index/sc_icon_coupon_recod.png new file mode 100644 index 0000000..136c829 Binary files /dev/null and b/sc_images/index/sc_icon_coupon_recod.png differ diff --git a/sc_images/index/sc_icon_dynamic_en.png b/sc_images/index/sc_icon_dynamic_en.png new file mode 100644 index 0000000..930a425 Binary files /dev/null and b/sc_images/index/sc_icon_dynamic_en.png differ diff --git a/sc_images/index/sc_icon_dynamic_no.png b/sc_images/index/sc_icon_dynamic_no.png new file mode 100644 index 0000000..08ef3fb Binary files /dev/null and b/sc_images/index/sc_icon_dynamic_no.png differ diff --git a/sc_images/index/sc_icon_explore_en.png b/sc_images/index/sc_icon_explore_en.png new file mode 100644 index 0000000..91a118f Binary files /dev/null and b/sc_images/index/sc_icon_explore_en.png differ diff --git a/sc_images/index/sc_icon_explore_no.png b/sc_images/index/sc_icon_explore_no.png new file mode 100644 index 0000000..3f5891d Binary files /dev/null and b/sc_images/index/sc_icon_explore_no.png differ diff --git a/sc_images/index/sc_icon_first_recharge_ar_text.png b/sc_images/index/sc_icon_first_recharge_ar_text.png new file mode 100644 index 0000000..8a1e8b7 Binary files /dev/null and b/sc_images/index/sc_icon_first_recharge_ar_text.png differ diff --git a/sc_images/index/sc_icon_first_recharge_btn.png b/sc_images/index/sc_icon_first_recharge_btn.png new file mode 100644 index 0000000..d6ec337 Binary files /dev/null and b/sc_images/index/sc_icon_first_recharge_btn.png differ diff --git a/sc_images/index/sc_icon_first_recharge_en_text.png b/sc_images/index/sc_icon_first_recharge_en_text.png new file mode 100644 index 0000000..bdbfe9b Binary files /dev/null and b/sc_images/index/sc_icon_first_recharge_en_text.png differ diff --git a/sc_images/index/sc_icon_first_recharge_room_tag.png b/sc_images/index/sc_icon_first_recharge_room_tag.png new file mode 100644 index 0000000..dfceb82 Binary files /dev/null and b/sc_images/index/sc_icon_first_recharge_room_tag.png differ diff --git a/sc_images/index/sc_icon_game_en.png b/sc_images/index/sc_icon_game_en.png new file mode 100644 index 0000000..95a53fd Binary files /dev/null and b/sc_images/index/sc_icon_game_en.png differ diff --git a/sc_images/index/sc_icon_gamebroad_lv1_bg.webp b/sc_images/index/sc_icon_gamebroad_lv1_bg.webp new file mode 100644 index 0000000..c4f00db Binary files /dev/null and b/sc_images/index/sc_icon_gamebroad_lv1_bg.webp differ diff --git a/sc_images/index/sc_icon_gamebroad_lv2_bg.webp b/sc_images/index/sc_icon_gamebroad_lv2_bg.webp new file mode 100644 index 0000000..8047272 Binary files /dev/null and b/sc_images/index/sc_icon_gamebroad_lv2_bg.webp differ diff --git a/sc_images/index/sc_icon_gamebroad_lv3_bg.webp b/sc_images/index/sc_icon_gamebroad_lv3_bg.webp new file mode 100644 index 0000000..f6f44f5 Binary files /dev/null and b/sc_images/index/sc_icon_gamebroad_lv3_bg.webp differ diff --git a/sc_images/index/sc_icon_gamebroad_lv4_bg.webp b/sc_images/index/sc_icon_gamebroad_lv4_bg.webp new file mode 100644 index 0000000..09082aa Binary files /dev/null and b/sc_images/index/sc_icon_gamebroad_lv4_bg.webp differ diff --git a/sc_images/index/sc_icon_gamebroad_lv5_bg.webp b/sc_images/index/sc_icon_gamebroad_lv5_bg.webp new file mode 100644 index 0000000..742e025 Binary files /dev/null and b/sc_images/index/sc_icon_gamebroad_lv5_bg.webp differ diff --git a/sc_images/index/sc_icon_home_en.png b/sc_images/index/sc_icon_home_en.png new file mode 100644 index 0000000..a46f891 Binary files /dev/null and b/sc_images/index/sc_icon_home_en.png differ diff --git a/sc_images/index/sc_icon_home_no.png b/sc_images/index/sc_icon_home_no.png new file mode 100644 index 0000000..585720d Binary files /dev/null and b/sc_images/index/sc_icon_home_no.png differ diff --git a/sc_images/index/sc_icon_honor.png b/sc_images/index/sc_icon_honor.png new file mode 100644 index 0000000..e3e856c Binary files /dev/null and b/sc_images/index/sc_icon_honor.png differ diff --git a/sc_images/index/sc_icon_honor_detail_bg.png b/sc_images/index/sc_icon_honor_detail_bg.png new file mode 100644 index 0000000..26fa8ec Binary files /dev/null and b/sc_images/index/sc_icon_honor_detail_bg.png differ diff --git a/sc_images/index/sc_icon_honor_item_bg.png b/sc_images/index/sc_icon_honor_item_bg.png new file mode 100644 index 0000000..91be972 Binary files /dev/null and b/sc_images/index/sc_icon_honor_item_bg.png differ diff --git a/sc_images/index/sc_icon_honor_userinfo_bg.png b/sc_images/index/sc_icon_honor_userinfo_bg.png new file mode 100644 index 0000000..db1295f Binary files /dev/null and b/sc_images/index/sc_icon_honor_userinfo_bg.png differ diff --git a/sc_images/index/sc_icon_honor_userinfo_btn.png b/sc_images/index/sc_icon_honor_userinfo_btn.png new file mode 100644 index 0000000..bc55fd1 Binary files /dev/null and b/sc_images/index/sc_icon_honor_userinfo_btn.png differ diff --git a/sc_images/index/sc_icon_host_center.png b/sc_images/index/sc_icon_host_center.png new file mode 100644 index 0000000..a63365e Binary files /dev/null and b/sc_images/index/sc_icon_host_center.png differ diff --git a/sc_images/index/sc_icon_hotgames_tag_bg.png b/sc_images/index/sc_icon_hotgames_tag_bg.png new file mode 100644 index 0000000..3986db6 Binary files /dev/null and b/sc_images/index/sc_icon_hotgames_tag_bg.png differ diff --git a/sc_images/index/sc_icon_index_bg.png b/sc_images/index/sc_icon_index_bg.png new file mode 100644 index 0000000..678d0bd Binary files /dev/null and b/sc_images/index/sc_icon_index_bg.png differ diff --git a/sc_images/index/sc_icon_index_creat_room_tag.png b/sc_images/index/sc_icon_index_creat_room_tag.png new file mode 100644 index 0000000..1ca1552 Binary files /dev/null and b/sc_images/index/sc_icon_index_creat_room_tag.png differ diff --git a/sc_images/index/sc_icon_index_room_brd.png b/sc_images/index/sc_icon_index_room_brd.png new file mode 100644 index 0000000..ce2c74a Binary files /dev/null and b/sc_images/index/sc_icon_index_room_brd.png differ diff --git a/sc_images/index/sc_icon_index_room_model_1.png b/sc_images/index/sc_icon_index_room_model_1.png new file mode 100644 index 0000000..5af08a8 Binary files /dev/null and b/sc_images/index/sc_icon_index_room_model_1.png differ diff --git a/sc_images/index/sc_icon_index_room_model_2.png b/sc_images/index/sc_icon_index_room_model_2.png new file mode 100644 index 0000000..c567ab8 Binary files /dev/null and b/sc_images/index/sc_icon_index_room_model_2.png differ diff --git a/sc_images/index/sc_icon_invite_new_users_to_earn_coins.png b/sc_images/index/sc_icon_invite_new_users_to_earn_coins.png new file mode 100644 index 0000000..b7c9e47 Binary files /dev/null and b/sc_images/index/sc_icon_invite_new_users_to_earn_coins.png differ diff --git a/sc_images/index/sc_icon_leader_spinner_charm_bg.png b/sc_images/index/sc_icon_leader_spinner_charm_bg.png new file mode 100644 index 0000000..41cc9e9 Binary files /dev/null and b/sc_images/index/sc_icon_leader_spinner_charm_bg.png differ diff --git a/sc_images/index/sc_icon_leader_spinner_room_bg.png b/sc_images/index/sc_icon_leader_spinner_room_bg.png new file mode 100644 index 0000000..e5528c7 Binary files /dev/null and b/sc_images/index/sc_icon_leader_spinner_room_bg.png differ diff --git a/sc_images/index/sc_icon_leader_spinner_wealth_bg.png b/sc_images/index/sc_icon_leader_spinner_wealth_bg.png new file mode 100644 index 0000000..620d877 Binary files /dev/null and b/sc_images/index/sc_icon_leader_spinner_wealth_bg.png differ diff --git a/sc_images/index/sc_icon_level.png b/sc_images/index/sc_icon_level.png new file mode 100644 index 0000000..7f156a8 Binary files /dev/null and b/sc_images/index/sc_icon_level.png differ diff --git a/sc_images/index/sc_icon_me_en.png b/sc_images/index/sc_icon_me_en.png new file mode 100644 index 0000000..30956ba Binary files /dev/null and b/sc_images/index/sc_icon_me_en.png differ diff --git a/sc_images/index/sc_icon_me_no.png b/sc_images/index/sc_icon_me_no.png new file mode 100644 index 0000000..c036ed8 Binary files /dev/null and b/sc_images/index/sc_icon_me_no.png differ diff --git a/sc_images/index/sc_icon_medal_detail_bg.png b/sc_images/index/sc_icon_medal_detail_bg.png new file mode 100644 index 0000000..c8cebae Binary files /dev/null and b/sc_images/index/sc_icon_medal_detail_bg.png differ diff --git a/sc_images/index/sc_icon_medals.png b/sc_images/index/sc_icon_medals.png new file mode 100644 index 0000000..6d9e09a Binary files /dev/null and b/sc_images/index/sc_icon_medals.png differ diff --git a/sc_images/index/sc_icon_medals_bg.png b/sc_images/index/sc_icon_medals_bg.png new file mode 100644 index 0000000..4131dca Binary files /dev/null and b/sc_images/index/sc_icon_medals_bg.png differ diff --git a/sc_images/index/sc_icon_medals_en.png b/sc_images/index/sc_icon_medals_en.png new file mode 100644 index 0000000..da3a4aa Binary files /dev/null and b/sc_images/index/sc_icon_medals_en.png differ diff --git a/sc_images/index/sc_icon_medals_no.png b/sc_images/index/sc_icon_medals_no.png new file mode 100644 index 0000000..94c5860 Binary files /dev/null and b/sc_images/index/sc_icon_medals_no.png differ diff --git a/sc_images/index/sc_icon_medals_userinfo_bg.png b/sc_images/index/sc_icon_medals_userinfo_bg.png new file mode 100644 index 0000000..363e049 Binary files /dev/null and b/sc_images/index/sc_icon_medals_userinfo_bg.png differ diff --git a/sc_images/index/sc_icon_message_en.png b/sc_images/index/sc_icon_message_en.png new file mode 100644 index 0000000..d8813e4 Binary files /dev/null and b/sc_images/index/sc_icon_message_en.png differ diff --git a/sc_images/index/sc_icon_message_no.png b/sc_images/index/sc_icon_message_no.png new file mode 100644 index 0000000..c72b71c Binary files /dev/null and b/sc_images/index/sc_icon_message_no.png differ diff --git a/sc_images/index/sc_icon_my_drawer_item_bg.png b/sc_images/index/sc_icon_my_drawer_item_bg.png new file mode 100644 index 0000000..ebf9d83 Binary files /dev/null and b/sc_images/index/sc_icon_my_drawer_item_bg.png differ diff --git a/sc_images/index/sc_icon_my_items.png b/sc_images/index/sc_icon_my_items.png new file mode 100644 index 0000000..8c15a28 Binary files /dev/null and b/sc_images/index/sc_icon_my_items.png differ diff --git a/sc_images/index/sc_icon_my_rechage_title.png b/sc_images/index/sc_icon_my_rechage_title.png new file mode 100644 index 0000000..29af297 Binary files /dev/null and b/sc_images/index/sc_icon_my_rechage_title.png differ diff --git a/sc_images/index/sc_icon_my_room_has_bg.png b/sc_images/index/sc_icon_my_room_has_bg.png new file mode 100644 index 0000000..41bb4c2 Binary files /dev/null and b/sc_images/index/sc_icon_my_room_has_bg.png differ diff --git a/sc_images/index/sc_icon_my_room_no_bg.png b/sc_images/index/sc_icon_my_room_no_bg.png new file mode 100644 index 0000000..ec4b0cb Binary files /dev/null and b/sc_images/index/sc_icon_my_room_no_bg.png differ diff --git a/sc_images/index/sc_icon_my_room_tag2.png b/sc_images/index/sc_icon_my_room_tag2.png new file mode 100644 index 0000000..0376192 Binary files /dev/null and b/sc_images/index/sc_icon_my_room_tag2.png differ diff --git a/sc_images/index/sc_icon_paid.png b/sc_images/index/sc_icon_paid.png new file mode 100644 index 0000000..afa5abe Binary files /dev/null and b/sc_images/index/sc_icon_paid.png differ diff --git a/sc_images/index/sc_icon_recharge_agency.png b/sc_images/index/sc_icon_recharge_agency.png new file mode 100644 index 0000000..a316fa0 Binary files /dev/null and b/sc_images/index/sc_icon_recharge_agency.png differ diff --git a/sc_images/index/sc_icon_recharge_recod.png b/sc_images/index/sc_icon_recharge_recod.png new file mode 100644 index 0000000..d343791 Binary files /dev/null and b/sc_images/index/sc_icon_recharge_recod.png differ diff --git a/sc_images/index/sc_icon_room_bord.png b/sc_images/index/sc_icon_room_bord.png new file mode 100644 index 0000000..ab7757e Binary files /dev/null and b/sc_images/index/sc_icon_room_bord.png differ diff --git a/sc_images/index/sc_icon_room_flot_ani.gif b/sc_images/index/sc_icon_room_flot_ani.gif new file mode 100644 index 0000000..39f080d Binary files /dev/null and b/sc_images/index/sc_icon_room_flot_ani.gif differ diff --git a/sc_images/index/sc_icon_room_flot_close.png b/sc_images/index/sc_icon_room_flot_close.png new file mode 100644 index 0000000..5ffd54e Binary files /dev/null and b/sc_images/index/sc_icon_room_flot_close.png differ diff --git a/sc_images/index/sc_icon_room_suo.png b/sc_images/index/sc_icon_room_suo.png new file mode 100644 index 0000000..96864dc Binary files /dev/null and b/sc_images/index/sc_icon_room_suo.png differ diff --git a/sc_images/index/sc_icon_serach.png b/sc_images/index/sc_icon_serach.png new file mode 100644 index 0000000..e49a701 Binary files /dev/null and b/sc_images/index/sc_icon_serach.png differ diff --git a/sc_images/index/sc_icon_serach2.png b/sc_images/index/sc_icon_serach2.png new file mode 100644 index 0000000..954dfc6 Binary files /dev/null and b/sc_images/index/sc_icon_serach2.png differ diff --git a/sc_images/index/sc_icon_settings.png b/sc_images/index/sc_icon_settings.png new file mode 100644 index 0000000..055c59e Binary files /dev/null and b/sc_images/index/sc_icon_settings.png differ diff --git a/sc_images/index/sc_icon_sgin_bg.png b/sc_images/index/sc_icon_sgin_bg.png new file mode 100644 index 0000000..3fffda0 Binary files /dev/null and b/sc_images/index/sc_icon_sgin_bg.png differ diff --git a/sc_images/index/sc_icon_sgin_item_bg1.png b/sc_images/index/sc_icon_sgin_item_bg1.png new file mode 100644 index 0000000..d651baa Binary files /dev/null and b/sc_images/index/sc_icon_sgin_item_bg1.png differ diff --git a/sc_images/index/sc_icon_sgin_item_bg2.png b/sc_images/index/sc_icon_sgin_item_bg2.png new file mode 100644 index 0000000..20f6bbe Binary files /dev/null and b/sc_images/index/sc_icon_sgin_item_bg2.png differ diff --git a/sc_images/index/sc_icon_sgin_item_bg3.png b/sc_images/index/sc_icon_sgin_item_bg3.png new file mode 100644 index 0000000..48933e6 Binary files /dev/null and b/sc_images/index/sc_icon_sgin_item_bg3.png differ diff --git a/sc_images/index/sc_icon_sgin_item_day7_bg1.png b/sc_images/index/sc_icon_sgin_item_day7_bg1.png new file mode 100644 index 0000000..fb4b578 Binary files /dev/null and b/sc_images/index/sc_icon_sgin_item_day7_bg1.png differ diff --git a/sc_images/index/sc_icon_sgin_item_day7_bg2.png b/sc_images/index/sc_icon_sgin_item_day7_bg2.png new file mode 100644 index 0000000..0b4d97c Binary files /dev/null and b/sc_images/index/sc_icon_sgin_item_day7_bg2.png differ diff --git a/sc_images/index/sc_icon_sgin_item_day7_bg3.png b/sc_images/index/sc_icon_sgin_item_day7_bg3.png new file mode 100644 index 0000000..e3319c0 Binary files /dev/null and b/sc_images/index/sc_icon_sgin_item_day7_bg3.png differ diff --git a/sc_images/index/sc_icon_sgin_rec_bg.png b/sc_images/index/sc_icon_sgin_rec_bg.png new file mode 100644 index 0000000..5bbf7c1 Binary files /dev/null and b/sc_images/index/sc_icon_sgin_rec_bg.png differ diff --git a/sc_images/index/sc_icon_shop.png b/sc_images/index/sc_icon_shop.png new file mode 100644 index 0000000..0fca3ef Binary files /dev/null and b/sc_images/index/sc_icon_shop.png differ diff --git a/sc_images/index/sc_icon_signedin_bg.png b/sc_images/index/sc_icon_signedin_bg.png new file mode 100644 index 0000000..b298cd4 Binary files /dev/null and b/sc_images/index/sc_icon_signedin_bg.png differ diff --git a/sc_images/index/sc_icon_splash_cp_name_bg.png b/sc_images/index/sc_icon_splash_cp_name_bg.png new file mode 100644 index 0000000..5648d17 Binary files /dev/null and b/sc_images/index/sc_icon_splash_cp_name_bg.png differ diff --git a/sc_images/index/sc_icon_splash_king_games_name_bg.png b/sc_images/index/sc_icon_splash_king_games_name_bg.png new file mode 100644 index 0000000..5ee51b8 Binary files /dev/null and b/sc_images/index/sc_icon_splash_king_games_name_bg.png differ diff --git a/sc_images/index/sc_icon_task.png b/sc_images/index/sc_icon_task.png new file mode 100644 index 0000000..e65cee6 Binary files /dev/null and b/sc_images/index/sc_icon_task.png differ diff --git a/sc_images/index/sc_icon_task_exp.png b/sc_images/index/sc_icon_task_exp.png new file mode 100644 index 0000000..1ef327a Binary files /dev/null and b/sc_images/index/sc_icon_task_exp.png differ diff --git a/sc_images/index/sc_icon_task_head_bg.png b/sc_images/index/sc_icon_task_head_bg.png new file mode 100644 index 0000000..27e0496 Binary files /dev/null and b/sc_images/index/sc_icon_task_head_bg.png differ diff --git a/sc_images/index/sc_icon_wallet_bg.png b/sc_images/index/sc_icon_wallet_bg.png new file mode 100644 index 0000000..5a963e0 Binary files /dev/null and b/sc_images/index/sc_icon_wallet_bg.png differ diff --git a/sc_images/index/sc_icon_wallet_icon.png b/sc_images/index/sc_icon_wallet_icon.png new file mode 100644 index 0000000..6c6177e Binary files /dev/null and b/sc_images/index/sc_icon_wallet_icon.png differ diff --git a/sc_images/index/sc_icon_wear_honor_dialog_bg.png b/sc_images/index/sc_icon_wear_honor_dialog_bg.png new file mode 100644 index 0000000..f756fd9 Binary files /dev/null and b/sc_images/index/sc_icon_wear_honor_dialog_bg.png differ diff --git a/sc_images/index/sc_icon_wear_honor_dialog_item_on_use.png b/sc_images/index/sc_icon_wear_honor_dialog_item_on_use.png new file mode 100644 index 0000000..a6328ec Binary files /dev/null and b/sc_images/index/sc_icon_wear_honor_dialog_item_on_use.png differ diff --git a/sc_images/index/sc_icon_wear_honor_dialog_item_un_use.png b/sc_images/index/sc_icon_wear_honor_dialog_item_un_use.png new file mode 100644 index 0000000..5e71612 Binary files /dev/null and b/sc_images/index/sc_icon_wear_honor_dialog_item_un_use.png differ diff --git a/sc_images/index/sc_index_bottom_navigation_bar_bg.png b/sc_images/index/sc_index_bottom_navigation_bar_bg.png new file mode 100644 index 0000000..71dc816 Binary files /dev/null and b/sc_images/index/sc_index_bottom_navigation_bar_bg.png differ diff --git a/sc_images/index/sc_index_msg_content_bg.png b/sc_images/index/sc_index_msg_content_bg.png new file mode 100644 index 0000000..4c61ae3 Binary files /dev/null and b/sc_images/index/sc_index_msg_content_bg.png differ diff --git a/sc_images/level/sc_icon_user_level_10_20.png b/sc_images/level/sc_icon_user_level_10_20.png new file mode 100644 index 0000000..ccfb822 Binary files /dev/null and b/sc_images/level/sc_icon_user_level_10_20.png differ diff --git a/sc_images/level/sc_icon_user_level_1_10.png b/sc_images/level/sc_icon_user_level_1_10.png new file mode 100644 index 0000000..0fff9df Binary files /dev/null and b/sc_images/level/sc_icon_user_level_1_10.png differ diff --git a/sc_images/level/sc_icon_user_level_20_30.png b/sc_images/level/sc_icon_user_level_20_30.png new file mode 100644 index 0000000..c6a9299 Binary files /dev/null and b/sc_images/level/sc_icon_user_level_20_30.png differ diff --git a/sc_images/level/sc_icon_user_level_30_40.png b/sc_images/level/sc_icon_user_level_30_40.png new file mode 100644 index 0000000..13322ce Binary files /dev/null and b/sc_images/level/sc_icon_user_level_30_40.png differ diff --git a/sc_images/level/sc_icon_user_level_40_50.png b/sc_images/level/sc_icon_user_level_40_50.png new file mode 100644 index 0000000..af0998d Binary files /dev/null and b/sc_images/level/sc_icon_user_level_40_50.png differ diff --git a/sc_images/level/sc_icon_user_level_center_bg_1.png b/sc_images/level/sc_icon_user_level_center_bg_1.png new file mode 100644 index 0000000..b1a0fdd Binary files /dev/null and b/sc_images/level/sc_icon_user_level_center_bg_1.png differ diff --git a/sc_images/level/sc_icon_user_level_center_bg_2.png b/sc_images/level/sc_icon_user_level_center_bg_2.png new file mode 100644 index 0000000..7e52f10 Binary files /dev/null and b/sc_images/level/sc_icon_user_level_center_bg_2.png differ diff --git a/sc_images/level/sc_icon_user_level_user_info_bg.png b/sc_images/level/sc_icon_user_level_user_info_bg.png new file mode 100644 index 0000000..a1d3526 Binary files /dev/null and b/sc_images/level/sc_icon_user_level_user_info_bg.png differ diff --git a/sc_images/level/sc_icon_user_level_wealth_info_bg.png b/sc_images/level/sc_icon_user_level_wealth_info_bg.png new file mode 100644 index 0000000..39b61fa Binary files /dev/null and b/sc_images/level/sc_icon_user_level_wealth_info_bg.png differ diff --git a/sc_images/level/sc_icon_user_wealth_center_bg_1.png b/sc_images/level/sc_icon_user_wealth_center_bg_1.png new file mode 100644 index 0000000..dea02ea Binary files /dev/null and b/sc_images/level/sc_icon_user_wealth_center_bg_1.png differ diff --git a/sc_images/level/sc_icon_user_wealth_center_bg_2.png b/sc_images/level/sc_icon_user_wealth_center_bg_2.png new file mode 100644 index 0000000..5c5ecd0 Binary files /dev/null and b/sc_images/level/sc_icon_user_wealth_center_bg_2.png differ diff --git a/sc_images/level/sc_icon_wealth_level_10_20.png b/sc_images/level/sc_icon_wealth_level_10_20.png new file mode 100644 index 0000000..a1cb04c Binary files /dev/null and b/sc_images/level/sc_icon_wealth_level_10_20.png differ diff --git a/sc_images/level/sc_icon_wealth_level_1_10.png b/sc_images/level/sc_icon_wealth_level_1_10.png new file mode 100644 index 0000000..43ef9f0 Binary files /dev/null and b/sc_images/level/sc_icon_wealth_level_1_10.png differ diff --git a/sc_images/level/sc_icon_wealth_level_20_30.png b/sc_images/level/sc_icon_wealth_level_20_30.png new file mode 100644 index 0000000..25aebf9 Binary files /dev/null and b/sc_images/level/sc_icon_wealth_level_20_30.png differ diff --git a/sc_images/level/sc_icon_wealth_level_30_40.png b/sc_images/level/sc_icon_wealth_level_30_40.png new file mode 100644 index 0000000..bf80fab Binary files /dev/null and b/sc_images/level/sc_icon_wealth_level_30_40.png differ diff --git a/sc_images/level/sc_icon_wealth_level_40_50.png b/sc_images/level/sc_icon_wealth_level_40_50.png new file mode 100644 index 0000000..5a4af5d Binary files /dev/null and b/sc_images/level/sc_icon_wealth_level_40_50.png differ diff --git a/sc_images/login/sc_icon_account.png b/sc_images/login/sc_icon_account.png new file mode 100644 index 0000000..6a68d9b Binary files /dev/null and b/sc_images/login/sc_icon_account.png differ diff --git a/sc_images/login/sc_icon_avar_sex_man.png b/sc_images/login/sc_icon_avar_sex_man.png new file mode 100644 index 0000000..adf3d2f Binary files /dev/null and b/sc_images/login/sc_icon_avar_sex_man.png differ diff --git a/sc_images/login/sc_icon_avar_sex_woman.png b/sc_images/login/sc_icon_avar_sex_woman.png new file mode 100644 index 0000000..b2f32d3 Binary files /dev/null and b/sc_images/login/sc_icon_avar_sex_woman.png differ diff --git a/sc_images/login/sc_icon_google.png b/sc_images/login/sc_icon_google.png new file mode 100644 index 0000000..445a5da Binary files /dev/null and b/sc_images/login/sc_icon_google.png differ diff --git a/sc_images/login/sc_icon_iphone.png b/sc_images/login/sc_icon_iphone.png new file mode 100644 index 0000000..85fc3dd Binary files /dev/null and b/sc_images/login/sc_icon_iphone.png differ diff --git a/sc_images/login/sc_icon_login_edit_data_bg.png b/sc_images/login/sc_icon_login_edit_data_bg.png new file mode 100644 index 0000000..77b7038 Binary files /dev/null and b/sc_images/login/sc_icon_login_edit_data_bg.png differ diff --git a/sc_images/login/sc_icon_login_ser_select.png b/sc_images/login/sc_icon_login_ser_select.png new file mode 100644 index 0000000..cfd5fdf Binary files /dev/null and b/sc_images/login/sc_icon_login_ser_select.png differ diff --git a/sc_images/login/sc_icon_login_ser_select_un.png b/sc_images/login/sc_icon_login_ser_select_un.png new file mode 100644 index 0000000..21e7afb Binary files /dev/null and b/sc_images/login/sc_icon_login_ser_select_un.png differ diff --git a/sc_images/login/sc_icon_pass.png b/sc_images/login/sc_icon_pass.png new file mode 100644 index 0000000..f29f56a Binary files /dev/null and b/sc_images/login/sc_icon_pass.png differ diff --git a/sc_images/login/sc_icon_pass1.png b/sc_images/login/sc_icon_pass1.png new file mode 100644 index 0000000..f01d1c4 Binary files /dev/null and b/sc_images/login/sc_icon_pass1.png differ diff --git a/sc_images/login/sc_icon_sc.png b/sc_images/login/sc_icon_sc.png new file mode 100644 index 0000000..07dd788 Binary files /dev/null and b/sc_images/login/sc_icon_sc.png differ diff --git a/sc_images/login/sc_icon_sex_man.png b/sc_images/login/sc_icon_sex_man.png new file mode 100644 index 0000000..3711aa3 Binary files /dev/null and b/sc_images/login/sc_icon_sex_man.png differ diff --git a/sc_images/login/sc_icon_sex_man_bg.png b/sc_images/login/sc_icon_sex_man_bg.png new file mode 100644 index 0000000..80a34ee Binary files /dev/null and b/sc_images/login/sc_icon_sex_man_bg.png differ diff --git a/sc_images/login/sc_icon_sex_woman.png b/sc_images/login/sc_icon_sex_woman.png new file mode 100644 index 0000000..a216859 Binary files /dev/null and b/sc_images/login/sc_icon_sex_woman.png differ diff --git a/sc_images/login/sc_icon_sex_woman_bg.png b/sc_images/login/sc_icon_sex_woman_bg.png new file mode 100644 index 0000000..cd2d578 Binary files /dev/null and b/sc_images/login/sc_icon_sex_woman_bg.png differ diff --git a/sc_images/msg/im_noti_music.MP3 b/sc_images/msg/im_noti_music.MP3 new file mode 100644 index 0000000..f3b58b2 Binary files /dev/null and b/sc_images/msg/im_noti_music.MP3 differ diff --git a/sc_images/msg/sc_icon_add.png b/sc_images/msg/sc_icon_add.png new file mode 100644 index 0000000..e100b83 Binary files /dev/null and b/sc_images/msg/sc_icon_add.png differ diff --git a/sc_images/msg/sc_icon_chat_key.png b/sc_images/msg/sc_icon_chat_key.png new file mode 100644 index 0000000..a7d6b02 Binary files /dev/null and b/sc_images/msg/sc_icon_chat_key.png differ diff --git a/sc_images/msg/sc_icon_chat_message_send.png b/sc_images/msg/sc_icon_chat_message_send.png new file mode 100644 index 0000000..1357033 Binary files /dev/null and b/sc_images/msg/sc_icon_chat_message_send.png differ diff --git a/sc_images/msg/sc_icon_hongbao.png b/sc_images/msg/sc_icon_hongbao.png new file mode 100644 index 0000000..0dcbd53 Binary files /dev/null and b/sc_images/msg/sc_icon_hongbao.png differ diff --git a/sc_images/msg/sc_icon_message_activity.png b/sc_images/msg/sc_icon_message_activity.png new file mode 100644 index 0000000..7b13eb4 Binary files /dev/null and b/sc_images/msg/sc_icon_message_activity.png differ diff --git a/sc_images/msg/sc_icon_message_noti.png b/sc_images/msg/sc_icon_message_noti.png new file mode 100644 index 0000000..1d3bf04 Binary files /dev/null and b/sc_images/msg/sc_icon_message_noti.png differ diff --git a/sc_images/msg/sc_icon_message_system.png b/sc_images/msg/sc_icon_message_system.png new file mode 100644 index 0000000..73761d8 Binary files /dev/null and b/sc_images/msg/sc_icon_message_system.png differ diff --git a/sc_images/msg/sc_icon_msg_emoji.png b/sc_images/msg/sc_icon_msg_emoji.png new file mode 100644 index 0000000..a26b48c Binary files /dev/null and b/sc_images/msg/sc_icon_msg_emoji.png differ diff --git a/sc_images/msg/sc_icon_msg_menu_copy.png b/sc_images/msg/sc_icon_msg_menu_copy.png new file mode 100644 index 0000000..92fcd10 Binary files /dev/null and b/sc_images/msg/sc_icon_msg_menu_copy.png differ diff --git a/sc_images/msg/sc_icon_msg_menu_delete.png b/sc_images/msg/sc_icon_msg_menu_delete.png new file mode 100644 index 0000000..0b401fd Binary files /dev/null and b/sc_images/msg/sc_icon_msg_menu_delete.png differ diff --git a/sc_images/msg/sc_icon_msg_menu_recall.png b/sc_images/msg/sc_icon_msg_menu_recall.png new file mode 100644 index 0000000..bbcd32a Binary files /dev/null and b/sc_images/msg/sc_icon_msg_menu_recall.png differ diff --git a/sc_images/msg/sc_icon_notifcation_title_bg.png b/sc_images/msg/sc_icon_notifcation_title_bg.png new file mode 100644 index 0000000..9a4e324 Binary files /dev/null and b/sc_images/msg/sc_icon_notifcation_title_bg.png differ diff --git a/sc_images/msg/sc_icon_notifcation_title_bg_ar.png b/sc_images/msg/sc_icon_notifcation_title_bg_ar.png new file mode 100644 index 0000000..61dd5da Binary files /dev/null and b/sc_images/msg/sc_icon_notifcation_title_bg_ar.png differ diff --git a/sc_images/msg/sc_icon_red_envelopes_msg_item_bg.png b/sc_images/msg/sc_icon_red_envelopes_msg_item_bg.png new file mode 100644 index 0000000..aa22106 Binary files /dev/null and b/sc_images/msg/sc_icon_red_envelopes_msg_item_bg.png differ diff --git a/sc_images/msg/sc_icon_system_title_bg.png b/sc_images/msg/sc_icon_system_title_bg.png new file mode 100644 index 0000000..d7c3215 Binary files /dev/null and b/sc_images/msg/sc_icon_system_title_bg.png differ diff --git a/sc_images/msg/sc_icon_system_title_bg_ar.png b/sc_images/msg/sc_icon_system_title_bg_ar.png new file mode 100644 index 0000000..d04332a Binary files /dev/null and b/sc_images/msg/sc_icon_system_title_bg_ar.png differ diff --git a/sc_images/msg/sc_icon_tupian.png b/sc_images/msg/sc_icon_tupian.png new file mode 100644 index 0000000..374f183 Binary files /dev/null and b/sc_images/msg/sc_icon_tupian.png differ diff --git a/sc_images/msg/sc_icon_xiangji.png b/sc_images/msg/sc_icon_xiangji.png new file mode 100644 index 0000000..bfdaf80 Binary files /dev/null and b/sc_images/msg/sc_icon_xiangji.png differ diff --git a/sc_images/person/sc_icon_cp_head_ring.png b/sc_images/person/sc_icon_cp_head_ring.png new file mode 100644 index 0000000..3f156ad Binary files /dev/null and b/sc_images/person/sc_icon_cp_head_ring.png differ diff --git a/sc_images/person/sc_icon_cp_head_ring2.png b/sc_images/person/sc_icon_cp_head_ring2.png new file mode 100644 index 0000000..65aac50 Binary files /dev/null and b/sc_images/person/sc_icon_cp_head_ring2.png differ diff --git a/sc_images/person/sc_icon_cp_helpe.png b/sc_images/person/sc_icon_cp_helpe.png new file mode 100644 index 0000000..f8ffa42 Binary files /dev/null and b/sc_images/person/sc_icon_cp_helpe.png differ diff --git a/sc_images/person/sc_icon_cp_opt_bg.png b/sc_images/person/sc_icon_cp_opt_bg.png new file mode 100644 index 0000000..99aab1a Binary files /dev/null and b/sc_images/person/sc_icon_cp_opt_bg.png differ diff --git a/sc_images/person/sc_icon_cp_value_tag.png b/sc_images/person/sc_icon_cp_value_tag.png new file mode 100644 index 0000000..47a5128 Binary files /dev/null and b/sc_images/person/sc_icon_cp_value_tag.png differ diff --git a/sc_images/person/sc_icon_edit_user_info2.png b/sc_images/person/sc_icon_edit_user_info2.png new file mode 100644 index 0000000..30185ff Binary files /dev/null and b/sc_images/person/sc_icon_edit_user_info2.png differ diff --git a/sc_images/person/sc_icon_edit_userinfo_bg.png b/sc_images/person/sc_icon_edit_userinfo_bg.png new file mode 100644 index 0000000..cc9d327 Binary files /dev/null and b/sc_images/person/sc_icon_edit_userinfo_bg.png differ diff --git a/sc_images/person/sc_icon_giftwall_item_bg.png b/sc_images/person/sc_icon_giftwall_item_bg.png new file mode 100644 index 0000000..d763a08 Binary files /dev/null and b/sc_images/person/sc_icon_giftwall_item_bg.png differ diff --git a/sc_images/person/sc_icon_honor_item_a_bg.png b/sc_images/person/sc_icon_honor_item_a_bg.png new file mode 100644 index 0000000..0c5137a Binary files /dev/null and b/sc_images/person/sc_icon_honor_item_a_bg.png differ diff --git a/sc_images/person/sc_icon_honor_item_b_bg.png b/sc_images/person/sc_icon_honor_item_b_bg.png new file mode 100644 index 0000000..39aaa3e Binary files /dev/null and b/sc_images/person/sc_icon_honor_item_b_bg.png differ diff --git a/sc_images/person/sc_icon_honor_item_c_bg.png b/sc_images/person/sc_icon_honor_item_c_bg.png new file mode 100644 index 0000000..0f32469 Binary files /dev/null and b/sc_images/person/sc_icon_honor_item_c_bg.png differ diff --git a/sc_images/person/sc_icon_honor_item_s_bg.png b/sc_images/person/sc_icon_honor_item_s_bg.png new file mode 100644 index 0000000..34a3728 Binary files /dev/null and b/sc_images/person/sc_icon_honor_item_s_bg.png differ diff --git a/sc_images/person/sc_icon_me_menu_1_bg.png b/sc_images/person/sc_icon_me_menu_1_bg.png new file mode 100644 index 0000000..d6f3b75 Binary files /dev/null and b/sc_images/person/sc_icon_me_menu_1_bg.png differ diff --git a/sc_images/person/sc_icon_medal_item_a_bg.png b/sc_images/person/sc_icon_medal_item_a_bg.png new file mode 100644 index 0000000..8918c7e Binary files /dev/null and b/sc_images/person/sc_icon_medal_item_a_bg.png differ diff --git a/sc_images/person/sc_icon_medal_item_b_bg.png b/sc_images/person/sc_icon_medal_item_b_bg.png new file mode 100644 index 0000000..4cb6f97 Binary files /dev/null and b/sc_images/person/sc_icon_medal_item_b_bg.png differ diff --git a/sc_images/person/sc_icon_medal_item_c_bg.png b/sc_images/person/sc_icon_medal_item_c_bg.png new file mode 100644 index 0000000..4e29c00 Binary files /dev/null and b/sc_images/person/sc_icon_medal_item_c_bg.png differ diff --git a/sc_images/person/sc_icon_medal_item_s_bg.png b/sc_images/person/sc_icon_medal_item_s_bg.png new file mode 100644 index 0000000..9922d13 Binary files /dev/null and b/sc_images/person/sc_icon_medal_item_s_bg.png differ diff --git a/sc_images/person/sc_icon_my_head_bg_defalt.png b/sc_images/person/sc_icon_my_head_bg_defalt.png new file mode 100644 index 0000000..7ef0c91 Binary files /dev/null and b/sc_images/person/sc_icon_my_head_bg_defalt.png differ diff --git a/sc_images/person/sc_icon_person_follow.png b/sc_images/person/sc_icon_person_follow.png new file mode 100644 index 0000000..5e95748 Binary files /dev/null and b/sc_images/person/sc_icon_person_follow.png differ diff --git a/sc_images/person/sc_icon_person_in_room.png b/sc_images/person/sc_icon_person_in_room.png new file mode 100644 index 0000000..f29ede2 Binary files /dev/null and b/sc_images/person/sc_icon_person_in_room.png differ diff --git a/sc_images/person/sc_icon_person_tochat.png b/sc_images/person/sc_icon_person_tochat.png new file mode 100644 index 0000000..0a84a1c Binary files /dev/null and b/sc_images/person/sc_icon_person_tochat.png differ diff --git a/sc_images/person/sc_icon_person_unfollow.png b/sc_images/person/sc_icon_person_unfollow.png new file mode 100644 index 0000000..69a2b9a Binary files /dev/null and b/sc_images/person/sc_icon_person_unfollow.png differ diff --git a/sc_images/person/sc_icon_send_cp_requst_cancel_bg.png b/sc_images/person/sc_icon_send_cp_requst_cancel_bg.png new file mode 100644 index 0000000..56bf59e Binary files /dev/null and b/sc_images/person/sc_icon_send_cp_requst_cancel_bg.png differ diff --git a/sc_images/person/sc_icon_send_cp_requst_dialog_content.png b/sc_images/person/sc_icon_send_cp_requst_dialog_content.png new file mode 100644 index 0000000..5273f6e Binary files /dev/null and b/sc_images/person/sc_icon_send_cp_requst_dialog_content.png differ diff --git a/sc_images/person/sc_icon_send_cp_requst_dialog_head.png b/sc_images/person/sc_icon_send_cp_requst_dialog_head.png new file mode 100644 index 0000000..0f7e583 Binary files /dev/null and b/sc_images/person/sc_icon_send_cp_requst_dialog_head.png differ diff --git a/sc_images/person/sc_icon_send_cp_requst_dialog_head2.png b/sc_images/person/sc_icon_send_cp_requst_dialog_head2.png new file mode 100644 index 0000000..fee7dea Binary files /dev/null and b/sc_images/person/sc_icon_send_cp_requst_dialog_head2.png differ diff --git a/sc_images/person/sc_icon_send_cp_requst_ok_bg.png b/sc_images/person/sc_icon_send_cp_requst_ok_bg.png new file mode 100644 index 0000000..8c6d485 Binary files /dev/null and b/sc_images/person/sc_icon_send_cp_requst_ok_bg.png differ diff --git a/sc_images/person/sc_icon_send_cp_requst_username_bg.png b/sc_images/person/sc_icon_send_cp_requst_username_bg.png new file mode 100644 index 0000000..ab25023 Binary files /dev/null and b/sc_images/person/sc_icon_send_cp_requst_username_bg.png differ diff --git a/sc_images/person/sc_icon_vistors_follow_fans_bg_man.png b/sc_images/person/sc_icon_vistors_follow_fans_bg_man.png new file mode 100644 index 0000000..04d59ad Binary files /dev/null and b/sc_images/person/sc_icon_vistors_follow_fans_bg_man.png differ diff --git a/sc_images/person/sc_icon_vistors_follow_fans_bg_woman.png b/sc_images/person/sc_icon_vistors_follow_fans_bg_woman.png new file mode 100644 index 0000000..86976c1 Binary files /dev/null and b/sc_images/person/sc_icon_vistors_follow_fans_bg_woman.png differ diff --git a/sc_images/room/anim/.keep b/sc_images/room/anim/.keep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/sc_images/room/anim/.keep @@ -0,0 +1 @@ + diff --git a/sc_images/room/entrance/sc_icon_room_entrance_no_vip_bg.png b/sc_images/room/entrance/sc_icon_room_entrance_no_vip_bg.png new file mode 100644 index 0000000..eae6d80 Binary files /dev/null and b/sc_images/room/entrance/sc_icon_room_entrance_no_vip_bg.png differ diff --git a/sc_images/room/icon_room_input_t.png b/sc_images/room/icon_room_input_t.png new file mode 100644 index 0000000..e438e04 Binary files /dev/null and b/sc_images/room/icon_room_input_t.png differ diff --git a/sc_images/room/sc_icon_activity_gift_head_bg_ar.png b/sc_images/room/sc_icon_activity_gift_head_bg_ar.png new file mode 100644 index 0000000..329c39f Binary files /dev/null and b/sc_images/room/sc_icon_activity_gift_head_bg_ar.png differ diff --git a/sc_images/room/sc_icon_activity_gift_head_bg_en.png b/sc_images/room/sc_icon_activity_gift_head_bg_en.png new file mode 100644 index 0000000..48c39de Binary files /dev/null and b/sc_images/room/sc_icon_activity_gift_head_bg_en.png differ diff --git a/sc_images/room/sc_icon_add_user.png b/sc_images/room/sc_icon_add_user.png new file mode 100644 index 0000000..6f7581d Binary files /dev/null and b/sc_images/room/sc_icon_add_user.png differ diff --git a/sc_images/room/sc_icon_all_in_the_room.png b/sc_images/room/sc_icon_all_in_the_room.png new file mode 100644 index 0000000..7ff09ff Binary files /dev/null and b/sc_images/room/sc_icon_all_in_the_room.png differ diff --git a/sc_images/room/sc_icon_all_on_microphone.png b/sc_images/room/sc_icon_all_on_microphone.png new file mode 100644 index 0000000..f01aa0b Binary files /dev/null and b/sc_images/room/sc_icon_all_on_microphone.png differ diff --git a/sc_images/room/sc_icon_at_tag_user.png b/sc_images/room/sc_icon_at_tag_user.png new file mode 100644 index 0000000..a994299 Binary files /dev/null and b/sc_images/room/sc_icon_at_tag_user.png differ diff --git a/sc_images/room/sc_icon_block_list_delete.png b/sc_images/room/sc_icon_block_list_delete.png new file mode 100644 index 0000000..bd283af Binary files /dev/null and b/sc_images/room/sc_icon_block_list_delete.png differ diff --git a/sc_images/room/sc_icon_botton_game.png b/sc_images/room/sc_icon_botton_game.png new file mode 100644 index 0000000..cf0434c Binary files /dev/null and b/sc_images/room/sc_icon_botton_game.png differ diff --git a/sc_images/room/sc_icon_botton_gift.png b/sc_images/room/sc_icon_botton_gift.png new file mode 100644 index 0000000..26a0e8f Binary files /dev/null and b/sc_images/room/sc_icon_botton_gift.png differ diff --git a/sc_images/room/sc_icon_botton_menu.png b/sc_images/room/sc_icon_botton_menu.png new file mode 100644 index 0000000..792aef6 Binary files /dev/null and b/sc_images/room/sc_icon_botton_menu.png differ diff --git a/sc_images/room/sc_icon_botton_message.png b/sc_images/room/sc_icon_botton_message.png new file mode 100644 index 0000000..2beba9c Binary files /dev/null and b/sc_images/room/sc_icon_botton_message.png differ diff --git a/sc_images/room/sc_icon_botton_mic_close.png b/sc_images/room/sc_icon_botton_mic_close.png new file mode 100644 index 0000000..56ad564 Binary files /dev/null and b/sc_images/room/sc_icon_botton_mic_close.png differ diff --git a/sc_images/room/sc_icon_botton_mic_open.png b/sc_images/room/sc_icon_botton_mic_open.png new file mode 100644 index 0000000..6005ea8 Binary files /dev/null and b/sc_images/room/sc_icon_botton_mic_open.png differ diff --git a/sc_images/room/sc_icon_customized_rule.png b/sc_images/room/sc_icon_customized_rule.png new file mode 100644 index 0000000..37a35d4 Binary files /dev/null and b/sc_images/room/sc_icon_customized_rule.png differ diff --git a/sc_images/room/sc_icon_emoji_vip1_3_bg.png b/sc_images/room/sc_icon_emoji_vip1_3_bg.png new file mode 100644 index 0000000..d265a5f Binary files /dev/null and b/sc_images/room/sc_icon_emoji_vip1_3_bg.png differ diff --git a/sc_images/room/sc_icon_emoji_vip4_6_bg.png b/sc_images/room/sc_icon_emoji_vip4_6_bg.png new file mode 100644 index 0000000..d5005ad Binary files /dev/null and b/sc_images/room/sc_icon_emoji_vip4_6_bg.png differ diff --git a/sc_images/room/sc_icon_exit_room.png b/sc_images/room/sc_icon_exit_room.png new file mode 100644 index 0000000..d83a317 Binary files /dev/null and b/sc_images/room/sc_icon_exit_room.png differ diff --git a/sc_images/room/sc_icon_follow_room_en.png b/sc_images/room/sc_icon_follow_room_en.png new file mode 100644 index 0000000..1fd9dbf Binary files /dev/null and b/sc_images/room/sc_icon_follow_room_en.png differ diff --git a/sc_images/room/sc_icon_follow_room_un.png b/sc_images/room/sc_icon_follow_room_un.png new file mode 100644 index 0000000..66ff219 Binary files /dev/null and b/sc_images/room/sc_icon_follow_room_un.png differ diff --git a/sc_images/room/sc_icon_game_king_day_bg.png b/sc_images/room/sc_icon_game_king_day_bg.png new file mode 100644 index 0000000..a9783fd Binary files /dev/null and b/sc_images/room/sc_icon_game_king_day_bg.png differ diff --git a/sc_images/room/sc_icon_game_king_week_bg.png b/sc_images/room/sc_icon_game_king_week_bg.png new file mode 100644 index 0000000..e361a61 Binary files /dev/null and b/sc_images/room/sc_icon_game_king_week_bg.png differ diff --git a/sc_images/room/sc_icon_gift_all_en.png b/sc_images/room/sc_icon_gift_all_en.png new file mode 100644 index 0000000..271c0ea Binary files /dev/null and b/sc_images/room/sc_icon_gift_all_en.png differ diff --git a/sc_images/room/sc_icon_gift_all_no.png b/sc_images/room/sc_icon_gift_all_no.png new file mode 100644 index 0000000..7c373c4 Binary files /dev/null and b/sc_images/room/sc_icon_gift_all_no.png differ diff --git a/sc_images/room/sc_icon_gift_cp.png b/sc_images/room/sc_icon_gift_cp.png new file mode 100644 index 0000000..91b15cd Binary files /dev/null and b/sc_images/room/sc_icon_gift_cp.png differ diff --git a/sc_images/room/sc_icon_gift_effect.png b/sc_images/room/sc_icon_gift_effect.png new file mode 100644 index 0000000..12d3f39 Binary files /dev/null and b/sc_images/room/sc_icon_gift_effect.png differ diff --git a/sc_images/room/sc_icon_gift_float_bg.png b/sc_images/room/sc_icon_gift_float_bg.png new file mode 100644 index 0000000..20396fa Binary files /dev/null and b/sc_images/room/sc_icon_gift_float_bg.png differ diff --git a/sc_images/room/sc_icon_gift_heartbeat.png b/sc_images/room/sc_icon_gift_heartbeat.png new file mode 100644 index 0000000..de9f5df Binary files /dev/null and b/sc_images/room/sc_icon_gift_heartbeat.png differ diff --git a/sc_images/room/sc_icon_gift_luck.png b/sc_images/room/sc_icon_gift_luck.png new file mode 100644 index 0000000..4843bdb Binary files /dev/null and b/sc_images/room/sc_icon_gift_luck.png differ diff --git a/sc_images/room/sc_icon_gift_music.png b/sc_images/room/sc_icon_gift_music.png new file mode 100644 index 0000000..d0f4e67 Binary files /dev/null and b/sc_images/room/sc_icon_gift_music.png differ diff --git a/sc_images/room/sc_icon_give_gift_type_bg.png b/sc_images/room/sc_icon_give_gift_type_bg.png new file mode 100644 index 0000000..8b06418 Binary files /dev/null and b/sc_images/room/sc_icon_give_gift_type_bg.png differ diff --git a/sc_images/room/sc_icon_inv_go_btn.png b/sc_images/room/sc_icon_inv_go_btn.png new file mode 100644 index 0000000..99291f7 Binary files /dev/null and b/sc_images/room/sc_icon_inv_go_btn.png differ diff --git a/sc_images/room/sc_icon_join_room_member.png b/sc_images/room/sc_icon_join_room_member.png new file mode 100644 index 0000000..8ca909e Binary files /dev/null and b/sc_images/room/sc_icon_join_room_member.png differ diff --git a/sc_images/room/sc_icon_k.png b/sc_images/room/sc_icon_k.png new file mode 100644 index 0000000..aa1da25 Binary files /dev/null and b/sc_images/room/sc_icon_k.png differ diff --git a/sc_images/room/sc_icon_luck_gift_float_bg1.png b/sc_images/room/sc_icon_luck_gift_float_bg1.png new file mode 100644 index 0000000..c9ea1fa Binary files /dev/null and b/sc_images/room/sc_icon_luck_gift_float_bg1.png differ diff --git a/sc_images/room/sc_icon_luck_gift_float_bg2.png b/sc_images/room/sc_icon_luck_gift_float_bg2.png new file mode 100644 index 0000000..e28f173 Binary files /dev/null and b/sc_images/room/sc_icon_luck_gift_float_bg2.png differ diff --git a/sc_images/room/sc_icon_luck_gift_float_bg3.png b/sc_images/room/sc_icon_luck_gift_float_bg3.png new file mode 100644 index 0000000..c6c0c35 Binary files /dev/null and b/sc_images/room/sc_icon_luck_gift_float_bg3.png differ diff --git a/sc_images/room/sc_icon_luck_gift_float_bg4.png b/sc_images/room/sc_icon_luck_gift_float_bg4.png new file mode 100644 index 0000000..75fa073 Binary files /dev/null and b/sc_images/room/sc_icon_luck_gift_float_bg4.png differ diff --git a/sc_images/room/sc_icon_luck_gift_float_bg5.png b/sc_images/room/sc_icon_luck_gift_float_bg5.png new file mode 100644 index 0000000..41194b3 Binary files /dev/null and b/sc_images/room/sc_icon_luck_gift_float_bg5.png differ diff --git a/sc_images/room/sc_icon_luck_gift_float_n_bg.png b/sc_images/room/sc_icon_luck_gift_float_n_bg.png new file mode 100644 index 0000000..8f24ffe Binary files /dev/null and b/sc_images/room/sc_icon_luck_gift_float_n_bg.png differ diff --git a/sc_images/room/sc_icon_luck_gift_msg_n_ball.png b/sc_images/room/sc_icon_luck_gift_msg_n_ball.png new file mode 100644 index 0000000..9f9c386 Binary files /dev/null and b/sc_images/room/sc_icon_luck_gift_msg_n_ball.png differ diff --git a/sc_images/room/sc_icon_luck_gift_msg_n_bg.png b/sc_images/room/sc_icon_luck_gift_msg_n_bg.png new file mode 100644 index 0000000..e902506 Binary files /dev/null and b/sc_images/room/sc_icon_luck_gift_msg_n_bg.png differ diff --git a/sc_images/room/sc_icon_luck_num_text_ar.png b/sc_images/room/sc_icon_luck_num_text_ar.png new file mode 100644 index 0000000..714e22c Binary files /dev/null and b/sc_images/room/sc_icon_luck_num_text_ar.png differ diff --git a/sc_images/room/sc_icon_luck_num_text_en.png b/sc_images/room/sc_icon_luck_num_text_en.png new file mode 100644 index 0000000..fc4c80f Binary files /dev/null and b/sc_images/room/sc_icon_luck_num_text_en.png differ diff --git a/sc_images/room/sc_icon_luckgift_coins_anim.webp b/sc_images/room/sc_icon_luckgift_coins_anim.webp new file mode 100644 index 0000000..000a398 Binary files /dev/null and b/sc_images/room/sc_icon_luckgift_coins_anim.webp differ diff --git a/sc_images/room/sc_icon_lucknumber_0.png b/sc_images/room/sc_icon_lucknumber_0.png new file mode 100644 index 0000000..641a942 Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_0.png differ diff --git a/sc_images/room/sc_icon_lucknumber_1.png b/sc_images/room/sc_icon_lucknumber_1.png new file mode 100644 index 0000000..ea25b67 Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_1.png differ diff --git a/sc_images/room/sc_icon_lucknumber_2.png b/sc_images/room/sc_icon_lucknumber_2.png new file mode 100644 index 0000000..2df5429 Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_2.png differ diff --git a/sc_images/room/sc_icon_lucknumber_3.png b/sc_images/room/sc_icon_lucknumber_3.png new file mode 100644 index 0000000..45fe9cf Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_3.png differ diff --git a/sc_images/room/sc_icon_lucknumber_4.png b/sc_images/room/sc_icon_lucknumber_4.png new file mode 100644 index 0000000..21dba8a Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_4.png differ diff --git a/sc_images/room/sc_icon_lucknumber_5.png b/sc_images/room/sc_icon_lucknumber_5.png new file mode 100644 index 0000000..e6763c7 Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_5.png differ diff --git a/sc_images/room/sc_icon_lucknumber_6.png b/sc_images/room/sc_icon_lucknumber_6.png new file mode 100644 index 0000000..45cff13 Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_6.png differ diff --git a/sc_images/room/sc_icon_lucknumber_7.png b/sc_images/room/sc_icon_lucknumber_7.png new file mode 100644 index 0000000..d0be8e7 Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_7.png differ diff --git a/sc_images/room/sc_icon_lucknumber_8.png b/sc_images/room/sc_icon_lucknumber_8.png new file mode 100644 index 0000000..a7753d2 Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_8.png differ diff --git a/sc_images/room/sc_icon_lucknumber_9.png b/sc_images/room/sc_icon_lucknumber_9.png new file mode 100644 index 0000000..19e2814 Binary files /dev/null and b/sc_images/room/sc_icon_lucknumber_9.png differ diff --git a/sc_images/room/sc_icon_m.png b/sc_images/room/sc_icon_m.png new file mode 100644 index 0000000..8756588 Binary files /dev/null and b/sc_images/room/sc_icon_m.png differ diff --git a/sc_images/room/sc_icon_menu_mic_model_change.png b/sc_images/room/sc_icon_menu_mic_model_change.png new file mode 100644 index 0000000..b46afe7 Binary files /dev/null and b/sc_images/room/sc_icon_menu_mic_model_change.png differ diff --git a/sc_images/room/sc_icon_mic_mute.png b/sc_images/room/sc_icon_mic_mute.png new file mode 100644 index 0000000..ddb55fc Binary files /dev/null and b/sc_images/room/sc_icon_mic_mute.png differ diff --git a/sc_images/room/sc_icon_mic_open.png b/sc_images/room/sc_icon_mic_open.png new file mode 100644 index 0000000..58223c7 Binary files /dev/null and b/sc_images/room/sc_icon_mic_open.png differ diff --git a/sc_images/room/sc_icon_mic_switch_mode.png b/sc_images/room/sc_icon_mic_switch_mode.png new file mode 100644 index 0000000..4b21898 Binary files /dev/null and b/sc_images/room/sc_icon_mic_switch_mode.png differ diff --git a/sc_images/room/sc_icon_min_room.png b/sc_images/room/sc_icon_min_room.png new file mode 100644 index 0000000..af18f64 Binary files /dev/null and b/sc_images/room/sc_icon_min_room.png differ diff --git a/sc_images/room/sc_icon_music_delete.png b/sc_images/room/sc_icon_music_delete.png new file mode 100644 index 0000000..e558785 Binary files /dev/null and b/sc_images/room/sc_icon_music_delete.png differ diff --git a/sc_images/room/sc_icon_music_to_up.png b/sc_images/room/sc_icon_music_to_up.png new file mode 100644 index 0000000..02c432f Binary files /dev/null and b/sc_images/room/sc_icon_music_to_up.png differ diff --git a/sc_images/room/sc_icon_number_0.png b/sc_images/room/sc_icon_number_0.png new file mode 100644 index 0000000..a3e867a Binary files /dev/null and b/sc_images/room/sc_icon_number_0.png differ diff --git a/sc_images/room/sc_icon_number_1.png b/sc_images/room/sc_icon_number_1.png new file mode 100644 index 0000000..8f7aaf5 Binary files /dev/null and b/sc_images/room/sc_icon_number_1.png differ diff --git a/sc_images/room/sc_icon_number_2.png b/sc_images/room/sc_icon_number_2.png new file mode 100644 index 0000000..027d752 Binary files /dev/null and b/sc_images/room/sc_icon_number_2.png differ diff --git a/sc_images/room/sc_icon_number_3.png b/sc_images/room/sc_icon_number_3.png new file mode 100644 index 0000000..6c4b2ef Binary files /dev/null and b/sc_images/room/sc_icon_number_3.png differ diff --git a/sc_images/room/sc_icon_number_4.png b/sc_images/room/sc_icon_number_4.png new file mode 100644 index 0000000..4b0fe26 Binary files /dev/null and b/sc_images/room/sc_icon_number_4.png differ diff --git a/sc_images/room/sc_icon_number_5.png b/sc_images/room/sc_icon_number_5.png new file mode 100644 index 0000000..830bcf5 Binary files /dev/null and b/sc_images/room/sc_icon_number_5.png differ diff --git a/sc_images/room/sc_icon_number_6.png b/sc_images/room/sc_icon_number_6.png new file mode 100644 index 0000000..b7782bf Binary files /dev/null and b/sc_images/room/sc_icon_number_6.png differ diff --git a/sc_images/room/sc_icon_number_7.png b/sc_images/room/sc_icon_number_7.png new file mode 100644 index 0000000..cff7412 Binary files /dev/null and b/sc_images/room/sc_icon_number_7.png differ diff --git a/sc_images/room/sc_icon_number_8.png b/sc_images/room/sc_icon_number_8.png new file mode 100644 index 0000000..8107c72 Binary files /dev/null and b/sc_images/room/sc_icon_number_8.png differ diff --git a/sc_images/room/sc_icon_number_9.png b/sc_images/room/sc_icon_number_9.png new file mode 100644 index 0000000..996b5df Binary files /dev/null and b/sc_images/room/sc_icon_number_9.png differ diff --git a/sc_images/room/sc_icon_online_peple.png b/sc_images/room/sc_icon_online_peple.png new file mode 100644 index 0000000..ffe71ac Binary files /dev/null and b/sc_images/room/sc_icon_online_peple.png differ diff --git a/sc_images/room/sc_icon_open_card.png b/sc_images/room/sc_icon_open_card.png new file mode 100644 index 0000000..f78b58e Binary files /dev/null and b/sc_images/room/sc_icon_open_card.png differ diff --git a/sc_images/room/sc_icon_redpackg_tag.png b/sc_images/room/sc_icon_redpackg_tag.png new file mode 100644 index 0000000..2fea67e Binary files /dev/null and b/sc_images/room/sc_icon_redpackg_tag.png differ diff --git a/sc_images/room/sc_icon_remve_block.png b/sc_images/room/sc_icon_remve_block.png new file mode 100644 index 0000000..e065870 Binary files /dev/null and b/sc_images/room/sc_icon_remve_block.png differ diff --git a/sc_images/room/sc_icon_room_charm.png b/sc_images/room/sc_icon_room_charm.png new file mode 100644 index 0000000..d30d91e Binary files /dev/null and b/sc_images/room/sc_icon_room_charm.png differ diff --git a/sc_images/room/sc_icon_room_charm_tag.png b/sc_images/room/sc_icon_room_charm_tag.png new file mode 100644 index 0000000..9eb6719 Binary files /dev/null and b/sc_images/room/sc_icon_room_charm_tag.png differ diff --git a/sc_images/room/sc_icon_room_contribute.png b/sc_images/room/sc_icon_room_contribute.png new file mode 100644 index 0000000..d02b898 Binary files /dev/null and b/sc_images/room/sc_icon_room_contribute.png differ diff --git a/sc_images/room/sc_icon_room_contribute_rank1.png b/sc_images/room/sc_icon_room_contribute_rank1.png new file mode 100644 index 0000000..6ad895e Binary files /dev/null and b/sc_images/room/sc_icon_room_contribute_rank1.png differ diff --git a/sc_images/room/sc_icon_room_contribute_rank2.png b/sc_images/room/sc_icon_room_contribute_rank2.png new file mode 100644 index 0000000..e70bd3e Binary files /dev/null and b/sc_images/room/sc_icon_room_contribute_rank2.png differ diff --git a/sc_images/room/sc_icon_room_contribute_rank3.png b/sc_images/room/sc_icon_room_contribute_rank3.png new file mode 100644 index 0000000..a36c8ef Binary files /dev/null and b/sc_images/room/sc_icon_room_contribute_rank3.png differ diff --git a/sc_images/room/sc_icon_room_defaut_bg.png b/sc_images/room/sc_icon_room_defaut_bg.png new file mode 100644 index 0000000..dcb9d58 Binary files /dev/null and b/sc_images/room/sc_icon_room_defaut_bg.png differ diff --git a/sc_images/room/sc_icon_room_edit.png b/sc_images/room/sc_icon_room_edit.png new file mode 100644 index 0000000..2a6b6c5 Binary files /dev/null and b/sc_images/room/sc_icon_room_edit.png differ diff --git a/sc_images/room/sc_icon_room_edit_noti.png b/sc_images/room/sc_icon_room_edit_noti.png new file mode 100644 index 0000000..4224837 Binary files /dev/null and b/sc_images/room/sc_icon_room_edit_noti.png differ diff --git a/sc_images/room/sc_icon_room_ext_min.png b/sc_images/room/sc_icon_room_ext_min.png new file mode 100644 index 0000000..33b17c5 Binary files /dev/null and b/sc_images/room/sc_icon_room_ext_min.png differ diff --git a/sc_images/room/sc_icon_room_follow_no.png b/sc_images/room/sc_icon_room_follow_no.png new file mode 100644 index 0000000..4373ec2 Binary files /dev/null and b/sc_images/room/sc_icon_room_follow_no.png differ diff --git a/sc_images/room/sc_icon_room_free_seat.png b/sc_images/room/sc_icon_room_free_seat.png new file mode 100644 index 0000000..fef933e Binary files /dev/null and b/sc_images/room/sc_icon_room_free_seat.png differ diff --git a/sc_images/room/sc_icon_room_free_sonic.png b/sc_images/room/sc_icon_room_free_sonic.png new file mode 100644 index 0000000..5d5ed9b Binary files /dev/null and b/sc_images/room/sc_icon_room_free_sonic.png differ diff --git a/sc_images/room/sc_icon_room_fz.png b/sc_images/room/sc_icon_room_fz.png new file mode 100644 index 0000000..9a557dc Binary files /dev/null and b/sc_images/room/sc_icon_room_fz.png differ diff --git a/sc_images/room/sc_icon_room_game_close.png b/sc_images/room/sc_icon_room_game_close.png new file mode 100644 index 0000000..1fff407 Binary files /dev/null and b/sc_images/room/sc_icon_room_game_close.png differ diff --git a/sc_images/room/sc_icon_room_game_history_bg.png b/sc_images/room/sc_icon_room_game_history_bg.png new file mode 100644 index 0000000..2056f3e Binary files /dev/null and b/sc_images/room/sc_icon_room_game_history_bg.png differ diff --git a/sc_images/room/sc_icon_room_game_item_bg_v.png b/sc_images/room/sc_icon_room_game_item_bg_v.png new file mode 100644 index 0000000..551565a Binary files /dev/null and b/sc_images/room/sc_icon_room_game_item_bg_v.png differ diff --git a/sc_images/room/sc_icon_room_game_mic_close.png b/sc_images/room/sc_icon_room_game_mic_close.png new file mode 100644 index 0000000..7b87727 Binary files /dev/null and b/sc_images/room/sc_icon_room_game_mic_close.png differ diff --git a/sc_images/room/sc_icon_room_game_mic_open.png b/sc_images/room/sc_icon_room_game_mic_open.png new file mode 100644 index 0000000..01419fe Binary files /dev/null and b/sc_images/room/sc_icon_room_game_mic_open.png differ diff --git a/sc_images/room/sc_icon_room_game_min.png b/sc_images/room/sc_icon_room_game_min.png new file mode 100644 index 0000000..0ce57b8 Binary files /dev/null and b/sc_images/room/sc_icon_room_game_min.png differ diff --git a/sc_images/room/sc_icon_room_gift_left_no_vip_bg.png b/sc_images/room/sc_icon_room_gift_left_no_vip_bg.png new file mode 100644 index 0000000..e09b8f4 Binary files /dev/null and b/sc_images/room/sc_icon_room_gift_left_no_vip_bg.png differ diff --git a/sc_images/room/sc_icon_room_gly.png b/sc_images/room/sc_icon_room_gly.png new file mode 100644 index 0000000..d676bed Binary files /dev/null and b/sc_images/room/sc_icon_room_gly.png differ diff --git a/sc_images/room/sc_icon_room_guest.png b/sc_images/room/sc_icon_room_guest.png new file mode 100644 index 0000000..28dec3c Binary files /dev/null and b/sc_images/room/sc_icon_room_guest.png differ diff --git a/sc_images/room/sc_icon_room_hy.png b/sc_images/room/sc_icon_room_hy.png new file mode 100644 index 0000000..1bbc8d5 Binary files /dev/null and b/sc_images/room/sc_icon_room_hy.png differ diff --git a/sc_images/room/sc_icon_room_jiesuo.png b/sc_images/room/sc_icon_room_jiesuo.png new file mode 100644 index 0000000..ab1d00d Binary files /dev/null and b/sc_images/room/sc_icon_room_jiesuo.png differ diff --git a/sc_images/room/sc_icon_room_menu_settins.png b/sc_images/room/sc_icon_room_menu_settins.png new file mode 100644 index 0000000..d3f5f5a Binary files /dev/null and b/sc_images/room/sc_icon_room_menu_settins.png differ diff --git a/sc_images/room/sc_icon_room_message_send.png b/sc_images/room/sc_icon_room_message_send.png new file mode 100644 index 0000000..adcc806 Binary files /dev/null and b/sc_images/room/sc_icon_room_message_send.png differ diff --git a/sc_images/room/sc_icon_room_mic_model_5.png b/sc_images/room/sc_icon_room_mic_model_5.png new file mode 100644 index 0000000..b28264e Binary files /dev/null and b/sc_images/room/sc_icon_room_mic_model_5.png differ diff --git a/sc_images/room/sc_icon_room_msg_clear.png b/sc_images/room/sc_icon_room_msg_clear.png new file mode 100644 index 0000000..8b3c49e Binary files /dev/null and b/sc_images/room/sc_icon_room_msg_clear.png differ diff --git a/sc_images/room/sc_icon_room_msg_pic.png b/sc_images/room/sc_icon_room_msg_pic.png new file mode 100644 index 0000000..68899a1 Binary files /dev/null and b/sc_images/room/sc_icon_room_msg_pic.png differ diff --git a/sc_images/room/sc_icon_room_music.png b/sc_images/room/sc_icon_room_music.png new file mode 100644 index 0000000..eca247a Binary files /dev/null and b/sc_images/room/sc_icon_room_music.png differ diff --git a/sc_images/room/sc_icon_room_music_add.png b/sc_images/room/sc_icon_room_music_add.png new file mode 100644 index 0000000..8a70426 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_add.png differ diff --git a/sc_images/room/sc_icon_room_music_contrl_bg.png b/sc_images/room/sc_icon_room_music_contrl_bg.png new file mode 100644 index 0000000..9f28a70 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_contrl_bg.png differ diff --git a/sc_images/room/sc_icon_room_music_empty.png b/sc_images/room/sc_icon_room_music_empty.png new file mode 100644 index 0000000..503b49a Binary files /dev/null and b/sc_images/room/sc_icon_room_music_empty.png differ diff --git a/sc_images/room/sc_icon_room_music_menu.png b/sc_images/room/sc_icon_room_music_menu.png new file mode 100644 index 0000000..19166d6 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_menu.png differ diff --git a/sc_images/room/sc_icon_room_music_model_1.png b/sc_images/room/sc_icon_room_music_model_1.png new file mode 100644 index 0000000..23f1a54 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_model_1.png differ diff --git a/sc_images/room/sc_icon_room_music_model_2.png b/sc_images/room/sc_icon_room_music_model_2.png new file mode 100644 index 0000000..545d672 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_model_2.png differ diff --git a/sc_images/room/sc_icon_room_music_model_3.png b/sc_images/room/sc_icon_room_music_model_3.png new file mode 100644 index 0000000..b372165 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_model_3.png differ diff --git a/sc_images/room/sc_icon_room_music_next.png b/sc_images/room/sc_icon_room_music_next.png new file mode 100644 index 0000000..8e0e401 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_next.png differ diff --git a/sc_images/room/sc_icon_room_music_pause.png b/sc_images/room/sc_icon_room_music_pause.png new file mode 100644 index 0000000..d7f268c Binary files /dev/null and b/sc_images/room/sc_icon_room_music_pause.png differ diff --git a/sc_images/room/sc_icon_room_music_play.png b/sc_images/room/sc_icon_room_music_play.png new file mode 100644 index 0000000..132fc3b Binary files /dev/null and b/sc_images/room/sc_icon_room_music_play.png differ diff --git a/sc_images/room/sc_icon_room_music_previous.png b/sc_images/room/sc_icon_room_music_previous.png new file mode 100644 index 0000000..6f78266 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_previous.png differ diff --git a/sc_images/room/sc_icon_room_music_select.png b/sc_images/room/sc_icon_room_music_select.png new file mode 100644 index 0000000..87dda20 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_select.png differ diff --git a/sc_images/room/sc_icon_room_music_tag.png b/sc_images/room/sc_icon_room_music_tag.png new file mode 100644 index 0000000..1a59e09 Binary files /dev/null and b/sc_images/room/sc_icon_room_music_tag.png differ diff --git a/sc_images/room/sc_icon_room_music_volume1.png b/sc_images/room/sc_icon_room_music_volume1.png new file mode 100644 index 0000000..a8c4b2b Binary files /dev/null and b/sc_images/room/sc_icon_room_music_volume1.png differ diff --git a/sc_images/room/sc_icon_room_report.png b/sc_images/room/sc_icon_room_report.png new file mode 100644 index 0000000..1d29a2c Binary files /dev/null and b/sc_images/room/sc_icon_room_report.png differ diff --git a/sc_images/room/sc_icon_room_seat_mic_mute.png b/sc_images/room/sc_icon_room_seat_mic_mute.png new file mode 100644 index 0000000..7a59ce0 Binary files /dev/null and b/sc_images/room/sc_icon_room_seat_mic_mute.png differ diff --git a/sc_images/room/sc_icon_room_special_effects.png b/sc_images/room/sc_icon_room_special_effects.png new file mode 100644 index 0000000..033618e Binary files /dev/null and b/sc_images/room/sc_icon_room_special_effects.png differ diff --git a/sc_images/room/sc_icon_room_suo.png b/sc_images/room/sc_icon_room_suo.png new file mode 100644 index 0000000..4b8b424 Binary files /dev/null and b/sc_images/room/sc_icon_room_suo.png differ diff --git a/sc_images/room/sc_icon_room_switch_mic_model_check.png b/sc_images/room/sc_icon_room_switch_mic_model_check.png new file mode 100644 index 0000000..a505eed Binary files /dev/null and b/sc_images/room/sc_icon_room_switch_mic_model_check.png differ diff --git a/sc_images/room/sc_icon_room_task.png b/sc_images/room/sc_icon_room_task.png new file mode 100644 index 0000000..f4ab98b Binary files /dev/null and b/sc_images/room/sc_icon_room_task.png differ diff --git a/sc_images/room/sc_icon_room_task_list_item_act_btn.png b/sc_images/room/sc_icon_room_task_list_item_act_btn.png new file mode 100644 index 0000000..226f97a Binary files /dev/null and b/sc_images/room/sc_icon_room_task_list_item_act_btn.png differ diff --git a/sc_images/room/sc_icon_room_task_list_item_bg.png b/sc_images/room/sc_icon_room_task_list_item_bg.png new file mode 100644 index 0000000..7247533 Binary files /dev/null and b/sc_images/room/sc_icon_room_task_list_item_bg.png differ diff --git a/sc_images/room/sc_icon_room_task_list_item_complete_btn.png b/sc_images/room/sc_icon_room_task_list_item_complete_btn.png new file mode 100644 index 0000000..761c332 Binary files /dev/null and b/sc_images/room/sc_icon_room_task_list_item_complete_btn.png differ diff --git a/sc_images/room/sc_icon_room_task_list_item_go_btn.png b/sc_images/room/sc_icon_room_task_list_item_go_btn.png new file mode 100644 index 0000000..3353085 Binary files /dev/null and b/sc_images/room/sc_icon_room_task_list_item_go_btn.png differ diff --git a/sc_images/room/sc_icon_room_task_rule_bg.png b/sc_images/room/sc_icon_room_task_rule_bg.png new file mode 100644 index 0000000..5395244 Binary files /dev/null and b/sc_images/room/sc_icon_room_task_rule_bg.png differ diff --git a/sc_images/room/sc_icon_room_task_tag.png b/sc_images/room/sc_icon_room_task_tag.png new file mode 100644 index 0000000..4c53a33 Binary files /dev/null and b/sc_images/room/sc_icon_room_task_tag.png differ diff --git a/sc_images/room/sc_icon_room_theme.png b/sc_images/room/sc_icon_room_theme.png new file mode 100644 index 0000000..fb5a5e5 Binary files /dev/null and b/sc_images/room/sc_icon_room_theme.png differ diff --git a/sc_images/room/sc_icon_room_user_card_setting.png b/sc_images/room/sc_icon_room_user_card_setting.png new file mode 100644 index 0000000..77a3ebc Binary files /dev/null and b/sc_images/room/sc_icon_room_user_card_setting.png differ diff --git a/sc_images/room/sc_icon_room_vip3_seat.png b/sc_images/room/sc_icon_room_vip3_seat.png new file mode 100644 index 0000000..cd7efeb Binary files /dev/null and b/sc_images/room/sc_icon_room_vip3_seat.png differ diff --git a/sc_images/room/sc_icon_room_vip3_sonic.png b/sc_images/room/sc_icon_room_vip3_sonic.png new file mode 100644 index 0000000..fa4a1a9 Binary files /dev/null and b/sc_images/room/sc_icon_room_vip3_sonic.png differ diff --git a/sc_images/room/sc_icon_room_vip3_sonic_anim.webp b/sc_images/room/sc_icon_room_vip3_sonic_anim.webp new file mode 100644 index 0000000..f4aaa12 Binary files /dev/null and b/sc_images/room/sc_icon_room_vip3_sonic_anim.webp differ diff --git a/sc_images/room/sc_icon_room_vip4_seat.png b/sc_images/room/sc_icon_room_vip4_seat.png new file mode 100644 index 0000000..e9f771a Binary files /dev/null and b/sc_images/room/sc_icon_room_vip4_seat.png differ diff --git a/sc_images/room/sc_icon_room_vip4_sonic.png b/sc_images/room/sc_icon_room_vip4_sonic.png new file mode 100644 index 0000000..ff8fa74 Binary files /dev/null and b/sc_images/room/sc_icon_room_vip4_sonic.png differ diff --git a/sc_images/room/sc_icon_room_vip4_sonic_anim.webp b/sc_images/room/sc_icon_room_vip4_sonic_anim.webp new file mode 100644 index 0000000..e02d240 Binary files /dev/null and b/sc_images/room/sc_icon_room_vip4_sonic_anim.webp differ diff --git a/sc_images/room/sc_icon_room_vip5_seat.png b/sc_images/room/sc_icon_room_vip5_seat.png new file mode 100644 index 0000000..4d3aa04 Binary files /dev/null and b/sc_images/room/sc_icon_room_vip5_seat.png differ diff --git a/sc_images/room/sc_icon_room_vip5_sonic.png b/sc_images/room/sc_icon_room_vip5_sonic.png new file mode 100644 index 0000000..71134d4 Binary files /dev/null and b/sc_images/room/sc_icon_room_vip5_sonic.png differ diff --git a/sc_images/room/sc_icon_room_vip5_sonic_anim.webp b/sc_images/room/sc_icon_room_vip5_sonic_anim.webp new file mode 100644 index 0000000..009f19b Binary files /dev/null and b/sc_images/room/sc_icon_room_vip5_sonic_anim.webp differ diff --git a/sc_images/room/sc_icon_room_vip6_seat.png b/sc_images/room/sc_icon_room_vip6_seat.png new file mode 100644 index 0000000..b8cc1af Binary files /dev/null and b/sc_images/room/sc_icon_room_vip6_seat.png differ diff --git a/sc_images/room/sc_icon_room_vip6_sonic.png b/sc_images/room/sc_icon_room_vip6_sonic.png new file mode 100644 index 0000000..3180edd Binary files /dev/null and b/sc_images/room/sc_icon_room_vip6_sonic.png differ diff --git a/sc_images/room/sc_icon_room_vip6_sonic_anim.webp b/sc_images/room/sc_icon_room_vip6_sonic_anim.webp new file mode 100644 index 0000000..a6e6638 Binary files /dev/null and b/sc_images/room/sc_icon_room_vip6_sonic_anim.webp differ diff --git a/sc_images/room/sc_icon_roomgift_rank_back_bg.png b/sc_images/room/sc_icon_roomgift_rank_back_bg.png new file mode 100644 index 0000000..70a8675 Binary files /dev/null and b/sc_images/room/sc_icon_roomgift_rank_back_bg.png differ diff --git a/sc_images/room/sc_icon_roomgift_rank_title_bg.png b/sc_images/room/sc_icon_roomgift_rank_title_bg.png new file mode 100644 index 0000000..6fb5dca Binary files /dev/null and b/sc_images/room/sc_icon_roomgift_rank_title_bg.png differ diff --git a/sc_images/room/sc_icon_roomgift_rule_back_bg.png b/sc_images/room/sc_icon_roomgift_rule_back_bg.png new file mode 100644 index 0000000..a78c500 Binary files /dev/null and b/sc_images/room/sc_icon_roomgift_rule_back_bg.png differ diff --git a/sc_images/room/sc_icon_rps_1.png b/sc_images/room/sc_icon_rps_1.png new file mode 100644 index 0000000..e35c7be Binary files /dev/null and b/sc_images/room/sc_icon_rps_1.png differ diff --git a/sc_images/room/sc_icon_rps_2.png b/sc_images/room/sc_icon_rps_2.png new file mode 100644 index 0000000..84136cd Binary files /dev/null and b/sc_images/room/sc_icon_rps_2.png differ diff --git a/sc_images/room/sc_icon_rps_3.png b/sc_images/room/sc_icon_rps_3.png new file mode 100644 index 0000000..5d392fb Binary files /dev/null and b/sc_images/room/sc_icon_rps_3.png differ diff --git a/sc_images/room/sc_icon_rps_animal.webp b/sc_images/room/sc_icon_rps_animal.webp new file mode 100644 index 0000000..ff1e2b6 Binary files /dev/null and b/sc_images/room/sc_icon_rps_animal.webp differ diff --git a/sc_images/room/sc_icon_rps_tag.png b/sc_images/room/sc_icon_rps_tag.png new file mode 100644 index 0000000..60ae1db Binary files /dev/null and b/sc_images/room/sc_icon_rps_tag.png differ diff --git a/sc_images/room/sc_icon_seat_lock.png b/sc_images/room/sc_icon_seat_lock.png new file mode 100644 index 0000000..2ceeb04 Binary files /dev/null and b/sc_images/room/sc_icon_seat_lock.png differ diff --git a/sc_images/room/sc_icon_seat_num_bg.png b/sc_images/room/sc_icon_seat_num_bg.png new file mode 100644 index 0000000..d0e2b7c Binary files /dev/null and b/sc_images/room/sc_icon_seat_num_bg.png differ diff --git a/sc_images/room/sc_icon_seat_open.png b/sc_images/room/sc_icon_seat_open.png new file mode 100644 index 0000000..6ecda46 Binary files /dev/null and b/sc_images/room/sc_icon_seat_open.png differ diff --git a/sc_images/room/sc_icon_send_user_gift.png b/sc_images/room/sc_icon_send_user_gift.png new file mode 100644 index 0000000..adb7638 Binary files /dev/null and b/sc_images/room/sc_icon_send_user_gift.png differ diff --git a/sc_images/room/sc_icon_send_user_message.png b/sc_images/room/sc_icon_send_user_message.png new file mode 100644 index 0000000..b945258 Binary files /dev/null and b/sc_images/room/sc_icon_send_user_message.png differ diff --git a/sc_images/room/sc_icon_task_item_select_bg.png b/sc_images/room/sc_icon_task_item_select_bg.png new file mode 100644 index 0000000..e292445 Binary files /dev/null and b/sc_images/room/sc_icon_task_item_select_bg.png differ diff --git a/sc_images/room/sc_icon_task_item_unselect_bg.png b/sc_images/room/sc_icon_task_item_unselect_bg.png new file mode 100644 index 0000000..7425308 Binary files /dev/null and b/sc_images/room/sc_icon_task_item_unselect_bg.png differ diff --git a/sc_images/room/sc_icon_times_text_ar.png b/sc_images/room/sc_icon_times_text_ar.png new file mode 100644 index 0000000..d6207dc Binary files /dev/null and b/sc_images/room/sc_icon_times_text_ar.png differ diff --git a/sc_images/room/sc_icon_times_text_en.png b/sc_images/room/sc_icon_times_text_en.png new file mode 100644 index 0000000..7a25405 Binary files /dev/null and b/sc_images/room/sc_icon_times_text_en.png differ diff --git a/sc_images/room/sc_icon_user_card_copy_id.png b/sc_images/room/sc_icon_user_card_copy_id.png new file mode 100644 index 0000000..2c589d8 Binary files /dev/null and b/sc_images/room/sc_icon_user_card_copy_id.png differ diff --git a/sc_images/room/sc_icon_user_card_report.png b/sc_images/room/sc_icon_user_card_report.png new file mode 100644 index 0000000..f7332eb Binary files /dev/null and b/sc_images/room/sc_icon_user_card_report.png differ diff --git a/sc_images/room/sc_icon_user_count_guard.png b/sc_images/room/sc_icon_user_count_guard.png new file mode 100644 index 0000000..e3d6583 Binary files /dev/null and b/sc_images/room/sc_icon_user_count_guard.png differ diff --git a/sc_images/room/sc_icon_user_count_guard1.png b/sc_images/room/sc_icon_user_count_guard1.png new file mode 100644 index 0000000..7964088 Binary files /dev/null and b/sc_images/room/sc_icon_user_count_guard1.png differ diff --git a/sc_images/room/sc_icon_user_count_guard1_user.png b/sc_images/room/sc_icon_user_count_guard1_user.png new file mode 100644 index 0000000..d4f0859 Binary files /dev/null and b/sc_images/room/sc_icon_user_count_guard1_user.png differ diff --git a/sc_images/room/sc_icon_user_count_guard2.png b/sc_images/room/sc_icon_user_count_guard2.png new file mode 100644 index 0000000..41acfcc Binary files /dev/null and b/sc_images/room/sc_icon_user_count_guard2.png differ diff --git a/sc_images/room/sc_icon_user_count_guard2_user.png b/sc_images/room/sc_icon_user_count_guard2_user.png new file mode 100644 index 0000000..79b1daa Binary files /dev/null and b/sc_images/room/sc_icon_user_count_guard2_user.png differ diff --git a/sc_images/room/sc_icon_user_count_guard3.png b/sc_images/room/sc_icon_user_count_guard3.png new file mode 100644 index 0000000..48f12ec Binary files /dev/null and b/sc_images/room/sc_icon_user_count_guard3.png differ diff --git a/sc_images/room/sc_icon_user_count_guard3_user.png b/sc_images/room/sc_icon_user_count_guard3_user.png new file mode 100644 index 0000000..c0d0dfa Binary files /dev/null and b/sc_images/room/sc_icon_user_count_guard3_user.png differ diff --git a/sc_images/room/sc_icon_user_follow.png b/sc_images/room/sc_icon_user_follow.png new file mode 100644 index 0000000..1ba062e Binary files /dev/null and b/sc_images/room/sc_icon_user_follow.png differ diff --git a/sc_images/room/sc_icon_user_un_follow.png b/sc_images/room/sc_icon_user_un_follow.png new file mode 100644 index 0000000..93a8849 Binary files /dev/null and b/sc_images/room/sc_icon_user_un_follow.png differ diff --git a/sc_images/room/sc_icon_userson_microphone.png b/sc_images/room/sc_icon_userson_microphone.png new file mode 100644 index 0000000..cfed889 Binary files /dev/null and b/sc_images/room/sc_icon_userson_microphone.png differ diff --git a/sc_images/room/sc_icon_x.png b/sc_images/room/sc_icon_x.png new file mode 100644 index 0000000..fd138b2 Binary files /dev/null and b/sc_images/room/sc_icon_x.png differ diff --git a/sc_images/splash/sc_icon_splash_icon.png b/sc_images/splash/sc_icon_splash_icon.png new file mode 100644 index 0000000..863d24e Binary files /dev/null and b/sc_images/splash/sc_icon_splash_icon.png differ diff --git a/sc_images/splash/sc_icon_splash_skip_bg.png b/sc_images/splash/sc_icon_splash_skip_bg.png new file mode 100644 index 0000000..27cc08a Binary files /dev/null and b/sc_images/splash/sc_icon_splash_skip_bg.png differ diff --git a/sc_images/splash/sc_splash.png b/sc_images/splash/sc_splash.png new file mode 100644 index 0000000..c9bdcae Binary files /dev/null and b/sc_images/splash/sc_splash.png differ diff --git a/sc_images/store/sc_icon_bag_clock.png b/sc_images/store/sc_icon_bag_clock.png new file mode 100644 index 0000000..5800914 Binary files /dev/null and b/sc_images/store/sc_icon_bag_clock.png differ diff --git a/sc_images/store/sc_icon_bag_shop.png b/sc_images/store/sc_icon_bag_shop.png new file mode 100644 index 0000000..660f08c Binary files /dev/null and b/sc_images/store/sc_icon_bag_shop.png differ diff --git a/sc_images/store/sc_icon_shop_bag.png b/sc_images/store/sc_icon_shop_bag.png new file mode 100644 index 0000000..724b477 Binary files /dev/null and b/sc_images/store/sc_icon_shop_bag.png differ diff --git a/sc_images/store/sc_icon_shop_item_play.png b/sc_images/store/sc_icon_shop_item_play.png new file mode 100644 index 0000000..09c26ca Binary files /dev/null and b/sc_images/store/sc_icon_shop_item_play.png differ diff --git a/sc_images/store/sc_icon_shop_item_search.png b/sc_images/store/sc_icon_shop_item_search.png new file mode 100644 index 0000000..711f94e Binary files /dev/null and b/sc_images/store/sc_icon_shop_item_search.png differ diff --git a/sc_images/store/sc_icon_store_theme_rev.png b/sc_images/store/sc_icon_store_theme_rev.png new file mode 100644 index 0000000..4a1b406 Binary files /dev/null and b/sc_images/store/sc_icon_store_theme_rev.png differ diff --git a/test/business_logic_strategy_test.dart b/test/business_logic_strategy_test.dart new file mode 100644 index 0000000..8837628 --- /dev/null +++ b/test/business_logic_strategy_test.dart @@ -0,0 +1,1707 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/app/config/app_config.dart'; +import 'package:yumi/app/config/business_logic_strategy.dart'; +import 'package:yumi/app/config/configs/variant1_config.dart'; +import 'package:yumi/app/config/strategies/base_business_logic_strategy.dart'; +import 'package:yumi/app/config/strategies/variant1_business_logic_strategy.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +void main() { + group('BusinessLogicStrategy Tests', () { + test('Home tab index test', () { + final strategy = BaseBusinessLogicStrategy(); + expect(strategy.getHomeInitialTabIndex(), isA()); + }); + + test( + 'First recharge position test', + () { + final strategy = BaseBusinessLogicStrategy(); + final pos = strategy.getFirstRechargePosition(); + expect(pos, isA>()); + expect(pos.containsKey('top'), true); + expect(pos.containsKey('bottom'), true); + expect(pos.containsKey('start'), true); + expect(pos.containsKey('end'), true); + }); + + test('Variant1 home tab index test', () { + final strategy = Variant1BusinessLogicStrategy(); + expect(strategy.getHomeInitialTabIndex(), 0); + }); + + test( + 'Variant1 recharge position test', + () { + final strategy = Variant1BusinessLogicStrategy(); + final pos = strategy.getFirstRechargePosition(); + expect(pos['top'], 100.0); + expect(pos['start'], 15.0); + expect(pos['bottom'], null); + expect(pos['end'], null); + }); + + test('Config strategy test', () { + final c = SCVariant1Config(); + expect(c.businessLogicStrategy, isA()); + }); + + test('Config init test', () { + // 直接测试初始化方法 + AppConfig.initialize(); + expect(AppConfig.current, isA()); + expect(AppConfig.current.businessLogicStrategy, + isA()); + }); + }); + + group('Admin Page Strategy Tests', () { + test('Admin editing test', () { + final s = BaseBusinessLogicStrategy(); + + // 测试编辑页面背景图片 + expect(s.getAdminEditingBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试编辑页面按钮渐变颜色 + final wg = s.getAdminEditingButtonGradient('warning'); + expect(wg, isA>()); + expect(wg.length, 2); + expect(wg[0], const Color(0xffC670FF)); + expect(wg[1], const Color(0xff7726FF)); + + // 测试编辑页面图标路径 + expect(s.getAdminEditingIcon('specialIdBg'), + "sc_images/general/at_icon_special_id_bg.png"); + expect(s.getAdminEditingIcon('normalIdBg'), + "sc_images/general/at_icon_id_bg.png"); + expect(s.getAdminEditingIcon('copyId'), + "sc_images/room/at_icon_user_card_copy_id.png"); + expect(s.getAdminEditingIcon('addPic'), + "sc_images/general/at_icon_add_pic.png"); + expect(s.getAdminEditingIcon('closePic'), + "sc_images/general/at_icon_pic_close.png"); + expect(s.getAdminEditingIcon('checked'), + "sc_images/general/at_icon_checked.png"); + expect(s.getAdminEditingIcon('unknown'), ""); + + // 测试编辑页面输入框装饰 + final inputDecoration = s.getAdminEditingInputDecoration(); + expect(inputDecoration, isA()); + expect(inputDecoration.color, Colors.white); + expect(inputDecoration.borderRadius, + BorderRadius.circular(8)); // 注意:BorderRadius.circular(8) 与 BorderRadius.circular(8) 比较 + + // 测试最大图片上传数量 + expect(s.getAdminEditingMaxImageUploadCount(), 3); + + // 测试描述最大长度 + expect(s.getAdminEditingDescriptionMaxLength(), 200); + + // 测试违规类型映射 + final userViolationMapping = + s.getAdminEditingViolationTypeMapping('User'); + expect(userViolationMapping, isA>()); + expect(userViolationMapping['Pornography'], 1); + expect(userViolationMapping['Illegal information'], 2); + + final roomViolationMapping = + s.getAdminEditingViolationTypeMapping('Room'); + expect(roomViolationMapping, isA>()); + expect(roomViolationMapping['Pornography'], 3); + expect(roomViolationMapping['Illegal information'], 4); + expect(roomViolationMapping['Fraud'], 5); + expect(roomViolationMapping['Other'], 6); + }); + + test('Search test', () { + final s = BaseBusinessLogicStrategy(); + + // 测试搜索页面背景图片 + expect(s.getAdminSearchBackgroundImage('userSearch'), + "sc_images/index/at_icon_coupon_head_bg.png"); + expect(s.getAdminSearchBackgroundImage('roomSearch'), + "sc_images/index/at_icon_coupon_head_bg.png"); + + // 测试搜索按钮渐变颜色 + final buttonGradient = s.getAdminSearchButtonGradient('userSearch'); + expect(buttonGradient, isA>()); + expect(buttonGradient.length, 2); + expect(buttonGradient[0], Colors.transparent); + expect(buttonGradient[1], Colors.transparent); + + // 测试搜索按钮文本颜色 + expect(s.getAdminSearchButtonTextColor('userSearch'), Colors.black); + + // 测试编辑图标路径 + expect(s.getAdminSearchEditIcon('userSearch'), + "sc_images/room/at_icon_room_edit.png"); + + // 测试搜索框边框颜色 + expect(s.getAdminSearchInputBorderColor('userSearch'), + Colors.black12); + + // 测试搜索框文本颜色 + expect(s.getAdminSearchInputTextColor('userSearch'), Colors.grey); + + // 测试用户信息图标路径 + expect(s.getAdminSearchUserInfoIcon('specialIdBg'), + "sc_images/general/at_icon_special_id_bg.png"); + expect(s.getAdminSearchUserInfoIcon('normalIdBg'), + "sc_images/general/at_icon_id_bg.png"); + expect(s.getAdminSearchUserInfoIcon('copyId'), + "sc_images/room/at_icon_user_card_copy_id.png"); + expect(s.getAdminSearchUserInfoIcon('unknown'), ""); + }); + + test('Variant1 editing test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试编辑页面背景图片(马甲包目前使用相同图片) + expect(strategy.getAdminEditingBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试编辑页面按钮渐变颜色(马甲包使用不同渐变) + final buttonGradient = strategy.getAdminEditingButtonGradient('warning'); + expect(buttonGradient, isA>()); + expect(buttonGradient.length, 2); + expect(buttonGradient[0], const Color(0xffFF9326)); + expect(buttonGradient[1], const Color(0xffFEB219)); + + // 测试编辑页面图标路径(马甲包目前使用相同图标) + expect(strategy.getAdminEditingIcon('specialIdBg'), + "sc_images/general/at_icon_special_id_bg.png"); + + // 测试编辑页面输入框装饰(马甲包使用不同装饰) + final inputDecoration = strategy.getAdminEditingInputDecoration(); + expect(inputDecoration, isA()); + expect(inputDecoration.color, const Color(0xfff2f2f2)); + expect(inputDecoration.borderRadius, BorderRadius.circular(56)); + + // 测试最大图片上传数量(马甲包使用不同值) + expect(strategy.getAdminEditingMaxImageUploadCount(), 5); + + // 测试描述最大长度(马甲包使用不同值) + expect(strategy.getAdminEditingDescriptionMaxLength(), 300); + + // 测试违规类型映射(马甲包使用相同映射) + final roomViolationMapping = + strategy.getAdminEditingViolationTypeMapping('Room'); + expect(roomViolationMapping['Pornography'], 3); + expect(roomViolationMapping['Other'], 6); + }); + + test('Variant1 search methods', + () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试搜索页面背景图片(马甲包使用相同图片) + expect(strategy.getAdminSearchBackgroundImage('userSearch'), + "sc_images/index/at_icon_coupon_head_bg.png"); + + // 测试搜索按钮渐变颜色(马甲包使用不同渐变) + final buttonGradient = strategy.getAdminSearchButtonGradient('userSearch'); + expect(buttonGradient, isA>()); + expect(buttonGradient.length, 2); + expect(buttonGradient[0], const Color(0xffFF9326)); + expect(buttonGradient[1], const Color(0xffFEB219)); + + // 测试搜索按钮文本颜色(马甲包使用不同颜色) + expect(strategy.getAdminSearchButtonTextColor('userSearch'), Colors.white); + + // 测试编辑图标路径(马甲包使用相同图标) + expect(strategy.getAdminSearchEditIcon('userSearch'), + "sc_images/room/at_icon_room_edit.png"); + + // 测试搜索框边框颜色(马甲包使用不同颜色) + expect(strategy.getAdminSearchInputBorderColor('userSearch'), + const Color(0xffFF5722)); + + // 测试搜索框文本颜色(马甲包使用不同颜色) + expect(strategy.getAdminSearchInputTextColor('userSearch'), Colors.black87); + + // 测试用户信息图标路径(马甲包使用相同图标) + expect(strategy.getAdminSearchUserInfoIcon('specialIdBg'), + "sc_images/general/at_icon_special_id_bg.png"); + }); + }); + + group('Media Page Strategy Tests', () { + test('Image preview test', + () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试图片预览页面背景颜色 + expect(strategy.getImagePreviewBackgroundColor(), Colors.black); + + // 测试图片预览画廊背景颜色 + expect(strategy.getImagePreviewGalleryBackgroundColor(), Colors.black); + + // 测试图片预览AppBar背景颜色 + expect(strategy.getImagePreviewAppBarBackgroundColor(), Colors.black54); + + // 测试图片预览返回图标颜色 + expect(strategy.getImagePreviewBackIconColor(), Colors.white); + + // 测试图片预览文本颜色 + expect(strategy.getImagePreviewTextColor(), Colors.white); + + // 测试图片预览加载指示器颜色 + expect(strategy.getImagePreviewLoadingIndicatorColor(), + const Color(0xFFFF9500)); + + // 测试图片预览AppBar透明度 + expect(strategy.getImagePreviewAppBarOpacity(), 0.54); + }); + + test('Video player test', + () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试视频播放页面背景颜色 + expect(strategy.getVideoPlayerBackgroundColor(), Colors.black); + + // 测试视频播放控制界面背景颜色 + expect(strategy.getVideoPlayerControlsBackgroundColor(), Colors.black38); + + // 测试视频播放图标颜色 + expect(strategy.getVideoPlayerIconColor(), Colors.white); + + // 测试视频播放底部控制栏背景颜色 + expect(strategy.getVideoPlayerBottomControlsBackgroundColor(), + Colors.black54); + + // 测试视频播放进度条活动颜色 + expect(strategy.getVideoPlayerProgressBarActiveColor(), Colors.red); + + // 测试视频播放进度条非活动颜色 + expect(strategy.getVideoPlayerProgressBarInactiveColor(), Colors.grey); + + // 测试视频播放文本颜色 + expect(strategy.getVideoPlayerTextColor(), Colors.white); + + // 测试视频播放加载指示器颜色 + expect(strategy.getVideoPlayerLoadingIndicatorColor(), Colors.red); + + // 测试视频播放控制界面透明度 + expect(strategy.getVideoPlayerControlsOpacity(), 0.38); + + // 测试视频播放底部控制栏透明度 + expect(strategy.getVideoPlayerBottomControlsOpacity(), 0.54); + + // 测试视频播放控制界面显示持续时间 + expect(strategy.getVideoPlayerControlsDisplayDuration(), 3); + }); + + test('Variant1 image preview test', + () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试马甲包图片预览页面背景颜色(深灰色) + expect(strategy.getImagePreviewBackgroundColor(), + const Color(0xff1a1a1a)); + + // 测试马甲包图片预览画廊背景颜色(深灰色) + expect(strategy.getImagePreviewGalleryBackgroundColor(), + const Color(0xff1a1a1a)); + + // 测试马甲包图片预览AppBar背景颜色(深灰色70%透明度) + expect(strategy.getImagePreviewAppBarBackgroundColor(), + const Color(0xff1a1a1a).withOpacity(0.7)); + + // 测试马甲包图片预览返回图标颜色(浅灰色) + expect(strategy.getImagePreviewBackIconColor(), + const Color(0xffe6e6e6)); + + // 测试马甲包图片预览文本颜色(浅灰色) + expect(strategy.getImagePreviewTextColor(), + const Color(0xffe6e6e6)); + + // 测试马甲包图片预览加载指示器颜色(橙色主题色) + expect(strategy.getImagePreviewLoadingIndicatorColor(), + const Color(0xffFF5722)); + + // 测试马甲包图片预览AppBar透明度 + expect(strategy.getImagePreviewAppBarOpacity(), 0.7); + }); + + test('Variant1 video player test', + () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试马甲包视频播放页面背景颜色(深灰色) + expect(strategy.getVideoPlayerBackgroundColor(), + const Color(0xff1a1a1a)); + + // 测试马甲包视频播放控制界面背景颜色(深灰色50%透明度) + expect(strategy.getVideoPlayerControlsBackgroundColor(), + const Color(0xff1a1a1a).withOpacity(0.5)); + + // 测试马甲包视频播放图标颜色(浅灰色) + expect(strategy.getVideoPlayerIconColor(), + const Color(0xffe6e6e6)); + + // 测试马甲包视频播放底部控制栏背景颜色(深灰色60%透明度) + expect(strategy.getVideoPlayerBottomControlsBackgroundColor(), + const Color(0xff1a1a1a).withOpacity(0.6)); + + // 测试马甲包视频播放进度条活动颜色(橙色主题色) + expect(strategy.getVideoPlayerProgressBarActiveColor(), + const Color(0xffFF5722)); + + // 测试马甲包视频播放进度条非活动颜色(深灰色) + expect(strategy.getVideoPlayerProgressBarInactiveColor(), + const Color(0xff666666)); + + // 测试马甲包视频播放文本颜色(浅灰色) + expect(strategy.getVideoPlayerTextColor(), + const Color(0xffe6e6e6)); + + // 测试马甲包视频播放加载指示器颜色(橙色主题色) + expect(strategy.getVideoPlayerLoadingIndicatorColor(), + const Color(0xffFF5722)); + + // 测试马甲包视频播放控制界面透明度 + expect(strategy.getVideoPlayerControlsOpacity(), 0.5); + + // 测试马甲包视频播放底部控制栏透明度 + expect(strategy.getVideoPlayerBottomControlsOpacity(), 0.6); + + // 测试马甲包视频播放控制界面显示持续时间(5秒) + expect(strategy.getVideoPlayerControlsDisplayDuration(), 5); + }); + }); + + group('Report Page Strategy Tests', () { + test('Report page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试报告页面背景图片 + expect(strategy.getReportPageBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试报告页面提示文本颜色 + expect(strategy.getReportPageHintTextColor(), Colors.black38); + + // 测试报告页面容器背景颜色 + expect(strategy.getReportPageContainerBackgroundColor(), Colors.white); + + // 测试报告页面边框颜色 + expect(strategy.getReportPageBorderColor(), const Color(0xffE6E6E6)); + + // 测试报告页面主要文本颜色 + expect(strategy.getReportPagePrimaryTextColor(), Colors.black); + + // 测试报告页面次要提示文本颜色 + expect(strategy.getReportPageSecondaryHintTextColor(), Colors.black45); + + // 测试报告页面未选中边框颜色 + expect(strategy.getReportPageUnselectedBorderColor(), Colors.grey); + + // 测试报告页面提交按钮背景颜色(原始应用使用主题色,但Base策略返回蓝色占位符) + expect(strategy.getReportPageSubmitButtonBackgroundColor(), Colors.blue); + + // 测试报告页面提交按钮文本颜色 + expect(strategy.getReportPageSubmitButtonTextColor(), Colors.white); + + // 测试报告页面图标 + expect(strategy.getReportPageIcon('addPic'), + "sc_images/general/at_icon_add_pic.png"); + expect(strategy.getReportPageIcon('closePic'), + "sc_images/general/at_icon_pic_close.png"); + expect(strategy.getReportPageIcon('checked'), + "sc_images/general/at_icon_checked.png"); + expect(strategy.getReportPageIcon('unknown'), ""); + }); + + test('Variant1 report page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试报告页面背景图片(马甲包使用相同图片) + expect(strategy.getReportPageBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试报告页面提示文本颜色(马甲包使用深灰色) + expect(strategy.getReportPageHintTextColor(), + const Color(0xff666666)); + + // 测试报告页面容器背景颜色(马甲包使用白色) + expect(strategy.getReportPageContainerBackgroundColor(), Colors.white); + + // 测试报告页面边框颜色(马甲包使用浅灰色) + expect(strategy.getReportPageBorderColor(), + const Color(0xffCCCCCC)); + + // 测试报告页面主要文本颜色(马甲包使用黑色) + expect(strategy.getReportPagePrimaryTextColor(), Colors.black); + + // 测试报告页面次要提示文本颜色(马甲包使用深灰色) + expect(strategy.getReportPageSecondaryHintTextColor(), + const Color(0xff666666)); + + // 测试报告页面未选中边框颜色(马甲包使用灰色) + expect(strategy.getReportPageUnselectedBorderColor(), Colors.grey); + + // 测试报告页面提交按钮背景颜色(马甲包使用橙色主题色) + expect(strategy.getReportPageSubmitButtonBackgroundColor(), + const Color(0xffFF5722)); + + // 测试报告页面提交按钮文本颜色(马甲包使用白色) + expect(strategy.getReportPageSubmitButtonTextColor(), Colors.white); + + // 测试报告页面图标(马甲包使用相同图标) + expect(strategy.getReportPageIcon('addPic'), + "sc_images/general/at_icon_add_pic.png"); + expect(strategy.getReportPageIcon('closePic'), + "sc_images/general/at_icon_pic_close.png"); + expect(strategy.getReportPageIcon('checked'), + "sc_images/general/at_icon_checked.png"); + expect(strategy.getReportPageIcon('unknown'), ""); + }); + }); + + group('Store Page Strategy Tests', () { + test('Store page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试Store页面背景图片 + expect(strategy.getStorePageBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试Store页面购物袋图标 + expect(strategy.getStorePageShoppingBagIcon(), + "sc_images/store/at_icon_shop_bag.png"); + + // 测试Store页面购物袋图标外边距 + final shoppingBagMargin = strategy.getStorePageShoppingBagMargin(); + expect(shoppingBagMargin, isA()); + expect(shoppingBagMargin.end, 10.0); + + // 测试Store页面Tab标签内边距 + final tabLabelPadding = strategy.getStorePageTabLabelPadding(); + expect(tabLabelPadding, isA()); + expect(tabLabelPadding.left, 12.0); + expect(tabLabelPadding.end, 12.0); + + // 测试Store页面Tab标签选中文本样式 + final selectedTabStyle = strategy.getStorePageTabLabelStyle(); + expect(selectedTabStyle, isA()); + expect(selectedTabStyle.fontSize, 15.0); + expect(selectedTabStyle.color, Colors.black87); + expect(selectedTabStyle.fontWeight, FontWeight.w600); + + // 测试Store页面Tab标签未选中文本样式 + final unselectedTabStyle = strategy.getStorePageTabUnselectedLabelStyle(); + expect(unselectedTabStyle, isA()); + expect(unselectedTabStyle.fontSize, 13.0); + expect(unselectedTabStyle.color, Colors.black38); + expect(unselectedTabStyle.fontWeight, FontWeight.w500); + + // 测试Store页面Tab指示器颜色 + expect(strategy.getStorePageTabIndicatorColor(), + const Color(0xffFF5722)); // SocialChatTheme.primaryColor + + // 测试Store页面Tab分割线颜色 + expect(strategy.getStorePageTabDividerColor(), Colors.transparent); + + // 测试Store页面底部Divider颜色 + expect(strategy.getStorePageBottomDividerColor(), + const Color(0x1F000000)); // Colors.black12 + + // 测试Store页面底部Divider厚度 + expect(strategy.getStorePageBottomDividerThickness(), 0.5); + + // 测试Store页面底部Divider高度 + expect(strategy.getStorePageBottomDividerHeight(), 24.0); + + // 测试Store页面金币图标 + expect(strategy.getStorePageGoldIcon(), + "sc_images/general/at_icon_jb.png"); + + // 测试Store页面金币文本颜色 + expect(strategy.getStorePageGoldTextColor(), + const Color(0xFFFFE134)); + + // 测试Store页面金币图标颜色 + expect(strategy.getStorePageGoldIconColor(), + const Color(0xFFFFE134)); + }); + + test('Store item test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试Store商品项背景颜色 + expect(strategy.getStoreItemBackgroundColor(), Colors.white); + + // 测试Store商品项未选中边框颜色 + expect(strategy.getStoreItemUnselectedBorderColor(), + const Color(0x1F000000)); // Colors.black12 + + // 测试Store商品项选中边框颜色 + expect(strategy.getStoreItemSelectedBorderColor(), + const Color(0xffFF9500)); // SocialChatTheme.primaryLight + + // 测试Store商品项金币图标 + expect(strategy.getStoreItemGoldIcon(), + "sc_images/general/at_icon_jb.png"); + + // 测试Store商品项价格文本颜色 + expect(strategy.getStoreItemPriceTextColor(), Colors.black); + + // 测试Store商品项购买按钮背景颜色 + expect(strategy.getStoreItemBuyButtonColor(), + const Color(0xffFF9500)); // SocialChatTheme.primaryLight + + // 测试Store商品项购买按钮文本颜色 + expect(strategy.getStoreItemBuyButtonTextColor(), Colors.white); + + // 测试Store商品项操作图标 + expect(strategy.getStoreItemActionIcon('headdress'), + "sc_images/store/at_icon_shop_item_search.png"); + expect(strategy.getStoreItemActionIcon('mountains'), + "sc_images/store/at_icon_shop_item_play.png"); + expect(strategy.getStoreItemActionIcon('chatbox'), + "sc_images/store/at_icon_shop_item_search.png"); + expect(strategy.getStoreItemActionIcon('theme'), + "sc_images/store/at_icon_store_theme_rev.png"); + expect(strategy.getStoreItemActionIcon('unknown'), "sc_images/store/at_icon_store_theme_rev.png"); + }); + + test('Variant1 store page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试Store页面背景图片(马甲包使用相同图片) + expect(strategy.getStorePageBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试Store页面购物袋图标(马甲包使用相同图标) + expect(strategy.getStorePageShoppingBagIcon(), + "sc_images/store/at_icon_shop_bag.png"); + + // 测试Store页面购物袋图标外边距(马甲包保持相同外边距) + final shoppingBagMargin = strategy.getStorePageShoppingBagMargin(); + expect(shoppingBagMargin, isA()); + expect(shoppingBagMargin.end, 10.0); + + // 测试Store页面Tab标签内边距(马甲包保持相同内边距) + final tabLabelPadding = strategy.getStorePageTabLabelPadding(); + expect(tabLabelPadding, isA()); + expect(tabLabelPadding.left, 12.0); + expect(tabLabelPadding.end, 12.0); + + // 测试Store页面Tab标签选中文本样式(马甲包使用主题色) + final selectedTabStyle = strategy.getStorePageTabLabelStyle(); + expect(selectedTabStyle, isA()); + expect(selectedTabStyle.fontSize, 15.0); + expect(selectedTabStyle.color, const Color(0xffFF5722)); // 马甲包主题橙色 + expect(selectedTabStyle.fontWeight, FontWeight.w600); + + // 测试Store页面Tab标签未选中文本样式(马甲包使用灰色) + final unselectedTabStyle = strategy.getStorePageTabUnselectedLabelStyle(); + expect(unselectedTabStyle, isA()); + expect(unselectedTabStyle.fontSize, 13.0); + expect(unselectedTabStyle.color, const Color(0xff757575)); // 马甲包灰色 + expect(unselectedTabStyle.fontWeight, FontWeight.w500); + + // 测试Store页面Tab指示器颜色(马甲包使用主题色) + expect(strategy.getStorePageTabIndicatorColor(), + const Color(0xffFF5722)); // 马甲包主题橙色 + + // 测试Store页面Tab分割线颜色(马甲包保持透明) + expect(strategy.getStorePageTabDividerColor(), Colors.transparent); + + // 测试Store页面底部Divider颜色(马甲包使用浅灰色) + expect(strategy.getStorePageBottomDividerColor(), + const Color(0xffEEEEEE)); // 浅灰色,替代Colors.black12 + + // 测试Store页面底部Divider厚度(马甲包保持相同) + expect(strategy.getStorePageBottomDividerThickness(), 0.5); + + // 测试Store页面底部Divider高度(马甲包保持相同) + expect(strategy.getStorePageBottomDividerHeight(), 24.0); + + // 测试Store页面金币图标(马甲包使用相同图标) + expect(strategy.getStorePageGoldIcon(), + "sc_images/general/at_icon_jb.png"); + + // 测试Store页面金币文本颜色(马甲包使用主题色) + expect(strategy.getStorePageGoldTextColor(), + const Color(0xffFF5722)); // 马甲包主题橙色 + + // 测试Store页面金币图标颜色(马甲包使用主题色) + expect(strategy.getStorePageGoldIconColor(), + const Color(0xffFF5722)); // 马甲包主题橙色 + }); + + test('Variant1 store item test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试Store商品项背景颜色(马甲包使用表面色) + expect(strategy.getStoreItemBackgroundColor(), + const Color(0xffffffff)); // SocialChatTheme.surfaceColor - 白色 + + // 测试Store商品项未选中边框颜色(马甲包使用浅灰色) + expect(strategy.getStoreItemUnselectedBorderColor(), + const Color(0xffE0E0E0)); // 浅灰色,替代Colors.black12 + + // 测试Store商品项选中边框颜色(马甲包使用主题色) + expect(strategy.getStoreItemSelectedBorderColor(), + const Color(0xffFF5722)); // 马甲包主题橙色 + + // 测试Store商品项金币图标(马甲包使用相同图标) + expect(strategy.getStoreItemGoldIcon(), + "sc_images/general/at_icon_jb.png"); + + // 测试Store商品项价格文本颜色(马甲包使用主要文本颜色) + expect(strategy.getStoreItemPriceTextColor(), + const Color(0xff212121)); // SocialChatTheme.textPrimary - 深灰色 + + // 测试Store商品项购买按钮背景颜色(马甲包使用主题色) + expect(strategy.getStoreItemBuyButtonColor(), + const Color(0xffFF5722)); // 马甲包主题橙色 + + // 测试Store商品项购买按钮文本颜色(马甲包使用主色上的文本颜色) + expect(strategy.getStoreItemBuyButtonTextColor(), + const Color(0xffffffff)); // SocialChatTheme.textOnPrimary - 白色 + + // 测试Store商品项操作图标(马甲包使用相同图标) + expect(strategy.getStoreItemActionIcon('headdress'), + "sc_images/store/at_icon_shop_item_search.png"); + expect(strategy.getStoreItemActionIcon('mountains'), + "sc_images/store/at_icon_shop_item_play.png"); + expect(strategy.getStoreItemActionIcon('chatbox'), + "sc_images/store/at_icon_shop_item_search.png"); + expect(strategy.getStoreItemActionIcon('theme'), + "sc_images/store/at_icon_store_theme_rev.png"); + expect(strategy.getStoreItemActionIcon('unknown'), "sc_images/store/at_icon_store_theme_rev.png"); + }); + }); + + group('Voice Room Page Strategy Tests', () { + test('Voice room test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试语音房间页面背景颜色 + expect(strategy.getVoiceRoomBackgroundColor(), Colors.transparent); + + // 测试语音房间页面Tab标签选中颜色 + expect(strategy.getVoiceRoomTabLabelColor(), Colors.white); + + // 测试语音房间页面Tab标签未选中颜色 + expect(strategy.getVoiceRoomTabUnselectedLabelColor(), Colors.white54); + + // 测试语音房间页面Tab指示器颜色 + expect(strategy.getVoiceRoomTabIndicatorColor(), Colors.transparent); + + // 测试语音房间页面Tab分割线颜色 + expect(strategy.getVoiceRoomTabDividerColor(), Colors.transparent); + + // 测试语音房间页面Tab标签内边距 + final tabLabelPadding = strategy.getVoiceRoomTabLabelPadding(); + expect(tabLabelPadding, isA()); + expect(tabLabelPadding.left, 12.0); + expect(tabLabelPadding.end, 12.0); + + // 测试语音房间页面聊天容器外边距 + final chatContainerMargin = strategy.getVoiceRoomChatContainerMargin(); + expect(chatContainerMargin, isA()); + expect(chatContainerMargin.right, 52.0); + + // 测试语音房间页面Tab标签选中文本样式 + final selectedTabStyle = strategy.getVoiceRoomTabLabelStyle(); + expect(selectedTabStyle, isA()); + expect(selectedTabStyle.fontSize, 15.0); + expect(selectedTabStyle.fontFamily, "MyCustomFont"); + + // 测试语音房间页面Tab标签未选中文本样式 + final unselectedTabStyle = strategy.getVoiceRoomTabUnselectedLabelStyle(); + expect(unselectedTabStyle, isA()); + expect(unselectedTabStyle.fontSize, 13.0); + expect(unselectedTabStyle.fontFamily, "MyCustomFont"); + + // 测试语音房间默认背景图像路径 + expect(strategy.getVoiceRoomDefaultBackgroundImage(), + "sc_images/room/at_icon_room_defaut_bg.png"); + }); + + test('Variant1 voice room test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试马甲包语音房间页面背景颜色(深灰色) + expect(strategy.getVoiceRoomBackgroundColor(), + const Color(0xff1a1a1a)); + + // 测试马甲包语音房间页面Tab标签选中颜色(浅灰色) + expect(strategy.getVoiceRoomTabLabelColor(), + const Color(0xffe6e6e6)); + + // 测试马甲包语音房间页面Tab标签未选中颜色(灰色) + expect(strategy.getVoiceRoomTabUnselectedLabelColor(), + const Color(0xff999999)); + + // 测试马甲包语音房间页面Tab指示器颜色(马甲包主题色) + expect(strategy.getVoiceRoomTabIndicatorColor(), + const Color(0xffFF5722)); + + // 测试马甲包语音房间页面Tab分割线颜色(透明) + expect(strategy.getVoiceRoomTabDividerColor(), Colors.transparent); + + // 测试马甲包语音房间页面Tab标签内边距(与原始应用相同) + final tabLabelPadding = strategy.getVoiceRoomTabLabelPadding(); + expect(tabLabelPadding, isA()); + expect(tabLabelPadding.left, 12.0); + expect(tabLabelPadding.end, 12.0); + + // 测试马甲包语音房间页面聊天容器外边距(与原始应用相同) + final chatContainerMargin = strategy.getVoiceRoomChatContainerMargin(); + expect(chatContainerMargin, isA()); + expect(chatContainerMargin.end, 52.0); + + // 测试马甲包语音房间页面Tab标签选中文本样式(与原始应用相同) + final selectedTabStyle = strategy.getVoiceRoomTabLabelStyle(); + expect(selectedTabStyle, isA()); + expect(selectedTabStyle.fontSize, 15.0); + expect(selectedTabStyle.fontFamily, "MyCustomFont"); + + // 测试马甲包语音房间页面Tab标签未选中文本样式(与原始应用相同) + final unselectedTabStyle = strategy.getVoiceRoomTabUnselectedLabelStyle(); + expect(unselectedTabStyle, isA()); + expect(unselectedTabStyle.fontSize, 13.0); + expect(unselectedTabStyle.fontFamily, "MyCustomFont"); + + // 测试马甲包语音房间默认背景图像路径(与原始应用相同) + expect(strategy.getVoiceRoomDefaultBackgroundImage(), + "sc_images/room/at_icon_room_defaut_bg.png"); + }); + }); + + group('Recharge Page Strategy Tests', () { + test('Recharge page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试充值页面背景图片 + expect(strategy.getRechargePageBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试充值页面Scaffold背景颜色 + expect(strategy.getRechargePageScaffoldBackgroundColor(), Colors.transparent); + + // 测试充值页面对话框屏障颜色 + expect(strategy.getRechargePageDialogBarrierColor(), Colors.transparent); + + // 测试充值页面对话框容器背景颜色 + expect(strategy.getRechargePageDialogContainerBackgroundColor(), Colors.white30); + + // 测试充值页面对话框文本颜色 + expect(strategy.getRechargePageDialogTextColor(), Colors.black38); + + // 测试充值页面主容器背景颜色 + expect(strategy.getRechargePageMainContainerBackgroundColor(), Colors.white); + + // 测试充值页面按钮背景颜色 + expect(strategy.getRechargePageButtonBackgroundColor(), + const Color(0xFFE6F7FF)); // 近似主题浅色 + + // 测试充值页面按钮文本颜色 + expect(strategy.getRechargePageButtonTextColor(), Colors.white); + + // 测试充值页面钱包背景图像路径 + expect(strategy.getRechargePageWalletBackgroundImage(), + "sc_images/index/at_icon_wallet_bg.png"); + + // 测试充值页面钱包图标路径 + expect(strategy.getRechargePageWalletIcon(), + "sc_images/index/at_icon_wallet_icon.png"); + + // 测试充值页面钱包文本颜色 + expect(strategy.getRechargePageWalletTextColor(), Colors.white); + + // 测试充值页面金币图标路径(默认图标) + expect(strategy.getRechargePageGoldIcon(), + "sc_images/general/at_icon_jb.png"); + + // 测试充值页面选中项背景颜色 + expect(strategy.getRechargePageSelectedItemBackgroundColor(), + const Color(0xFFE6F7FF)); // 近似主题浅色 + + // 测试充值页面选中项边框颜色 + expect(strategy.getRechargePageSelectedItemBorderColor(), + const Color(0xFF1890FF)); // 近似主题主色 + + // 测试充值页面未选中项边框颜色 + expect(strategy.getRechargePageUnselectedItemBorderColor(), + const Color(0xffE6E6E6)); + + // 测试充值页面未选中项背景颜色 + expect(strategy.getRechargePageUnselectedItemBackgroundColor(), Colors.white); + + // 测试充值页面项目文本颜色 + expect(strategy.getRechargePageItemTextColor(), Colors.black54); + + // 测试充值页面价格文本颜色 + expect(strategy.getRechargePageItemPriceTextColor(), Colors.black); + + // 测试根据索引获取充值页面金币图标路径 + expect(strategy.getRechargePageGoldIconByIndex(0), + "sc_images/general/at_icon_jb.png"); + expect(strategy.getRechargePageGoldIconByIndex(1), + "sc_images/general/at_icon_jb2.png"); + expect(strategy.getRechargePageGoldIconByIndex(2), + "sc_images/general/at_icon_jb3.png"); + expect(strategy.getRechargePageGoldIconByIndex(99), + "sc_images/general/at_icon_jb.png"); // 默认图标 + + // 测试充值页面记录图标路径 + expect(strategy.getRechargePageRecordIcon(), + "sc_images/index/at_icon_recharge_recod.png"); + + // 测试充值页面Apple产品项背景颜色 + expect(strategy.getRechargePageAppleItemBackgroundColor(), Colors.white); + }); + + test('Variant1 recharge page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试充值页面背景图片(马甲包使用相同图片) + expect(strategy.getRechargePageBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试充值页面Scaffold背景颜色(马甲包使用深灰色) + expect(strategy.getRechargePageScaffoldBackgroundColor(), + const Color(0xff1a1a1a)); // #1a1a1a + + // 测试充值页面对话框屏障颜色(马甲包使用深灰色半透明) + expect(strategy.getRechargePageDialogBarrierColor(), + const Color(0xCC1a1a1a)); // withOpacity(0.8) + + // 测试充值页面对话框容器背景颜色(马甲包使用深灰色) + expect(strategy.getRechargePageDialogContainerBackgroundColor(), + const Color(0xff333333)); // 深灰色 + + // 测试充值页面对话框文本颜色(马甲包使用浅灰色) + expect(strategy.getRechargePageDialogTextColor(), + const Color(0xffe6e6e6)); // #e6e6e6 + + // 测试充值页面主容器背景颜色(马甲包使用深灰色) + expect(strategy.getRechargePageMainContainerBackgroundColor(), + const Color(0xff1a1a1a)); // #1a1a1a + + // 测试充值页面按钮背景颜色(马甲包使用主题色) + expect(strategy.getRechargePageButtonBackgroundColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试充值页面按钮文本颜色(马甲包使用白色,与原始应用相同) + expect(strategy.getRechargePageButtonTextColor(), Colors.white); + + // 测试充值页面钱包背景图像路径(马甲包使用相同图片) + expect(strategy.getRechargePageWalletBackgroundImage(), + "sc_images/index/at_icon_wallet_bg.png"); + + // 测试充值页面钱包图标路径(马甲包使用相同图标) + expect(strategy.getRechargePageWalletIcon(), + "sc_images/index/at_icon_wallet_icon.png"); + + // 测试充值页面钱包文本颜色(马甲包使用浅灰色) + expect(strategy.getRechargePageWalletTextColor(), + const Color(0xffe6e6e6)); // #e6e6e6 + + // 测试充值页面金币图标路径(马甲包使用相同图标) + expect(strategy.getRechargePageGoldIcon(), + "sc_images/general/at_icon_jb.png"); + + // 测试充值页面选中项背景颜色(马甲包使用主题色半透明) + expect(strategy.getRechargePageSelectedItemBackgroundColor(), + const Color(0x33FF5722)); // withOpacity(0.2) + + // 测试充值页面选中项边框颜色(马甲包使用主题色) + expect(strategy.getRechargePageSelectedItemBorderColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试充值页面未选中项边框颜色(马甲包使用深灰色) + expect(strategy.getRechargePageUnselectedItemBorderColor(), + const Color(0xff666666)); // 深灰色 + + // 测试充值页面未选中项背景颜色(马甲包使用深灰色) + expect(strategy.getRechargePageUnselectedItemBackgroundColor(), + const Color(0xff2a2a2a)); // 深灰色背景 + + // 测试充值页面项目文本颜色(马甲包使用浅灰色) + expect(strategy.getRechargePageItemTextColor(), + const Color(0xffe6e6e6)); // #e6e6e6 + + // 测试充值页面价格文本颜色(马甲包使用白色) + expect(strategy.getRechargePageItemPriceTextColor(), Colors.white); + + // 测试根据索引获取充值页面金币图标路径(马甲包使用相同图标) + expect(strategy.getRechargePageGoldIconByIndex(0), + "sc_images/general/at_icon_jb.png"); + expect(strategy.getRechargePageGoldIconByIndex(1), + "sc_images/general/at_icon_jb2.png"); + expect(strategy.getRechargePageGoldIconByIndex(2), + "sc_images/general/at_icon_jb3.png"); + expect(strategy.getRechargePageGoldIconByIndex(99), + "sc_images/general/at_icon_jb.png"); // 默认图标 + + // 测试充值页面记录图标路径(马甲包使用相同图标) + expect(strategy.getRechargePageRecordIcon(), + "sc_images/index/at_icon_recharge_recod.png"); + + // 测试充值页面Apple产品项背景颜色(马甲包使用深灰色) + expect(strategy.getRechargePageAppleItemBackgroundColor(), + const Color(0xff2a2a2a)); // 深灰色 + }); + }); + + group('Dynamic Page Strategy Tests', () { + test('Dynamic page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试动态页面背景图像路径 + expect(strategy.getDynamicPageBackgroundImage(), + "sc_images/index/at_icon_index_mask.png"); + + // 测试动态页面Scaffold背景颜色 + expect(strategy.getDynamicPageScaffoldBackgroundColor(), Colors.transparent); + + // 测试动态页面AppBar背景颜色 + expect(strategy.getDynamicPageAppBarBackgroundColor(), Colors.transparent); + + // 测试动态页面Tab标签选中颜色 + expect(strategy.getDynamicPageTabLabelColor(), Colors.black87); + + // 测试动态页面Tab标签未选中颜色 + expect(strategy.getDynamicPageTabUnselectedLabelColor(), Colors.black38); + + // 测试动态页面Tab指示器颜色 + expect(strategy.getDynamicPageTabIndicatorColor(), Colors.transparent); + + // 测试动态页面Tab分割线颜色 + expect(strategy.getDynamicPageTabDividerColor(), Colors.transparent); + + // 测试动态页面添加动态按钮图标路径 + expect(strategy.getDynamicPageAddButtonIcon(), + "sc_images/dynamic/at_icon_add_dynamic_btn.png"); + + // 测试动态页面Tab标签是否可滚动 + expect(strategy.shouldDynamicPageTabScrollable(), true); + + // 测试动态页面Tab标签选中文本样式 + expect(strategy.getDynamicPageTabLabelStyle(), + const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + )); + + // 测试动态页面Tab标签未选中文本样式 + expect(strategy.getDynamicPageTabUnselectedLabelStyle(), + const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 15, + )); + }); + + test('Variant1 dynamic page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试动态页面背景图像路径(马甲包使用相同图片) + expect(strategy.getDynamicPageBackgroundImage(), + "sc_images/index/at_icon_index_mask.png"); + + // 测试动态页面Scaffold背景颜色(马甲包保持透明) + expect(strategy.getDynamicPageScaffoldBackgroundColor(), Colors.transparent); + + // 测试动态页面AppBar背景颜色(马甲包保持透明) + expect(strategy.getDynamicPageAppBarBackgroundColor(), Colors.transparent); + + // 测试动态页面Tab标签选中颜色(马甲包使用主要文本颜色) + expect(strategy.getDynamicPageTabLabelColor(), + const Color(0xff212121)); // SocialChatTheme.textPrimary + + // 测试动态页面Tab标签未选中颜色(马甲包使用次要文本颜色) + expect(strategy.getDynamicPageTabUnselectedLabelColor(), + const Color(0xff757575)); // SocialChatTheme.textSecondary + + // 测试动态页面Tab指示器颜色(马甲包使用主题色) + expect(strategy.getDynamicPageTabIndicatorColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试动态页面Tab分割线颜色(马甲包保持透明) + expect(strategy.getDynamicPageTabDividerColor(), Colors.transparent); + + // 测试动态页面添加动态按钮图标路径(马甲包使用相同图标) + expect(strategy.getDynamicPageAddButtonIcon(), + "sc_images/dynamic/at_icon_add_dynamic_btn.png"); + + // 测试动态页面Tab标签是否可滚动(马甲包保持相同行为) + expect(strategy.shouldDynamicPageTabScrollable(), true); + + // 测试动态页面Tab标签选中文本样式(马甲包应用主题颜色) + expect(strategy.getDynamicPageTabLabelStyle(), + const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Color(0xff212121), // SocialChatTheme.textPrimary + )); + + // 测试动态页面Tab标签未选中文本样式(马甲包应用主题颜色) + expect(strategy.getDynamicPageTabUnselectedLabelStyle(), + const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 15, + color: Color(0xff757575), // SocialChatTheme.textSecondary + )); + }); + }); + + group('Splash Page Strategy Tests', () { + test('Splash page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试启动页面背景图像路径 + expect(strategy.getSplashPageBackgroundImage(), "sc_images/splash/splash.png"); + + // 测试启动页面图标路径 + expect(strategy.getSplashPageIcon(), "sc_images/splash/at_icon_splash_icon.png"); + + // 测试启动页面跳过按钮背景图像路径 + expect(strategy.getSplashPageSkipButtonBackground(), + "sc_images/splash/at_icon_splash_skip_bg.png"); + + // 测试启动页面跳过按钮文本颜色 + expect(strategy.getSplashPageSkipButtonTextColor(), Colors.white); + + // 测试启动页面游戏名称背景图像路径 + expect(strategy.getSplashPageKingGamesNameBackground(), + "sc_images/index/at_icon_splash_king_games_name_bg.png"); + + // 测试启动页面游戏名称文本颜色 + expect(strategy.getSplashPageKingGamesTextColor(), Colors.white); + + // 测试启动页面CP名称背景图像路径 + expect(strategy.getSplashPageCpNameBackground(), + "sc_images/index/at_icon_splash_cp_name_bg.png"); + + // 测试启动页面CP名称文本颜色 + expect(strategy.getSplashPageCpTextColor(), Colors.white); + }); + + test('Variant1 splash page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试启动页面背景图像路径(马甲包目前使用相同图像) + expect(strategy.getSplashPageBackgroundImage(), "sc_images/splash/splash.png"); + + // 测试启动页面图标路径(马甲包目前使用相同图标) + expect(strategy.getSplashPageIcon(), "sc_images/splash/at_icon_splash_icon.png"); + + // 测试启动页面跳过按钮背景图像路径(马甲包目前使用相同图像) + expect(strategy.getSplashPageSkipButtonBackground(), + "sc_images/splash/at_icon_splash_skip_bg.png"); + + // 测试启动页面跳过按钮文本颜色(马甲包使用主色上的文本颜色) + expect(strategy.getSplashPageSkipButtonTextColor(), isA()); + + // 测试启动页面游戏名称背景图像路径(马甲包目前使用相同图像) + expect(strategy.getSplashPageKingGamesNameBackground(), + "sc_images/index/at_icon_splash_king_games_name_bg.png"); + + // 测试启动页面游戏名称文本颜色(马甲包使用主色上的文本颜色) + expect(strategy.getSplashPageKingGamesTextColor(), isA()); + + // 测试启动页面CP名称背景图像路径(马甲包目前使用相同图像) + expect(strategy.getSplashPageCpNameBackground(), + "sc_images/index/at_icon_splash_cp_name_bg.png"); + + // 测试启动页面CP名称文本颜色(马甲包使用主色上的文本颜色) + expect(strategy.getSplashPageCpTextColor(), isA()); + }); + + }); + + group('WebView Page Strategy Tests', () { + test('Webview page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试WebView页面AppBar背景颜色 + expect(strategy.getWebViewPageAppBarBackgroundColor(), Colors.white); + + // 测试WebView页面标题文本颜色 + expect(strategy.getWebViewPageTitleTextColor(), const Color(0xff333333)); + + // 测试WebView页面返回箭头图标颜色 + expect(strategy.getWebViewPageBackArrowColor(), const Color(0xff333333)); + + // 测试WebView页面进度条背景颜色 + expect(strategy.getWebViewPageProgressBarBackgroundColor(), Colors.white70.withOpacity(0)); + + // 测试WebView页面进度条活动颜色 + expect(strategy.getWebViewPageProgressBarActiveColor(), const Color(0xffFF6000)); + + // 测试游戏WebView页面关闭图标颜色 + expect(strategy.getGameWebViewCloseIconColor(), Colors.white); + + // 测试游戏WebView页面Scaffold背景颜色 + expect(strategy.getGameWebViewScaffoldBackgroundColor(), Colors.transparent); + + // 测试游戏WebView页面加载指示器颜色 + expect(strategy.getGameWebViewLoadingIndicatorColor(), const Color(0xffFF6000)); + + // 测试游戏WebView页面进度条背景颜色 + expect(strategy.getGameWebViewProgressBarBackgroundColor(), Colors.white70.withOpacity(0)); + }); + + test('Variant1 webview page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试WebView页面AppBar背景颜色(马甲包使用背景颜色) + expect(strategy.getWebViewPageAppBarBackgroundColor(), isA()); + + // 测试WebView页面标题文本颜色(马甲包使用主要文本颜色) + expect(strategy.getWebViewPageTitleTextColor(), isA()); + + // 测试WebView页面返回箭头图标颜色(马甲包使用主要文本颜色) + expect(strategy.getWebViewPageBackArrowColor(), isA()); + + // 测试WebView页面进度条背景颜色(马甲包使用透明背景) + expect(strategy.getWebViewPageProgressBarBackgroundColor(), Colors.transparent); + + // 测试WebView页面进度条活动颜色(马甲包使用主题色) + expect(strategy.getWebViewPageProgressBarActiveColor(), isA()); + + // 测试游戏WebView页面关闭图标颜色(马甲包使用主要文本颜色) + expect(strategy.getGameWebViewCloseIconColor(), isA()); + + // 测试游戏WebView页面Scaffold背景颜色(马甲包使用背景颜色) + expect(strategy.getGameWebViewScaffoldBackgroundColor(), isA()); + + // 测试游戏WebView页面进度条背景颜色(马甲包使用背景颜色半透明) + expect(strategy.getGameWebViewProgressBarBackgroundColor(), isA()); + + // 测试游戏WebView页面加载指示器颜色(马甲包使用主题色) + expect(strategy.getGameWebViewLoadingIndicatorColor(), isA()); + }); + }); + + group('Search Page Strategy Tests', () { + test('Search page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试搜索页面Scaffold背景颜色 + expect(strategy.getSearchPageScaffoldBackgroundColor(), Colors.white); + + // 测试搜索页面返回图标颜色 + expect(strategy.getSearchPageBackIconColor(), const Color(0xffBBBBBB)); + + // 测试搜索页面输入框边框颜色 + expect(strategy.getSearchPageInputBorderColor(), Colors.transparent); + + // 测试搜索页面输入框文本颜色 + expect(strategy.getSearchPageInputTextColor(), Colors.grey); + + // 测试搜索页面按钮文本颜色 + expect(strategy.getSearchPageButtonTextColor(), Colors.black); + + // 测试搜索页面按钮渐变颜色 + final buttonGradient = strategy.getSearchPageButtonGradient(); + expect(buttonGradient, isA>()); + expect(buttonGradient.length, 2); + expect(buttonGradient[0], Colors.transparent); + expect(buttonGradient[1], Colors.transparent); + + // 测试搜索页面搜索结果Tab标签内边距 + final tabLabelPadding = strategy.getSearchPageResultTabLabelPadding(); + expect(tabLabelPadding, isA()); + expect(tabLabelPadding.left, 15.0); + expect(tabLabelPadding.right, 15.0); + + // 测试搜索页面搜索结果Tab标签选中颜色 + expect(strategy.getSearchPageResultTabLabelColor(), Colors.black); + + // 测试搜索页面搜索结果Tab标签未选中颜色 + expect(strategy.getSearchPageResultTabUnselectedLabelColor(), Colors.black26); + + // 测试搜索页面搜索结果Tab标签未选中文本样式 + final unselectedTabStyle = strategy.getSearchPageResultTabUnselectedLabelStyle(); + expect(unselectedTabStyle, isA()); + expect(unselectedTabStyle.fontSize, 17.0); + expect(unselectedTabStyle.fontWeight, FontWeight.w400); + + // 测试搜索页面搜索结果Tab标签选中文本样式 + final selectedTabStyle = strategy.getSearchPageResultTabLabelStyle(); + expect(selectedTabStyle, isA()); + expect(selectedTabStyle.fontSize, 18.0); + expect(selectedTabStyle.fontWeight, FontWeight.w600); + + // 测试搜索页面搜索结果Tab指示器颜色 + expect(strategy.getSearchPageResultTabIndicatorColor(), isA()); + + // 测试搜索页面搜索结果Tab指示器宽度 + expect(strategy.getSearchPageResultTabIndicatorWidth(), 15.0); + + // 测试搜索页面搜索结果背景颜色 + expect(strategy.getSearchPageResultBackgroundColor(), const Color(0xffFFF8DA)); + + // 测试搜索页面历史项背景颜色 + expect(strategy.getSearchPageHistoryItemBackgroundColor(), const Color(0xffFF9500)); + + // 测试搜索页面历史项文本颜色 + expect(strategy.getSearchPageHistoryItemTextColor(), Colors.white); + + // 测试搜索页面清空历史图标路径 + expect(strategy.getSearchPageClearHistoryIcon(), + "sc_images/general/at_icon_delete.png"); + + // 测试搜索页面空数据图标路径 + expect(strategy.getSearchPageEmptyDataIcon(), + "sc_images/room/at_icon_room_defaut_bg.png"); + }); + + test('Variant1 search page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试搜索页面Scaffold背景颜色(马甲包使用背景颜色) + expect(strategy.getSearchPageScaffoldBackgroundColor(), + const Color(0xfff5f5f5)); // 马甲包背景颜色 + + // 测试搜索页面返回图标颜色(马甲包使用主要文本颜色) + expect(strategy.getSearchPageBackIconColor(), + const Color(0xff666666)); // 深灰色 + + // 测试搜索页面输入框边框颜色(马甲包使用主题色) + expect(strategy.getSearchPageInputBorderColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试搜索页面输入框文本颜色(马甲包使用主要文本颜色) + expect(strategy.getSearchPageInputTextColor(), + const Color(0xff212121)); // 主要文本颜色 + + // 测试搜索页面按钮文本颜色(马甲包使用主色上的文本颜色) + expect(strategy.getSearchPageButtonTextColor(), + Colors.white); // 白色 + + // 测试搜索页面按钮渐变颜色(马甲包使用主题渐变) + final buttonGradient = strategy.getSearchPageButtonGradient(); + expect(buttonGradient, isA>()); + expect(buttonGradient.length, 2); + expect(buttonGradient[0], const Color(0xffFF5722).withOpacity(0.8)); // 马甲包主题色,不透明度0.8 + expect(buttonGradient[1], const Color(0xffFF5722).withOpacity(0.6)); // 马甲包主题色,不透明度0.6 + + // 测试搜索页面搜索结果Tab标签内边距(与原始应用相同) + final tabLabelPadding = strategy.getSearchPageResultTabLabelPadding(); + expect(tabLabelPadding, isA()); + expect(tabLabelPadding.left, 15.0); + expect(tabLabelPadding.right, 15.0); + + // 测试搜索页面搜索结果Tab标签选中颜色(马甲包使用主要文本颜色) + expect(strategy.getSearchPageResultTabLabelColor(), + const Color(0xff212121)); // 主要文本颜色 + + // 测试搜索页面搜索结果Tab标签未选中颜色(马甲包使用次要文本颜色) + expect(strategy.getSearchPageResultTabUnselectedLabelColor(), + const Color(0xff757575)); // 次要文本颜色 + + // 测试搜索页面搜索结果Tab标签未选中文本样式(马甲包使用次要文本颜色) + final unselectedTabStyle = strategy.getSearchPageResultTabUnselectedLabelStyle(); + expect(unselectedTabStyle, isA()); + expect(unselectedTabStyle.fontSize, 17.0); + expect(unselectedTabStyle.fontWeight, FontWeight.w400); + expect(unselectedTabStyle.color, const Color(0xff757575)); // 次要文本颜色 + + // 测试搜索页面搜索结果Tab标签选中文本样式(马甲包使用主要文本颜色) + final selectedTabStyle = strategy.getSearchPageResultTabLabelStyle(); + expect(selectedTabStyle, isA()); + expect(selectedTabStyle.fontSize, 18.0); + expect(selectedTabStyle.fontWeight, FontWeight.w600); + expect(selectedTabStyle.color, const Color(0xff212121)); // 主要文本颜色 + + // 测试搜索页面搜索结果Tab指示器颜色(马甲包使用主题色) + final indicatorGradient = strategy.getSearchPageResultTabIndicatorColor(); + expect(indicatorGradient, isA()); + expect(indicatorGradient.colors.length, 2); + expect(indicatorGradient.colors[0], const Color(0xffFF5722)); // 马甲包主题色 + expect(indicatorGradient.colors[1], const Color(0xffFF9A00)); // 主题色渐变 + + // 测试搜索页面搜索结果Tab指示器宽度(与原始应用相同) + expect(strategy.getSearchPageResultTabIndicatorWidth(), 15.0); + + // 测试搜索页面搜索结果背景颜色(马甲包使用浅主题色) + expect(strategy.getSearchPageResultBackgroundColor(), + const Color(0xffFFECB3)); // 浅橙色背景 + + // 测试搜索页面历史项背景颜色(马甲包使用主题色) + expect(strategy.getSearchPageHistoryItemBackgroundColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试搜索页面历史项文本颜色(马甲包使用主色上的文本颜色) + expect(strategy.getSearchPageHistoryItemTextColor(), + Colors.white); // 白色 + + // 测试搜索页面清空历史图标路径(与原始应用相同) + expect(strategy.getSearchPageClearHistoryIcon(), + "sc_images/general/at_icon_delete.png"); + + // 测试搜索页面空数据图标路径(与原始应用相同) + expect(strategy.getSearchPageEmptyDataIcon(), + "sc_images/room/at_icon_room_defaut_bg.png"); + }); + }); + + group('Task Page Strategy Tests', () { + test('Task page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试任务页面背景图片 + expect(strategy.getTaskPageBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试任务页面返回按钮颜色 + expect(strategy.getTaskPageBackButtonColor(), Colors.black); + + // 测试任务页面Scaffold背景颜色 + expect(strategy.getTaskPageScaffoldBackgroundColor(), Colors.white); + + // 测试任务页面容器背景颜色 + expect(strategy.getTaskPageContainerBackgroundColor(), Colors.white); + + // 测试任务页面边框颜色 + expect(strategy.getTaskPageBorderColor(), const Color(0xffE6E6E6)); + + // 测试任务页面特殊边框颜色 + expect(strategy.getTaskPageSpecialBorderColor(), const Color(0xffBB92FF)); + + // 测试任务页面主要文本颜色 + expect(strategy.getTaskPagePrimaryTextColor(), Colors.white); + + // 测试任务页面金币文本颜色 + expect(strategy.getTaskPageGoldTextColor(), const Color(0xffFFB627)); + + // 测试任务页面经验文本颜色 + expect(strategy.getTaskPageExpTextColor(), const Color(0xff52FF90)); + + // 测试任务页面主题颜色 + expect(strategy.getTaskPageThemeColor(), SocialChatTheme.primaryColor); + + // 测试任务页面主题浅色 + expect(strategy.getTaskPageThemeLightColor(), SocialChatTheme.primaryLight); + + // 测试任务页面可领取按钮渐变颜色 + final receivableGradient = strategy.getTaskPageReceivableButtonGradient(); + expect(receivableGradient, isA>()); + expect(receivableGradient.length, 2); + expect(receivableGradient[0], const Color(0xffBB92FF)); + expect(receivableGradient[1], const Color(0xff8B45FF)); + + // 测试任务页面已完成按钮渐变颜色 + final completedGradient = strategy.getTaskPageCompletedButtonGradient(); + expect(completedGradient, isA>()); + expect(completedGradient.length, 2); + expect(completedGradient[0], const Color(0xffDADADA)); + expect(completedGradient[1], const Color(0xff585859)); + + // 测试任务页面按钮文本颜色 + expect(strategy.getTaskPageButtonTextColor(), Colors.white); + + // 测试任务页面蒙版颜色 + expect(strategy.getTaskPageMaskColor(), Colors.black); + + // 测试任务页面头部背景图片 + expect(strategy.getTaskPageHeadBackgroundImage(), + "sc_images/index/at_icon_task_head_bg.png"); + + // 测试任务页面金币图标 + expect(strategy.getTaskPageGoldIcon(), + "sc_images/general/at_icon_jb.png"); + + // 测试任务页面经验图标 + expect(strategy.getTaskPageExpIcon(), + "sc_images/index/at_icon_task_exp.png"); + + // 测试任务页面邀请奖励背景图片 + expect(strategy.getTaskPageInvitationRewardBackgroundImage(), + "sc_images/index/at_icon_invitation_bg.png"); + + // 测试任务页面礼物袋文本颜色 + expect(strategy.getTaskPageGiftBagTextColor(), Colors.white); + + // 测试任务页面AppBar背景颜色 + expect(strategy.getTaskPageAppBarBackgroundColor(), Colors.transparent); + + // 测试任务页面透明容器颜色 + expect(strategy.getTaskPageTransparentContainerColor(), Colors.transparent); + }); + + test('Variant1 task page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试任务页面背景图片(马甲包使用相同图片) + expect(strategy.getTaskPageBackgroundImage(), + "sc_images/room/at_icon_room_settig_bg.png"); + + // 测试任务页面返回按钮颜色(马甲包使用主要文本颜色) + expect(strategy.getTaskPageBackButtonColor(), + const Color(0xff212121)); // SocialChatTheme.textPrimary + + // 测试任务页面Scaffold背景颜色(马甲包使用背景颜色) + expect(strategy.getTaskPageScaffoldBackgroundColor(), + const Color(0xfff5f5f5)); // 马甲包背景颜色 + + // 测试任务页面容器背景颜色(马甲包使用表面颜色) + expect(strategy.getTaskPageContainerBackgroundColor(), + const Color(0xffffffff)); // SocialChatTheme.surfaceColor + + // 测试任务页面边框颜色(马甲包使用边框颜色) + expect(strategy.getTaskPageBorderColor(), + const Color(0xffE0E0E0)); // SocialChatTheme.borderColor + + // 测试任务页面特殊边框颜色(马甲包使用主题色) + expect(strategy.getTaskPageSpecialBorderColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试任务页面主要文本颜色(马甲包使用主要文本颜色) + expect(strategy.getTaskPagePrimaryTextColor(), + const Color(0xff212121)); // SocialChatTheme.textPrimary + + // 测试任务页面金币文本颜色(马甲包使用主题色) + expect(strategy.getTaskPageGoldTextColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试任务页面经验文本颜色(马甲包使用成功颜色) + expect(strategy.getTaskPageExpTextColor(), + const Color(0xff4CAF50)); // SocialChatTheme.successColor + + // 测试任务页面主题颜色(马甲包使用主题色) + expect(strategy.getTaskPageThemeColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试任务页面主题浅色(马甲包使用主题浅色) + expect(strategy.getTaskPageThemeLightColor(), + const Color(0xffFF9500)); // 马甲包主题浅色 + + // 测试任务页面可领取按钮渐变颜色(马甲包使用主题渐变) + final receivableGradient = strategy.getTaskPageReceivableButtonGradient(); + expect(receivableGradient, isA>()); + expect(receivableGradient.length, 2); + expect(receivableGradient[0], const Color(0xffFF5722)); // 马甲包主题色 + expect(receivableGradient[1], const Color(0xffFF9A00)); // 主题色渐变 + + // 测试任务页面已完成按钮渐变颜色(马甲包使用灰色渐变) + final completedGradient = strategy.getTaskPageCompletedButtonGradient(); + expect(completedGradient, isA>()); + expect(completedGradient.length, 2); + expect(completedGradient[0], const Color(0xffCCCCCC)); // 浅灰色 + expect(completedGradient[1], const Color(0xff999999)); // 深灰色 + + // 测试任务页面按钮文本颜色(马甲包使用主色上的文本颜色) + expect(strategy.getTaskPageButtonTextColor(), + const Color(0xffffffff)); // SocialChatTheme.textOnPrimary + + // 测试任务页面蒙版颜色(马甲包使用透明黑色) + expect(strategy.getTaskPageMaskColor(), + Colors.black.withOpacity(0.5)); // 半透明黑色 + + // 测试任务页面头部背景图片(马甲包使用相同图片) + expect(strategy.getTaskPageHeadBackgroundImage(), + "sc_images/index/at_icon_task_head_bg.png"); + + // 测试任务页面金币图标(马甲包使用相同图标) + expect(strategy.getTaskPageGoldIcon(), + "sc_images/general/at_icon_jb.png"); + + // 测试任务页面经验图标(马甲包使用相同图标) + expect(strategy.getTaskPageExpIcon(), + "sc_images/index/at_icon_task_exp.png"); + + // 测试任务页面邀请奖励背景图片(马甲包使用相同图片) + expect(strategy.getTaskPageInvitationRewardBackgroundImage(), + "sc_images/index/at_icon_invitation_bg.png"); + + // 测试任务页面礼物袋文本颜色(马甲包使用白色) + expect(strategy.getTaskPageGiftBagTextColor(), Colors.white); + + // 测试任务页面AppBar背景颜色(马甲包使用透明) + expect(strategy.getTaskPageAppBarBackgroundColor(), Colors.transparent); + + // 测试任务页面透明容器颜色(马甲包使用透明) + expect(strategy.getTaskPageTransparentContainerColor(), Colors.transparent); + }); + }); + + group('Settings Pages Strategy Tests', () { + test('Settings page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试设置页面背景图片 + expect(strategy.getSettingsPageBackgroundImage(), + "sc_images/person/at_icon_edit_userinfo_bg.png"); + + // 测试设置页面次要文本颜色 + expect(strategy.getSettingsPageSecondaryTextColor(), Colors.black38); + + // 测试设置页面主要容器背景颜色 + expect(strategy.getSettingsPageMainContainerBackgroundColor(), Colors.white); + + // 测试设置页面主要容器边框颜色 + expect(strategy.getSettingsPageMainContainerBorderColor(), + const Color(0xffE6E6E6)); + + // 测试设置页面主要文本颜色 + expect(strategy.getSettingsPagePrimaryTextColor(), Colors.black); + + // 测试设置页面图标颜色 + expect(strategy.getSettingsPageIconColor(), Colors.black26); + }); + + test('Language page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试语言页面背景图片 + expect(strategy.getLanguagePageBackgroundImage(), + "sc_images/person/at_icon_edit_userinfo_bg.png"); + + // 测试语言页面主要文本颜色 + expect(strategy.getLanguagePagePrimaryTextColor(), Colors.black); + + // 测试语言页面复选框活动颜色 + expect(strategy.getLanguagePageCheckboxActiveColor(), + SocialChatTheme.primaryColor); + + // 测试语言页面复选框边框颜色 + expect(strategy.getLanguagePageCheckboxBorderColor(), + const Color(0xffD4D4D4)); + + // 测试语言页面复选框对勾颜色 + expect(strategy.getLanguagePageCheckColor(), Colors.transparent); + + // 测试语言页面复选框边框圆角 + expect(strategy.getLanguagePageCheckboxBorderRadius(), 15); + + // 测试语言页面复选框边框宽度 + expect(strategy.getLanguagePageCheckboxBorderWidth(), 2); + + // 测试语言页面Scaffold背景颜色 + expect(strategy.getLanguagePageScaffoldBackgroundColor(), Colors.transparent); + }); + + test('Account page test', () { + final strategy = BaseBusinessLogicStrategy(); + + // 测试账户页面背景图片 + expect(strategy.getAccountPageBackgroundImage(), + "sc_images/person/at_icon_edit_userinfo_bg.png"); + + // 测试账户页面次要文本颜色 + expect(strategy.getAccountPageSecondaryTextColor(), + Colors.black38); // 原始应用次要文本颜色 + + // 测试账户页面主容器背景颜色 + expect(strategy.getAccountPageMainContainerBackgroundColor(), Colors.white); + + // 测试账户页面主容器边框颜色 + expect(strategy.getAccountPageMainContainerBorderColor(), + const Color(0xffE6E6E6)); + + // 测试账户页面主要文本颜色 + expect(strategy.getAccountPagePrimaryTextColor(), Colors.black); + + // 测试账户页面图标颜色 + expect(strategy.getAccountPageIconColor(), Colors.black26); + + // 测试账户页面分隔线颜色 + expect(strategy.getAccountPageDividerColor(), Colors.black12); + }); + + test('Variant1 settings page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试马甲包设置页面背景图片(目前使用相同图片) + expect(strategy.getSettingsPageBackgroundImage(), + "sc_images/person/at_icon_edit_userinfo_bg.png"); + + // 测试马甲包设置页面次要文本颜色(使用次要文本颜色) + expect(strategy.getSettingsPageSecondaryTextColor(), + const Color(0xff757575)); // SocialChatTheme.textSecondary + + // 测试马甲包设置页面主要容器背景颜色(使用表面颜色) + expect(strategy.getSettingsPageMainContainerBackgroundColor(), + const Color(0xffffffff)); // SocialChatTheme.surfaceColor + + // 测试马甲包设置页面主要容器边框颜色(使用边框颜色) + expect(strategy.getSettingsPageMainContainerBorderColor(), + const Color(0xffE0E0E0)); // SocialChatTheme.borderColor + + // 测试马甲包设置页面主要文本颜色(使用主要文本颜色) + expect(strategy.getSettingsPagePrimaryTextColor(), + const Color(0xff212121)); // SocialChatTheme.textPrimary + + // 测试马甲包设置页面图标颜色(使用次要文本颜色) + expect(strategy.getSettingsPageIconColor(), + const Color(0xff757575)); // SocialChatTheme.textSecondary + }); + + test('Variant1 language page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试马甲包语言页面背景图片(目前使用相同图片) + expect(strategy.getLanguagePageBackgroundImage(), + "sc_images/person/at_icon_edit_userinfo_bg.png"); + + // 测试马甲包语言页面主要文本颜色(使用主要文本颜色) + expect(strategy.getLanguagePagePrimaryTextColor(), + const Color(0xff212121)); // SocialChatTheme.textPrimary + + // 测试马甲包语言页面复选框活动颜色(使用马甲包主题色) + expect(strategy.getLanguagePageCheckboxActiveColor(), + const Color(0xffFF5722)); // 马甲包主题色 + + // 测试马甲包语言页面复选框边框颜色(使用马甲包边框色) + expect(strategy.getLanguagePageCheckboxBorderColor(), + SocialChatTheme.borderColor); // 马甲包边框色 + + // 测试马甲包语言页面复选框对勾颜色(使用透明色) + expect(strategy.getLanguagePageCheckColor(), Colors.transparent); + + // 测试马甲包语言页面复选框边框圆角(使用相同圆角值15) + expect(strategy.getLanguagePageCheckboxBorderRadius(), 15); + + // 测试马甲包语言页面复选框边框宽度(使用相同边框宽度2) + expect(strategy.getLanguagePageCheckboxBorderWidth(), 2); + + // 测试马甲包语言页面Scaffold背景颜色(使用透明色) + expect(strategy.getLanguagePageScaffoldBackgroundColor(), Colors.transparent); + }); + + test('Variant1 account page test', () { + final strategy = Variant1BusinessLogicStrategy(); + + // 测试马甲包账户页面背景图片(目前使用相同图片) + expect(strategy.getAccountPageBackgroundImage(), + "sc_images/person/at_icon_edit_userinfo_bg.png"); + + // 测试马甲包账户页面次要文本颜色(使用次要文本颜色) + expect(strategy.getAccountPageSecondaryTextColor(), + SocialChatTheme.textSecondary); // 马甲包次要文本颜色 + + // 测试马甲包账户页面主容器背景颜色(使用表面颜色) + expect(strategy.getAccountPageMainContainerBackgroundColor(), + const Color(0xffffffff)); // SocialChatTheme.surfaceColor + + // 测试马甲包账户页面主容器边框颜色(使用边框颜色) + expect(strategy.getAccountPageMainContainerBorderColor(), + const Color(0xffE0E0E0)); // SocialChatTheme.borderColor + + // 测试马甲包账户页面主要文本颜色(使用主要文本颜色) + expect(strategy.getAccountPagePrimaryTextColor(), + const Color(0xff212121)); // SocialChatTheme.textPrimary + + // 测试马甲包账户页面图标颜色(使用次要文本颜色) + expect(strategy.getAccountPageIconColor(), + const Color(0xff757575)); // SocialChatTheme.textSecondary + + // 测试马甲包账户页面分隔线颜色(使用分隔线颜色) + expect(strategy.getAccountPageDividerColor(), + const Color(0xffE0E0E0)); // SocialChatTheme.dividerColor + }); + }); + +} \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..e99b99a --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,20 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can create tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:yumi/main.dart'; + +void main() { + testWidgets('App launches successfully', (WidgetTester tester) async { + // Build the application and ensure it doesn't throw + await tester.pumpWidget(const YumiApplication()); + // Verify that the app launches without errors + expect(find.byType(YumiApplication), findsOneWidget); + }); +}