Deploy: 2026-05-24 23:27:32

This commit is contained in:
Hamza-Ayed
2026-05-24 23:27:32 +03:00
parent 2ceffc47d9
commit b20f457eaf
156 changed files with 8308 additions and 0 deletions

45
mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

17
mobile/README.md Normal file
View File

@@ -0,0 +1,17 @@
# nabeh
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -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

14
mobile/android/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.nabeh.nabeh"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.nabeh.nabeh"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="nabeh"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.nabeh.nabeh
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

34
mobile/ios/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
mobile/ios/Podfile Normal file
View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
platform :ios, '17.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

29
mobile/ios/Podfile.lock Normal file
View File

@@ -0,0 +1,29 @@
PODS:
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PODFILE CHECKSUM: 912e34f102da91123e38965ab4e5b3fb21e195f0
COCOAPODS: 1.16.2

View File

@@ -0,0 +1,735 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
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 */; };
CBA2E9E62501D359B245ECEA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BE246A93B75BF79087F6016 /* Pods_Runner.framework */; };
D35D0BE6AA281F3C0AFA5F94 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C906123B1A240AB1A8159A38 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0D7BC64BC15C3D1622EAE826 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2220EA81CEBCF068D6F3FBA7 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3BE246A93B75BF79087F6016 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4D196AD247F4CB184C87F520 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
5F67C1A9F2BBE935C19BC982 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
79E5A6F3F41074AD93D3FE67 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9DB9D60ABA54EABFC41D4762 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
C906123B1A240AB1A8159A38 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CBA2E9E62501D359B245ECEA /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
BE07FE64D099BF98C83A12DC /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D35D0BE6AA281F3C0AFA5F94 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
A85D1785512BFE32F7400D48 /* Pods */,
E87A64125BF3C88FFDB77E72 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
A85D1785512BFE32F7400D48 /* Pods */ = {
isa = PBXGroup;
children = (
0D7BC64BC15C3D1622EAE826 /* Pods-Runner.debug.xcconfig */,
9DB9D60ABA54EABFC41D4762 /* Pods-Runner.release.xcconfig */,
79E5A6F3F41074AD93D3FE67 /* Pods-Runner.profile.xcconfig */,
2220EA81CEBCF068D6F3FBA7 /* Pods-RunnerTests.debug.xcconfig */,
5F67C1A9F2BBE935C19BC982 /* Pods-RunnerTests.release.xcconfig */,
4D196AD247F4CB184C87F520 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
E87A64125BF3C88FFDB77E72 /* Frameworks */ = {
isa = PBXGroup;
children = (
3BE246A93B75BF79087F6016 /* Pods_Runner.framework */,
C906123B1A240AB1A8159A38 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
365A42E1028C255B96E17278 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
BE07FE64D099BF98C83A12DC /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9A9B489A796B494946F7B54D /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
5CCB6021BDBEF9D0B2A6F633 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
365A42E1028C255B96E17278 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
5CCB6021BDBEF9D0B2A6F633 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
9A9B489A796B494946F7B54D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.nabeh.nabeh;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 2220EA81CEBCF068D6F3FBA7 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nabeh.nabeh.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 5F67C1A9F2BBE935C19BC982 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nabeh.nabeh.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 4D196AD247F4CB184C87F520 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nabeh.nabeh.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.nabeh.nabeh;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 63CVT8G5P8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.nabeh.nabeh;
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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import UIKit
import Flutter
@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)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -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.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Nabeh</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>nabeh</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
}

View File

@@ -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.
}
}

View File

@@ -0,0 +1,45 @@
class ApiConstants {
// English: The base URL for the backend API endpoints.
// Arabic: عنوان الرابط الأساسي لنقاط نهاية واجهة برمجة التطبيقات الخلفية.
static const String baseUrl = 'https://nabeh.intaleqapp.com/api';
// English: The endpoint for user login.
// Arabic: نقطة النهاية لتسجيل دخول المستخدم.
static const String loginEndpoint = '/auth/login';
// English: The endpoint for fetching current authenticated user details.
// Arabic: نقطة النهاية لجلب تفاصيل المستخدم الحالي المصادق عليه.
static const String meEndpoint = '/auth/me';
// English: The endpoint for WhatsApp connection status checks.
// Arabic: نقطة النهاية للتحقق من حالة اتصال الواتساب.
static const String whatsappStatusEndpoint = '/whatsapp/status';
// English: The endpoint to request a new WhatsApp QR connection.
// Arabic: نقطة النهاية لطلب اتصال واتساب جديد برمز استجابة سريع.
static const String whatsappQrEndpoint = '/whatsapp/qr';
// English: The endpoint to disconnect an active WhatsApp session.
// Arabic: نقطة النهاية لقطع اتصال جلسة واتساب نشطة.
static const String whatsappDisconnectEndpoint = '/whatsapp/disconnect';
// English: The endpoint to list subscription plans.
// Arabic: نقطة النهاية لعرض قائمة باقات الاشتراك.
static const String plansEndpoint = '/plans';
// English: The endpoint to retrieve the contacts directory.
// Arabic: نقطة النهاية لاسترداد دليل جهات الاتصال.
static const String contactsEndpoint = '/contacts';
// English: The endpoint to manage chatbot auto-reply rules.
// Arabic: نقطة النهاية لإدارة قواعد الرد الآلي للروبوت.
static const String chatbotRulesEndpoint = '/chatbot/rules';
// English: The endpoint for Super Admin platform statistics.
// Arabic: نقطة النهاية لإحصائيات منصة المشرف العام.
static const String adminStatsEndpoint = '/admin/stats';
// English: The endpoint to approve a pending company subscription.
// Arabic: نقطة النهاية للموافقة على اشتراك معلق للشركة.
static const String approveBillingEndpoint = '/admin/companies/approve-billing';
}

View File

@@ -0,0 +1,179 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../constants/api_constants.dart';
class ApiService {
// English: Client instance to perform network requests.
// Arabic: نسخة العميل لإجراء طلبات الشبكة.
final http.Client _client = http.Client();
// English: Perform a login request with email and password parameters.
// Arabic: تنفيذ طلب تسجيل الدخول باستخدام معلمات البريد الإلكتروني وكلمة المرور.
Future<http.Response> login(String email, String password) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.loginEndpoint}');
// English: Send POST request containing JSON body to login endpoint.
// Arabic: إرسال طلب من نوع بوست يحتوي على نص جيسون إلى نقطة تسجيل الدخول.
return await _client.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'email': email,
'password': password,
}),
);
}
// English: Fetch current user details by supplying the JWT token.
// Arabic: جلب تفاصيل المستخدم الحالي من خلال توفير رمز التحقق.
Future<http.Response> getMe(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.meEndpoint}');
// English: Send GET request with authorization token header.
// Arabic: إرسال طلب من نوع غيت مع ترويسة رمز التفويض.
return await _client.get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Fetch WhatsApp connection status.
// Arabic: جلب حالة اتصال الواتساب.
Future<http.Response> getWhatsAppStatus(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.whatsappStatusEndpoint}');
return await _client.get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Fetch subscription plans list.
// Arabic: جلب قائمة باقات الاشتراك.
Future<http.Response> getPlans(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.plansEndpoint}');
return await _client.get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Fetch CRM contacts directory.
// Arabic: جلب دليل جهات الاتصال الخاص بنظام إدارة العملاء.
Future<http.Response> getContacts(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.contactsEndpoint}');
return await _client.get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Create a new contact in CRM directory.
// Arabic: إنشاء جهة اتصال جديدة في دليل إدارة العملاء.
Future<http.Response> addContact(String token, String name, String phone) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.contactsEndpoint}');
return await _client.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode({
'name': name,
'phone': phone,
}),
);
}
// English: Fetch chatbot rules.
// Arabic: جلب قواعد الرد الآلي لروبوت الدردشة.
Future<http.Response> getChatbotRules(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.chatbotRulesEndpoint}');
return await _client.get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Fetch Super Admin platform statistics.
// Arabic: جلب إحصائيات منصة المشرف العام.
Future<http.Response> getAdminStats(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.adminStatsEndpoint}');
return await _client.get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Request a new WhatsApp QR connection.
// Arabic: طلب اتصال واتساب جديد برمز استجابة سريع.
Future<http.Response> requestWhatsAppQr(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.whatsappQrEndpoint}');
return await _client.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Disconnect the active WhatsApp connection session.
// Arabic: قطع اتصال جلسة الواتساب النشطة.
Future<http.Response> disconnectWhatsApp(String token) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.whatsappDisconnectEndpoint}');
return await _client.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
);
}
// English: Approve a pending company subscription billing.
// Arabic: الموافقة على اشتراك فوترة الشركة المعلق.
Future<http.Response> approveBilling(String token, int companyId) async {
final url = Uri.parse('${ApiConstants.baseUrl}${ApiConstants.approveBillingEndpoint}');
return await _client.post(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
body: jsonEncode({
'company_id': companyId,
}),
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
// English: Create an instance of secure storage to write keys safely.
// Arabic: إنشاء نسخة من التخزين الآمن لكتابة المفاتيح بشكل آمن.
final FlutterSecureStorage _storage = const FlutterSecureStorage();
// English: The key used to store the JWT token.
// Arabic: المفتاح المستخدم لتخزين رمز التحقق.
static const String _tokenKey = 'jwt_token';
// English: Write the JWT token to secure storage.
// Arabic: كتابة رمز التحقق في التخزين الآمن.
Future<void> writeToken(String token) async {
await _storage.write(key: _tokenKey, value: token);
}
// English: Read the JWT token from secure storage.
// Arabic: قراءة رمز التحقق من التخزين الآمن.
Future<String?> readToken() async {
return await _storage.read(key: _tokenKey);
}
// English: Delete the JWT token from secure storage when user logs out.
// Arabic: حذف رمز التحقق من التخزين الآمن عند تسجيل خروج المستخدم.
Future<void> deleteToken() async {
await _storage.delete(key: _tokenKey);
}
}

View File

@@ -0,0 +1,66 @@
import 'dart:convert';
import '../../../core/services/api_service.dart';
import '../../../core/services/secure_storage_service.dart';
import 'models/user_model.dart';
class AuthRepository {
final ApiService _apiService = ApiService();
final SecureStorageService _secureStorage = SecureStorageService();
// English: Perform login by email/password, verify response status, and save JWT token.
// Arabic: إجراء تسجيل الدخول عن طريق البريد الإلكتروني وكلمة المرور، والتحقق من حالة الاستجابة، وحفظ رمز التحقق.
Future<UserModel> login(String email, String password) async {
final response = await _apiService.login(email, password);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final token = data['token'] as String;
final userJson = data['user'] as Map<String, dynamic>;
// English: Store JWT token securely using secure storage.
// Arabic: تخزين رمز التحقق بشكل آمن باستخدام التخزين الآمن.
await _secureStorage.writeToken(token);
return UserModel.fromJson(userJson);
} else {
// English: Handle server error messages securely.
// Arabic: معالجة رسائل خطأ الخادم بشكل آمن.
final data = jsonDecode(response.body) as Map<String, dynamic>;
final errorMessage = data['error'] as String? ?? 'Failed to authenticate';
throw Exception(errorMessage);
}
}
// English: Check if a valid JWT token exists and fetch the authenticated user data.
// Arabic: التحقق من وجود رمز تحقق صالح وجلب بيانات المستخدم المصادق عليه.
Future<UserModel?> getCachedUser() async {
final token = await _secureStorage.readToken();
if (token == null || token.isEmpty) {
return null;
}
try {
final response = await _apiService.getMe(token);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final userJson = data['user'] as Map<String, dynamic>;
return UserModel.fromJson(userJson);
} else {
// English: Token is likely invalid or expired, clear it.
// Arabic: من المحتمل أن يكون الرمز غير صالح أو منتهي الصلاحية، قم بمسحه.
await _secureStorage.deleteToken();
return null;
}
} catch (_) {
// English: Return null if offline or server is unreachable.
// Arabic: إرجاع قيمة فارغة إذا كان الجهاز غير متصل أو تعذر الوصول إلى الخادم.
return null;
}
}
// English: Clear stored JWT credentials.
// Arabic: مسح بيانات اعتماد رمز التحقق المخزنة.
Future<void> logout() async {
await _secureStorage.deleteToken();
}
}

