commit 068930c7bd784179d213f0e31b6063887c3fae67 Author: Amirmahdi Nourkazemi Date: Mon Jun 15 16:42:28 2026 +0330 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c113f --- /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..9fc6a8a --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: web + 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/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3360c37 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "حکم‌شو (موبایل واقعی - LAN)", + "request": "launch", + "type": "dart", + "program": "lib/main.dart", + "args": [ + "--dart-define=BASE_URL=http://192.168.100.28:8080", + "--target-platform=android-arm64" + ] + }, + { + "name": "حکم‌شو (امولاتور اندروید)", + "request": "launch", + "type": "dart", + "program": "lib/main.dart", + "args": ["--dart-define=BASE_URL=http://10.0.2.2:8080"] + }, + { + "name": "حکم‌شو (وب / iOS sim - localhost)", + "request": "launch", + "type": "dart", + "program": "lib/main.dart", + "args": ["--dart-define=BASE_URL=http://localhost:8080"] + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..66aa702 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# حکم‌شو — فرانت (Flutter + Flame) + +کلاینت بازی حکم. فازهای انجام‌شده: **اسکلت + احراز هویت OTP + لابی/کیف‌پول + فروشگاه + صحنه‌ی بازی با Flame**. + +## پیش‌نیاز شبکه (میرور مایکت) + +به‌خاطر محدودیت دسترسی، از میرور مایکت استفاده می‌شود: + +```bash +export PUB_HOSTED_URL=https://pub.myket.ir +export FLUTTER_STORAGE_BASE_URL=https://pub.myket.ir +``` + +این دو خط در `~/.zshrc` ست شده‌اند (دائمی). برای Gradle هم یک init script سراسری در `~/.gradle/init.gradle` همه‌ی buildها را به `maven.myket.ir` هدایت می‌کند، و `android/` به همین مخزن تنظیم شده است. + +## اجرا + +```bash +flutter pub get +flutter run -d chrome # وب (بدون نیاز به NDK/Gradle) ✓ +flutter run # موبایل (نیازمند نصب NDK — به پایین رجوع کنید) +``` + +اتصال به بک‌اند با `--dart-define=BASE_URL=...` قابل تنظیم است: +- امولاتور اندروید → پیش‌فرض `http://10.0.2.2:8080` (localhostِ میزبان). +- وب / شبیه‌ساز iOS → `--dart-define=BASE_URL=http://localhost:8080`. +- پروداکشن → `--dart-define=BASE_URL=https://api.hakemsho.ir`. + +## وضعیت تأیید + +- `flutter analyze` → بدون خطا ✓ +- `flutter test` → پاس (تست مدل Wallet) ✓ +- `flutter build web` → موفق ✓ + +## نکات build اندروید (شبکه‌ی محدود) + +- **NDK**: AGP به NDK `27.0.12077973` نیاز دارد. اگر NDK را دستی دانلود کرده‌اید (مثلاً `android-ndk-r27`)، باید پوشه به‌نامِ **شماره‌ی نسخه** باشد تا AGP پیدایش کند: + ```bash + cd ~/Library/Android/sdk/ndk && ln -sfn android-ndk-r27 27.0.12077973 + ``` +- **گریدل از مایکت**: `~/.gradle/init.gradle` همه‌ی buildها را (شامل `buildscript` پلاگین‌ها) به `maven.myket.ir` هدایت می‌کند. این برای `flutter_secure_storage` لازم است که خودش `com.android.tools.build:gradle:8.13.2` را اعلام می‌کند. +- **minSdk 23 / compileSdk 36**: به‌خاطر `flutter_secure_storage` در [android/app/build.gradle.kts](android/app/build.gradle.kts) تنظیم شده. +- هشدار «unable to detect Gradle/AGP versions» بی‌ضرر است (به‌خاطر بلاک‌بودن `dl.google.com` برای چک نسخه). + +`flutter build apk --debug` ✓ موفق. + +## ساختار + +``` +lib/ +├── main.dart نقطه ورود (تعیین مسیر اولیه بر اساس توکن) +├── app.dart MaterialApp.router + RTL + BlocProviderها +├── core/ +│ ├── config.dart baseUrl و آدرس WebSocket +│ ├── network/api_client.dart Dio + تزریق خودکار Bearer +│ ├── storage/token_storage.dart ذخیره‌ی امن JWT +│ └── theme/app_theme.dart تم قرمز/طلایی، فونت فارسی، RTL +└── features/ + ├── auth/ repository، cubit، صفحه موبایل و OTP + ├── lobby/ مدل Wallet، cubit، صفحه لابی + ├── shop/ مدل‌ها، repository، cubit، صفحه فروشگاه (۴ تب) + └── game/ مدل‌ها، WS cubit، لیست میزها، صفحه بازی + └── flame/ HokmGame (صحنه میز) + CardComponent +``` + +کارت‌ها از `assets/images/cards/.png` بارگذاری می‌شوند (۵۲ کارت + `back.jpg`)؛ +اگر تصویری نبود، `CardComponent` نسخه‌ی برداری می‌کشد. + +## قدم بعدی + +- انیمیشن‌ها (پخش/انداختن کارت)، صدا، و صفحه‌ی نتایج بهتر. +- اتصال خرید واقعی IAP (پلاگین بازار/مایکت) و تبلیغ تپسل (فعلاً توکن توسعه‌ای به بک‌اند می‌رود). +- به [../FRONTEND_PLAN.md](../FRONTEND_PLAN.md) رجوع کنید. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /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..be3943c --- /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/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..9cb45ed --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,47 @@ +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 = "ir.hakemsho.hakemsho" + // flutter_secure_storage نیازمند compileSdk 36 است. + compileSdk = maxOf(36, flutter.compileSdkVersion) + // NDK لازم نیست (کد native نداریم) و دانلودش روی این شبکه بلاک است؛ غیرفعال شد. + ndkVersion = "27.0.12077973" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "ir.hakemsho.hakemsho" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + // flutter_secure_storage حداقل SDK 23 را لازم دارد. + minSdk = maxOf(23, 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/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /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..39928d2 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/ir/hakemsho/hakemsho/MainActivity.kt b/android/app/src/main/kotlin/ir/hakemsho/hakemsho/MainActivity.kt new file mode 100644 index 0000000..ec5e900 --- /dev/null +++ b/android/app/src/main/kotlin/ir/hakemsho/hakemsho/MainActivity.kt @@ -0,0 +1,5 @@ +package ir.hakemsho.hakemsho + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() 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..f74085f --- /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..304732f --- /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..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /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/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..31e6114 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,22 @@ +allprojects { + repositories { + maven { url = uri("https://maven.myket.ir/") } + 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/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..d7bdba4 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,9 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# همه‌ی وابستگی‌ها AndroidX هستند؛ Jetifier لازم نیست و build را کند می‌کند. +android.enableJetifier=false +# تسریع build +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a84fc88 --- /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\://maven.myket.ir/gradle/distributions/gradle-8.10.2-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..e56f21b --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + maven { url = uri("https://maven.myket.ir/") } + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.0" apply false + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/assets/fonts/IRANSans.ttf b/assets/fonts/IRANSans.ttf new file mode 100644 index 0000000..b330aaf Binary files /dev/null and b/assets/fonts/IRANSans.ttf differ diff --git a/assets/fonts/dana_bold.ttf b/assets/fonts/dana_bold.ttf new file mode 100644 index 0000000..a0bf09c Binary files /dev/null and b/assets/fonts/dana_bold.ttf differ diff --git a/assets/fonts/dana_medium.ttf b/assets/fonts/dana_medium.ttf new file mode 100644 index 0000000..fa4c39c Binary files /dev/null and b/assets/fonts/dana_medium.ttf differ diff --git a/assets/fonts/dana_regular.ttf b/assets/fonts/dana_regular.ttf new file mode 100644 index 0000000..dd3d861 Binary files /dev/null and b/assets/fonts/dana_regular.ttf differ diff --git a/assets/images/cards/10C.png b/assets/images/cards/10C.png new file mode 100644 index 0000000..18af741 Binary files /dev/null and b/assets/images/cards/10C.png differ diff --git a/assets/images/cards/10D.png b/assets/images/cards/10D.png new file mode 100644 index 0000000..3bbc4e0 Binary files /dev/null and b/assets/images/cards/10D.png differ diff --git a/assets/images/cards/10H.png b/assets/images/cards/10H.png new file mode 100644 index 0000000..3eb83d7 Binary files /dev/null and b/assets/images/cards/10H.png differ diff --git a/assets/images/cards/10S.png b/assets/images/cards/10S.png new file mode 100644 index 0000000..0b3d294 Binary files /dev/null and b/assets/images/cards/10S.png differ diff --git a/assets/images/cards/2C.png b/assets/images/cards/2C.png new file mode 100644 index 0000000..291ed97 Binary files /dev/null and b/assets/images/cards/2C.png differ diff --git a/assets/images/cards/2D.png b/assets/images/cards/2D.png new file mode 100644 index 0000000..4deee7c Binary files /dev/null and b/assets/images/cards/2D.png differ diff --git a/assets/images/cards/2H.png b/assets/images/cards/2H.png new file mode 100644 index 0000000..75a014f Binary files /dev/null and b/assets/images/cards/2H.png differ diff --git a/assets/images/cards/2S.png b/assets/images/cards/2S.png new file mode 100644 index 0000000..1ce0ffe Binary files /dev/null and b/assets/images/cards/2S.png differ diff --git a/assets/images/cards/3C.png b/assets/images/cards/3C.png new file mode 100644 index 0000000..076ab31 Binary files /dev/null and b/assets/images/cards/3C.png differ diff --git a/assets/images/cards/3D.png b/assets/images/cards/3D.png new file mode 100644 index 0000000..8ee0b4b Binary files /dev/null and b/assets/images/cards/3D.png differ diff --git a/assets/images/cards/3H.png b/assets/images/cards/3H.png new file mode 100644 index 0000000..8e74673 Binary files /dev/null and b/assets/images/cards/3H.png differ diff --git a/assets/images/cards/3S.png b/assets/images/cards/3S.png new file mode 100644 index 0000000..f9e06b4 Binary files /dev/null and b/assets/images/cards/3S.png differ diff --git a/assets/images/cards/4C.png b/assets/images/cards/4C.png new file mode 100644 index 0000000..8be9e08 Binary files /dev/null and b/assets/images/cards/4C.png differ diff --git a/assets/images/cards/4D.png b/assets/images/cards/4D.png new file mode 100644 index 0000000..70e82e8 Binary files /dev/null and b/assets/images/cards/4D.png differ diff --git a/assets/images/cards/4H.png b/assets/images/cards/4H.png new file mode 100644 index 0000000..ceecbfe Binary files /dev/null and b/assets/images/cards/4H.png differ diff --git a/assets/images/cards/4S.png b/assets/images/cards/4S.png new file mode 100644 index 0000000..95abe3e Binary files /dev/null and b/assets/images/cards/4S.png differ diff --git a/assets/images/cards/5C.png b/assets/images/cards/5C.png new file mode 100644 index 0000000..bde9777 Binary files /dev/null and b/assets/images/cards/5C.png differ diff --git a/assets/images/cards/5D.png b/assets/images/cards/5D.png new file mode 100644 index 0000000..bb92525 Binary files /dev/null and b/assets/images/cards/5D.png differ diff --git a/assets/images/cards/5H.png b/assets/images/cards/5H.png new file mode 100644 index 0000000..d923456 Binary files /dev/null and b/assets/images/cards/5H.png differ diff --git a/assets/images/cards/5S.png b/assets/images/cards/5S.png new file mode 100644 index 0000000..53a1aad Binary files /dev/null and b/assets/images/cards/5S.png differ diff --git a/assets/images/cards/6C.png b/assets/images/cards/6C.png new file mode 100644 index 0000000..a9660a0 Binary files /dev/null and b/assets/images/cards/6C.png differ diff --git a/assets/images/cards/6D.png b/assets/images/cards/6D.png new file mode 100644 index 0000000..78a80ad Binary files /dev/null and b/assets/images/cards/6D.png differ diff --git a/assets/images/cards/6H.png b/assets/images/cards/6H.png new file mode 100644 index 0000000..361643e Binary files /dev/null and b/assets/images/cards/6H.png differ diff --git a/assets/images/cards/6S.png b/assets/images/cards/6S.png new file mode 100644 index 0000000..40242a7 Binary files /dev/null and b/assets/images/cards/6S.png differ diff --git a/assets/images/cards/7C.png b/assets/images/cards/7C.png new file mode 100644 index 0000000..9d6b545 Binary files /dev/null and b/assets/images/cards/7C.png differ diff --git a/assets/images/cards/7D.png b/assets/images/cards/7D.png new file mode 100644 index 0000000..6ad5f15 Binary files /dev/null and b/assets/images/cards/7D.png differ diff --git a/assets/images/cards/7H.png b/assets/images/cards/7H.png new file mode 100644 index 0000000..19b89a2 Binary files /dev/null and b/assets/images/cards/7H.png differ diff --git a/assets/images/cards/7S.png b/assets/images/cards/7S.png new file mode 100644 index 0000000..b9f1b93 Binary files /dev/null and b/assets/images/cards/7S.png differ diff --git a/assets/images/cards/8C.png b/assets/images/cards/8C.png new file mode 100644 index 0000000..cec743c Binary files /dev/null and b/assets/images/cards/8C.png differ diff --git a/assets/images/cards/8D.png b/assets/images/cards/8D.png new file mode 100644 index 0000000..ed12951 Binary files /dev/null and b/assets/images/cards/8D.png differ diff --git a/assets/images/cards/8H.png b/assets/images/cards/8H.png new file mode 100644 index 0000000..fb39723 Binary files /dev/null and b/assets/images/cards/8H.png differ diff --git a/assets/images/cards/8S.png b/assets/images/cards/8S.png new file mode 100644 index 0000000..b6b3b38 Binary files /dev/null and b/assets/images/cards/8S.png differ diff --git a/assets/images/cards/9C.png b/assets/images/cards/9C.png new file mode 100644 index 0000000..2174db5 Binary files /dev/null and b/assets/images/cards/9C.png differ diff --git a/assets/images/cards/9D.png b/assets/images/cards/9D.png new file mode 100644 index 0000000..0b933fb Binary files /dev/null and b/assets/images/cards/9D.png differ diff --git a/assets/images/cards/9H.png b/assets/images/cards/9H.png new file mode 100644 index 0000000..7b196d6 Binary files /dev/null and b/assets/images/cards/9H.png differ diff --git a/assets/images/cards/9S.png b/assets/images/cards/9S.png new file mode 100644 index 0000000..3c3b5ff Binary files /dev/null and b/assets/images/cards/9S.png differ diff --git a/assets/images/cards/AC.png b/assets/images/cards/AC.png new file mode 100644 index 0000000..42bf5ec Binary files /dev/null and b/assets/images/cards/AC.png differ diff --git a/assets/images/cards/AD.png b/assets/images/cards/AD.png new file mode 100644 index 0000000..79cd3b8 Binary files /dev/null and b/assets/images/cards/AD.png differ diff --git a/assets/images/cards/AH.png b/assets/images/cards/AH.png new file mode 100644 index 0000000..b422124 Binary files /dev/null and b/assets/images/cards/AH.png differ diff --git a/assets/images/cards/AS.png b/assets/images/cards/AS.png new file mode 100644 index 0000000..fbc3a2d Binary files /dev/null and b/assets/images/cards/AS.png differ diff --git a/assets/images/cards/JC.png b/assets/images/cards/JC.png new file mode 100644 index 0000000..5e003be Binary files /dev/null and b/assets/images/cards/JC.png differ diff --git a/assets/images/cards/JD.png b/assets/images/cards/JD.png new file mode 100644 index 0000000..131a977 Binary files /dev/null and b/assets/images/cards/JD.png differ diff --git a/assets/images/cards/JH.png b/assets/images/cards/JH.png new file mode 100644 index 0000000..bf342bc Binary files /dev/null and b/assets/images/cards/JH.png differ diff --git a/assets/images/cards/JS.png b/assets/images/cards/JS.png new file mode 100644 index 0000000..f539c19 Binary files /dev/null and b/assets/images/cards/JS.png differ diff --git a/assets/images/cards/KC.png b/assets/images/cards/KC.png new file mode 100644 index 0000000..68e5774 Binary files /dev/null and b/assets/images/cards/KC.png differ diff --git a/assets/images/cards/KD.png b/assets/images/cards/KD.png new file mode 100644 index 0000000..e21d6a0 Binary files /dev/null and b/assets/images/cards/KD.png differ diff --git a/assets/images/cards/KH.png b/assets/images/cards/KH.png new file mode 100644 index 0000000..1d3c468 Binary files /dev/null and b/assets/images/cards/KH.png differ diff --git a/assets/images/cards/KS.png b/assets/images/cards/KS.png new file mode 100644 index 0000000..2edbbc1 Binary files /dev/null and b/assets/images/cards/KS.png differ diff --git a/assets/images/cards/QC.png b/assets/images/cards/QC.png new file mode 100644 index 0000000..7be5f9a Binary files /dev/null and b/assets/images/cards/QC.png differ diff --git a/assets/images/cards/QD.png b/assets/images/cards/QD.png new file mode 100644 index 0000000..928f650 Binary files /dev/null and b/assets/images/cards/QD.png differ diff --git a/assets/images/cards/QH.png b/assets/images/cards/QH.png new file mode 100644 index 0000000..21839e6 Binary files /dev/null and b/assets/images/cards/QH.png differ diff --git a/assets/images/cards/QS.png b/assets/images/cards/QS.png new file mode 100644 index 0000000..7983d03 Binary files /dev/null and b/assets/images/cards/QS.png differ diff --git a/assets/images/cards/README.md b/assets/images/cards/README.md new file mode 100644 index 0000000..d236e6e --- /dev/null +++ b/assets/images/cards/README.md @@ -0,0 +1,5 @@ +دسته‌کارت تصویری را اینجا بگذارید: یک فایل PNG برای هر کارت با نام کد کوتاه: + AS.png 2S.png ... 10S.png JS.png QS.png KS.png (پیک/spades = S) + ...H (دل/hearts), ...D (خشت/diamonds), ...C (گشنیز/clubs) +و back.png برای پشت کارت. +تا وقتی این تصاویر نباشند، صحنه به‌صورت برداری (vector) کارت‌ها را می‌کشد. diff --git a/assets/images/cards/back.jpg b/assets/images/cards/back.jpg new file mode 100644 index 0000000..2a5ce27 Binary files /dev/null and b/assets/images/cards/back.jpg differ diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..e549ee2 --- /dev/null +++ b/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/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8783340 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + 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 */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + 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 */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ir.hakemsho.hakemsho; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ir.hakemsho.hakemsho.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ir.hakemsho.hakemsho.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ir.hakemsho.hakemsho.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ir.hakemsho.hakemsho; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ir.hakemsho.hakemsho; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..15cada4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /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..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 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..7353c41 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..797d452 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..6ed2d93 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..4cd7b00 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..fe73094 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..321773c 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..797d452 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..502f463 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..0ec3034 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-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 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..e9f5fea 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-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a 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..8953cba 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..0467bf1 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..0bedcf2 --- /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..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /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..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..7a3e160 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Hakemsho + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + hakemsho + 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/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..08a6a04 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import 'core/network/api_client.dart'; +import 'core/network/ws_client.dart'; +import 'core/storage/token_storage.dart'; +import 'core/theme/app_theme.dart'; +import 'features/auth/auth_cubit.dart'; +import 'features/auth/auth_repository.dart'; +import 'features/auth/mobile_screen.dart'; +import 'features/auth/otp_screen.dart'; +import 'features/game/game_cubit.dart'; +import 'features/game/game_repository.dart'; +import 'features/game/game_screen.dart'; +import 'features/game/tier_list_screen.dart'; +import 'features/lobby/lobby_screen.dart'; +import 'features/lobby/wallet_cubit.dart'; +import 'features/shop/shop_cubit.dart'; +import 'features/shop/shop_repository.dart'; +import 'features/shop/shop_screen.dart'; + +class HakemApp extends StatelessWidget { + final ApiClient api; + final AuthRepository authRepo; + final TokenStorage tokenStorage; + final bool loggedIn; + + const HakemApp({ + super.key, + required this.api, + required this.authRepo, + required this.tokenStorage, + required this.loggedIn, + }); + + @override + Widget build(BuildContext context) { + final router = GoRouter( + initialLocation: loggedIn ? '/lobby' : '/login', + routes: [ + GoRoute(path: '/login', builder: (_, __) => const MobileScreen()), + GoRoute(path: '/otp', builder: (_, __) => const OtpScreen()), + GoRoute(path: '/lobby', builder: (_, __) => const LobbyScreen()), + GoRoute( + path: '/shop', + builder: (_, __) => BlocProvider( + create: (_) => ShopCubit(ShopRepository(api))..load(), + child: const ShopScreen(), + ), + ), + GoRoute( + path: '/game/tiers', + builder: (_, __) => TierListScreen(repo: GameRepository(api)), + ), + GoRoute( + path: '/game/:tier', + builder: (_, st) { + final tier = st.pathParameters['tier']!; + final prize = int.tryParse(st.uri.queryParameters['prize'] ?? '') ?? 0; + return FutureBuilder( + future: tokenStorage.read(), + builder: (context, snap) { + if (!snap.hasData) { + return const Scaffold( + body: Center(child: CircularProgressIndicator())); + } + return BlocProvider( + create: (_) => GameCubit(WsClient(snap.data!), tier), + child: GameScreen(prize: prize), + ); + }, + ); + }, + ), + ], + ); + + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => AuthCubit(authRepo)), + BlocProvider(create: (_) => WalletCubit(api)), + ], + child: MaterialApp.router( + title: 'سلطان حکم', + debugShowCheckedModeBanner: false, + theme: AppTheme.build(), + routerConfig: router, + // اعمال راست‌به‌چپ برای کل اپ. + builder: (context, child) => Directionality( + textDirection: TextDirection.rtl, + child: child!, + ), + ), + ); + } +} diff --git a/lib/core/config.dart b/lib/core/config.dart new file mode 100644 index 0000000..f65439c --- /dev/null +++ b/lib/core/config.dart @@ -0,0 +1,17 @@ +/// تنظیمات سراسری اپ. +class AppConfig { + /// آدرس پایه‌ی سرور. + /// - موبایل واقعی روی همان Wi-Fi: IP محلی مک (پیش‌فرض فعلی). + /// - امولاتور اندروید: `--dart-define=BASE_URL=http://10.0.2.2:8080` + /// - وب / شبیه‌ساز iOS: `--dart-define=BASE_URL=http://localhost:8080` + /// - پروداکشن: `--dart-define=BASE_URL=https://api.hakemsho.ir` + /// با تغییر شبکه/IP مک، مقدار dart-define یا همین پیش‌فرض را عوض کنید. + static const String baseUrl = + String.fromEnvironment('BASE_URL', defaultValue: 'http://192.168.100.28:8080'); + + static String get apiUrl => '$baseUrl/api'; + + /// آدرس WebSocket بازی به‌همراه توکن احراز هویت. + static String wsUrl(String token) => + '${baseUrl.replaceFirst('http', 'ws')}/ws?token=$token'; +} diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart new file mode 100644 index 0000000..52a6a7e --- /dev/null +++ b/lib/core/network/api_client.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart'; + +import '../config.dart'; +import '../storage/token_storage.dart'; + +/// کلاینت HTTP با تزریق خودکار توکن Bearer. +class ApiClient { + final Dio dio; + + ApiClient(TokenStorage storage) + : dio = Dio(BaseOptions( + baseUrl: AppConfig.apiUrl, + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + headers: {'Accept': 'application/json'}, + )) { + dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) async { + final token = await storage.read(); + if (token != null && token.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $token'; + } + handler.next(options); + }, + )); + } +} diff --git a/lib/core/network/ws_client.dart b/lib/core/network/ws_client.dart new file mode 100644 index 0000000..98c50ee --- /dev/null +++ b/lib/core/network/ws_client.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../config.dart'; + +enum WsStatus { connecting, connected, disconnected } + +/// کلاینت WebSocket بازی: اتصال با توکن، ارسال/دریافت JSON و اتصال مجدد خودکار. +class WsClient { + final String token; + WebSocketChannel? _channel; + StreamSubscription? _sub; + Timer? _reconnectTimer; + bool _disposed = false; + int _attempt = 0; + + final _messages = StreamController>.broadcast(); + final _status = StreamController.broadcast(); + + WsClient(this.token); + + /// پیام‌های دیکدشده‌ی سرور. + Stream> get messages => _messages.stream; + + /// وضعیت اتصال (برای نمایش «در حال اتصال مجدد»). + Stream get status => _status.stream; + + void connect() { + if (_disposed) return; + _status.add(WsStatus.connecting); + try { + final ch = WebSocketChannel.connect(Uri.parse(AppConfig.wsUrl(token))); + _channel = ch; + _sub = ch.stream.listen( + _onData, + onError: (_) => _scheduleReconnect(), + onDone: _scheduleReconnect, + cancelOnError: true, + ); + _attempt = 0; + _status.add(WsStatus.connected); + } catch (_) { + _scheduleReconnect(); + } + } + + void _onData(dynamic raw) { + try { + final msg = jsonDecode(raw as String); + if (msg is Map) _messages.add(msg); + } catch (_) {/* پیام نامعتبر نادیده گرفته می‌شود */} + } + + void send(Map msg) { + _channel?.sink.add(jsonEncode(msg)); + } + + void _scheduleReconnect() { + if (_disposed) return; + _status.add(WsStatus.disconnected); + _sub?.cancel(); + _sub = null; + _channel = null; + _reconnectTimer?.cancel(); + // backoff تا حداکثر ۵ ثانیه؛ سرور با همان توکن بازیکن را به میز برمی‌گرداند. + final delayMs = (500 * (1 << _attempt)).clamp(500, 5000); + _attempt = (_attempt + 1).clamp(0, 4); + _reconnectTimer = Timer(Duration(milliseconds: delayMs), connect); + } + + void dispose() { + _disposed = true; + _reconnectTimer?.cancel(); + _sub?.cancel(); + _channel?.sink.close(); + _messages.close(); + _status.close(); + } +} diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart new file mode 100644 index 0000000..9c4be52 --- /dev/null +++ b/lib/core/storage/token_storage.dart @@ -0,0 +1,16 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +/// ذخیره‌ی امن توکن JWT. +class TokenStorage { + static const _key = 'jwt_token'; + final FlutterSecureStorage _storage; + + TokenStorage([FlutterSecureStorage? storage]) + : _storage = storage ?? const FlutterSecureStorage(); + + Future read() => _storage.read(key: _key); + + Future write(String token) => _storage.write(key: _key, value: token); + + Future clear() => _storage.delete(key: _key); +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..e376ed3 --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +/// تم بصری حکم‌شو (الهام‌گرفته از اپ مرجع: قرمز تیره و طلایی). +class AppColors { + static const bg = Color(0xFF3A0A12); + static const bgDark = Color(0xFF240108); + static const panel = Color(0xFF5A0E1A); + static const gold = Color(0xFFE9B949); + static const goldDark = Color(0xFFB8860B); + static const accent = Color(0xFFB81D2A); + static const green = Color(0xFF2E7D32); + static const text = Color(0xFFF5E9D0); +} + +class AppTheme { + static ThemeData build() { + const fontFamily = 'Dana'; + final base = ThemeData.dark(useMaterial3: true); + return base.copyWith( + scaffoldBackgroundColor: AppColors.bg, + primaryColor: AppColors.accent, + colorScheme: base.colorScheme.copyWith( + primary: AppColors.gold, + secondary: AppColors.accent, + surface: AppColors.panel, + ), + textTheme: base.textTheme.apply( + fontFamily: fontFamily, + bodyColor: AppColors.text, + displayColor: AppColors.text, + ), + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.bgDark, + foregroundColor: AppColors.gold, + centerTitle: true, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: AppColors.bgDark.withValues(alpha: 0.6), + hintStyle: const TextStyle(color: Colors.white38), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.goldDark), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.goldDark), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: AppColors.gold, width: 2), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.accent, + foregroundColor: AppColors.text, + minimumSize: const Size.fromHeight(54), + textStyle: const TextStyle( + fontFamily: fontFamily, fontSize: 18, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: const BorderSide(color: AppColors.gold, width: 1.5), + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/auth_cubit.dart b/lib/features/auth/auth_cubit.dart new file mode 100644 index 0000000..2557562 --- /dev/null +++ b/lib/features/auth/auth_cubit.dart @@ -0,0 +1,76 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'auth_repository.dart'; + +enum AuthStatus { initial, loading, otpSent, authenticated, error } + +class AuthState extends Equatable { + final AuthStatus status; + final String mobile; + final String? error; + + const AuthState({ + this.status = AuthStatus.initial, + this.mobile = '', + this.error, + }); + + AuthState copyWith({AuthStatus? status, String? mobile, String? error}) => + AuthState( + status: status ?? this.status, + mobile: mobile ?? this.mobile, + error: error, + ); + + @override + List get props => [status, mobile, error]; +} + +class AuthCubit extends Cubit { + final AuthRepository _repo; + AuthCubit(this._repo) : super(const AuthState()); + + Future requestOtp(String mobile) async { + emit(state.copyWith(status: AuthStatus.loading, mobile: mobile)); + try { + await _repo.requestOtp(mobile); + emit(state.copyWith(status: AuthStatus.otpSent, mobile: mobile)); + } catch (e) { + emit(state.copyWith(status: AuthStatus.error, error: _msg(e))); + } + } + + Future verifyOtp(String code) async { + emit(state.copyWith(status: AuthStatus.loading)); + try { + await _repo.verifyOtp(state.mobile, code); + emit(state.copyWith(status: AuthStatus.authenticated)); + } catch (e) { + emit(state.copyWith(status: AuthStatus.error, error: _msg(e))); + } + } + + Future logout() async { + await _repo.logout(); + emit(const AuthState()); + } + + /// بازنشانی وضعیت خطا به حالت مناسب فرم. + void resetError({required bool onOtpScreen}) { + emit(state.copyWith( + status: onOtpScreen ? AuthStatus.otpSent : AuthStatus.initial)); + } + + String _msg(Object e) { + if (e is DioException) { + final data = e.response?.data; + if (data is Map && data['message'] != null) { + return data['message'].toString(); + } + return 'خطا در ارتباط با سرور'; + } + return 'خطای نامشخص'; + } +} diff --git a/lib/features/auth/auth_repository.dart b/lib/features/auth/auth_repository.dart new file mode 100644 index 0000000..bac5bc1 --- /dev/null +++ b/lib/features/auth/auth_repository.dart @@ -0,0 +1,35 @@ +import '../../core/network/api_client.dart'; +import '../../core/storage/token_storage.dart'; + +/// دسترسی به endpointهای احراز هویت (login-otp / check-otp). +class AuthRepository { + final ApiClient _api; + final TokenStorage _storage; + + AuthRepository(this._api, this._storage); + + /// درخواست ارسال کد یک‌بارمصرف. + Future requestOtp(String mobile) async { + await _api.dio.post('/auth/login-otp', data: {'mobile': mobile}); + } + + /// اعتبارسنجی کد و ذخیره‌ی توکن JWT. + Future verifyOtp(String mobile, String code) async { + final res = await _api.dio.post( + '/auth/check-otp', + data: {'mobile': mobile, 'token': code}, + ); + final token = res.data['token'] as String?; + if (token == null || token.isEmpty) { + throw Exception('no token in response'); + } + await _storage.write(token); + } + + Future isLoggedIn() async { + final t = await _storage.read(); + return t != null && t.isNotEmpty; + } + + Future logout() => _storage.clear(); +} diff --git a/lib/features/auth/mobile_screen.dart b/lib/features/auth/mobile_screen.dart new file mode 100644 index 0000000..59b65c3 --- /dev/null +++ b/lib/features/auth/mobile_screen.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/theme/app_theme.dart'; +import 'auth_cubit.dart'; + +/// صفحه‌ی ورود شماره موبایل. +class MobileScreen extends StatefulWidget { + const MobileScreen({super.key}); + + @override + State createState() => _MobileScreenState(); +} + +class _MobileScreenState extends State { + final _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + bool get _valid => RegExp(r'^09\d{9}$').hasMatch(_controller.text.trim()); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: BlocConsumer( + listener: (context, state) { + if (state.status == AuthStatus.otpSent) { + context.push('/otp'); + } else if (state.status == AuthStatus.error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error ?? 'خطا')), + ); + } + }, + builder: (context, state) { + final loading = state.status == AuthStatus.loading; + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('سلطان حکم', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: AppColors.gold)), + const SizedBox(height: 8), + const Text('برای ورود شماره موبایلت رو وارد کن', + style: TextStyle(color: Colors.white70)), + const SizedBox(height: 32), + TextField( + controller: _controller, + keyboardType: TextInputType.phone, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 20, letterSpacing: 2), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(11), + ], + decoration: const InputDecoration(hintText: '09xxxxxxxxx'), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: (!_valid || loading) + ? null + : () => context + .read() + .requestOtp(_controller.text.trim()), + child: loading + ? const SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator(strokeWidth: 2)) + : const Text('دریافت کد'), + ), + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/features/auth/otp_screen.dart b/lib/features/auth/otp_screen.dart new file mode 100644 index 0000000..32c3f24 --- /dev/null +++ b/lib/features/auth/otp_screen.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/theme/app_theme.dart'; +import 'auth_cubit.dart'; + +/// صفحه‌ی ورود کد یک‌بارمصرف (۵ رقمی). +class OtpScreen extends StatefulWidget { + const OtpScreen({super.key}); + + @override + State createState() => _OtpScreenState(); +} + +class _OtpScreenState extends State { + final _controller = TextEditingController(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + bool get _valid => _controller.text.trim().length == 5; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('تأیید کد')), + body: SafeArea( + child: BlocConsumer( + listener: (context, state) { + if (state.status == AuthStatus.authenticated) { + context.go('/lobby'); + } else if (state.status == AuthStatus.error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error ?? 'خطا')), + ); + context.read().resetError(onOtpScreen: true); + } + }, + builder: (context, state) { + final loading = state.status == AuthStatus.loading; + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('کد پیامک‌شده به ${state.mobile} را وارد کنید', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white70)), + const SizedBox(height: 32), + TextField( + controller: _controller, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 28, letterSpacing: 12), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(5), + ], + decoration: const InputDecoration(hintText: '- - - - -'), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: (!_valid || loading) + ? null + : () => context + .read() + .verifyOtp(_controller.text.trim()), + child: loading + ? const SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator(strokeWidth: 2)) + : const Text('ورود'), + ), + TextButton( + onPressed: loading ? null : () => context.pop(), + child: const Text('تغییر شماره', + style: TextStyle(color: AppColors.gold)), + ), + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/features/game/flame/card_component.dart b/lib/features/game/flame/card_component.dart new file mode 100644 index 0000000..6935052 --- /dev/null +++ b/lib/features/game/flame/card_component.dart @@ -0,0 +1,120 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flutter/material.dart'; + +/// نسبت ابعاد کارت (۵۰۰×۷۲۶ منبع). +const double kCardRatio = 726 / 500; + +/// یک کارت روی میز؛ اگر تصویر `assets/images/cards/.png` موجود باشد از آن +/// استفاده می‌کند، وگرنه نسخه‌ی برداری می‌کشد. برای پشت کارت `back.jpg`. +class CardComponent extends PositionComponent with TapCallbacks, HasGameReference { + final String code; // مثل "AS"؛ برای پشت کارت خالی + final bool faceUp; + VoidCallback? onTap; // با تغییر نوبت/مجاز‌بودن به‌روز می‌شود + bool dimmed; // کارت غیرمجاز/غیرفعال + Sprite? _sprite; + + CardComponent({ + required this.code, + required this.faceUp, + this.onTap, + this.dimmed = false, + super.position, + super.size, + super.angle, + super.priority, + super.anchor = Anchor.center, + }); + + @override + Future onLoad() async { + try { + _sprite = await game.loadSprite(faceUp ? 'cards/$code.png' : 'cards/back.jpg'); + } catch (_) { + _sprite = null; // fallback برداری + } + } + + @override + void onTapDown(TapDownEvent event) { + if (onTap != null && !dimmed) onTap!.call(); + } + + @override + void render(Canvas canvas) { + final rect = size.toRect(); + final radius = Radius.circular(size.x * 0.09); + final rrect = RRect.fromRectAndRadius(rect, radius); + + if (_sprite != null) { + canvas.save(); + canvas.clipRRect(rrect); + _sprite!.render(canvas, size: size); + canvas.restore(); + } else if (faceUp) { + _drawVectorFace(canvas, rrect); + } else { + _drawVectorBack(canvas, rrect); + } + + if (dimmed) { + canvas.drawRRect(rrect, Paint()..color = const Color(0x73000000)); + } + canvas.drawRRect( + rrect, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5 + ..color = const Color(0xFF1A1A1A), + ); + } + + void _drawVectorBack(Canvas canvas, RRect rrect) { + canvas.drawRRect(rrect, Paint()..color = const Color(0xFF1C3A66)); + final inner = rrect.deflate(size.x * 0.08); + canvas.drawRRect( + inner, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2 + ..color = const Color(0xFFE9B949), + ); + } + + void _drawVectorFace(Canvas canvas, RRect rrect) { + canvas.drawRRect(rrect, Paint()..color = Colors.white); + final rank = code.substring(0, code.length - 1); + final suit = code.substring(code.length - 1); + final (symbol, color) = _suit(suit); + + _text(canvas, '$rank$symbol', size.x * 0.26, color, + Offset(size.x * 0.08, size.y * 0.05)); + // نماد بزرگ وسط + _text(canvas, symbol, size.x * 0.5, color, + Offset(size.x * 0.5, size.y * 0.5), center: true); + } + + (String, Color) _suit(String s) { + switch (s) { + case 'H': + return ('♥', const Color(0xFFD32F2F)); + case 'D': + return ('♦', const Color(0xFFD32F2F)); + case 'C': + return ('♣', const Color(0xFF1A1A1A)); + default: + return ('♠', const Color(0xFF1A1A1A)); + } + } + + void _text(Canvas canvas, String s, double fontSize, Color color, Offset at, + {bool center = false}) { + final tp = TextPainter( + text: TextSpan( + text: s, style: TextStyle(color: color, fontSize: fontSize)), + textDirection: TextDirection.ltr, + )..layout(); + final offset = center ? at - Offset(tp.width / 2, tp.height / 2) : at; + tp.paint(canvas, offset); + } +} diff --git a/lib/features/game/flame/hokm_game.dart b/lib/features/game/flame/hokm_game.dart new file mode 100644 index 0000000..3185b82 --- /dev/null +++ b/lib/features/game/flame/hokm_game.dart @@ -0,0 +1,615 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +import '../game_cubit.dart'; +import '../game_models.dart'; +import 'card_component.dart'; + +/// صحنه‌ی میز حکم با انیمیشن. شما همیشه پایین میز (rel=0) هستید. +/// کامپوننت‌های دست و trick ماندگارند و با افکت حرکت جابه‌جا می‌شوند. +class HokmGame extends FlameGame { + final GameCubit cubit; + GameState? _s; + StreamSubscription? _sub; + + final Map _hand = {}; // کارت‌های دستِ شما (با کد) + final Map _trick = {}; // کارت‌های روی زمین (با کد) + final List _backs = []; // پشت‌کارت حریفان + final List _info = []; // نام/امتیاز/حکم/نوبت + + double _cardW = 60; + double _cardH = 87; + + double _shake = 0; // شدت تکانِ صفحه (افکت برد با حکم) + double _t = 0; // زمان برای نوسان تکان + bool _prevTrickDone = false; + + HokmGame(this.cubit); + + @override + Color backgroundColor() => const Color(0xFF2C0A10); + + @override + Future onLoad() async { + add(_Felt()); + _sub = cubit.stream.listen((ui) { + if (ui.state != null) { + final s = ui.state!; + if (s.trickDone && !_prevTrickDone) _onTrickComplete(s); + _prevTrickDone = s.trickDone; + _s = s; + _relayout(); + } + }); + if (cubit.state.state != null) { + _s = cubit.state.state; + _relayout(); + } + } + + @override + void update(double dt) { + super.update(dt); + _t += dt; + if (_shake > 0) _shake = math.max(0, _shake - dt * 2.2); + } + + @override + void render(Canvas canvas) { + if (_shake > 0) { + final dx = math.sin(_t * 55) * _shake * size.x * 0.018; + final dy = math.cos(_t * 70) * _shake * size.y * 0.010; + canvas.save(); + canvas.translate(dx, dy); + super.render(canvas); + canvas.restore(); + } else { + super.render(canvas); + } + } + + // هنگام کامل‌شدن یک دست: اگر برنده «بریده» باشد (خالِ زمینه آتو نبوده ولی + // کارتِ برنده آتوست — یعنی بازیکن چون خال زمینه را نداشت آتو زد) تکان + رعد. + void _onTrickComplete(GameState s) { + if (s.trick.length < 4) return; + final winner = _winnerCard(s); + final cutWithTrump = s.trump != null && + s.leadSuit != s.trump && + _suitName(winner.card) == s.trump; + _shake = cutWithTrump ? 1.0 : 0.3; + if (cutWithTrump) add(_Lightning()); + } + + TrickCard _winnerCard(GameState s) { + var best = s.trick.first; + for (final tc in s.trick.skip(1)) { + if (_beats(tc.card, best.card, s.trump, s.leadSuit)) best = tc; + } + return best; + } + + // آیا کارت a کارت b را می‌برد (با توجه به حکم و خال زمینه)؟ مطابق منطق سرور. + bool _beats(String a, String b, String? trump, String? lead) { + final aT = _suitName(a) == trump, bT = _suitName(b) == trump; + if (aT && !bT) return true; + if (!aT && bT) return false; + if (aT && bT) return _rankValue(a) > _rankValue(b); + if (_suitName(a) != lead) return false; + if (_suitName(b) != lead) return true; + return _rankValue(a) > _rankValue(b); + } + + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + if (isLoaded) _relayout(); + } + + @override + void onRemove() { + _sub?.cancel(); + super.onRemove(); + } + + int _rel(int seat) => (seat - _s!.yourSeat + 4) % 4; + + static String _suitName(String code) { + switch (code[code.length - 1]) { + case 'H': + return 'hearts'; + case 'D': + return 'diamonds'; + case 'C': + return 'clubs'; + default: + return 'spades'; + } + } + + bool _legal(String code) { + final s = _s!; + if (s.phase != 'playing' || s.trickDone || !s.isMyTurn) return false; + final lead = s.leadSuit; + if (lead == null || lead.isEmpty) return true; + final hasLead = s.yourHand.any((c) => _suitName(c) == lead); + return !hasLead || _suitName(code) == lead; + } + + void _relayout() { + final s = _s; + if (s == null) return; + _cardW = size.x * 0.19; // کارت‌های بزرگ‌تر (مطابق تصویر مرجع) + _cardH = _cardW * kCardRatio; + + _rebuildBacksAndInfo(s); + _layoutTrick(s); + _layoutHand(s); + } + + // افکت حرکت بدون انباشته‌شدن. + void _moveTo(CardComponent c, Vector2 target, {double dur = 0.38}) { + c.children.whereType().toList().forEach((e) => e.removeFromParent()); + if ((c.position - target).length < 0.5) { + c.position = target; + return; + } + c.add(MoveToEffect( + target, EffectController(duration: dur, curve: Curves.easeOutCubic))); + } + + // ترتیب خال‌ها برای چیدنِ دست (سیاه/قرمز متناوب تا تفکیک بصری راحت‌تر باشد). + static const _suitOrder = {'S': 0, 'H': 1, 'C': 2, 'D': 3}; + + static int _rankValue(String code) { + switch (code.substring(0, code.length - 1)) { + case 'A': + return 14; + case 'K': + return 13; + case 'Q': + return 12; + case 'J': + return 11; + default: + return int.tryParse(code.substring(0, code.length - 1)) ?? 0; + } + } + + static int _cardCompare(String a, String b) { + final sa = _suitOrder[a[a.length - 1]] ?? 0; + final sb = _suitOrder[b[b.length - 1]] ?? 0; + if (sa != sb) return sa - sb; + return _rankValue(a) - _rankValue(b); + } + + void _layoutHand(GameState s) { + // کارت‌های دست بر اساس خال و سپس رتبه مرتب می‌شوند تا کار بازیکن راحت‌تر باشد. + final codes = List.from(s.yourHand)..sort(_cardCompare); + final n = codes.length; + + // کارت‌هایی که دیگر در دست نیستند و به trick هم نرفته‌اند ⇒ حذف. + for (final code in _hand.keys.toList()) { + if (!codes.contains(code)) { + final c = _hand.remove(code)!; + if (!_trick.containsKey(code)) c.removeFromParent(); + } + } + + final handW = size.x * 0.72; + final step = n > 1 ? (handW - _cardW) / (n - 1) : 0.0; + final startX = size.x / 2 - (step * (n - 1)) / 2; + final y = size.y * 0.84; + + for (var i = 0; i < n; i++) { + final code = codes[i]; + final target = Vector2(startX + i * step, y); + final legal = _legal(code); + final dim = s.phase == 'playing' && s.isMyTurn && !legal; + var c = _hand[code]; + if (c == null) { + // کارت جدید ⇒ از مرکز میز پخش می‌شود. + c = CardComponent( + code: code, + faceUp: true, + size: Vector2(_cardW, _cardH), + position: size / 2, + ); + _hand[code] = c; + add(c); + } else { + c.size = Vector2(_cardW, _cardH); + } + c.onTap = legal ? () => cubit.playCard(code) : null; + c.dimmed = dim; + c.priority = 10 + i; + _moveTo(c, target); + } + } + + void _layoutTrick(GameState s) { + final present = s.trick.map((t) => t.card).toSet(); + + // کارت‌های trick که دیگر نیستند (دست جمع شد) ⇒ به سمت برنده (نوبت بعدی) برو و حذف شو. + for (final code in _trick.keys.toList()) { + if (!present.contains(code)) { + final c = _trick.remove(code)!; + final winnerOrigin = _seatOrigin(_rel(s.turn)); + c.children.whereType().toList().forEach((e) => e.removeFromParent()); + c.add(SequenceEffect([ + MoveToEffect(winnerOrigin, + EffectController(duration: 0.25, curve: Curves.easeIn)), + RemoveEffect(), + ])); + } + } + + for (final tc in s.trick) { + final slot = size / 2 + _trickOffset(_rel(tc.seat)); + var c = _trick[tc.card]; + if (c == null) { + // اگر خودِ شما بازی کردید، همان کارتِ دست را منتقل کن (پرواز به وسط). + c = _hand.remove(tc.card); + if (c != null) { + c.onTap = null; + c.dimmed = false; + } else { + c = CardComponent( + code: tc.card, + faceUp: true, + size: Vector2(_cardW, _cardH), + position: _seatOrigin(_rel(tc.seat)), + ); + add(c); + } + _trick[tc.card] = c; + } + c.size = Vector2(_cardW, _cardH); + c.priority = 5; + _moveTo(c, slot); + } + } + + Vector2 _seatOrigin(int rel) { + final c = size / 2; + switch (rel) { + case 1: + return Vector2(size.x * 0.10, c.y); + case 2: + return Vector2(c.x, size.y * 0.14); + case 3: + return Vector2(size.x * 0.90, c.y); + default: + return Vector2(c.x, size.y * 0.82); + } + } + + Vector2 _trickOffset(int rel) { + final d = _cardW * 0.62; + switch (rel) { + case 1: + return Vector2(-d, 0); + case 2: + return Vector2(0, -d); + case 3: + return Vector2(d, 0); + default: + return Vector2(0, d); + } + } + + void _rebuildBacksAndInfo(GameState s) { + for (final c in _backs) { + c.removeFromParent(); + } + for (final c in _info) { + c.removeFromParent(); + } + _backs.clear(); + _info.clear(); + + // پشت‌کارت حریفان + for (var seat = 0; seat < 4; seat++) { + if (seat == s.yourSeat) continue; + final rel = _rel(seat); + final count = seat < s.handCounts.length ? s.handCounts[seat] : 0; + _addBacks(rel, count); + } + _addTricksWon(s); + _addScorePucks(s); + _addInfo(s); + } + + // شمارنده ۱: دست‌های برده‌ی این هَند (tricks_won) — دسته‌کارتِ پشت‌رو + عدد. + // با هر دست یکی اضافه می‌شود؛ رسیدن به ۷ یعنی پایان هَند (سرور صفر می‌کند). + void _addTricksWon(GameState s) { + const gold = Color(0xFFE9B949); + for (var team = 0; team < 2; team++) { + final won = team < s.tricksWon.length ? s.tricksWon[team] : 0; + if (won <= 0) continue; + final mine = team == s.yourSeat % 2; + final w = _cardW * 0.40, h = w * kCardRatio; + final base = mine + ? Vector2(size.x * 0.30, size.y * 0.60) + : Vector2(size.x * 0.70, size.y * 0.40); + final step = Vector2(w * 0.40, 0); + final n = won.clamp(1, 7); + final start = base - step * ((n - 1) / 2); + for (var i = 0; i < n; i++) { + final c = CardComponent( + code: '', + faceUp: false, + size: Vector2(w, h), + position: start + step * i.toDouble(), + priority: 2, + ); + _backs.add(c); + add(c); + } + final badge = TextComponent( + text: '$won', + anchor: Anchor.center, + position: base - Vector2(0, h * 0.7), + priority: 21, + textRenderer: TextPaint( + style: TextStyle( + fontFamily: 'Dana', + color: gold, + fontSize: _cardW * 0.34, + fontWeight: FontWeight.bold, + shadows: const [Shadow(color: Colors.black, blurRadius: 4)], + ), + ), + ); + _info.add(badge); + add(badge); + } + } + + // شمارنده ۲: امتیاز بازی (scores = هندهای برده) — دیسک مرکزیِ هر تیم، ۰ تا ۷. + // رسیدن به ۷ یعنی پایان کل بازی. + void _addScorePucks(GameState s) { + final radius = size.x * 0.05; + for (var team = 0; team < 2; team++) { + final score = team < s.scores.length ? s.scores[team] : 0; + final mine = team == s.yourSeat % 2; + final pos = mine + ? Vector2(size.x / 2, size.y * 0.72) // تیم شما: پایینِ مرکز + : Vector2(size.x / 2, size.y * 0.28); // تیم حریف: بالای مرکز + final puck = _Puck(score, radius: radius, position: pos)..priority = 22; + _info.add(puck); + add(puck); + } + } + + void _addBacks(int rel, int count) { + if (count <= 0) return; + final n = count.clamp(1, 13); + final w = _cardW * 0.7, h = _cardH * 0.7; + final c = size / 2; + Vector2 base, stepV; + switch (rel) { + case 2: + base = Vector2(c.x, size.y * 0.12); + stepV = Vector2(size.x * 0.45 / 13, 0); + break; + case 1: + base = Vector2(size.x * 0.07, c.y); + stepV = Vector2(0, size.y * 0.4 / 13); + break; + default: + base = Vector2(size.x * 0.93, c.y); + stepV = Vector2(0, size.y * 0.4 / 13); + } + final start = base - stepV * ((n - 1) / 2); + for (var i = 0; i < n; i++) { + final comp = CardComponent( + code: '', + faceUp: false, + size: Vector2(w, h), + position: start + stepV * i.toDouble(), + priority: 1, + ); + _backs.add(comp); + add(comp); + } + } + + void _addInfo(GameState s) { + void addText(String text, Vector2 pos, double fs, Color color, + {Anchor anchor = Anchor.center, bool bold = false}) { + final t = TextComponent( + text: text, + anchor: anchor, + position: pos, + priority: 20, + textRenderer: TextPaint( + style: TextStyle( + fontFamily: 'Dana', + color: color, + fontSize: fs, + fontWeight: bold ? FontWeight.bold : FontWeight.normal)), + ); + _info.add(t); + add(t); + } + + if (s.trump != null) { + final (sym, col) = _trumpGlyph(s.trump!); + addText('حکم: $sym', Vector2(size.x * 0.04, size.y * 0.04), size.x * 0.05, + col, anchor: Anchor.topLeft, bold: true); + } + addText( + '${s.scores.isNotEmpty ? s.scores[0] : 0} - ${s.scores.length > 1 ? s.scores[1] : 0}', + Vector2(size.x / 2, size.y * 0.04), + size.x * 0.05, + const Color(0xFFE9B949), + anchor: Anchor.topCenter); + + for (final p in s.players) { + final rel = _rel(p.seat); + final pos = _seatLabelPos(rel); + final isTurn = s.turn == p.seat; + addText( + '${p.name}${p.bot ? ' (ربات)' : ''}${p.connected ? '' : ' …'}', + pos, + size.x * 0.035, + isTurn ? const Color(0xFFE9B949) : Colors.white70, + bold: isTurn, + ); + if (isTurn) { + final dot = CircleComponent( + radius: size.x * 0.012, + anchor: Anchor.center, + position: pos - Vector2(0, size.y * 0.03), + paint: Paint()..color = const Color(0xFFE9B949), + )..priority = 20; + _info.add(dot); + add(dot); + } + } + } + + Vector2 _seatLabelPos(int rel) { + switch (rel) { + case 1: + return Vector2(size.x * 0.07, size.y * 0.30); + case 2: + return Vector2(size.x / 2, size.y * 0.07); + case 3: + return Vector2(size.x * 0.93, size.y * 0.30); + default: + return Vector2(size.x / 2, size.y * 0.975); + } + } + + (String, Color) _trumpGlyph(String suit) { + switch (suit) { + case 'hearts': + return ('♥', const Color(0xFFD32F2F)); + case 'diamonds': + return ('♦', const Color(0xFFD32F2F)); + case 'clubs': + return ('♣', Colors.white); + default: + return ('♠', Colors.white); + } + } +} + +/// افکت رعد روی زمین هنگام برد با حکم؛ پس از مدت کوتاهی خودش حذف می‌شود. +class _Lightning extends PositionComponent with HasGameReference { + double _life = 0.55; + static const _max = 0.55; + final _rng = math.Random(); + final List> _bolts = []; + + @override + Future onLoad() async { + size = game.size; + final center = Offset(size.x / 2, size.y / 2); + for (var i = 0; i < 3; i++) { + // هر رعد یک خط شکسته از یک نقطه‌ی تصادفیِ کناری به سمت مرکز. + final start = Offset(_rng.nextDouble() * size.x, _rng.nextDouble() * size.y * 0.4); + final pts = [start]; + const segs = 6; + for (var s = 1; s <= segs; s++) { + final t = s / segs; + final base = Offset.lerp(start, center, t)!; + final jitter = (1 - t) * size.x * 0.06; + pts.add(base + Offset((_rng.nextDouble() - 0.5) * jitter, (_rng.nextDouble() - 0.5) * jitter)); + } + _bolts.add(pts); + } + } + + @override + void update(double dt) { + _life -= dt; + if (_life <= 0) removeFromParent(); + } + + @override + void render(Canvas canvas) { + final op = (_life / _max).clamp(0.0, 1.0); + final glow = Paint() + ..color = const Color(0xFFFFE082).withValues(alpha: op * 0.5) + ..style = PaintingStyle.stroke + ..strokeWidth = size.x * 0.02 + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6); + final core = Paint() + ..color = const Color(0xFFFFFDE7).withValues(alpha: op) + ..style = PaintingStyle.stroke + ..strokeWidth = size.x * 0.006 + ..strokeCap = StrokeCap.round; + for (final bolt in _bolts) { + final path = Path()..moveTo(bolt.first.dx, bolt.first.dy); + for (final p in bolt.skip(1)) { + path.lineTo(p.dx, p.dy); + } + canvas.drawPath(path, glow); + canvas.drawPath(path, core); + } + } +} + +/// دیسکِ فلزیِ شمارش دست‌های برده‌ی یک تیم (۰ تا ۷). +class _Puck extends PositionComponent { + final int count; + _Puck(this.count, {required double radius, super.position}) + : super(size: Vector2.all(radius * 2), anchor: Anchor.center); + + @override + void render(Canvas canvas) { + final r = size.x / 2; + final c = Offset(r, r); + canvas.drawCircle(c, r, Paint()..color = const Color(0xFF14110F)); + canvas.drawCircle( + c, + r * 0.92, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = r * 0.22 + ..color = const Color(0xFFB8860B), + ); + final tp = TextPainter( + text: TextSpan( + text: '$count', + style: TextStyle( + fontFamily: 'Dana', + color: const Color(0xFFE9B949), + fontSize: r * 1.05, + fontWeight: FontWeight.bold, + ), + ), + textDirection: TextDirection.ltr, + )..layout(); + tp.paint(canvas, c - Offset(tp.width / 2, tp.height / 2)); + } +} + +/// نمدِ سبز میز. +class _Felt extends PositionComponent with HasGameReference { + @override + void render(Canvas canvas) { + final w = game.size.x, h = game.size.y; + final rect = Rect.fromCenter( + center: Offset(w / 2, h / 2), + width: w * 0.86, + height: h * 0.62, + ); + final rrect = RRect.fromRectAndRadius(rect, Radius.circular(h * 0.3)); + canvas.drawRRect(rrect, Paint()..color = const Color(0xFF1B5E20)); + canvas.drawRRect( + rrect, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 6 + ..color = const Color(0xFFB8860B), + ); + } +} diff --git a/lib/features/game/game_cubit.dart b/lib/features/game/game_cubit.dart new file mode 100644 index 0000000..2842d04 --- /dev/null +++ b/lib/features/game/game_cubit.dart @@ -0,0 +1,103 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../core/network/ws_client.dart'; +import 'game_models.dart'; + +class GameUiState extends Equatable { + final WsStatus connection; + final GameState? state; + final HandResult? handResult; // اوورلی نتیجه‌ی هَند (گذرا) + final GameOver? gameOver; // اوورلی پایان بازی + final String? notice; // پیام گذرا (خطا/خروج بازیکن) + + const GameUiState({ + this.connection = WsStatus.connecting, + this.state, + this.handResult, + this.gameOver, + this.notice, + }); + + GameUiState copyWith({ + WsStatus? connection, + GameState? state, + HandResult? handResult, + GameOver? gameOver, + String? notice, + bool clearHandResult = false, + bool clearNotice = false, + }) => + GameUiState( + connection: connection ?? this.connection, + state: state ?? this.state, + handResult: clearHandResult ? null : (handResult ?? this.handResult), + gameOver: gameOver ?? this.gameOver, + notice: clearNotice ? null : (notice ?? this.notice), + ); + + @override + List get props => [connection, state, handResult, gameOver, notice]; +} + +class GameCubit extends Cubit { + final WsClient _ws; + final String tier; + late final StreamSubscription _msgSub; + late final StreamSubscription _statusSub; + + GameCubit(this._ws, this.tier) : super(const GameUiState()) { + _msgSub = _ws.messages.listen(_onMessage); + _statusSub = _ws.status.listen(_onStatus); + _ws.connect(); + } + + void _onStatus(WsStatus s) { + emit(state.copyWith(connection: s)); + // پس از برقراری اتصال، درخواست ورود به صف؛ در صورت reconnect سرور خودش + // بازیکن را به میز برمی‌گرداند (این پیام را نادیده می‌گیرد). + if (s == WsStatus.connected) { + _ws.send({'type': 'join_queue', 'tier': tier}); + } + } + + void _onMessage(Map msg) { + switch (msg['type']) { + case 'state': + final gs = GameState.fromJson(msg); + // با شروع دست/هَند جدید، اوورلی نتیجه پاک می‌شود. + final clear = gs.phase == 'choose_trump' || gs.phase == 'playing'; + emit(state.copyWith(state: gs, clearHandResult: clear)); + case 'hand_over': + emit(state.copyWith(handResult: HandResult.fromJson(msg))); + case 'game_over': + emit(state.copyWith(gameOver: GameOver.fromJson(msg))); + case 'player_disconnected': + emit(state.copyWith(notice: 'یک بازیکن قطع شد')); + case 'player_reconnected': + emit(state.copyWith(notice: 'بازیکن بازگشت')); + case 'player_left': + emit(state.copyWith(notice: 'یک بازیکن میز را ترک کرد')); + case 'error': + emit(state.copyWith(notice: (msg['message'] ?? 'خطا').toString())); + } + } + + void chooseTrump(String suit) => _ws.send({'type': 'choose_trump', 'suit': suit}); + + void playCard(String card) => _ws.send({'type': 'play_card', 'card': card}); + + void leave() => _ws.send({'type': 'leave'}); + + void clearNotice() => emit(state.copyWith(clearNotice: true)); + + @override + Future close() { + _msgSub.cancel(); + _statusSub.cancel(); + _ws.dispose(); + return super.close(); + } +} diff --git a/lib/features/game/game_models.dart b/lib/features/game/game_models.dart new file mode 100644 index 0000000..04d61cd --- /dev/null +++ b/lib/features/game/game_models.dart @@ -0,0 +1,123 @@ +// مدل‌های وضعیت بازی (پیام‌های WebSocket سرور). + +class GamePlayer { + final int seat; + final String name; + final bool bot; + final bool connected; + + GamePlayer.fromJson(Map j) + : seat = (j['seat'] ?? 0) as int, + name = (j['name'] ?? '') as String, + bot = (j['bot'] ?? false) as bool, + connected = (j['connected'] ?? false) as bool; +} + +class TrickCard { + final int seat; + final String card; + TrickCard(this.seat, this.card); + factory TrickCard.fromJson(Map j) => + TrickCard((j['seat'] ?? 0) as int, (j['card'] ?? '') as String); +} + +/// نمای وضعیت بازی برای بازیکن جاری (پیام type=state). +class GameState { + final String room; + final String phase; // choose_trump | playing | hand_over | game_over + final int yourSeat; + final int hakem; + final int turn; + final String? trump; // پس از انتخاب حکم + final bool trickDone; // دستِ کامل در حال نمایش (بازی ممنوع) + final List yourHand; + final List handCounts; + final List trick; + final String? leadSuit; + final List tricksWon; + final List scores; + final int targetScore; + final List players; + + GameState({ + required this.room, + required this.phase, + required this.yourSeat, + required this.hakem, + required this.turn, + required this.trump, + required this.trickDone, + required this.yourHand, + required this.handCounts, + required this.trick, + required this.leadSuit, + required this.tricksWon, + required this.scores, + required this.targetScore, + required this.players, + }); + + factory GameState.fromJson(Map j) { + List ints(dynamic v) => + ((v as List?) ?? []).map((e) => (e as num).toInt()).toList(); + return GameState( + room: (j['room'] ?? '') as String, + phase: (j['phase'] ?? '') as String, + yourSeat: (j['your_seat'] ?? 0) as int, + hakem: (j['hakem'] ?? 0) as int, + turn: (j['turn'] ?? 0) as int, + trump: j['trump'] as String?, + trickDone: (j['trick_done'] ?? false) as bool, + yourHand: ((j['your_hand'] as List?) ?? []).map((e) => e as String).toList(), + handCounts: ints(j['hand_counts']), + trick: ((j['trick'] as List?) ?? []) + .map((e) => TrickCard.fromJson(Map.from(e as Map))) + .toList(), + leadSuit: j['lead_suit'] as String?, + tricksWon: ints(j['tricks_won']), + scores: ints(j['scores']), + targetScore: (j['target_score'] ?? 7) as int, + players: ((j['players'] as List?) ?? []) + .map((e) => GamePlayer.fromJson(Map.from(e as Map))) + .toList(), + ); + } + + bool get isMyTurn => turn == yourSeat; + bool get amHakem => hakem == yourSeat; + GamePlayer? playerAt(int seat) => + players.where((p) => p.seat == seat).cast().firstOrNull; +} + +/// نتیجه‌ی یک هَند (پیام type=hand_over). +class HandResult { + final int winnerTeam; + final bool kot; + final int points; + final List scores; + HandResult.fromJson(Map j) + : winnerTeam = (j['winner_team'] ?? 0) as int, + kot = (j['kot'] ?? false) as bool, + points = (j['points'] ?? 0) as int, + scores = ((j['scores'] as List?) ?? []) + .map((e) => (e as num).toInt()) + .toList(); +} + +/// نتیجه‌ی پایان بازی (پیام type=game_over). +class GameOver { + final int winnerTeam; + final List scores; + GameOver.fromJson(Map j) + : winnerTeam = (j['winner_team'] ?? 0) as int, + scores = ((j['scores'] as List?) ?? []) + .map((e) => (e as num).toInt()) + .toList(); +} + +extension _FirstOrNull on Iterable { + E? get firstOrNull { + final it = iterator; + return it.moveNext() ? it.current : null; + } +} diff --git a/lib/features/game/game_repository.dart b/lib/features/game/game_repository.dart new file mode 100644 index 0000000..e9ddf4b --- /dev/null +++ b/lib/features/game/game_repository.dart @@ -0,0 +1,17 @@ +import '../../core/network/api_client.dart'; +import 'tier.dart'; + +/// واکشی انواع میز برای صفحه‌ی لیست میزها. +class GameRepository { + final ApiClient _api; + GameRepository(this._api); + + Future> getTiers() async { + final res = await _api.dio.get('/shop'); + final cat = Map.from(res.data['catalog'] as Map); + final list = (cat['table_tiers'] as List?) ?? []; + return list + .map((e) => TableTier.fromJson(Map.from(e as Map))) + .toList(); + } +} diff --git a/lib/features/game/game_screen.dart b/lib/features/game/game_screen.dart new file mode 100644 index 0000000..4bace34 --- /dev/null +++ b/lib/features/game/game_screen.dart @@ -0,0 +1,373 @@ +import 'dart:async'; + +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/network/ws_client.dart'; +import '../../core/theme/app_theme.dart'; +import '../lobby/wallet_cubit.dart'; +import 'flame/hokm_game.dart'; +import 'game_cubit.dart'; +import 'game_models.dart'; + +/// صفحه‌ی میز بازی: صحنه‌ی Flame + اوورلی‌های وضعیت (انتخاب حکم، نتیجه، پایان، اتصال). +class GameScreen extends StatefulWidget { + final int prize; // جایزه‌ی میز برای نمایش در دیالوگ جستجو + const GameScreen({super.key, this.prize = 0}); + + @override + State createState() => _GameScreenState(); +} + +class _GameScreenState extends State { + late final HokmGame _game; + Timer? _introTimer; + bool _introHidden = false; + + @override + void initState() { + super.initState(); + _game = HokmGame(context.read()); + } + + @override + void dispose() { + _introTimer?.cancel(); + super.dispose(); + } + + // دیالوگ جستجو تا یافتن حریفان و کمی پس از آن نمایش داده می‌شود. + bool _showSearch(GameUiState s) => s.state == null || !_introHidden; + + @override + Widget build(BuildContext context) { + return PopScope( + // خروج با دکمه‌ی back سیستم هم باید با تأیید باشد. + canPop: false, + onPopInvokedWithResult: (didPop, _) { + if (!didPop) _confirmLeave(context); + }, + child: Scaffold( + body: BlocConsumer( + listenWhen: (a, b) => a.notice != b.notice && b.notice != null, + listener: (context, state) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.notice!), duration: const Duration(seconds: 2)), + ); + context.read().clearNotice(); + }, + builder: (context, state) { + // پس از یافتن حریفان، دیالوگ جستجو را کمی نگه می‌داریم بعد مخفی می‌کنیم. + if (state.state != null && _introTimer == null) { + _introTimer = Timer(const Duration(milliseconds: 1600), () { + if (mounted) setState(() => _introHidden = true); + }); + } + return Stack( + children: [ + GameWidget(game: _game), + _backButton(context), + if (state.connection == WsStatus.disconnected) _connBanner(), + if (_showSearch(state)) _searchPanel(state), + if (!_showSearch(state) && _showTrumpPicker(state)) + _trumpPicker(context), + if (state.handResult != null && state.gameOver == null) + _handResult(state), + if (state.gameOver != null) _gameOver(context, state), + ], + ); + }, + ), + ), + ); + } + + bool _showTrumpPicker(GameUiState s) => + s.state != null && + s.state!.phase == 'choose_trump' && + s.state!.amHakem && + s.gameOver == null; + + Widget _backButton(BuildContext context) => Positioned( + top: 8, + right: 8, + child: SafeArea( + child: IconButton( + icon: const Icon(Icons.arrow_back, color: AppColors.gold), + onPressed: () => _confirmLeave(context), + ), + ), + ); + + Future _confirmLeave(BuildContext context) async { + // اگر بازی تمام شده، بدون تأیید خارج شو. + if (context.read().state.gameOver != null) { + _exitToLobby(context); + return; + } + final yes = await showDialog( + context: context, + builder: (_) => AlertDialog( + backgroundColor: AppColors.panel, + title: const Text('خروج از میز'), + content: const Text('از میز خارج می‌شوید؟ ورودی بازگردانده نمی‌شود.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('ماندن')), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('خروج')), + ], + ), + ); + if (yes == true && context.mounted) { + context.read().leave(); + _exitToLobby(context); + } + } + + void _exitToLobby(BuildContext context) { + context.read().load(); + context.go('/lobby'); + } + + Widget _connBanner() => Positioned( + top: 0, + left: 0, + right: 0, + child: Material( + color: Colors.orange.shade900, + child: const SafeArea( + bottom: false, + child: Padding( + padding: EdgeInsets.all(8), + child: Text('ارتباط با سرور قطع شد، در حال تلاش برای اتصال مجدد…', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white)), + ), + ), + ), + ); + + // دیالوگ «جستجوی حریف» مطابق اپ مرجع: ۴ جایگاه بازیکن + جایزه. + Widget _searchPanel(GameUiState state) { + final players = state.state?.players ?? const []; + final mySeat = state.state?.yourSeat ?? -1; + final searching = state.state == null; + GamePlayer? at(int seat) { + for (final p in players) { + if (p.seat == seat) return p; + } + return null; + } + + return Container( + color: Colors.black.withValues(alpha: 0.78), + alignment: Alignment.center, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.fromLTRB(16, 14, 16, 18), + decoration: BoxDecoration( + color: AppColors.panel, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.gold, width: 2.5), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('جستجوی حریف', + style: TextStyle( + color: AppColors.gold, + fontSize: 22, + fontWeight: FontWeight.bold)), + const SizedBox(height: 18), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (var seat = 0; seat < 4; seat++) + _searchSlot(at(seat), seat == mySeat), + ], + ), + const SizedBox(height: 18), + Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), + decoration: BoxDecoration( + color: AppColors.bgDark, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: AppColors.goldDark), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.monetization_on, color: AppColors.gold), + const SizedBox(width: 8), + Text('${widget.prize}', + style: const TextStyle( + color: AppColors.gold, + fontSize: 18, + fontWeight: FontWeight.bold)), + ]), + ), + if (searching) ...[ + const SizedBox(height: 16), + const SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator( + strokeWidth: 2, color: AppColors.gold), + ), + ], + ], + ), + ), + ); + } + + Widget _searchSlot(GamePlayer? p, bool isYou) { + final found = p != null; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(found ? p.name : '...', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: isYou ? AppColors.gold : Colors.white70, fontSize: 12)), + const SizedBox(height: 4), + Container( + width: 58, + height: 58, + decoration: BoxDecoration( + color: AppColors.bgDark, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isYou ? AppColors.gold : AppColors.goldDark, width: 1.5), + ), + child: Icon( + found ? (p.bot ? Icons.smart_toy : Icons.person) : Icons.help_outline, + color: found ? AppColors.gold : Colors.white24, + size: 30, + ), + ), + const SizedBox(height: 2), + Text(found ? (p.bot ? 'ربات' : (isYou ? 'شما' : 'حریف')) : '', + style: const TextStyle(color: Colors.white38, fontSize: 10)), + ]), + ); + } + + Widget _trumpPicker(BuildContext context) { + const suits = [ + ('spades', '♠', 'پیک', Colors.white), + ('hearts', '♥', 'دل', Color(0xFFD32F2F)), + ('diamonds', '♦', 'خشت', Color(0xFFD32F2F)), + ('clubs', '♣', 'گشنیز', Colors.white), + ]; + return Container( + color: Colors.black54, + alignment: Alignment.center, + child: Container( + margin: const EdgeInsets.all(24), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.panel, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.gold, width: 2), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('حکم را انتخاب کن', + style: TextStyle(color: AppColors.gold, fontSize: 20)), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + alignment: WrapAlignment.center, + children: [ + for (final (id, sym, name, color) in suits) + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.bgDark, + minimumSize: const Size(120, 64), + ), + onPressed: () => + context.read().chooseTrump(id), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text(sym, style: TextStyle(fontSize: 26, color: color)), + const SizedBox(width: 8), + Text(name, style: const TextStyle(color: AppColors.text)), + ]), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _handResult(GameUiState s) { + final r = s.handResult!; + final mySeat = s.state?.yourSeat ?? 0; + final myTeam = mySeat % 2; + final won = r.winnerTeam == myTeam; + return IgnorePointer( + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 18), + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.gold), + ), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(won ? 'این دست را بردید!' : 'این دست را باختید', + style: TextStyle( + color: won ? AppColors.green : Colors.redAccent, + fontSize: 20, + fontWeight: FontWeight.bold)), + if (r.kot) + const Padding( + padding: EdgeInsets.only(top: 6), + child: Text('کُت! (دو امتیاز)', + style: TextStyle(color: AppColors.gold)), + ), + const SizedBox(height: 6), + Text('امتیاز: ${r.scores.isNotEmpty ? r.scores[0] : 0} - ${r.scores.length > 1 ? r.scores[1] : 0}', + style: const TextStyle(color: Colors.white70)), + ]), + ), + ), + ); + } + + Widget _gameOver(BuildContext context, GameUiState s) { + final g = s.gameOver!; + final mySeat = s.state?.yourSeat ?? 0; + final won = g.winnerTeam == mySeat % 2; + return Container( + color: Colors.black87, + alignment: Alignment.center, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Text(won ? 'بردید! 🎉' : 'باختید', + style: TextStyle( + color: won ? AppColors.gold : Colors.redAccent, + fontSize: 32, + fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + Text('نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}', + style: const TextStyle(color: Colors.white70, fontSize: 18)), + const SizedBox(height: 28), + SizedBox( + width: 220, + child: ElevatedButton( + onPressed: () => _exitToLobby(context), + child: const Text('بازگشت به لابی'), + ), + ), + ]), + ); + } +} diff --git a/lib/features/game/tier.dart b/lib/features/game/tier.dart new file mode 100644 index 0000000..a4a9e58 --- /dev/null +++ b/lib/features/game/tier.dart @@ -0,0 +1,19 @@ +// نوع میز (از catalog.table_tiers در GET /api/shop). +class TableTier { + final String id; + final String title; + final int hands; + final int entry; + final int prize; + final int xp; + final int trophy; + + TableTier.fromJson(Map j) + : id = j['id'] as String, + title = j['title'] as String, + hands = (j['hands'] ?? 0) as int, + entry = (j['entry'] ?? 0) as int, + prize = (j['prize'] ?? 0) as int, + xp = (j['xp'] ?? 0) as int, + trophy = (j['trophy'] ?? 0) as int; +} diff --git a/lib/features/game/tier_list_screen.dart b/lib/features/game/tier_list_screen.dart new file mode 100644 index 0000000..651c7fc --- /dev/null +++ b/lib/features/game/tier_list_screen.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/theme/app_theme.dart'; +import 'game_repository.dart'; +import 'tier.dart'; + +/// لیست میزها (tierها) با ورودی/جایزه؛ انتخاب → ورود به میز. +class TierListScreen extends StatefulWidget { + final GameRepository repo; + const TierListScreen({super.key, required this.repo}); + + @override + State createState() => _TierListScreenState(); +} + +class _TierListScreenState extends State { + late Future> _future; + + @override + void initState() { + super.initState(); + _future = widget.repo.getTiers(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('انتخاب میز')), + body: FutureBuilder>( + future: _future, + builder: (context, snap) { + if (snap.connectionState != ConnectionState.done) { + return const Center(child: CircularProgressIndicator()); + } + if (snap.hasError || snap.data == null) { + return Center( + child: Column(mainAxisSize: MainAxisSize.min, children: [ + const Text('خطا در بارگذاری میزها'), + TextButton( + onPressed: () => + setState(() => _future = widget.repo.getTiers()), + child: const Text('تلاش مجدد'), + ), + ]), + ); + } + final tiers = snap.data!; + return ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: tiers.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (_, i) => _TierCard(tier: tiers[i]), + ); + }, + ), + ); + } +} + +class _TierCard extends StatelessWidget { + final TableTier tier; + const _TierCard({required this.tier}); + + @override + Widget build(BuildContext context) { + return InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () => context.push('/game/${tier.id}?prize=${tier.prize}'), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.panel, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.goldDark), + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(tier.title, + style: const TextStyle( + color: AppColors.gold, + fontSize: 18, + fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text('${tier.hands} دست', + style: const TextStyle(color: Colors.white60)), + ], + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _row(Icons.login, 'ورودی', tier.entry), + _row(Icons.emoji_events, 'جایزه', tier.prize), + ], + ), + ], + ), + ), + ); + } + + Widget _row(IconData icon, String label, int value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text('$label: $value', + style: const TextStyle(color: AppColors.text, fontSize: 13)), + const SizedBox(width: 4), + Icon(icon, size: 16, color: AppColors.gold), + ]), + ); + } +} diff --git a/lib/features/lobby/lobby_screen.dart b/lib/features/lobby/lobby_screen.dart new file mode 100644 index 0000000..c76c129 --- /dev/null +++ b/lib/features/lobby/lobby_screen.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/theme/app_theme.dart'; +import '../auth/auth_cubit.dart'; +import 'wallet.dart'; +import 'wallet_cubit.dart'; + +/// لابی اصلی: کیف‌پول، دکمه بازی، سکه روزانه. +class LobbyScreen extends StatefulWidget { + const LobbyScreen({super.key}); + + @override + State createState() => _LobbyScreenState(); +} + +class _LobbyScreenState extends State { + @override + void initState() { + super.initState(); + context.read().load(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _TopBar(wallet: state.wallet), + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('سلطان حکم', + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + color: AppColors.gold)), + const SizedBox(height: 48), + _MenuButton( + label: 'بازی', + icon: Icons.style, + color: const Color(0xFF8E1B5B), + onTap: () async { + await context.push('/game/tiers'); + if (context.mounted) { + context.read().load(); + } + }, + ), + const SizedBox(height: 16), + _MenuButton( + label: 'فروشگاه', + icon: Icons.storefront, + color: const Color(0xFF4A148C), + onTap: () async { + await context.push('/shop'); + if (context.mounted) { + context.read().load(); + } + }, + ), + const SizedBox(height: 16), + _MenuButton( + label: 'سکه روزانه', + icon: Icons.monetization_on, + color: AppColors.green, + onTap: () => _claimDaily(context), + ), + ], + ), + ), + ), + TextButton.icon( + onPressed: () async { + await context.read().logout(); + if (context.mounted) context.go('/login'); + }, + icon: const Icon(Icons.logout, color: Colors.white54), + label: const Text('خروج', + style: TextStyle(color: Colors.white54)), + ), + const SizedBox(height: 8), + ], + ); + }, + ), + ), + ); + } + + Future _claimDaily(BuildContext context) async { + final amount = await context.read().claimDaily(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(amount != null + ? 'سکه روزانه دریافت شد: +$amount' + : 'سکه روزانه را قبلاً امروز گرفته‌اید'), + ), + ); + } +} + +class _TopBar extends StatelessWidget { + final Wallet? wallet; + const _TopBar({this.wallet}); + + @override + Widget build(BuildContext context) { + final w = wallet; + return Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: AppColors.bgDark, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.goldDark), + ), + child: Row( + children: [ + const CircleAvatar( + backgroundColor: AppColors.panel, + child: Icon(Icons.person, color: AppColors.gold), + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('سطح ${w?.level ?? '-'}', + style: const TextStyle(color: AppColors.gold)), + if (w != null) + SizedBox( + width: 90, + child: LinearProgressIndicator( + value: w.xpForNext == 0 ? 0 : w.xpIntoLevel / w.xpForNext, + backgroundColor: Colors.white12, + color: AppColors.gold, + minHeight: 5, + ), + ), + ], + ), + const Spacer(), + _Chip(icon: Icons.confirmation_number, value: w?.tickets ?? 0), + const SizedBox(width: 8), + _Chip(icon: Icons.monetization_on, value: w?.coins ?? 0), + ], + ), + ); + } +} + +class _Chip extends StatelessWidget { + final IconData icon; + final int value; + const _Chip({required this.icon, required this.value}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: AppColors.panel, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.goldDark), + ), + child: Row( + children: [ + Icon(icon, color: AppColors.gold, size: 18), + const SizedBox(width: 6), + Text('$value', style: const TextStyle(color: AppColors.text)), + ], + ), + ); + } +} + +class _MenuButton extends StatelessWidget { + final String label; + final IconData icon; + final Color color; + final VoidCallback onTap; + const _MenuButton({ + required this.label, + required this.icon, + required this.color, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 280, + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom(backgroundColor: color), + onPressed: onTap, + icon: Icon(icon, color: AppColors.gold), + label: Text(label), + ), + ); + } +} diff --git a/lib/features/lobby/wallet.dart b/lib/features/lobby/wallet.dart new file mode 100644 index 0000000..f567046 --- /dev/null +++ b/lib/features/lobby/wallet.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; + +/// وضعیت اقتصادی کاربر (پاسخ GET /api/wallet). +class Wallet extends Equatable { + final int coins; + final int tickets; + final int xp; + final int trophies; + final int level; + final int xpIntoLevel; + final int xpForNext; + final bool vip; + final String selectedCard; + + const Wallet({ + required this.coins, + required this.tickets, + required this.xp, + required this.trophies, + required this.level, + required this.xpIntoLevel, + required this.xpForNext, + required this.vip, + required this.selectedCard, + }); + + factory Wallet.fromJson(Map j) => Wallet( + coins: (j['coins'] ?? 0) as int, + tickets: (j['tickets'] ?? 0) as int, + xp: (j['xp'] ?? 0) as int, + trophies: (j['trophies'] ?? 0) as int, + level: (j['level'] ?? 1) as int, + xpIntoLevel: (j['xp_into_level'] ?? 0) as int, + xpForNext: (j['xp_for_next'] ?? 1) as int, + vip: (j['vip'] ?? false) as bool, + selectedCard: (j['selected_card'] ?? 'simple') as String, + ); + + @override + List get props => + [coins, tickets, xp, trophies, level, xpIntoLevel, xpForNext, vip, selectedCard]; +} diff --git a/lib/features/lobby/wallet_cubit.dart b/lib/features/lobby/wallet_cubit.dart new file mode 100644 index 0000000..93c7ad6 --- /dev/null +++ b/lib/features/lobby/wallet_cubit.dart @@ -0,0 +1,46 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../core/network/api_client.dart'; +import 'wallet.dart'; + +enum WalletStatus { initial, loading, loaded, error } + +class WalletState extends Equatable { + final WalletStatus status; + final Wallet? wallet; + + const WalletState({this.status = WalletStatus.initial, this.wallet}); + + @override + List get props => [status, wallet]; +} + +class WalletCubit extends Cubit { + final ApiClient _api; + WalletCubit(this._api) : super(const WalletState()); + + Future load() async { + emit(const WalletState(status: WalletStatus.loading)); + try { + final res = await _api.dio.get('/wallet'); + emit(WalletState( + status: WalletStatus.loaded, + wallet: Wallet.fromJson(Map.from(res.data)), + )); + } catch (_) { + emit(const WalletState(status: WalletStatus.error)); + } + } + + /// دریافت سکه روزانه و سپس به‌روزرسانی کیف‌پول. + Future claimDaily() async { + try { + final res = await _api.dio.post('/rewards/daily'); + await load(); + return (res.data['amount'] ?? 0) as int; + } catch (_) { + return null; + } + } +} diff --git a/lib/features/shop/shop_cubit.dart b/lib/features/shop/shop_cubit.dart new file mode 100644 index 0000000..6eac90a --- /dev/null +++ b/lib/features/shop/shop_cubit.dart @@ -0,0 +1,79 @@ +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'shop_models.dart'; +import 'shop_repository.dart'; + +enum ShopStatus { initial, loading, loaded, error } + +class ShopState extends Equatable { + final ShopStatus status; + final ShopData? data; + final bool busy; // در حال انجام یک عملیات (خرید/انتخاب) + + const ShopState({this.status = ShopStatus.initial, this.data, this.busy = false}); + + ShopState copyWith({ShopStatus? status, ShopData? data, bool? busy}) => ShopState( + status: status ?? this.status, + data: data ?? this.data, + busy: busy ?? this.busy, + ); + + @override + List get props => [status, data, busy]; +} + +class ShopCubit extends Cubit { + final ShopRepository _repo; + ShopCubit(this._repo) : super(const ShopState()); + + Future load() async { + emit(state.copyWith(status: ShopStatus.loading)); + try { + emit(state.copyWith(status: ShopStatus.loaded, data: await _repo.getShop())); + } catch (_) { + emit(state.copyWith(status: ShopStatus.error)); + } + } + + /// یک عملیات را اجرا، فروشگاه را بازخوانی و پیام نتیجه را برمی‌گرداند. + Future _run(Future Function() action, String okMsg) async { + if (state.busy) return ''; + emit(state.copyWith(busy: true)); + try { + await action(); + final data = await _repo.getShop(); + emit(state.copyWith(status: ShopStatus.loaded, data: data, busy: false)); + return okMsg; + } on DioException catch (e) { + emit(state.copyWith(busy: false)); + final d = e.response?.data; + if (d is Map && d['message'] != null) return d['message'].toString(); + return 'خطا در ارتباط با سرور'; + } catch (_) { + emit(state.copyWith(busy: false)); + return 'خطای نامشخص'; + } + } + + Future buyCard(String id) => _run(() => _repo.buyCard(id), 'کارت خریداری شد'); + + Future selectCard(String id) => + _run(() => _repo.selectCard(id), 'کارت انتخاب شد'); + + Future purchase(String kind, String id) => _run( + () => _repo.purchase( + store: 'bazaar', + kind: kind, + productId: id, + token: 'dev-$kind-$id-${DateTime.now().millisecondsSinceEpoch}', + ), + 'خرید با موفقیت انجام شد', + ); + + Future claimAd() => _run( + () => _repo.adReward('dev-ad-${DateTime.now().millisecondsSinceEpoch}'), + 'سکه رایگان دریافت شد', + ); +} diff --git a/lib/features/shop/shop_models.dart b/lib/features/shop/shop_models.dart new file mode 100644 index 0000000..89b6528 --- /dev/null +++ b/lib/features/shop/shop_models.dart @@ -0,0 +1,94 @@ +// مدل‌های کاتالوگ فروشگاه (پاسخ GET /api/shop). + +class CoinPackage { + final String id; + final String title; + final int coins; + final int vipDays; + final int priceToman; + final int bonusPct; + + CoinPackage.fromJson(Map j) + : id = j['id'] as String, + title = j['title'] as String, + coins = (j['coins'] ?? 0) as int, + vipDays = (j['vip_days'] ?? 0) as int, + priceToman = (j['price_toman'] ?? 0) as int, + bonusPct = (j['bonus_pct'] ?? 0) as int; +} + +class TicketPackage { + final String id; + final String title; + final int tickets; + final int priceToman; + + TicketPackage.fromJson(Map j) + : id = j['id'] as String, + title = j['title'] as String, + tickets = (j['tickets'] ?? 0) as int, + priceToman = (j['price_toman'] ?? 0) as int; +} + +class CardSkin { + final String id; + final String title; + final int priceCoins; + + CardSkin.fromJson(Map j) + : id = j['id'] as String, + title = j['title'] as String, + priceCoins = (j['price_coins'] ?? 0) as int; +} + +class Booster { + final String id; + final String title; + final int multiplier; + final int hours; + final int priceToman; + + Booster.fromJson(Map j) + : id = j['id'] as String, + title = j['title'] as String, + multiplier = (j['multiplier'] ?? 1) as int, + hours = (j['hours'] ?? 0) as int, + priceToman = (j['price_toman'] ?? 0) as int; +} + +/// داده‌ی کامل فروشگاه: کاتالوگ + کارت‌های متعلق به کاربر + کارت انتخابی. +class ShopData { + final List coinPackages; + final List ticketPackages; + final List cardSkins; + final List boosters; + final List ownedCards; + final String selectedCard; + + ShopData({ + required this.coinPackages, + required this.ticketPackages, + required this.cardSkins, + required this.boosters, + required this.ownedCards, + required this.selectedCard, + }); + + factory ShopData.fromJson(Map j) { + final cat = Map.from(j['catalog'] as Map); + List parse(String key, T Function(Map) f) => + ((cat[key] as List?) ?? []) + .map((e) => f(Map.from(e as Map))) + .toList(); + return ShopData( + coinPackages: parse('coin_packages', CoinPackage.fromJson), + ticketPackages: parse('ticket_packages', TicketPackage.fromJson), + cardSkins: parse('card_skins', CardSkin.fromJson), + boosters: parse('boosters', Booster.fromJson), + ownedCards: ((j['owned_cards'] as List?) ?? []).map((e) => e as String).toList(), + selectedCard: (j['selected_card'] ?? 'simple') as String, + ); + } + + bool owns(String cardId) => cardId == 'simple' || ownedCards.contains(cardId); +} diff --git a/lib/features/shop/shop_repository.dart b/lib/features/shop/shop_repository.dart new file mode 100644 index 0000000..a7a87bb --- /dev/null +++ b/lib/features/shop/shop_repository.dart @@ -0,0 +1,40 @@ +import '../../core/network/api_client.dart'; +import 'shop_models.dart'; + +/// دسترسی به endpointهای فروشگاه و پاداش‌ها. +class ShopRepository { + final ApiClient _api; + ShopRepository(this._api); + + Future getShop() async { + final res = await _api.dio.get('/shop'); + return ShopData.fromJson(Map.from(res.data)); + } + + Future buyCard(String cardId) => + _api.dio.post('/shop/buy-card', data: {'card_id': cardId}); + + Future selectCard(String cardId) => + _api.dio.post('/shop/select-card', data: {'card_id': cardId}); + + /// تأیید خرید IAP. در حالت واقعی token از SDK بازار/مایکت می‌آید؛ + /// فعلاً توکن توسعه‌ای فرستاده می‌شود (بک‌اند در حالت dev هر توکن غیرخالی را می‌پذیرد). + Future purchase({ + required String store, + required String kind, + required String productId, + required String token, + }) => + _api.dio.post('/shop/purchase', data: { + 'store': store, + 'kind': kind, + 'product_id': productId, + 'token': token, + }); + + /// سکه رایگان پس از تبلیغ rewarded. token از SDK تپسل می‌آید (فعلاً توسعه‌ای). + Future adReward(String token) async { + final res = await _api.dio.post('/rewards/ad', data: {'token': token}); + return (res.data['amount'] ?? 0) as int; + } +} diff --git a/lib/features/shop/shop_screen.dart b/lib/features/shop/shop_screen.dart new file mode 100644 index 0000000..e7f1691 --- /dev/null +++ b/lib/features/shop/shop_screen.dart @@ -0,0 +1,384 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../core/theme/app_theme.dart'; +import '../lobby/wallet_cubit.dart'; +import 'shop_cubit.dart'; +import 'shop_models.dart'; + +/// صفحه‌ی فروشگاه با تب‌های سکه/بلیط/کارت/تجهیزات. +class ShopScreen extends StatelessWidget { + const ShopScreen({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 4, + child: Scaffold( + appBar: AppBar( + title: const Text('فروشگاه'), + actions: [ + BlocBuilder( + builder: (context, s) => Center( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Row(children: [ + const Icon(Icons.monetization_on, + color: AppColors.gold, size: 18), + const SizedBox(width: 4), + Text('${s.wallet?.coins ?? 0}', + style: const TextStyle(color: AppColors.text)), + ]), + ), + ), + ), + ], + bottom: const TabBar( + isScrollable: true, + labelColor: AppColors.gold, + indicatorColor: AppColors.gold, + tabs: [ + Tab(text: 'سکه'), + Tab(text: 'بلیط'), + Tab(text: 'کارت'), + Tab(text: 'تجهیزات'), + ], + ), + ), + body: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + if (state.status == ShopStatus.loading || + state.status == ShopStatus.initial) { + return const Center(child: CircularProgressIndicator()); + } + if (state.status == ShopStatus.error || state.data == null) { + return Center( + child: Column(mainAxisSize: MainAxisSize.min, children: [ + const Text('خطا در بارگذاری فروشگاه'), + TextButton( + onPressed: () => context.read().load(), + child: const Text('تلاش مجدد'), + ), + ]), + ); + } + final d = state.data!; + return TabBarView( + children: [ + _CoinsTab(packages: d.coinPackages), + _TicketsTab(packages: d.ticketPackages), + _CardsTab(data: d), + _BoostersTab(boosters: d.boosters), + ], + ); + }, + ), + ), + ); + } +} + +/// اجرای یک عملیات فروشگاه و نمایش نتیجه + بازخوانی کیف‌پول. +Future _do(BuildContext context, Future Function() action) async { + final msg = await action(); + if (!context.mounted || msg.isEmpty) return; + await context.read().load(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); +} + +class _CoinsTab extends StatelessWidget { + final List packages; + const _CoinsTab({required this.packages}); + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + childAspectRatio: 0.82, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + children: [ + _FreeCoinCard( + onTap: () => _do(context, () => context.read().claimAd()), + ), + for (final p in packages) + _StoreCard( + title: p.title, + badge: p.bonusPct > 0 ? '+${p.bonusPct}%' : null, + lines: [ + '${p.coins} سکه', + if (p.vipDays > 0) '${p.vipDays} روز VIP', + ], + priceLabel: '${p.priceToman} تومان', + icon: Icons.savings, + onTap: () => + _do(context, () => context.read().purchase('coin', p.id)), + ), + ], + ); + } +} + +class _TicketsTab extends StatelessWidget { + final List packages; + const _TicketsTab({required this.packages}); + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + childAspectRatio: 0.82, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + children: [ + for (final p in packages) + _StoreCard( + title: p.title, + lines: ['${p.tickets} بلیط'], + priceLabel: '${p.priceToman} تومان', + icon: Icons.confirmation_number, + onTap: () => + _do(context, () => context.read().purchase('ticket', p.id)), + ), + ], + ); + } +} + +class _BoostersTab extends StatelessWidget { + final List boosters; + const _BoostersTab({required this.boosters}); + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + childAspectRatio: 0.82, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + children: [ + for (final b in boosters) + _StoreCard( + title: b.title, + lines: ['تجربه ×${b.multiplier}', '${b.hours} ساعت'], + priceLabel: '${b.priceToman} تومان', + icon: Icons.bolt, + onTap: () => + _do(context, () => context.read().purchase('booster', b.id)), + ), + ], + ); + } +} + +class _CardsTab extends StatelessWidget { + final ShopData data; + const _CardsTab({required this.data}); + + @override + Widget build(BuildContext context) { + return GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + childAspectRatio: 0.82, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + children: [ + for (final c in data.cardSkins) + _CardSkinCard( + skin: c, + owned: data.owns(c.id), + selected: data.selectedCard == c.id, + ), + ], + ); + } +} + +class _CardSkinCard extends StatelessWidget { + final CardSkin skin; + final bool owned; + final bool selected; + const _CardSkinCard( + {required this.skin, required this.owned, required this.selected}); + + @override + Widget build(BuildContext context) { + final Widget action; + if (selected) { + action = const _Pill(text: 'انتخاب شده', color: AppColors.goldDark); + } else if (owned) { + action = _ActionButton( + label: 'انتخاب', + color: AppColors.green, + onTap: () => + _do(context, () => context.read().selectCard(skin.id)), + ); + } else { + action = _ActionButton( + label: '${skin.priceCoins} سکه', + color: AppColors.accent, + onTap: () => _do(context, () => context.read().buyCard(skin.id)), + ); + } + return _Panel( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(skin.title, + textAlign: TextAlign.center, + style: const TextStyle(color: AppColors.gold)), + const Icon(Icons.style, size: 48, color: AppColors.text), + SizedBox(width: double.infinity, child: action), + ], + ), + ); + } +} + +class _FreeCoinCard extends StatelessWidget { + final VoidCallback onTap; + const _FreeCoinCard({required this.onTap}); + + @override + Widget build(BuildContext context) { + return _Panel( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('سکه رایگان', + textAlign: TextAlign.center, + style: TextStyle(color: AppColors.gold)), + const Icon(Icons.ondemand_video, size: 44, color: AppColors.green), + const Text('با دیدن تبلیغ', + style: TextStyle(color: Colors.white60, fontSize: 12)), + SizedBox( + width: double.infinity, + child: _ActionButton( + label: 'رایگان', color: AppColors.green, onTap: onTap), + ), + ], + ), + ); + } +} + +class _StoreCard extends StatelessWidget { + final String title; + final List lines; + final String priceLabel; + final IconData icon; + final String? badge; + final VoidCallback onTap; + const _StoreCard({ + required this.title, + required this.lines, + required this.priceLabel, + required this.icon, + required this.onTap, + this.badge, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _Panel( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, + textAlign: TextAlign.center, + style: const TextStyle(color: AppColors.gold)), + Icon(icon, size: 40, color: AppColors.gold), + Column( + children: [ + for (final l in lines) + Text(l, style: const TextStyle(color: AppColors.text)), + ], + ), + SizedBox( + width: double.infinity, + child: _ActionButton( + label: priceLabel, color: AppColors.accent, onTap: onTap), + ), + ], + ), + ), + if (badge != null) + Positioned( + top: 4, + left: 4, + child: _Pill(text: badge!, color: AppColors.green), + ), + ], + ); + } +} + +class _Panel extends StatelessWidget { + final Widget child; + const _Panel({required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.panel, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.goldDark), + ), + child: child, + ); + } +} + +class _ActionButton extends StatelessWidget { + final String label; + final Color color; + final VoidCallback onTap; + const _ActionButton( + {required this.label, required this.color, required this.onTap}); + + @override + Widget build(BuildContext context) { + final busy = context.select((ShopCubit c) => c.state.busy); + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: color, + minimumSize: const Size.fromHeight(38), + padding: const EdgeInsets.symmetric(horizontal: 6), + ), + onPressed: busy ? null : onTap, + child: Text(label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13)), + ); + } +} + +class _Pill extends StatelessWidget { + final String text; + final Color color; + const _Pill({required this.text, required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + child: Text(text, + style: const TextStyle(color: Colors.white, fontSize: 12)), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..c819d85 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'app.dart'; +import 'core/network/api_client.dart'; +import 'core/storage/token_storage.dart'; +import 'features/auth/auth_repository.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + final storage = TokenStorage(); + final api = ApiClient(storage); + final authRepo = AuthRepository(api, storage); + final loggedIn = await authRepo.isLoggedIn(); + + runApp(HakemApp( + api: api, + authRepo: authRepo, + tokenStorage: storage, + loggedIn: loggedIn, + )); +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..abf5c5a --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,498 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.myket.ir" + source: hosted + version: "2.12.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413 + url: "https://pub.myket.ir" + source: hosted + version: "9.2.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.myket.ir" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.myket.ir" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.myket.ir" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.myket.ir" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.myket.ir" + source: hosted + version: "1.0.8" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.myket.ir" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.2" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.myket.ir" + source: hosted + version: "2.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.myket.ir" + source: hosted + version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.myket.ir" + source: hosted + version: "2.2.0" + flame: + dependency: "direct main" + description: + name: flame + sha256: c50e2f39e118f5f6a6f3339ce6825ee803c7e5fada95ec50fb02eb27944e0e76 + url: "https://pub.myket.ir" + source: hosted + version: "1.30.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + url: "https://pub.myket.ir" + source: hosted + version: "9.1.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.myket.ir" + source: hosted + version: "5.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e" + url: "https://pub.myket.ir" + source: hosted + version: "10.3.1" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149" + url: "https://pub.myket.ir" + source: hosted + version: "0.3.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5 + url: "https://pub.myket.ir" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.myket.ir" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.myket.ir" + source: hosted + version: "4.1.0" + 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" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: c92d18e1fe994cb06d48aa786c46b142a5633067e8297cff6b5a3ac742620104 + url: "https://pub.myket.ir" + source: hosted + version: "17.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.myket.ir" + source: hosted + version: "4.1.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.myket.ir" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.myket.ir" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.myket.ir" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.myket.ir" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.myket.ir" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.myket.ir" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.myket.ir" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.myket.ir" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.myket.ir" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.myket.ir" + source: hosted + version: "1.0.0" + ordered_set: + dependency: transitive + description: + name: ordered_set + sha256: d6c1d053a533e84931a388cbf03f1ad21a0543bf06c7a281859d3ffacd8e15f2 + url: "https://pub.myket.ir" + source: hosted + version: "8.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.myket.ir" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" + url: "https://pub.myket.ir" + source: hosted + version: "2.2.19" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.myket.ir" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.myket.ir" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.myket.ir" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.myket.ir" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.myket.ir" + source: hosted + version: "6.1.5+1" + 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.myket.ir" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.myket.ir" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.myket.ir" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.myket.ir" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.myket.ir" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.myket.ir" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.myket.ir" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.myket.ir" + source: hosted + version: "14.3.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.myket.ir" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.myket.ir" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.myket.ir" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.myket.ir" + source: hosted + version: "5.13.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.myket.ir" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.7.2 <4.0.0" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..f10fb10 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,106 @@ +name: hakemsho +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.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 + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + flutter_bloc: ^9.1.1 + dio: ^5.9.2 + web_socket_channel: ^3.0.3 + go_router: ^17.0.0 + flutter_secure_storage: ^10.3.1 + equatable: ^2.0.8 + flame: ^1.30.1 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + uses-material-design: true + + assets: + - assets/images/ + - assets/images/cards/ + + fonts: + - family: Dana + fonts: + - asset: assets/fonts/dana_regular.ttf + - asset: assets/fonts/dana_medium.ttf + weight: 500 + - asset: assets/fonts/dana_bold.ttf + weight: 700 + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..26547fd --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hakemsho/features/lobby/wallet.dart'; + +void main() { + test('Wallet.fromJson parses server response', () { + final w = Wallet.fromJson(const { + 'coins': 4560, + 'tickets': 3, + 'xp': 325, + 'trophies': 12, + 'level': 8, + 'xp_into_level': 325, + 'xp_for_next': 400, + 'vip': true, + 'selected_card': 'swiss', + }); + expect(w.coins, 4560); + expect(w.tickets, 3); + expect(w.level, 8); + expect(w.vip, true); + expect(w.selectedCard, 'swiss'); + }); + + test('Wallet.fromJson uses defaults for missing fields', () { + final w = Wallet.fromJson(const {}); + expect(w.coins, 0); + expect(w.level, 1); + expect(w.selectedCard, 'simple'); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..ab782d2 --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + hakemsho + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..c871aef --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "hakemsho", + "short_name": "hakemsho", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}