View File

@@ -0,0 +1,77 @@
import 'package:equatable/equatable.dart';
class UserModel extends Equatable {
// English: Unique identifier for the user.
// Arabic: المعرف الفريد للمستخدم.
final int id;
// English: The ID of the company this user belongs to.
// Arabic: معرف الشركة التي ينتمي إليها هذا المستخدم.
final int companyId;
// English: The name of the user.
// Arabic: اسم المستخدم.
final String name;
// English: The email address of the user.
// Arabic: البريد الإلكتروني للمستخدم.
final String email;
// English: The system role assigned to the user (e.g. admin, staff).
// Arabic: دور النظام المعين للمستخدم (مثل مشرف، موظف).
final String role;
// English: Indicator if the user is a super admin (company ID 1).
// Arabic: مؤشر ما إذا كان المستخدم مشرفاً عاماً (معرف الشركة 1).
final bool isSuperAdmin;
const UserModel({
required this.id,
required this.companyId,
required this.name,
required this.email,
required this.role,
required this.isSuperAdmin,
});
// English: Factory constructor to create a UserModel instance from JSON mapping.
// Arabic: منشئ المصنع لإنشاء نسخة نموذج المستخدم من خريطة جيسون.
factory UserModel.fromJson(Map<String, dynamic> json) {
// English: Safely parse isSuperAdmin from dynamic input (boolean or integer).
// Arabic: تحليل آمن لمؤشر المشرف العام من المدخلات الديناميكية (بولين أو عدد صحيح).
final rawSuperAdmin = json['is_super_admin'];
bool isSuper = false;
if (rawSuperAdmin is bool) {
isSuper = rawSuperAdmin;
} else if (rawSuperAdmin is int) {
isSuper = rawSuperAdmin == 1;
} else if (rawSuperAdmin is String) {
isSuper = rawSuperAdmin == '1' || rawSuperAdmin.toLowerCase() == 'true';
}
return UserModel(
id: json['id'] as int? ?? 0,
companyId: json['company_id'] as int? ?? 0,
name: json['name'] as String? ?? '',
email: json['email'] as String? ?? '',
role: json['role'] as String? ?? '',
isSuperAdmin: isSuper,
);
}
// English: Convert UserModel instance to JSON map for local caching.
// Arabic: تحويل نسخة نموذج المستخدم إلى خريطة جيسون للتخزين المؤقت المحلي.
Map<String, dynamic> toJson() {
return {
'id': id,
'company_id': companyId,
'name': name,
'email': email,
'role': role,
'is_super_admin': isSuperAdmin,
};
}
@override
List<Object?> get props => [id, companyId, name, email, role, isSuperAdmin];
}

View File

@@ -0,0 +1,81 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/auth_repository.dart';
import 'auth_state.dart';
// English: AuthCubit manages authentication states.
// Arabic: يدير ملف الكيوبيت حالات المصادقة للمستخدمين.
//
// English: In GetX, you would use a GetxController and update observable variables.
// Arabic: في غيت إكس، كنت ستستخدم وحدة التحكم وتحدث المتغيرات المرصودة.
//
// English: In Bloc/Cubit, you inherit from Cubit and emit new immutable states.
// Arabic: في بلوك أو كيوبيت، ترث من الكيوبيت وترسل حالات غير قابلة للتعديل.
class AuthCubit extends Cubit<AuthState> {
final AuthRepository _authRepository;
// English: Constructor initializing the Cubit with the AuthInitial state.
// Arabic: منشئ يقوم بتهيئة الكيوبيت بالحالة الأولية.
AuthCubit(this._authRepository) : super(const AuthInitial());
// English: Check if the user is already authenticated on app launch.
// Arabic: التحقق مما إذا كان المستخدم مصدقًا عليه بالفعل عند تشغيل التطبيق.
Future<void> checkAuthStatus() async {
// English: Emit loading state while reading storage.
// Arabic: إرسال حالة التحميل أثناء قراءة وحدة التخزين.
emit(const AuthLoading());
try {
final user = await _authRepository.getCachedUser();
if (user != null) {
// English: Emit Authenticated state if user exists.
// Arabic: إرسال حالة مصادق عليه إذا كان المستخدم موجودًا.
emit(Authenticated(user));
} else {
// English: Emit Unauthenticated if no cached token is found.
// Arabic: إرسال حالة غير مصادق عليه في حال لم يتم العثور على رمز مخزن مؤقتًا.
emit(const Unauthenticated());
}
} catch (e) {
// English: Fail check, treat as unauthenticated.
// Arabic: فشل التحقق، يتم اعتباره غير مصادق عليه.
emit(const Unauthenticated());
}
}
// English: Perform login request using email and password.
// Arabic: إجراء طلب تسجيل الدخول باستخدام البريد الإلكتروني وكلمة المرور.
Future<void> login(String email, String password) async {
// English: Validate input locally before hitting the server.
// Arabic: التحقق من صحة المدخلات محليًا قبل الاتصال بالخادم.
if (email.isEmpty || password.isEmpty) {
emit(const AuthFailure('الرجاء إدخال جميع الحقول بشكل صحيح'));
return;
}
emit(const AuthLoading());
try {
final user = await _authRepository.login(email, password);
// English: Emit Authenticated state with user profile on success.
// Arabic: إرسال حالة مصادق عليه مع ملف تعريف المستخدم عند النجاح.
emit(Authenticated(user));
} catch (e) {
// English: Clean the error message to display to the user.
// Arabic: تنظيف رسالة الخطأ لعرضها على المستخدم.
final cleanError = e.toString().replaceAll('Exception: ', '');
emit(AuthFailure(cleanError));
}
}
// English: Clear user token and log out.
// Arabic: مسح رمز المستخدم وتسجيل الخروج.
Future<void> logout() async {
emit(const AuthLoading());
await _authRepository.logout();
// English: Emit Unauthenticated state to return user to login screen.
// Arabic: إرسال حالة غير مصادق عليه لإرجاع المستخدم إلى شاشة تسجيل الدخول.
emit(const Unauthenticated());
}
}

View File

@@ -0,0 +1,49 @@
import 'package:equatable/equatable.dart';
import '../../data/models/user_model.dart';
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> get props => [];
}
// English: Initial state when the app is checking for existing authentication tokens.
// Arabic: الحالة الأولية عندما يقوم التطبيق بالتحقق من وجود رموز مصادقة حالية.
class AuthInitial extends AuthState {
const AuthInitial();
}
// English: Loading state shown during credential verification or API login request.
// Arabic: حالة التحميل المعروضة أثناء التحقق من بيانات الاعتماد أو طلب تسجيل الدخول.
class AuthLoading extends AuthState {
const AuthLoading();
}
// English: State emitted when the user successfully authenticates. Contains user details.
// Arabic: الحالة المرسلة عندما ينجح المستخدم في المصادقة. تحتوي على تفاصيل المستخدم.
class Authenticated extends AuthState {
final UserModel user;
const Authenticated(this.user);
@override
List<Object?> get props => [user];
}
// English: State emitted when the user is not authenticated or has logged out.
// Arabic: الحالة المرسلة عندما لا يكون المستخدم مصدقًا أو قد سجل خروجه.
class Unauthenticated extends AuthState {
const Unauthenticated();
}
// English: Failure state containing the error message in case authentication fails.
// Arabic: حالة الفشل التي تحتوي على رسالة الخطأ في حالة فشل المصادقة.
class AuthFailure extends AuthState {
final String errorMessage;
const AuthFailure(this.errorMessage);
@override
List<Object?> get props => [errorMessage];
}

View File

@@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cubit/auth_cubit.dart';
import '../cubit/auth_state.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
// English: Form key to manage state and invoke validation.
// Arabic: مفتاح النموذج لإدارة الحالة واستدعاء التحقق من الصحة.
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isPasswordVisible = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// English: We use a dark, premium theme with purple/blue gradients.
// Arabic: نحن نستخدم سمة داكنة ومميزة مع تدرجات أرجوانية وزرقاء.
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF0F0C20), Color(0xFF15102A), Color(0xFF0A0814)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Form(
key: _formKey,
child: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthFailure) {
// English: Show failure message as SnackBar when state is AuthFailure.
// Arabic: عرض رسالة الفشل كشريط وجبة خفيفة عندما تكون الحالة فشل المصادقة.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
state.errorMessage,
style: const TextStyle(color: Colors.white, fontFamily: 'Outfit'),
),
backgroundColor: Colors.redAccent,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
},
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// English: Logo placeholder with dynamic design.
// Arabic: رمز شعار تجريبي بتصميم ديناميكي.
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.deepPurple.withOpacity(0.2),
),
child: const Icon(
Icons.security,
size: 64,
color: Colors.purpleAccent,
),
),
const SizedBox(height: 24),
const Text(
'نـبـيـه',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
const SizedBox(height: 8),
Text(
'تسجيل الدخول للمشرفين والعملاء',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.6),
),
),
const SizedBox(height: 40),
// English: Email text input with custom borders and icons.
// Arabic: إدخال نص البريد الإلكتروني مع حدود وأيقونات مخصصة.
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'البريد الإلكتروني',
labelStyle: TextStyle(color: Colors.white.withOpacity(0.6)),
prefixIcon: const Icon(Icons.email_outlined, color: Colors.purpleAccent),
filled: true,
fillColor: Colors.white.withOpacity(0.05),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.white.withOpacity(0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.purpleAccent, width: 2),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'الرجاء إدخال البريد الإلكتروني';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'الرجاء إدخال بريد إلكتروني صالح';
}
return null;
},
),
const SizedBox(height: 20),
// English: Password text input with visibility toggle.
// Arabic: إدخال نص كلمة المرور مع إمكانية التبديل بين إظهارها وإخفائها.
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'كلمة المرور',
labelStyle: TextStyle(color: Colors.white.withOpacity(0.6)),
prefixIcon: const Icon(Icons.lock_outline, color: Colors.purpleAccent),
suffixIcon: IconButton(
icon: Icon(
_isPasswordVisible ? Icons.visibility : Icons.visibility_off,
color: Colors.white.withOpacity(0.6),
),
onPressed: () {
setState(() {
_isPasswordVisible = !_isPasswordVisible;
});
},
),
filled: true,
fillColor: Colors.white.withOpacity(0.05),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.white.withOpacity(0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.purpleAccent, width: 2),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'الرجاء إدخال كلمة المرور';
}
return null;
},
),
const SizedBox(height: 40),
// English: Submit button. Shows progress bar during login request.
// Arabic: زر الإرسال. يعرض شريط تقدم تحميل البيانات أثناء المصادقة.
if (state is AuthLoading)
const CircularProgressIndicator(color: Colors.purpleAccent)
else
GestureDetector(
onTap: () {
if (_formKey.currentState!.validate()) {
// English: Dispatch login method to AuthCubit.
// Arabic: استدعاء دالة تسجيل الدخول إلى الكيوبيت.
context.read<AuthCubit>().login(
_emailController.text.trim(),
_passwordController.text,
);
}
},
child: Container(
width: double.infinity,
height: 56,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.purpleAccent, Colors.deepPurpleAccent],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.purpleAccent.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
)
],
),
child: const Center(
child: Text(
'تسجيل الدخول',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
},
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,160 @@
import 'dart:convert';
import '../../../core/services/api_service.dart';
import '../../../core/services/secure_storage_service.dart';
import 'models/chatbot_rule_model.dart';
import 'models/contact_model.dart';
import 'models/plan_model.dart';
import 'models/super_admin_stats_model.dart';
import 'models/whatsapp_status_model.dart';
class DashboardRepository {
final ApiService _apiService = ApiService();
final SecureStorageService _secureStorage = SecureStorageService();
// English: Helper to retrieve token from secure storage. Throws exception if token is missing.
// Arabic: دالة مساعدة لاسترداد الرمز من التخزين الآمن. تطلق استثناء إذا كان الرمز مفقودًا.
Future<String> _getToken() async {
final token = await _secureStorage.readToken();
if (token == null || token.isEmpty) {
throw Exception('أنت غير مصرح لك بالوصول، يرجى إعادة تسجيل الدخول');
}
return token;
}
// English: Load the active WhatsApp connection session status.
// Arabic: تحميل حالة اتصال جلسة الواتساب النشطة.
Future<WhatsAppStatusModel?> getWhatsAppStatus() async {
final token = await _getToken();
final response = await _apiService.getWhatsAppStatus(token);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final statusData = decoded['data'] as Map<String, dynamic>?;
if (statusData == null) return null;
return WhatsAppStatusModel.fromJson(statusData);
} else {
throw Exception('فشل في جلب حالة الواتساب');
}
}
// English: Load available subscription plans.
// Arabic: تحميل باقات الاشتراك المتاحة.
Future<List<PlanModel>> getPlans() async {
final token = await _getToken();
final response = await _apiService.getPlans(token);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final list = decoded['data'] as List<dynamic>? ?? [];
return list.map((item) => PlanModel.fromJson(item as Map<String, dynamic>)).toList();
} else {
throw Exception('فشل في جلب الباقات المتاحة');
}
}
// English: Load CRM contacts directory.
// Arabic: تحميل دليل جهات اتصال إدارة العملاء.
Future<List<ContactModel>> getContacts() async {
final token = await _getToken();
final response = await _apiService.getContacts(token);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final list = decoded['data'] as List<dynamic>? ?? [];
return list.map((item) => ContactModel.fromJson(item as Map<String, dynamic>)).toList();
} else {
throw Exception('فشل في جلب جهات الاتصال');
}
}
// English: Create a new contact inside remote CRM directory.
// Arabic: إنشاء جهة اتصال جديدة داخل دليل إدارة العملاء البعيد.
Future<void> addContact(String name, String phone) async {
final token = await _getToken();
final response = await _apiService.addContact(token, name, phone);
if (response.statusCode == 201 || response.statusCode == 200) {
return;
} else if (response.statusCode == 409) {
throw Exception('رقم الهاتف هذا مسجل بالفعل في جهات الاتصال الخاصة بك');
} else {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final error = decoded['message'] as String? ?? 'فشل في إضافة جهة الاتصال';
throw Exception(error);
}
}
// English: Load chatbot auto-reply rules.
// Arabic: تحميل قواعد الرد الآلي لروبوت الدردشة.
Future<List<ChatbotRuleModel>> getChatbotRules() async {
final token = await _getToken();
final response = await _apiService.getChatbotRules(token);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final list = decoded['data'] as List<dynamic>? ?? [];
return list.map((item) => ChatbotRuleModel.fromJson(item as Map<String, dynamic>)).toList();
} else {
throw Exception('فشل في جلب قواعد الرد الآلي');
}
}
// English: Load platform statistics (Super Admin only).
// Arabic: تحميل إحصائيات المنصة (للمشرف العام فقط).
Future<SuperAdminStatsModel> getAdminStats() async {
final token = await _getToken();
final response = await _apiService.getAdminStats(token);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final statsData = decoded['data'] as Map<String, dynamic>?;
if (statsData == null) {
throw Exception('البيانات غير صالحة');
}
return SuperAdminStatsModel.fromJson(decoded['data'] as Map<String, dynamic>);
} else {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final error = decoded['error'] as String? ?? 'فشل في جلب إحصائيات المشرف العام';
throw Exception(error);
}
}
// English: Request a new WhatsApp QR connection.
// Arabic: طلب اتصال واتساب جديد برمز استجابة سريع.
Future<void> requestWhatsAppQr() async {
final token = await _getToken();
final response = await _apiService.requestWhatsAppQr(token);
if (response.statusCode != 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final error = decoded['message'] as String? ?? 'فشل في طلب رمز الاستجابة السريعة';
throw Exception(error);
}
}
// English: Disconnect the active WhatsApp connection.
// Arabic: قطع اتصال الواتساب النشط.
Future<void> disconnectWhatsApp() async {
final token = await _getToken();
final response = await _apiService.disconnectWhatsApp(token);
if (response.statusCode != 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final error = decoded['message'] as String? ?? 'فشل في قطع الاتصال';
throw Exception(error);
}
}
// English: Approve a pending company subscription.
// Arabic: الموافقة على اشتراك شركة معلق.
Future<void> approveBilling(int companyId) async {
final token = await _getToken();
final response = await _apiService.approveBilling(token, companyId);
if (response.statusCode != 200) {
final decoded = jsonDecode(response.body) as Map<String, dynamic>;
final error = decoded['error'] as String? ?? 'فشل في الموافقة على الاشتراك';
throw Exception(error);
}
}
}

View File

@@ -0,0 +1,72 @@
import 'package:equatable/equatable.dart';
class ChatbotRuleModel extends Equatable {
// English: Unique identifier for the rule.
// Arabic: المعرف الفريد للقاعدة.
final int id;
// English: The ID of the company this rule belongs to.
// Arabic: معرف الشركة التي تنتمي إليها هذه القاعدة.
final int companyId;
// English: The session ID this rule is bound to.
// Arabic: معرف الجلسة المرتبطة بها هذه القاعدة.
final int? sessionId;
// English: Trigger type (keyword, gemini_ai).
// Arabic: نوع المشغل (كلمة مفتاحية، ذكاء اصطناعي).
final String triggerType;
// English: The comma-separated static reply keywords.
// Arabic: الكلمات المفتاحية للردود الثابتة مفصولة بفاصلة.
final String? keyword;
// English: Predefined reply content or Gemini prompt instructions.
// Arabic: محتوى الرد المحدد مسبقاً أو تعليمات موجه الذكاء الاصطناعي.
final String? aiPrompt;
// English: Active state flag.
// Arabic: علم الحالة النشطة.
final bool isActive;
const ChatbotRuleModel({
required this.id,
required this.companyId,
this.sessionId,
required this.triggerType,
this.keyword,
this.aiPrompt,
required this.isActive,
});
// English: Factory constructor to parse ChatbotRuleModel from JSON data map.
// Arabic: منشئ المصنع لتحليل نموذج قاعدة الرد الآلي من خريطة بيانات جيسون.
factory ChatbotRuleModel.fromJson(Map<String, dynamic> json) {
return ChatbotRuleModel(
id: json['id'] as int? ?? 0,
companyId: json['company_id'] as int? ?? 0,
sessionId: json['session_id'] as int?,
triggerType: json['trigger_type'] as String? ?? 'keyword',
keyword: json['keyword'] as String?,
aiPrompt: json['ai_prompt'] as String?,
isActive: (json['is_active'] as int? ?? 0) == 1,
);
}
// English: Convert model to JSON map.
// Arabic: تحويل النموذج إلى خريطة جيسون.
Map<String, dynamic> toJson() {
return {
'id': id,
'company_id': companyId,
'session_id': sessionId,
'trigger_type': triggerType,
'keyword': keyword,
'ai_prompt': aiPrompt,
'is_active': isActive ? 1 : 0,
};
}
@override
List<Object?> get props => [id, companyId, sessionId, triggerType, keyword, aiPrompt, isActive];
}

View File

@@ -0,0 +1,58 @@
import 'package:equatable/equatable.dart';
class ContactModel extends Equatable {
// English: Unique identifier for the contact.
// Arabic: المعرف الفريد لجهة الاتصال.
final int id;
// English: Name of the contact.
// Arabic: اسم جهة الاتصال.
final String name;
// English: Phone number.
// Arabic: رقم الهاتف.
final String phone;
// English: Optional email address.
// Arabic: البريد الإلكتروني الاختياري.
final String? email;
// English: Optional descriptive notes.
// Arabic: ملاحظات وصفية اختيارية.
final String? notes;
const ContactModel({
required this.id,
required this.name,
required this.phone,
this.email,
this.notes,
});
// English: Factory constructor to parse ContactModel from JSON map.
// Arabic: منشئ المصنع لتحليل نموذج جهة الاتصال من خريطة جيسون.
factory ContactModel.fromJson(Map<String, dynamic> json) {
return ContactModel(
id: json['id'] as int? ?? 0,
name: json['name'] as String? ?? '',
phone: json['phone'] as String? ?? '',
email: json['email'] as String?,
notes: json['notes'] as String?,
);
}
// English: Convert model to JSON map.
// Arabic: تحويل النموذج إلى خريطة جيسون.
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'phone': phone,
'email': email,
'notes': notes,
};
}
@override
List<Object?> get props => [id, name, phone, email, notes];
}

View File

@@ -0,0 +1,44 @@
import 'package:equatable/equatable.dart';
class PlanModel extends Equatable {
// English: Unique plan identifier.
// Arabic: المعرف الفريد للباقة.
final int id;
// English: Name of the plan (e.g. Free Trial, Pro, Enterprise).
// Arabic: اسم الباقة (مثل التجريبية، الاحترافية، الشركات).
final String name;
// English: Monthly price of the plan.
// Arabic: السعر الشهري للباقة.
final double price;
const PlanModel({
required this.id,
required this.name,
required this.price,
});
// English: Factory constructor to parse PlanModel from JSON data map.
// Arabic: منشئ المصنع لتحليل نموذج الباقة من خريطة بيانات جيسون.
factory PlanModel.fromJson(Map<String, dynamic> json) {
return PlanModel(
id: json['id'] as int? ?? 0,
name: json['name'] as String? ?? 'Nabeh Plan',
price: (json['price'] as num?)?.toDouble() ?? 0.0,
);
}
// English: Convert model to JSON map.
// Arabic: تحويل النموذج إلى خريطة جيسون.
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'price': price,
};
}
@override
List<Object?> get props => [id, name, price];
}

View File

@@ -0,0 +1,56 @@
import 'package:equatable/equatable.dart';
class SuperAdminStatsModel extends Equatable {
// English: Overall platform stats.
// Arabic: إحصائيات المنصة الإجمالية.
final int totalCompanies;
final int totalSessions;
final int connectedSessions;
// English: Lists of active companies, pending billing approvals, and subscription plans.
// Arabic: قوائم بالشركات النشطة، وموافقات الفواتير المعلقة، وباقات الاشتراك المتاحة.
final List<dynamic> companies;
final List<dynamic> pendingApprovals;
final List<dynamic> plans;
const SuperAdminStatsModel({
required this.totalCompanies,
required this.totalSessions,
required this.connectedSessions,
required this.companies,
required this.pendingApprovals,
required this.plans,
});
// English: Factory constructor to parse SuperAdminStatsModel from JSON data map.
// Arabic: منشئ المصنع لتحليل نموذج إحصائيات المشرف العام من خريطة بيانات جيسون.
factory SuperAdminStatsModel.fromJson(Map<String, dynamic> json) {
final stats = json['stats'] as Map<String, dynamic>? ?? {};
return SuperAdminStatsModel(
totalCompanies: stats['total_companies'] as int? ?? 0,
totalSessions: stats['total_sessions'] as int? ?? 0,
connectedSessions: stats['connected_sessions'] as int? ?? 0,
companies: json['companies'] as List<dynamic>? ?? [],
pendingApprovals: json['pending_approvals'] as List<dynamic>? ?? [],
plans: json['plans'] as List<dynamic>? ?? [],
);
}
// English: Convert model to JSON map.
// Arabic: تحويل النموذج إلى خريطة جيسون.
Map<String, dynamic> toJson() {
return {
'stats': {
'total_companies': totalCompanies,
'total_sessions': totalSessions,
'connected_sessions': connectedSessions,
},
'companies': companies,
'pending_approvals': pendingApprovals,
'plans': plans,
};
}
@override
List<Object?> get props => [totalCompanies, totalSessions, connectedSessions, companies, pendingApprovals, plans];
}

View File

@@ -0,0 +1,65 @@
import 'package:equatable/equatable.dart';
class WhatsAppStatusModel extends Equatable {
// English: Unique session identifier in database.
// Arabic: معرف الجلسة الفريد في قاعدة البيانات.
final int id;
// English: Name of the session (e.g. support, sales).
// Arabic: اسم الجلسة (مثل الدعم، المبيعات).
final String name;
// English: Unique session key sent to Baileys Node.js gateway.
// Arabic: مفتاح الجلسة الفريد المرسل إلى بوابة الجيسون الخاصة بالواتساب.
final String sessionKey;
// English: Current connection status (waiting_qr, connected, disconnected).
// Arabic: حالة الاتصال الحالية (بانتظار رمز الاستجابة، متصل، غير متصل).
final String status;
// English: Base64 or text QR code from Baileys if waiting for scan.
// Arabic: رمز الاستجابة السريعة (QR) من البوابة إذا كان بانتظار المسح.
final String? qrCode;
// English: Linked phone number on success connection.
// Arabic: رقم الهاتف المرتبط بالجلسة عند الاتصال بنجاح.
final String? phone;
const WhatsAppStatusModel({
required this.id,
required this.name,
required this.sessionKey,
required this.status,
this.qrCode,
this.phone,
});
// English: Factory constructor to parse WhatsApp status session model from JSON.
// Arabic: منشئ المصنع لتحليل نموذج حالة جلسة الواتساب من استجابة جيسون.
factory WhatsAppStatusModel.fromJson(Map<String, dynamic> json) {
return WhatsAppStatusModel(
id: json['id'] as int? ?? 0,
name: json['name'] as String? ?? 'WhatsApp Team',
sessionKey: json['session_key'] as String? ?? '',
status: json['status'] as String? ?? 'disconnected',
qrCode: json['qr_code'] as String?,
phone: json['phone'] as String?,
);
}
// English: Convert model to JSON payload.
// Arabic: تحويل النموذج إلى حمولة جيسون.
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'session_key': sessionKey,
'status': status,
'qr_code': qrCode,
'phone': phone,
};
}
@override
List<Object?> get props => [id, name, sessionKey, status, qrCode, phone];
}

View File

@@ -0,0 +1,126 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/dashboard_repository.dart';
import 'dashboard_state.dart';
// English: DashboardCubit manages the active tab and data fetching.
// Arabic: يدير ملف الكيوبيت الباب النشط وجلب البيانات المخصصة له.
//
// English: In the web client, AlpineJS switches active tabs and calls fetch functions.
// Arabic: في عميل الويب، تبدل مكتبة ألبين الباب النشط وتستدعي دوال الجلب.
//
// English: Here, this Cubit replicates that behavior by capturing changes and emitting updated states.
// Arabic: هنا، يكرر هذا الكيوبيت هذا السلوك من خلال رصد التغييرات وإرسال حالات محدثة.
class DashboardCubit extends Cubit<DashboardState> {
final DashboardRepository _repository;
DashboardCubit(this._repository)
: super(const DashboardState(
activeTab: DashboardTab.whatsapp,
isLoading: false,
));
// English: Switch the active dashboard view and fetch the required API data.
// Arabic: تبديل عرض لوحة التحكم النشط وجلب بيانات واجهة برمجة التطبيقات المطلوبة.
Future<void> changeTab(DashboardTab tab) async {
// English: Update tab state first to give instant visual feedback in UI.
// Arabic: تحديث حالة الباب أولاً لتقديم استجابة مرئية فورية في الواجهة.
emit(state.copyWith(activeTab: tab, isLoading: true));
try {
switch (tab) {
case DashboardTab.whatsapp:
final status = await _repository.getWhatsAppStatus();
emit(state.copyWith(whatsappStatus: status, isLoading: false));
break;
case DashboardTab.billing:
final plans = await _repository.getPlans();
emit(state.copyWith(plans: plans, isLoading: false));
break;
case DashboardTab.contacts:
final contacts = await _repository.getContacts();
emit(state.copyWith(contacts: contacts, isLoading: false));
break;
case DashboardTab.chatbot:
final rules = await _repository.getChatbotRules();
emit(state.copyWith(chatbotRules: rules, isLoading: false));
break;
case DashboardTab.superAdmin:
final adminStats = await _repository.getAdminStats();
emit(state.copyWith(superAdminStats: adminStats, isLoading: false));
break;
default:
// English: Placeholder views do not hit any backend API.
// Arabic: شاشات الحجز المؤقتة لا تتصل بأي واجهة برمجة تطبيقات.
emit(state.copyWith(isLoading: false));
break;
}
} catch (e) {
final cleanMsg = e.toString().replaceAll('Exception: ', '');
emit(state.copyWith(errorMessage: cleanMsg, isLoading: false));
}
}
// English: Action trigger to reload current active tab data.
// Arabic: مشغل الإجراء لإعادة تحميل بيانات الباب النشط الحالي.
Future<void> refreshCurrentTab() async {
await changeTab(state.activeTab);
}
// English: Add a new contact and automatically reload the updated contacts list.
// Arabic: إضافة جهة اتصال جديدة وإعادة تحميل قائمة جهات الاتصال المحدثة تلقائياً.
Future<bool> addContact(String name, String phone) async {
emit(state.copyWith(isLoading: true));
try {
await _repository.addContact(name, phone);
final contacts = await _repository.getContacts();
emit(state.copyWith(contacts: contacts, isLoading: false));
return true;
} catch (e) {
final cleanMsg = e.toString().replaceAll('Exception: ', '');
emit(state.copyWith(errorMessage: cleanMsg, isLoading: false));
return false;
}
}
// English: Request a new WhatsApp QR connection session and refresh status.
// Arabic: طلب جلسة اتصال واتساب جديدة برمز استجابة سريع وتحديث الحالة.
Future<void> requestWhatsAppQr() async {
emit(state.copyWith(isLoading: true));
try {
await _repository.requestWhatsAppQr();
final status = await _repository.getWhatsAppStatus();
emit(state.copyWith(whatsappStatus: status, isLoading: false));
} catch (e) {
final cleanMsg = e.toString().replaceAll('Exception: ', '');
emit(state.copyWith(errorMessage: cleanMsg, isLoading: false));
}
}
// English: Disconnect active WhatsApp connection and refresh status.
// Arabic: قطع اتصال الواتساب النشط وتحديث الحالة.
Future<void> disconnectWhatsApp() async {
emit(state.copyWith(isLoading: true));
try {
await _repository.disconnectWhatsApp();
final status = await _repository.getWhatsAppStatus();
emit(state.copyWith(whatsappStatus: status, isLoading: false));
} catch (e) {
final cleanMsg = e.toString().replaceAll('Exception: ', '');
emit(state.copyWith(errorMessage: cleanMsg, isLoading: false));
}
}
// English: Approve a pending company subscription billing and reload admin stats.
// Arabic: الموافقة على اشتراك شركة معلق وإعادة تحميل إحصائيات المدير العام.
Future<void> approveBilling(int companyId) async {
emit(state.copyWith(isLoading: true));
try {
await _repository.approveBilling(companyId);
final stats = await _repository.getAdminStats();
emit(state.copyWith(superAdminStats: stats, isLoading: false));
} catch (e) {
final cleanMsg = e.toString().replaceAll('Exception: ', '');
emit(state.copyWith(errorMessage: cleanMsg, isLoading: false));
}
}
}

View File

@@ -0,0 +1,89 @@
import 'package:equatable/equatable.dart';
import '../../data/models/chatbot_rule_model.dart';
import '../../data/models/contact_model.dart';
import '../../data/models/plan_model.dart';
import '../../data/models/super_admin_stats_model.dart';
import '../../data/models/whatsapp_status_model.dart';
// English: Enum representing the 9 tabs in the Nabeh web/mobile dashboard.
// Arabic: قائمة تعداد تمثل الأبواب التسعة في لوحة تحكم نبيه على الويب والهاتف.
enum DashboardTab {
superAdmin,
whatsapp,
billing,
contacts,
templates,
campaigns,
chatbot,
integrations,
staff
}
class DashboardState extends Equatable {
// English: The currently selected tab in navigation.
// Arabic: الباب المحدد حالياً في التنقل.
final DashboardTab activeTab;
// English: Indicator if data is being retrieved from backend.
// Arabic: مؤشر ما إذا كان يتم استرداد البيانات من الواجهة الخلفية.
final bool isLoading;
// English: Error message if API requests fail.
// Arabic: رسالة الخطأ إذا فشلت طلبات واجهة برمجة التطبيقات.
final String? errorMessage;
// English: Feature data models parsed from network API responses.
// Arabic: نماذج بيانات الميزات التي تم تحليلها من استجابات الشبكة.
final WhatsAppStatusModel? whatsappStatus;
final List<PlanModel> plans;
final List<ContactModel> contacts;
final List<ChatbotRuleModel> chatbotRules;
final SuperAdminStatsModel? superAdminStats;
const DashboardState({
required this.activeTab,
required this.isLoading,
this.errorMessage,
this.whatsappStatus,
this.plans = const [],
this.contacts = const [],
this.chatbotRules = const [],
this.superAdminStats,
});
// English: Helper copyWith constructor to copy immutable state data safely.
// Arabic: منشئ مساعد لنسخ بيانات الحالة الثابتة بشكل آمن.
DashboardState copyWith({
DashboardTab? activeTab,
bool? isLoading,
String? errorMessage,
WhatsAppStatusModel? whatsappStatus,
List<PlanModel>? plans,
List<ContactModel>? contacts,
List<ChatbotRuleModel>? chatbotRules,
SuperAdminStatsModel? superAdminStats,
}) {
return DashboardState(
activeTab: activeTab ?? this.activeTab,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage, // Reset if null
whatsappStatus: whatsappStatus ?? this.whatsappStatus,
plans: plans ?? this.plans,
contacts: contacts ?? this.contacts,
chatbotRules: chatbotRules ?? this.chatbotRules,
superAdminStats: superAdminStats ?? this.superAdminStats,
);
}
@override
List<Object?> get props => [
activeTab,
isLoading,
errorMessage,
whatsappStatus,
plans,
contacts,
chatbotRules,
superAdminStats,
];
}

View File

@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cubit/dashboard_cubit.dart';
import '../cubit/dashboard_state.dart';
class AddContactScreen extends StatefulWidget {
const AddContactScreen({super.key});
@override
State<AddContactScreen> createState() => _AddContactScreenState();
}
class _AddContactScreenState extends State<AddContactScreen> {
// English: Form key to execute field validations.
// Arabic: مفتاح النموذج لتنفيذ عمليات التحقق من صحة الحقول.
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _phoneController = TextEditingController();
@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0F0C20),
appBar: AppBar(
backgroundColor: const Color(0xFF15102A),
elevation: 0,
title: const Text(
'إضافة جهة اتصال جديدة',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
body: BlocConsumer<DashboardCubit, DashboardState>(
listener: (context, state) {
if (state.errorMessage != null) {
// English: Show API failure messages as SnackBar alert.
// Arabic: عرض رسائل فشل واجهة برمجة التطبيقات كشريط تنبيه.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
state.errorMessage!,
style: const TextStyle(color: Colors.white),
),
backgroundColor: Colors.redAccent,
),
);
}
},
builder: (context, state) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'تفاصيل جهة الاتصال',
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// English: Input field for contact name.
// Arabic: حقل إدخال اسم جهة الاتصال.
TextFormField(
controller: _nameController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'الاسم الكامل',
labelStyle: TextStyle(color: Colors.white.withOpacity(0.6)),
prefixIcon: const Icon(Icons.person_outline, color: Colors.purpleAccent),
filled: true,
fillColor: Colors.white.withOpacity(0.05),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.white.withOpacity(0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.purpleAccent, width: 2),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'الرجاء إدخال اسم جهة الاتصال';
}
return null;
},
),
const SizedBox(height: 20),
// English: Input field for phone number.
// Arabic: حقل إدخال رقم الهاتف.
TextFormField(
controller: _phoneController,
keyboardType: TextInputType.phone,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'رقم الهاتف (مع رمز الدولة)',
labelStyle: TextStyle(color: Colors.white.withOpacity(0.6)),
prefixIcon: const Icon(Icons.phone_outlined, color: Colors.purpleAccent),
filled: true,
fillColor: Colors.white.withOpacity(0.05),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.white.withOpacity(0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.purpleAccent, width: 2),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'الرجاء إدخال رقم الهاتف';
}
return null;
},
),
const SizedBox(height: 40),
// English: Save button. Shows progress bar or calls Cubit and Pops on success.
// Arabic: زر الحفظ. يعرض شريط تقدم التحميل أو يستدعي الكيوبيت ويغلق الصفحة عند النجاح.
if (state.isLoading)
const Center(child: CircularProgressIndicator(color: Colors.purpleAccent))
else
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purpleAccent,
minimumSize: const Size(double.infinity, 56),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () async {
if (_formKey.currentState!.validate()) {
// English: Dispatch addContact action to DashboardCubit.
// Arabic: استدعاء إجراء إضافة جهة اتصال في الكيوبيت.
final success = await context.read<DashboardCubit>().addContact(
_nameController.text.trim(),
_phoneController.text.trim(),
);
if (success && mounted) {
// English: Use Navigator.pop to return to previous contacts screen.
// Arabic: استخدام ميزة الموجه لإغلاق الشاشة والرجوع لقائمة جهات الاتصال.
Navigator.pop(context);
}
}
},
child: const Text(
'حفظ جهة الاتصال',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
),
),
],
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,293 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../auth/data/models/user_model.dart';
import '../../../auth/presentation/cubit/auth_cubit.dart';
import '../../data/dashboard_repository.dart';
import '../cubit/dashboard_cubit.dart';
import '../cubit/dashboard_state.dart';
import '../widgets/billing_view.dart';
import '../widgets/chatbot_view.dart';
import '../widgets/contacts_view.dart';
import '../widgets/simple_placeholder_view.dart';
import '../widgets/super_admin_view.dart';
import '../widgets/whatsapp_view.dart';
class DashboardScreen extends StatelessWidget {
final UserModel user;
const DashboardScreen({super.key, required this.user});
@override
Widget build(BuildContext context) {
// English: Wrap dashboard view in a local BlocProvider to manage dashboard tab states.
// Arabic: تغليف واجهة لوحة التحكم في موفر كتلة محلي لإدارة حالات تبويبات لوحة التحكم.
return BlocProvider<DashboardCubit>(
create: (context) {
final cubit = DashboardCubit(DashboardRepository());
// English: Load WhatsApp status as the default view on launch.
// Arabic: تحميل حالة الواتساب كعرض افتراضي عند بدء التشغيل.
cubit.changeTab(DashboardTab.whatsapp);
return cubit;
},
child: BlocBuilder<DashboardCubit, DashboardState>(
builder: (context, state) {
return Scaffold(
backgroundColor: const Color(0xFF0F0C20),
appBar: AppBar(
backgroundColor: const Color(0xFF15102A),
elevation: 0,
title: Text(
_getAppBarTitle(state.activeTab),
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold),
),
actions: [
IconButton(
icon: const Icon(Icons.refresh, color: Colors.purpleAccent),
tooltip: 'تحديث البيانات',
onPressed: () {
context.read<DashboardCubit>().refreshCurrentTab();
},
),
],
),
// English: Slide drawer menu offering navigation to all 9 dashboard tabs.
// Arabic: قائمة درج جانبية توفر التنقل إلى جميع أبواب لوحة التحكم التسعة.
drawer: Drawer(
backgroundColor: const Color(0xFF15102A),
child: Column(
children: [
UserAccountsDrawerHeader(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF281C5C), Color(0xFF15102A)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
currentAccountPicture: CircleAvatar(
backgroundColor: Colors.purpleAccent.withOpacity(0.2),
child: const Icon(Icons.person,
color: Colors.purpleAccent, size: 40),
),
accountName: Text(
user.name,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
accountEmail: Text(
user.isSuperAdmin
? '👑 المشرف العام للمنصة'
: '🏢 مدير الشركة',
style: const TextStyle(
color: Colors.purpleAccent, fontSize: 13),
),
),
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
// English: Show Super Admin tab only if current user is_super_admin is true.
// Arabic: عرض تبويب المشرف العام فقط إذا كان المستخدم الحالي مشرفاً عاماً.
if (user.isSuperAdmin)
_buildDrawerItem(
context,
'👑 لوحة المشرف العام',
DashboardTab.superAdmin,
state.activeTab,
Icons.admin_panel_settings,
),
_buildDrawerItem(
context,
'📱 اتصال الواتساب',
DashboardTab.whatsapp,
state.activeTab,
Icons.phone_android,
),
_buildDrawerItem(
context,
'💳 الباقات والاشتراكات',
DashboardTab.billing,
state.activeTab,
Icons.credit_card,
),
_buildDrawerItem(
context,
'👥 دليل جهات الاتصال',
DashboardTab.contacts,
state.activeTab,
Icons.contacts_outlined,
),
_buildDrawerItem(
context,
'📝 قوالب الرسائل',
DashboardTab.templates,
state.activeTab,
Icons.message_outlined,
),
_buildDrawerItem(
context,
'📣 الحملات التسويقية',
DashboardTab.campaigns,
state.activeTab,
Icons.campaign_outlined,
),
_buildDrawerItem(
context,
'🤖 قواعد الرد الآلي',
DashboardTab.chatbot,
state.activeTab,
Icons.android_outlined,
),
_buildDrawerItem(
context,
'🔌 التكاملات والربط',
DashboardTab.integrations,
state.activeTab,
Icons.integration_instructions_outlined,
),
_buildDrawerItem(
context,
'👤 الموظفين والدعم',
DashboardTab.staff,
state.activeTab,
Icons.people_outline,
),
],
),
),
const Divider(color: Colors.white10),
ListTile(
leading: const Icon(Icons.logout, color: Colors.redAccent),
title: const Text('تسجيل الخروج',
style: TextStyle(color: Colors.redAccent)),
onTap: () {
context.read<AuthCubit>().logout();
},
),
const SizedBox(height: 20),
],
),
),
body: state.isLoading
? const Center(
child:
CircularProgressIndicator(color: Colors.purpleAccent))
: state.errorMessage != null
? Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
state.errorMessage!,
style: const TextStyle(
color: Colors.redAccent, fontSize: 14),
textAlign: TextAlign.center,
),
),
)
: SingleChildScrollView(
child: _renderActiveTabContent(context, state),
),
);
},
),
);
}
Widget _buildDrawerItem(
BuildContext context,
String title,
DashboardTab tab,
DashboardTab activeTab,
IconData icon,
) {
final isSelected = tab == activeTab;
return ListTile(
leading:
Icon(icon, color: isSelected ? Colors.purpleAccent : Colors.white60),
title: Text(
title,
style: TextStyle(
color: isSelected ? Colors.purpleAccent : Colors.white,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
selected: isSelected,
selectedTileColor: Colors.purpleAccent.withOpacity(0.05),
onTap: () {
// English: Close drawer and switch tab.
// Arabic: إغلاق درج القائمة الجانبية وتبديل التبويب.
Navigator.pop(context);
context.read<DashboardCubit>().changeTab(tab);
},
);
}
String _getAppBarTitle(DashboardTab tab) {
switch (tab) {
case DashboardTab.superAdmin:
return 'المشرف العام - نبيه';
case DashboardTab.whatsapp:
return 'اتصال الواتساب';
case DashboardTab.billing:
return 'الباقات والاشتراكات';
case DashboardTab.contacts:
return 'دليل جهات الاتصال';
case DashboardTab.templates:
return 'قوالب الرسائل';
case DashboardTab.campaigns:
return 'الحملات التسويقية';
case DashboardTab.chatbot:
return 'قواعد الرد الآلي';
case DashboardTab.integrations:
return 'التكاملات والربط';
case DashboardTab.staff:
return 'الموظفين والدعم';
}
}
Widget _renderActiveTabContent(BuildContext context, DashboardState state) {
switch (state.activeTab) {
case DashboardTab.superAdmin:
return SuperAdminView(stats: state.superAdminStats);
case DashboardTab.whatsapp:
return WhatsAppView(
status: state.whatsappStatus,
onRefresh: () => context.read<DashboardCubit>().refreshCurrentTab(),
);
case DashboardTab.billing:
return BillingView(plans: state.plans);
case DashboardTab.contacts:
return ContactsView(contacts: state.contacts);
case DashboardTab.chatbot:
return ChatbotView(rules: state.chatbotRules);
case DashboardTab.templates:
return const SimplePlaceholderView(
title: 'قوالب الرسائل',
description:
'ميزة تصميم قوالب رسائل الواتساب الديناميكية تحت التطوير.',
icon: Icons.message,
);
case DashboardTab.campaigns:
return const SimplePlaceholderView(
title: 'الحملات التسويقية',
description:
'ميزة إرسال وإدارة الحملات البريدية والجماعية تحت التطوير.',
icon: Icons.campaign,
);
case DashboardTab.integrations:
return const SimplePlaceholderView(
title: 'التكاملات والربط',
description: 'ربط البوابة مع منصات سلة وووردبريس عبر واجهات البرمجة.',
icon: Icons.integration_instructions,
);
case DashboardTab.staff:
return const SimplePlaceholderView(
title: 'الموظفين والدعم',
description:
'إضافة وإدارة حسابات الموظفين والعملاء لتوزيع المحادثات.',
icon: Icons.people_outline,
);
}
}
}

View File

@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import '../../data/models/plan_model.dart';
class BillingView extends StatelessWidget {
final List<PlanModel> plans;
const BillingView({
super.key,
required this.plans,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'💳 الباقات والاشتراكات',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'اختر الباقة المناسبة لاحتياجات شركتك لتفعيل ميزات الردود التلقائية غير المحدودة.',
style: TextStyle(color: Colors.white60, fontSize: 13),
),
const SizedBox(height: 24),
// English: Render list of subscription plan cards.
// Arabic: عرض قائمة ببطاقات خطط الاشتراك المتاحة.
if (plans.isEmpty)
const Center(
child: Text(
'لا توجد باقات متاحة حالياً.',
style: TextStyle(color: Colors.white70),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: plans.length,
itemBuilder: (context, index) {
final plan = plans[index];
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
plan.name,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 6),
const Text(
'الدفع شهرياً - إلغاء في أي وقت',
style: TextStyle(color: Colors.white60, fontSize: 12),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'\$${plan.price.toStringAsFixed(2)}',
style: const TextStyle(
color: Colors.purpleAccent,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
const Text(
'/شهرياً',
style: TextStyle(color: Colors.white60, fontSize: 12),
),
],
),
],
),
);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import '../../data/models/chatbot_rule_model.dart';
class ChatbotView extends StatelessWidget {
final List<ChatbotRuleModel> rules;
const ChatbotView({
super.key,
required this.rules,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'🤖 قواعد الرد الآلي للدردشة',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'إدارة قواعد الذكاء الاصطناعي والكلمات المفتاحية المخصصة لتلقي ومعالجة المحادثات الواردة.',
style: TextStyle(color: Colors.white60, fontSize: 13),
),
const SizedBox(height: 24),
// English: Render list of chatbot rule cards.
// Arabic: عرض قائمة ببطاقات قواعد روبوت الدردشة المكونة.
if (rules.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40.0),
child: Text(
'لم يتم تكوين قواعد رد آلي بعد.',
style: TextStyle(color: Colors.white70),
),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: rules.length,
itemBuilder: (context, index) {
final rule = rules[index];
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildTriggerBadge(rule.triggerType),
_buildStatusBadge(rule.isActive),
],
),
const Divider(color: Colors.white10, height: 24),
if (rule.triggerType == 'keyword' && rule.keyword != null) ...[
const Text(
'الكلمة المفتاحية للمشغل:',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 4),
Text(
rule.keyword!,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
],
const Text(
'رد الروبوت أو تعليمات موجه الذكاء الاصطناعي (AI Prompt):',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
const SizedBox(height: 4),
Text(
rule.aiPrompt ?? 'لا توجد تعليمات',
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 13, height: 1.4),
),
],
),
);
},
),
],
),
);
}
Widget _buildTriggerBadge(String type) {
final isAi = type == 'gemini_ai';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isAi ? Colors.purpleAccent.withOpacity(0.2) : Colors.blue.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: isAi ? Colors.purpleAccent : Colors.blue),
),
child: Text(
isAi ? '🧠 ذكاء اصطناعي (Gemini)' : '⌨️ كلمات مفتاحية',
style: TextStyle(color: isAi ? Colors.purpleAccent : Colors.blue, fontSize: 11, fontWeight: FontWeight.bold),
),
);
}
Widget _buildStatusBadge(bool isActive) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isActive ? Colors.green.withOpacity(0.2) : Colors.grey.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: isActive ? Colors.green : Colors.grey),
),
child: Text(
isActive ? 'نشط' : 'غير نشط',
style: TextStyle(color: isActive ? Colors.green : Colors.grey, fontSize: 10, fontWeight: FontWeight.bold),
),
);
}
}

View File

@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/contact_model.dart';
import '../cubit/dashboard_cubit.dart';
import '../screens/add_contact_screen.dart';
class ContactsView extends StatelessWidget {
final List<ContactModel> contacts;
const ContactsView({
super.key,
required this.contacts,
});
// English: Show alert confirmation dialog before navigating.
// Arabic: عرض مربع حوار تأكيدي قبل الانتقال إلى الشاشة التالية.
void _showNavigationDialog(BuildContext context) {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: const Color(0xFF15102A),
title: const Text('إضافة جهة اتصال جديدة', style: TextStyle(color: Colors.white)),
content: const Text(
'هل تريد الانتقال لصفحة إضافة جهة اتصال جديدة لتعبئة البيانات؟',
style: TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء', style: TextStyle(color: Colors.white60)),
),
TextButton(
onPressed: () {
// English: Pop dialog window first.
// Arabic: إغلاق مربع الحوار أولاً.
Navigator.pop(dialogContext);
// English: Push AddContactScreen, sharing the existing Cubit instance.
// Arabic: الانتقال إلى شاشة إضافة جهة اتصال ومشاركة نفس الكيوبيت الحالي.
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: context.read<DashboardCubit>(),
child: const AddContactScreen(),
),
),
);
},
child: const Text('نعم، انتقل', style: TextStyle(color: Colors.purpleAccent)),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'👥 دليل جهات الاتصال',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purpleAccent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
icon: const Icon(Icons.add, color: Colors.white, size: 16),
label: const Text(
'إضافة جهة',
style: TextStyle(color: Colors.white, fontSize: 12),
),
onPressed: () => _showNavigationDialog(context),
),
],
),
const SizedBox(height: 8),
const Text(
'عرض وإدارة دليل العملاء والجهات التي تواصلت مع النظام الآلي.',
style: TextStyle(color: Colors.white60, fontSize: 13),
),
const SizedBox(height: 24),
// English: Render list of parsed contacts in custom list view.
// Arabic: عرض قائمة بجهات الاتصال المحللة في طريقة عرض القائمة المخصصة.
if (contacts.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40.0),
child: Text(
'دليل جهات الاتصال فارغ حالياً.',
style: TextStyle(color: Colors.white70),
),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: contacts.length,
itemBuilder: (context, index) {
final contact = contacts[index];
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.purpleAccent.withOpacity(0.1),
),
child: const Icon(Icons.person, color: Colors.purpleAccent),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
contact.name,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
contact.phone,
style: const TextStyle(color: Colors.purpleAccent, fontSize: 13),
),
if (contact.notes != null && contact.notes!.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
contact.notes!,
style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12),
),
],
],
),
),
],
),
);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
class SimplePlaceholderView extends StatelessWidget {
final String title;
final String description;
final IconData icon;
const SimplePlaceholderView({
super.key,
required this.title,
required this.description,
required this.icon,
});
@override
Widget build(BuildContext context) {
// English: Placeholders mimic undeveloped features in standard premium dark cards.
// Arabic: تحاكي شاشات الحجز الميزات التي لم يتم تطويرها في بطاقات داكنة مميزة قياسية.
return Center(
child: Container(
margin: const EdgeInsets.all(20),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 64, color: Colors.purpleAccent),
const SizedBox(height: 20),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Text(
description,
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,336 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/super_admin_stats_model.dart';
import '../cubit/dashboard_cubit.dart';
class SuperAdminView extends StatelessWidget {
final SuperAdminStatsModel? stats;
const SuperAdminView({
super.key,
required this.stats,
});
// English: Show a confirmation dialog before approving company billing subscription.
// Arabic: عرض مربع حوار تأكيدي للموافقة على ترقية الفوترة والاشتراك.
void _showApproveDialog(BuildContext context, int companyId, String companyName) {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: const Color(0xFF15102A),
title: const Text('الموافقة على الاشتراك', style: TextStyle(color: Colors.white)),
content: Text(
'هل أنت متأكد من تفعيل اشتراك شركة "$companyName"؟ سيتم ترقية حسابهم على الفور.',
style: const TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء', style: TextStyle(color: Colors.white60)),
),
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
// English: Dispatch approveBilling command to DashboardCubit.
// Arabic: استدعاء أمر الموافقة على الاشتراك في الكيوبيت.
context.read<DashboardCubit>().approveBilling(companyId);
},
child: const Text('نعم، وافق', style: TextStyle(color: Colors.green)),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final model = stats;
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'👑 لوحة تحكم المشرف العام',
style: TextStyle(
color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'مراقبة إحصائيات منصة نبيه بالكامل وإدارة تراخيص الشركات وطلبات الترقية.',
style: TextStyle(color: Colors.white60, fontSize: 13),
),
const SizedBox(height: 24),
if (model == null)
const Center(
child: Text(
'فشل تحميل إحصائيات المشرف العام.',
style: TextStyle(color: Colors.white70),
),
)
else ...[
// English: Stats widgets mirroring the web interface dashboard indicators.
// Arabic: أدوات إحصائية تحاكي مؤشرات لوحة معلومات واجهة الويب.
Row(
children: [
Expanded(
child: _buildMetricCard(
'الشركات المسجلة',
model.totalCompanies.toString(),
Icons.business,
Colors.purpleAccent,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildMetricCard(
'جلسات الواتساب',
'${model.connectedSessions}/${model.totalSessions}',
Icons.phone_android,
Colors.green,
),
),
],
),
const SizedBox(height: 30),
// English: Display pending billing approval requests.
// Arabic: عرض طلبات الموافقة على ترقية الباقات المعلقة.
const Text(
'طلبات الترقية المعلقة للموافقة',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
if (model.pendingApprovals.isEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.02),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white12),
),
child: const Center(
child: Text(
'لا توجد طلبات ترقية معلقة حالياً.',
style: TextStyle(color: Colors.white60, fontSize: 13),
),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: model.pendingApprovals.length,
itemBuilder: (context, index) {
final company =
model.pendingApprovals[index] as Map<String, dynamic>;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF1E1446),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.purpleAccent.withOpacity(0.3)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
company['name'] as String? ?? 'شركة غير معروفة',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
Text(
'الباقة المطلوبة: ${company['plan_name'] ?? 'لا يوجد'}',
style: const TextStyle(
color: Colors.purpleAccent, fontSize: 12),
),
],
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
child: const Text('موافقة',
style: TextStyle(fontSize: 12)),
onPressed: () {
_showApproveDialog(
context,
company['id'] as int? ?? 0,
company['name'] as String? ?? 'الشركة',
);
},
),
],
),
);
},
),
const SizedBox(height: 30),
// English: Display list of SaaS Companies.
// Arabic: عرض قائمة الشركات المسجلة وتفاصيل استخداماتها.
const Text(
'الشركات والعملاء المشتركين',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: model.companies.length,
itemBuilder: (context, index) {
final company = model.companies[index] as Map<String, dynamic>;
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
company['name'] as String? ?? 'شركة',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold),
),
_buildSubBadge(
company['sub_status'] as String? ?? 'expired',
company['plan_name'] as String?),
],
),
const Divider(color: Colors.white10, height: 24),
_buildDetailRow('الجلسات النشطة',
'${company['active_sessions'] ?? 0}/${company['sessions_count'] ?? 0}'),
const SizedBox(height: 8),
// English: Display SaaS resource usages metrics (Requests, Voice, OCR).
// Arabic: عرض مقاييس استخدام موارد النظام (الطلبات، الصوت، تحليل الصور).
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildUsageText('الطلبات',
(company['request_usage'] ?? 0).toString()),
_buildUsageText('الصوت',
(company['voice_usage'] ?? 0).toString()),
_buildUsageText(
'OCR', (company['ocr_usage'] ?? 0).toString()),
],
),
],
),
);
},
),
],
],
),
);
}
Widget _buildMetricCard(
String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 12),
Text(title,
style: const TextStyle(color: Colors.white60, fontSize: 12)),
const SizedBox(height: 4),
Text(value,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildSubBadge(String status, String? planName) {
final name = planName ?? 'لا توجد باقة';
Color color = Colors.grey;
if (status == 'active') {
color = Colors.green;
} else if (status == 'trialing') {
color = Colors.blue;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color),
),
child: Text(
name,
style:
TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label,
style: const TextStyle(color: Colors.white60, fontSize: 13)),
Text(value,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold)),
],
);
}
Widget _buildUsageText(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(color: Colors.white54, fontSize: 11)),
const SizedBox(height: 2),
Text(value,
style: const TextStyle(
color: Colors.purpleAccent,
fontSize: 13,
fontWeight: FontWeight.bold)),
],
);
}
}

View File

@@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/whatsapp_status_model.dart';
import '../cubit/dashboard_cubit.dart';
class WhatsAppView extends StatelessWidget {
final WhatsAppStatusModel? status;
final VoidCallback onRefresh;
const WhatsAppView({
super.key,
required this.status,
required this.onRefresh,
});
// English: Show a confirmation dialog before disconnecting.
// Arabic: عرض مربع حوار تأكيدي قبل قطع الاتصال لتجنب الإجراء المفاجئ.
void _showDisconnectDialog(BuildContext context) {
showDialog(
context: context,
builder: (dialogContext) {
return AlertDialog(
backgroundColor: const Color(0xFF15102A),
title: const Text('قطع اتصال الواتساب', style: TextStyle(color: Colors.white)),
content: const Text(
'هل أنت متأكد من رغبتك في قطع الاتصال؟ سيؤدي ذلك إلى تعطيل روبوت خدمة العملاء والردود التلقائية.',
style: TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('إلغاء', style: TextStyle(color: Colors.white60)),
),
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
// English: Dispatch disconnectWhatsApp command to DashboardCubit.
// Arabic: استدعاء أمر قطع اتصال الواتساب في الكيوبيت.
context.read<DashboardCubit>().disconnectWhatsApp();
},
child: const Text('نعم، اقطع الاتصال', style: TextStyle(color: Colors.redAccent)),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final session = status;
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'📱 إعدادات اتصال الواتساب',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'اربط حسابك مع بوابة واتساب التابعة لنظام نبيه لتفعيل الردود التلقائية والتحقق.',
style: TextStyle(color: Colors.white60, fontSize: 13),
),
const SizedBox(height: 24),
// English: Display WhatsApp connection card containing session and status.
// Arabic: عرض بطاقة اتصال الواتساب التي تحتوي على الجلسة والحالة.
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF15102A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withOpacity(0.05)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'حالة الجلسة الحالية',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
_buildStatusBadge(session?.status ?? 'disconnected'),
],
),
const Divider(color: Colors.white10, height: 24),
_buildRow('اسم الجلسة', session?.name ?? 'WhatsApp Team'),
_buildRow('مفتاح التعريف', session?.sessionKey ?? 'لا يوجد'),
if (session?.phone != null) _buildRow('رقم الهاتف المرتبط', session!.phone!),
const SizedBox(height: 24),
// English: Render different elements depending on connection status.
// Arabic: عرض عناصر مختلفة حسب حالة الاتصال.
if (session == null || session.status == 'disconnected') ...[
const Text(
'⚠️ الحساب غير متصل. يرجى توليد رمز الاستجابة السريعة (QR Code) ومسحه ضوئياً لتفعيل الاتصال.',
style: TextStyle(color: Colors.orangeAccent, fontSize: 13),
),
const SizedBox(height: 20),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purpleAccent,
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
icon: const Icon(Icons.qr_code_scanner),
label: const Text('توليد رمز الاستجابة QR'),
onPressed: () {
// English: Dispatch requestWhatsAppQr command to DashboardCubit.
// Arabic: استدعاء أمر طلب رمز الاستجابة في الكيوبيت.
context.read<DashboardCubit>().requestWhatsAppQr();
},
),
] else if (session.status == 'waiting_qr') ...[
const Text(
'🔍 رمز الاستجابة جاهز للمسح. يرجى فتح الواتساب في هاتفك واختيار "الأجهزة المرتبطة" ثم مسح الرمز أدناه:',
style: TextStyle(color: Colors.yellowAccent, fontSize: 13),
),
const SizedBox(height: 20),
Center(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.qr_code_2, size: 200, color: Colors.black),
),
),
const SizedBox(height: 20),
] else if (session.status == 'connected') ...[
const Text(
'✅ الحساب متصل وجاهز للعمل. الردود التلقائية وروبوت خدمة العملاء نشطان الآن.',
style: TextStyle(color: Colors.greenAccent, fontSize: 13),
),
const SizedBox(height: 20),
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
foregroundColor: Colors.redAccent,
side: const BorderSide(color: Colors.redAccent),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
icon: const Icon(Icons.link_off),
label: const Text('قطع الاتصال'),
onPressed: () => _showDisconnectDialog(context),
),
],
],
),
),
],
),
);
}
Widget _buildRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.white70, fontSize: 13)),
Text(value, style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold)),
],
),
);
}
Widget _buildStatusBadge(String status) {
Color color;
String text;
switch (status) {
case 'connected':
color = Colors.green;
text = 'متصل';
break;
case 'waiting_qr':
color = Colors.orange;
text = 'بانتظار المسح';
break;
case 'connecting':
color = Colors.blue;
text = 'جاري الاتصال';
break;
default:
color = Colors.red;
text = 'غير متصل';
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color),
),
child: Text(text, style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold)),
);
}
}

86
mobile/lib/main.dart Normal file
View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'features/auth/data/auth_repository.dart';
import 'features/auth/presentation/cubit/auth_cubit.dart';
import 'features/auth/presentation/cubit/auth_state.dart';
import 'features/auth/presentation/screens/login_screen.dart';
import 'features/dashboard/presentation/screens/dashboard_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// English: Instantiate repositories to supply to Cubits.
// Arabic: إنشاء مثيلات المستودعات لتزويد الكيوبيتس بها.
final authRepository = AuthRepository();
runApp(
// English: Provide AuthCubit globally so it is accessible on all screens.
// Arabic: توفير الكيوبيت لمصادقة المستخدمين عالمياً ليكون متاحاً في جميع الشاشات.
BlocProvider<AuthCubit>(
create: (context) => AuthCubit(authRepository)..checkAuthStatus(),
child: const NabehApp(),
),
);
}
class NabehApp extends StatelessWidget {
const NabehApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'نبيه',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.deepPurple,
fontFamily: 'Outfit',
),
// English: Dynamically route home screen based on active authentication state.
// Arabic: توجيه الشاشة الرئيسية ديناميكيًا بناءً على حالة المصادقة النشطة.
home: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
// English: Navigate to dashboard screen on success.
// Arabic: الانتقال إلى شاشة لوحة التحكم عند النجاح.
return DashboardScreen(user: state.user);
} else if (state is Unauthenticated || state is AuthFailure) {
// English: Navigate to login screen on unauthenticated state.
// Arabic: الانتقال إلى شاشة تسجيل الدخول عند عدم المصادقة.
return const LoginScreen();
} else {
// English: Show splash loader during initial authentication check.
// Arabic: عرض شاشة تحميل أولية أثناء التحقق من المصادقة لأول مرة.
return const SplashScreen();
}
},
),
);
}
}
// English: Splash Screen displayed during initialization status check.
// Arabic: شاشة البداية المعروضة أثناء التحقق من حالة التهيئة.
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
backgroundColor: Color(0xFF0F0C20),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Colors.purpleAccent),
SizedBox(height: 16),
Text(
'جاري تهيئة النظام...',
style: TextStyle(color: Colors.white70, fontSize: 16),
),
],
),
),
);
}
}

1
mobile/linux/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
flutter/ephemeral

128
mobile/linux/CMakeLists.txt Normal file
View File

@@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "nabeh")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.nabeh.nabeh")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
}

View File

@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -0,0 +1,24 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
flutter_secure_storage_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

View File

@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "nabeh");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "nabeh");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

View File

@@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
mobile/macos/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

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