admin front start

This commit is contained in:
wikm 2026-03-07 19:18:52 +03:30
parent a99d920f2c
commit c5c4fff0db
159 changed files with 8895 additions and 0 deletions

45
admin_panel/.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

45
admin_panel/.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "cd9e44ff10cac283eafc97d6ed58720c45f1be9e"
channel: "master"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
base_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
- platform: android
create_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
base_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
- platform: ios
create_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
base_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
- platform: linux
create_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
base_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
- platform: macos
create_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
base_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
- platform: web
create_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
base_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
- platform: windows
create_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
base_revision: cd9e44ff10cac283eafc97d6ed58720c45f1be9e
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
admin_panel/README.md Normal file
View File

@ -0,0 +1,3 @@
# admin_panel
A new Flutter project.

View File

@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

14
admin_panel/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.example.admin_panel"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.admin_panel"
// 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="admin_panel"
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.example.admin_panel
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
admin_panel/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 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,620 @@
// !$*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 */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; 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>"; };
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>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<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 */,
);
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>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
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)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.adminPanel;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.adminPanel.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.adminPanel.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.adminPanel.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.adminPanel;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.adminPanel;
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,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.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,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

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>Admin Panel</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>admin_panel</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: FlutterSceneDelegate {
}

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,19 @@
/// Application configuration
/// Set [debugMode] to true to use fake/mock data instead of real API
class AppConfig {
// Debug Toggle
/// When true, the app uses in-memory mock data (no network calls).
/// When false, the app communicates with the real backend API.
static const bool debugMode = true;
// API Settings
static const String baseUrl = 'http://localhost:8000';
// App Metadata
static const String appName = 'NEDA Admin';
static const String appVersion = '1.0.0';
// Mock Credentials (only used in debugMode)
static const String mockAdminUsername = 'admin';
static const String mockAdminSecret = 'admin123';
}

35
admin_panel/lib/main.dart Normal file
View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'config/app_config.dart';
import 'providers/auth_provider.dart';
import 'providers/user_provider.dart';
import 'providers/group_provider.dart';
import 'router/app_router.dart';
import 'services/service_locator.dart';
import 'theme/app_theme.dart';
void main() {
ServiceLocator().initialize();
runApp(const NedaAdminApp());
}
class NedaAdminApp extends StatelessWidget {
const NedaAdminApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => UserProvider()),
ChangeNotifierProvider(create: (_) => GroupProvider()),
],
child: MaterialApp.router(
title: AppConfig.appName,
debugShowCheckedModeBanner: false,
theme: AppTheme.theme,
routerConfig: appRouter,
),
);
}
}

View File

@ -0,0 +1,70 @@
enum GroupRole { manager, member }
extension GroupRoleExtension on GroupRole {
String get label {
switch (this) {
case GroupRole.manager:
return 'Manager';
case GroupRole.member:
return 'Member';
}
}
String get apiValue => name;
}
class GroupMemberModel {
final String userId;
final String groupId;
final GroupRole role;
/// Denormalized username for display populated from local user cache
final String? username;
final DateTime? joinedAt;
const GroupMemberModel({
required this.userId,
required this.groupId,
required this.role,
this.username,
this.joinedAt,
});
factory GroupMemberModel.fromJson(Map<String, dynamic> json) {
return GroupMemberModel(
userId: json['user_id'] as String,
groupId: json['group_id'] as String,
role: GroupRole.values.firstWhere(
(r) => r.name == (json['role'] as String),
orElse: () => GroupRole.member,
),
username: json['username'] as String?,
joinedAt: json['joined_at'] != null
? DateTime.tryParse(json['joined_at'] as String)
: null,
);
}
Map<String, dynamic> toJson() => {
'user_id': userId,
'group_id': groupId,
'role': role.apiValue,
if (username != null) 'username': username,
if (joinedAt != null) 'joined_at': joinedAt!.toIso8601String(),
};
GroupMemberModel copyWith({
String? userId,
String? groupId,
GroupRole? role,
String? username,
DateTime? joinedAt,
}) {
return GroupMemberModel(
userId: userId ?? this.userId,
groupId: groupId ?? this.groupId,
role: role ?? this.role,
username: username ?? this.username,
joinedAt: joinedAt ?? this.joinedAt,
);
}
}

View File

@ -0,0 +1,68 @@
enum GroupType { group, direct }
class GroupModel {
final String id;
final String name;
final String? description;
final bool isActive;
final GroupType type;
final DateTime? createdAt;
final int memberCount;
const GroupModel({
required this.id,
required this.name,
this.description,
required this.isActive,
this.type = GroupType.group,
this.createdAt,
this.memberCount = 0,
});
factory GroupModel.fromJson(Map<String, dynamic> json) {
return GroupModel(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String?,
isActive: (json['is_active'] as bool?) ?? true,
type: GroupType.values.firstWhere(
(t) => t.name == (json['type'] as String? ?? 'group'),
orElse: () => GroupType.group,
),
createdAt: json['created_at'] != null
? DateTime.tryParse(json['created_at'] as String)
: null,
memberCount: (json['member_count'] as int?) ?? 0,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
if (description != null) 'description': description,
'is_active': isActive,
'type': type.name,
if (createdAt != null) 'created_at': createdAt!.toIso8601String(),
'member_count': memberCount,
};
GroupModel copyWith({
String? id,
String? name,
String? description,
bool? isActive,
GroupType? type,
DateTime? createdAt,
int? memberCount,
}) {
return GroupModel(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
isActive: isActive ?? this.isActive,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
memberCount: memberCount ?? this.memberCount,
);
}
}

View File

@ -0,0 +1,76 @@
enum UserRole { admin, group_manager, member }
extension UserRoleExtension on UserRole {
String get label {
switch (this) {
case UserRole.admin:
return 'Admin';
case UserRole.group_manager:
return 'Group Manager';
case UserRole.member:
return 'Member';
}
}
String get apiValue => name;
}
class UserModel {
final String id;
final String username;
final UserRole role;
final bool isActive;
final DateTime? createdAt;
/// Only available immediately after creation or secret reset
final String? secret;
const UserModel({
required this.id,
required this.username,
required this.role,
required this.isActive,
this.createdAt,
this.secret,
});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'] as String,
username: json['username'] as String,
role: UserRole.values.firstWhere(
(r) => r.name == (json['role'] as String),
orElse: () => UserRole.member,
),
isActive: (json['is_active'] as bool?) ?? true,
createdAt: json['created_at'] != null
? DateTime.tryParse(json['created_at'] as String)
: null,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'username': username,
'role': role.apiValue,
'is_active': isActive,
if (createdAt != null) 'created_at': createdAt!.toIso8601String(),
};
UserModel copyWith({
String? id,
String? username,
UserRole? role,
bool? isActive,
DateTime? createdAt,
String? secret,
}) {
return UserModel(
id: id ?? this.id,
username: username ?? this.username,
role: role ?? this.role,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
secret: secret ?? this.secret,
);
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import '../services/service_locator.dart';
import '../services/api/api_client.dart';
enum AuthStatus { initial, loading, authenticated, unauthenticated }
class AuthProvider extends ChangeNotifier {
AuthStatus _status = AuthStatus.initial;
String? _error;
String? _username;
AuthStatus get status => _status;
String? get error => _error;
String? get username => _username;
bool get isAuthenticated => _status == AuthStatus.authenticated;
Future<bool> login(String username, String secret) async {
_status = AuthStatus.loading;
_error = null;
notifyListeners();
try {
final token = await ServiceLocator().auth.login(username, secret);
ServiceLocator().setToken(token);
_username = username;
_status = AuthStatus.authenticated;
notifyListeners();
return true;
} on ApiException catch (e) {
_error = e.message;
_status = AuthStatus.unauthenticated;
notifyListeners();
return false;
} catch (e) {
_error = 'خطا در اتصال به سرور';
_status = AuthStatus.unauthenticated;
notifyListeners();
return false;
}
}
void logout() {
ServiceLocator().clearToken();
_username = null;
_status = AuthStatus.unauthenticated;
_error = null;
notifyListeners();
}
}

View File

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import '../models/group_model.dart';
import '../models/group_member_model.dart';
import '../services/service_locator.dart';
import '../services/api/api_client.dart';
class GroupProvider extends ChangeNotifier {
List<GroupModel> _groups = [];
final Map<String, List<GroupMemberModel>> _membersCache = {};
LoadStatus _status = LoadStatus.idle;
String? _error;
List<GroupModel> get groups => List.unmodifiable(_groups);
LoadStatus get status => _status;
String? get error => _error;
bool get isLoading => _status == LoadStatus.loading;
int get totalCount => _groups.length;
int get activeCount => _groups.where((g) => g.isActive).length;
List<GroupMemberModel> membersOf(String groupId) =>
List.unmodifiable(_membersCache[groupId] ?? []);
Future<void> loadGroups() async {
_status = LoadStatus.loading;
_error = null;
notifyListeners();
try {
_groups = await ServiceLocator().groups.getGroups();
_status = LoadStatus.success;
} on ApiException catch (e) {
_error = e.message;
_status = LoadStatus.error;
} catch (e) {
_error = 'خطا در دریافت گروه‌ها';
_status = LoadStatus.error;
}
notifyListeners();
}
Future<GroupModel?> createGroup(String name, String? description) async {
_error = null;
try {
final group = await ServiceLocator().groups.createGroup(name, description);
_groups = [..._groups, group];
_membersCache[group.id] = [];
notifyListeners();
return group;
} on ApiException catch (e) {
_error = e.message;
notifyListeners();
return null;
} catch (e) {
_error = 'خطا در ایجاد گروه';
notifyListeners();
return null;
}
}
Future<GroupMemberModel?> addMember(
String groupId,
String userId,
GroupRole role,
String? username,
) async {
_error = null;
try {
final member =
await ServiceLocator().groups.addMember(groupId, userId, role);
final withUsername = member.copyWith(username: username);
_membersCache.putIfAbsent(groupId, () => []).add(withUsername);
// Refresh member count on cached group
final idx = _groups.indexWhere((g) => g.id == groupId);
if (idx != -1) {
_groups[idx] = _groups[idx].copyWith(
memberCount: _membersCache[groupId]!.length,
);
}
notifyListeners();
return withUsername;
} on ApiException catch (e) {
_error = e.message;
notifyListeners();
return null;
} catch (e) {
_error = 'خطا در افزودن عضو';
notifyListeners();
return null;
}
}
Future<void> loadGroupMembers(String groupId) async {
try {
final members = await ServiceLocator().groups.getGroupMembers(groupId);
_membersCache[groupId] = members.toList();
notifyListeners();
} on ApiException catch (e) {
_error = e.message;
notifyListeners();
} catch (_) {
// Silently fail members may just not be loaded
}
}
void clearError() {
_error = null;
notifyListeners();
}
}
// Re-export to avoid importing user_provider for the enum
enum LoadStatus { idle, loading, success, error }

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../models/user_model.dart';
import '../services/service_locator.dart';
import '../services/api/api_client.dart';
enum LoadStatus { idle, loading, success, error }
class UserProvider extends ChangeNotifier {
List<UserModel> _users = [];
LoadStatus _status = LoadStatus.idle;
String? _error;
List<UserModel> get users => List.unmodifiable(_users);
LoadStatus get status => _status;
String? get error => _error;
bool get isLoading => _status == LoadStatus.loading;
int get totalCount => _users.length;
int get activeCount => _users.where((u) => u.isActive).length;
Future<void> loadUsers() async {
_status = LoadStatus.loading;
_error = null;
notifyListeners();
try {
_users = await ServiceLocator().users.getUsers();
_status = LoadStatus.success;
} on ApiException catch (e) {
_error = e.message;
_status = LoadStatus.error;
} catch (e) {
_error = 'خطا در دریافت کاربران';
_status = LoadStatus.error;
}
notifyListeners();
}
/// Returns the new user and its generated secret.
Future<({UserModel user, String secret})?> createUser(
String username,
UserRole role,
) async {
_error = null;
try {
final result = await ServiceLocator().users.createUser(username, role);
_users = [..._users, result.user];
notifyListeners();
return result;
} on ApiException catch (e) {
_error = e.message;
notifyListeners();
return null;
} catch (e) {
_error = 'خطا در ایجاد کاربر';
notifyListeners();
return null;
}
}
/// Returns the new secret string, or null on failure.
Future<String?> resetSecret(String userId) async {
_error = null;
try {
final secret = await ServiceLocator().users.resetSecret(userId);
return secret;
} on ApiException catch (e) {
_error = e.message;
notifyListeners();
return null;
} catch (e) {
_error = 'خطا در ریست رمز';
notifyListeners();
return null;
}
}
void clearError() {
_error = null;
notifyListeners();
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../screens/login_screen.dart';
import '../screens/dashboard_screen.dart';
import '../screens/users_screen.dart';
import '../screens/groups_screen.dart';
import '../screens/group_detail_screen.dart';
final appRouter = GoRouter(
initialLocation: '/login',
redirect: _guardRedirect,
routes: [
GoRoute(
path: '/login',
builder: (_, __) => const LoginScreen(),
),
GoRoute(
path: '/dashboard',
builder: (_, __) => const DashboardScreen(),
),
GoRoute(
path: '/users',
builder: (_, __) => const UsersScreen(),
),
GoRoute(
path: '/groups',
builder: (_, __) => const GroupsScreen(),
),
GoRoute(
path: '/groups/:id',
builder: (_, state) => GroupDetailScreen(
groupId: state.pathParameters['id']!,
),
),
],
);
String? _guardRedirect(BuildContext context, GoRouterState state) {
final auth = context.read<AuthProvider>();
final isLoginPage = state.matchedLocation == '/login';
if (!auth.isAuthenticated && !isLoginPage) {
return '/login';
}
if (auth.isAuthenticated && isLoginPage) {
return '/dashboard';
}
return null;
}

View File

@ -0,0 +1,301 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../providers/user_provider.dart';
import '../providers/group_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/app_sidebar.dart';
import '../widgets/responsive_layout.dart';
import '../widgets/stat_card.dart';
import '../config/app_config.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<UserProvider>().loadUsers();
context.read<GroupProvider>().loadGroups();
});
}
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
title: 'داشبورد',
sidebar: const AppSidebar(),
body: _DashboardBody(),
);
}
}
class _DashboardBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'داشبورد',
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
'خلاصه وضعیت سیستم NEDA',
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
if (AppConfig.debugMode)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppTheme.warning.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.warning.withValues(alpha: 0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.bug_report_rounded,
size: 14, color: AppTheme.warning),
const SizedBox(width: 6),
Text(
'Debug Mode',
style: TextStyle(
color: AppTheme.warning,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
const SizedBox(height: 28),
// Stat cards
_StatsGrid(),
const SizedBox(height: 28),
// Quick actions
const Text(
'دسترسی سریع',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
_QuickActions(),
const SizedBox(height: 28),
// Info banner (real API mode)
if (!AppConfig.debugMode)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.primary.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.primary.withValues(alpha: 0.2)),
),
child: Row(
children: [
const Icon(Icons.info_outline_rounded,
color: AppTheme.primary, size: 20),
const SizedBox(width: 12),
const Expanded(
child: Text(
'در حالت واقعی، لیست کاربران و گروه‌ها فقط شامل آیتم‌های ایجادشده در همین نشست می‌باشد. بک‌اند فاقد endpoint لیست است.',
style: TextStyle(
fontSize: 13,
color: AppTheme.primary,
),
),
),
],
),
),
],
),
);
}
}
class _StatsGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer2<UserProvider, GroupProvider>(
builder: (_, users, groups, __) {
return LayoutBuilder(
builder: (context, constraints) {
final crossAxisCount = constraints.maxWidth >= 700 ? 4 : 2;
return GridView.count(
crossAxisCount: crossAxisCount,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: constraints.maxWidth >= 700 ? 1.6 : 1.4,
children: [
StatCard(
title: 'کل کاربران',
value: users.isLoading ? '...' : '${users.totalCount}',
icon: Icons.people_rounded,
color: AppTheme.primary,
subtitle: '${users.activeCount} فعال',
),
StatCard(
title: 'کل گروه‌ها',
value: groups.isLoading ? '...' : '${groups.totalCount}',
icon: Icons.groups_rounded,
color: const Color(0xFF7C3AED),
subtitle: '${groups.activeCount} فعال',
),
StatCard(
title: 'کاربران غیرفعال',
value: users.isLoading
? '...'
: '${users.totalCount - users.activeCount}',
icon: Icons.person_off_rounded,
color: AppTheme.warning,
),
StatCard(
title: 'گروه‌های غیرفعال',
value: groups.isLoading
? '...'
: '${groups.totalCount - groups.activeCount}',
icon: Icons.group_off_rounded,
color: AppTheme.danger,
),
],
);
},
);
},
);
}
}
class _QuickActions extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final isWide = constraints.maxWidth >= 500;
return Wrap(
spacing: 12,
runSpacing: 12,
children: [
_ActionCard(
label: 'ایجاد کاربر جدید',
icon: Icons.person_add_rounded,
color: AppTheme.primary,
onTap: () => context.go('/users'),
),
_ActionCard(
label: 'ایجاد گروه جدید',
icon: Icons.group_add_rounded,
color: const Color(0xFF7C3AED),
onTap: () => context.go('/groups'),
),
_ActionCard(
label: 'مدیریت کاربران',
icon: Icons.manage_accounts_rounded,
color: const Color(0xFF0891B2),
onTap: () => context.go('/users'),
),
_ActionCard(
label: 'مدیریت گروه‌ها',
icon: Icons.settings_rounded,
color: AppTheme.success,
onTap: () => context.go('/groups'),
),
].map((w) => SizedBox(
width: isWide
? (constraints.maxWidth - 36) / 4
: (constraints.maxWidth - 12) / 2,
child: w,
)).toList(),
);
});
}
}
class _ActionCard extends StatelessWidget {
final String label;
final IconData icon;
final Color color;
final VoidCallback onTap;
const _ActionCard({
required this.label,
required this.icon,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(height: 12),
Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,501 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:provider/provider.dart';
import '../models/group_member_model.dart';
import '../models/user_model.dart';
import '../providers/group_provider.dart';
import '../providers/user_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/app_sidebar.dart';
import '../widgets/responsive_layout.dart';
class GroupDetailScreen extends StatefulWidget {
final String groupId;
const GroupDetailScreen({super.key, required this.groupId});
@override
State<GroupDetailScreen> createState() => _GroupDetailScreenState();
}
class _GroupDetailScreenState extends State<GroupDetailScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final groups = context.read<GroupProvider>();
groups.loadGroupMembers(widget.groupId);
if (groups.groups.isEmpty) groups.loadGroups();
context.read<UserProvider>().loadUsers();
});
}
@override
Widget build(BuildContext context) {
return Consumer<GroupProvider>(builder: (_, groupProvider, __) {
final group = groupProvider.groups
.where((g) => g.id == widget.groupId)
.firstOrNull;
return ResponsiveLayout(
title: group?.name ?? 'جزئیات گروه',
sidebar: const AppSidebar(),
body: _GroupDetailBody(
groupId: widget.groupId,
groupName: group?.name ?? '...',
groupDescription: group?.description,
isActive: group?.isActive ?? true,
createdAt: group?.createdAt,
),
);
});
}
}
class _GroupDetailBody extends StatelessWidget {
final String groupId;
final String groupName;
final String? groupDescription;
final bool isActive;
final DateTime? createdAt;
const _GroupDetailBody({
required this.groupId,
required this.groupName,
required this.groupDescription,
required this.isActive,
required this.createdAt,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Back button + Header
Row(
children: [
IconButton(
onPressed: () => context.go('/groups'),
icon: const Icon(Icons.arrow_back_rounded),
tooltip: 'بازگشت',
),
const SizedBox(width: 8),
Expanded(
child: Text(
groupName,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary,
),
),
),
ElevatedButton.icon(
onPressed: () => _showAddMemberDialog(context),
icon: const Icon(Icons.person_add_rounded, size: 18),
label: const Text('افزودن عضو'),
),
],
),
const SizedBox(height: 20),
// Group info card
Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: const Color(0xFF7C3AED).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.groups_rounded,
color: Color(0xFF7C3AED),
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
groupName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
),
const SizedBox(width: 10),
_StatusBadge(isActive: isActive),
],
),
if (groupDescription != null) ...[
const SizedBox(height: 4),
Text(
groupDescription!,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
],
const SizedBox(height: 4),
Text(
createdAt != null
? 'ایجاد شده در ${DateFormat('yyyy/MM/dd').format(createdAt!)}'
: '',
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 24),
// Members section
const Text(
'اعضای گروه',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 12),
_MembersTable(groupId: groupId),
],
),
);
}
void _showAddMemberDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => _AddMemberDialog(groupId: groupId),
);
}
}
// Add Member Dialog
class _AddMemberDialog extends StatefulWidget {
final String groupId;
const _AddMemberDialog({required this.groupId});
@override
State<_AddMemberDialog> createState() => _AddMemberDialogState();
}
class _AddMemberDialogState extends State<_AddMemberDialog> {
UserModel? _selectedUser;
GroupRole _role = GroupRole.member;
bool _loading = false;
String? _error;
Future<void> _submit() async {
if (_selectedUser == null) {
setState(() => _error = 'لطفاً یک کاربر انتخاب کنید');
return;
}
setState(() {
_loading = true;
_error = null;
});
final provider = context.read<GroupProvider>();
final currentMembers = provider.membersOf(widget.groupId);
if (currentMembers.any((m) => m.userId == _selectedUser!.id)) {
setState(() {
_loading = false;
_error = 'این کاربر قبلاً عضو این گروه است';
});
return;
}
final result = await provider.addMember(
widget.groupId,
_selectedUser!.id,
_role,
_selectedUser!.username,
);
if (!mounted) return;
setState(() => _loading = false);
if (result != null) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('«${_selectedUser!.username}» به گروه اضافه شد'),
backgroundColor: AppTheme.success,
),
);
} else {
setState(() => _error = provider.error);
}
}
@override
Widget build(BuildContext context) {
return Consumer<UserProvider>(builder: (_, userProvider, __) {
final users = userProvider.users;
return AlertDialog(
title: const Row(
children: [
Icon(Icons.person_add_rounded, color: AppTheme.primary),
SizedBox(width: 10),
Text('افزودن عضو به گروه'),
],
),
content: SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// User selector
DropdownButtonFormField<UserModel>(
initialValue: _selectedUser,
decoration: const InputDecoration(
labelText: 'انتخاب کاربر',
prefixIcon: Icon(Icons.person_outline_rounded),
),
hint: const Text('کاربر را انتخاب کنید'),
items: users
.map((u) => DropdownMenuItem(
value: u,
child: Text(
'${u.username} (${u.role.label})',
),
))
.toList(),
onChanged: (v) => setState(() => _selectedUser = v),
),
const SizedBox(height: 16),
// Role selector
DropdownButtonFormField<GroupRole>(
initialValue: _role,
decoration: const InputDecoration(
labelText: 'نقش در گروه',
prefixIcon: Icon(Icons.badge_outlined),
),
items: GroupRole.values
.map((r) => DropdownMenuItem(
value: r,
child: Text(r.label),
))
.toList(),
onChanged: (v) => setState(() => _role = v!),
),
if (_error != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.danger.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.danger.withValues(alpha: 0.3)),
),
child: Text(
_error!,
style: const TextStyle(
color: AppTheme.danger, fontSize: 13),
),
),
],
],
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('انصراف'),
),
ElevatedButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: const Text('افزودن'),
),
],
);
});
}
}
// Members Table
class _MembersTable extends StatelessWidget {
final String groupId;
const _MembersTable({required this.groupId});
@override
Widget build(BuildContext context) {
return Consumer<GroupProvider>(builder: (_, provider, __) {
final members = provider.membersOf(groupId);
if (members.isEmpty) {
return Card(
child: Padding(
padding: const EdgeInsets.all(40),
child: Center(
child: Column(
children: [
const Icon(Icons.people_outline_rounded,
size: 56, color: AppTheme.border),
const SizedBox(height: 12),
const Text(
'هنوز عضوی به این گروه اضافه نشده است',
style: TextStyle(color: AppTheme.textSecondary),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => showDialog(
context: context,
builder: (_) => _AddMemberDialog(groupId: groupId),
),
icon: const Icon(Icons.person_add_rounded, size: 16),
label: const Text('افزودن اولین عضو'),
),
],
),
),
),
);
}
return Card(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: const [
DataColumn(label: Text('کاربر')),
DataColumn(label: Text('نقش در گروه')),
DataColumn(label: Text('تاریخ عضویت')),
],
rows: members.map((m) => _buildRow(m)).toList(),
),
),
),
),
);
});
}
DataRow _buildRow(GroupMemberModel member) {
final color = member.role == GroupRole.manager
? const Color(0xFF0891B2)
: AppTheme.textSecondary;
return DataRow(
cells: [
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 16,
backgroundColor: AppTheme.primary.withValues(alpha: 0.12),
child: Text(
(member.username ?? member.userId)[0].toUpperCase(),
style: const TextStyle(
color: AppTheme.primary,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 10),
Text(
member.username ?? member.userId,
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
DataCell(
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Text(
member.role.label,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
DataCell(
Text(
member.joinedAt != null
? DateFormat('yyyy/MM/dd').format(member.joinedAt!)
: '',
style: const TextStyle(color: AppTheme.textSecondary),
),
),
],
);
}
}
// Status Badge
class _StatusBadge extends StatelessWidget {
final bool isActive;
const _StatusBadge({required this.isActive});
@override
Widget build(BuildContext context) {
final color = isActive ? AppTheme.success : AppTheme.danger;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Text(
isActive ? 'فعال' : 'غیرفعال',
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
}

View File

@ -0,0 +1,480 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:provider/provider.dart';
import '../models/group_model.dart';
import '../providers/group_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/app_sidebar.dart';
import '../widgets/responsive_layout.dart';
class GroupsScreen extends StatefulWidget {
const GroupsScreen({super.key});
@override
State<GroupsScreen> createState() => _GroupsScreenState();
}
class _GroupsScreenState extends State<GroupsScreen> {
final _searchCtrl = TextEditingController();
String _search = '';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<GroupProvider>().loadGroups();
});
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
title: 'گروه‌ها',
sidebar: const AppSidebar(),
body: _GroupsBody(
search: _search,
searchCtrl: _searchCtrl,
onSearch: (v) => setState(() => _search = v.toLowerCase()),
),
);
}
}
class _GroupsBody extends StatelessWidget {
final String search;
final TextEditingController searchCtrl;
final ValueChanged<String> onSearch;
const _GroupsBody({
required this.search,
required this.searchCtrl,
required this.onSearch,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'مدیریت گروه‌ها',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 4),
Text(
'ایجاد و مدیریت گروه‌های ارتباطی',
style: TextStyle(
fontSize: 14, color: AppTheme.textSecondary),
),
],
),
),
ElevatedButton.icon(
onPressed: () => _showCreateDialog(context),
icon: const Icon(Icons.group_add_rounded, size: 18),
label: const Text('گروه جدید'),
),
],
),
const SizedBox(height: 20),
// Search
TextField(
controller: searchCtrl,
onChanged: onSearch,
decoration: const InputDecoration(
hintText: 'جستجوی گروه...',
prefixIcon: Icon(Icons.search_rounded),
),
),
const SizedBox(height: 16),
// Table
Expanded(
child: Consumer<GroupProvider>(
builder: (_, provider, __) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.status == LoadStatus.error) {
return _ErrorView(
message: provider.error ?? 'خطا',
onRetry: () => provider.loadGroups(),
);
}
final filtered = provider.groups
.where((g) =>
search.isEmpty ||
g.name.toLowerCase().contains(search) ||
(g.description?.toLowerCase().contains(search) ??
false))
.toList();
if (filtered.isEmpty) {
return const _EmptyView(
icon: Icons.groups_outlined,
message: 'گروهی یافت نشد',
);
}
return _GroupsTable(groups: filtered);
},
),
),
],
),
);
}
void _showCreateDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => const _CreateGroupDialog(),
);
}
}
// Create Group Dialog
class _CreateGroupDialog extends StatefulWidget {
const _CreateGroupDialog();
@override
State<_CreateGroupDialog> createState() => _CreateGroupDialogState();
}
class _CreateGroupDialogState extends State<_CreateGroupDialog> {
final _formKey = GlobalKey<FormState>();
final _nameCtrl = TextEditingController();
final _descCtrl = TextEditingController();
bool _loading = false;
String? _error;
@override
void dispose() {
_nameCtrl.dispose();
_descCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
final provider = context.read<GroupProvider>();
final group = await provider.createGroup(
_nameCtrl.text.trim(),
_descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
);
if (!mounted) return;
setState(() => _loading = false);
if (group != null) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('گروه «${group.name}» با موفقیت ایجاد شد'),
backgroundColor: AppTheme.success,
),
);
} else {
setState(() => _error = provider.error);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.group_add_rounded, color: AppTheme.primary),
SizedBox(width: 10),
Text('ایجاد گروه جدید'),
],
),
content: SizedBox(
width: 400,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _nameCtrl,
decoration: const InputDecoration(
labelText: 'نام گروه',
prefixIcon: Icon(Icons.groups_rounded),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'نام گروه الزامی است' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _descCtrl,
maxLines: 3,
decoration: const InputDecoration(
labelText: 'توضیحات (اختیاری)',
prefixIcon: Icon(Icons.description_outlined),
alignLabelWithHint: true,
),
),
if (_error != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.danger.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.danger.withValues(alpha: 0.3)),
),
child: Text(
_error!,
style: const TextStyle(
color: AppTheme.danger, fontSize: 13),
),
),
],
],
),
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('انصراف'),
),
ElevatedButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: const Text('ایجاد'),
),
],
);
}
}
// Groups Table
class _GroupsTable extends StatelessWidget {
final List<GroupModel> groups;
const _GroupsTable({required this.groups});
@override
Widget build(BuildContext context) {
return Card(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: const [
DataColumn(label: Text('نام گروه')),
DataColumn(label: Text('توضیحات')),
DataColumn(label: Text('اعضا')),
DataColumn(label: Text('وضعیت')),
DataColumn(label: Text('تاریخ ایجاد')),
DataColumn(label: Text('عملیات')),
],
rows: groups
.map((g) => _buildRow(context, g))
.toList(),
),
),
),
),
);
}
DataRow _buildRow(BuildContext context, GroupModel group) {
return DataRow(
cells: [
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: const Color(0xFF7C3AED).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.groups_rounded,
color: Color(0xFF7C3AED),
size: 18,
),
),
const SizedBox(width: 10),
Text(
group.name,
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
DataCell(
SizedBox(
width: 180,
child: Text(
group.description ?? '',
style: const TextStyle(color: AppTheme.textSecondary),
overflow: TextOverflow.ellipsis,
),
),
),
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.people_rounded,
size: 14, color: AppTheme.textSecondary),
const SizedBox(width: 4),
Text('${group.memberCount}'),
],
),
),
DataCell(_StatusBadge(isActive: group.isActive)),
DataCell(
Text(
group.createdAt != null
? DateFormat('yyyy/MM/dd').format(group.createdAt!)
: '',
style: const TextStyle(color: AppTheme.textSecondary),
),
),
DataCell(
TextButton.icon(
onPressed: () => context.go('/groups/${group.id}'),
icon: const Icon(Icons.arrow_forward_rounded, size: 16),
label: const Text('جزئیات'),
),
),
],
);
}
}
// Shared small widgets
class _StatusBadge extends StatelessWidget {
final bool isActive;
const _StatusBadge({required this.isActive});
@override
Widget build(BuildContext context) {
final color = isActive ? AppTheme.success : AppTheme.danger;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 5),
Text(
isActive ? 'فعال' : 'غیرفعال',
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
class _EmptyView extends StatelessWidget {
final IconData icon;
final String message;
const _EmptyView({required this.icon, required this.message});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 64, color: AppTheme.border),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
color: AppTheme.textSecondary, fontSize: 16),
),
],
),
);
}
}
class _ErrorView extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorView({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline_rounded,
size: 64, color: AppTheme.danger),
const SizedBox(height: 16),
Text(message,
style: const TextStyle(color: AppTheme.textSecondary)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh_rounded),
label: const Text('تلاش مجدد'),
),
],
),
);
}
}

View File

@ -0,0 +1,275 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../config/app_config.dart';
import '../providers/auth_provider.dart';
import '../theme/app_theme.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _usernameCtrl = TextEditingController();
final _secretCtrl = TextEditingController();
bool _obscure = true;
@override
void dispose() {
_usernameCtrl.dispose();
_secretCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
final auth = context.read<AuthProvider>();
final ok = await auth.login(
_usernameCtrl.text.trim(),
_secretCtrl.text.trim(),
);
if (ok && mounted) {
context.go('/dashboard');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.sidebarBg,
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.circular(20),
),
child: const Icon(
Icons.radio_rounded,
color: Colors.white,
size: 38,
),
),
const SizedBox(height: 16),
const Text(
'NEDA',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w800,
letterSpacing: 3,
),
),
const SizedBox(height: 6),
const Text(
'پنل مدیریت سیستم',
style: TextStyle(
color: AppTheme.sidebarText,
fontSize: 14,
),
),
const SizedBox(height: 40),
// Card
Container(
padding: const EdgeInsets.all(28),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 40,
offset: const Offset(0, 8),
),
],
),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'ورود به حساب',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
const Text(
'اطلاعات مدیر سیستم را وارد کنید',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 24),
// Username field
TextFormField(
controller: _usernameCtrl,
textDirection: TextDirection.ltr,
decoration: const InputDecoration(
labelText: 'نام کاربری',
prefixIcon: Icon(Icons.person_outline_rounded),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'نام کاربری الزامی است' : null,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
// Secret field
TextFormField(
controller: _secretCtrl,
obscureText: _obscure,
textDirection: TextDirection.ltr,
decoration: InputDecoration(
labelText: 'رمز عبور',
prefixIcon: const Icon(Icons.lock_outline_rounded),
suffixIcon: IconButton(
icon: Icon(
_obscure
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
onPressed: () =>
setState(() => _obscure = !_obscure),
),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'رمز عبور الزامی است' : null,
onFieldSubmitted: (_) => _submit(),
),
const SizedBox(height: 8),
// Error message
Consumer<AuthProvider>(
builder: (_, auth, __) {
if (auth.error == null) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.danger.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.danger.withValues(alpha: 0.3)),
),
child: Row(
children: [
const Icon(Icons.error_outline_rounded,
color: AppTheme.danger, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
auth.error!,
style: const TextStyle(
color: AppTheme.danger,
fontSize: 13,
),
),
),
],
),
),
);
},
),
const SizedBox(height: 24),
// Login button
Consumer<AuthProvider>(
builder: (_, auth, __) => SizedBox(
height: 48,
child: ElevatedButton(
onPressed: auth.status == AuthStatus.loading
? null
: _submit,
child: auth.status == AuthStatus.loading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text('ورود'),
),
),
),
],
),
),
),
// Debug hint
if (AppConfig.debugMode) ...[
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.warning.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppTheme.warning.withValues(alpha: 0.3)),
),
child: const Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.bug_report_rounded,
size: 14, color: AppTheme.warning),
SizedBox(width: 6),
Text(
'حالت آزمایشی فعال',
style: TextStyle(
color: AppTheme.warning,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 4),
Text(
'نام کاربری: ${AppConfig.mockAdminUsername} | رمز: ${AppConfig.mockAdminSecret}',
style: TextStyle(
color: AppTheme.sidebarText,
fontSize: 11,
),
),
],
),
),
],
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,551 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:provider/provider.dart';
import '../models/user_model.dart';
import '../providers/user_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/app_sidebar.dart';
import '../widgets/responsive_layout.dart';
import '../widgets/secret_dialog.dart';
class UsersScreen extends StatefulWidget {
const UsersScreen({super.key});
@override
State<UsersScreen> createState() => _UsersScreenState();
}
class _UsersScreenState extends State<UsersScreen> {
final _searchCtrl = TextEditingController();
String _search = '';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<UserProvider>().loadUsers();
});
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
title: 'کاربران',
sidebar: const AppSidebar(),
body: _UsersBody(
search: _search,
searchCtrl: _searchCtrl,
onSearch: (v) => setState(() => _search = v.toLowerCase()),
),
);
}
}
class _UsersBody extends StatelessWidget {
final String search;
final TextEditingController searchCtrl;
final ValueChanged<String> onSearch;
const _UsersBody({
required this.search,
required this.searchCtrl,
required this.onSearch,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'مدیریت کاربران',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 4),
Text(
'ایجاد و مدیریت کاربران سیستم',
style: TextStyle(
fontSize: 14, color: AppTheme.textSecondary),
),
],
),
),
ElevatedButton.icon(
onPressed: () => _showCreateDialog(context),
icon: const Icon(Icons.person_add_rounded, size: 18),
label: const Text('کاربر جدید'),
),
],
),
const SizedBox(height: 20),
// Search
TextField(
controller: searchCtrl,
onChanged: onSearch,
decoration: const InputDecoration(
hintText: 'جستجوی کاربر...',
prefixIcon: Icon(Icons.search_rounded),
),
),
const SizedBox(height: 16),
// Table
Expanded(
child: Consumer<UserProvider>(
builder: (_, provider, __) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.status.name == 'error') {
return _ErrorView(
message: provider.error ?? 'خطا',
onRetry: () => provider.loadUsers());
}
final filtered = provider.users
.where((u) =>
search.isEmpty ||
u.username.toLowerCase().contains(search) ||
u.role.label.toLowerCase().contains(search))
.toList();
if (filtered.isEmpty) {
return const _EmptyView(
icon: Icons.people_outline_rounded,
message: 'کاربری یافت نشد',
);
}
return _UsersTable(users: filtered);
},
),
),
],
),
);
}
void _showCreateDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => const _CreateUserDialog(),
);
}
}
// Create User Dialog
class _CreateUserDialog extends StatefulWidget {
const _CreateUserDialog();
@override
State<_CreateUserDialog> createState() => _CreateUserDialogState();
}
class _CreateUserDialogState extends State<_CreateUserDialog> {
final _formKey = GlobalKey<FormState>();
final _usernameCtrl = TextEditingController();
UserRole _role = UserRole.member;
bool _loading = false;
String? _error;
@override
void dispose() {
_usernameCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
final provider = context.read<UserProvider>();
final result = await provider.createUser(
_usernameCtrl.text.trim(),
_role,
);
if (!mounted) return;
setState(() => _loading = false);
if (result != null) {
Navigator.of(context).pop();
await SecretDialog.show(
context,
username: result.user.username,
secret: result.secret,
);
} else {
setState(() => _error = provider.error);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.person_add_rounded, color: AppTheme.primary),
SizedBox(width: 10),
Text('ایجاد کاربر جدید'),
],
),
content: SizedBox(
width: 400,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _usernameCtrl,
textDirection: TextDirection.ltr,
decoration: const InputDecoration(
labelText: 'نام کاربری',
prefixIcon: Icon(Icons.person_outline_rounded),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'نام کاربری الزامی است' : null,
),
const SizedBox(height: 16),
DropdownButtonFormField<UserRole>(
initialValue: _role,
decoration: const InputDecoration(
labelText: 'نقش',
prefixIcon: Icon(Icons.badge_outlined),
),
items: UserRole.values
.map((r) => DropdownMenuItem(
value: r,
child: Text(r.label),
))
.toList(),
onChanged: (v) => setState(() => _role = v!),
),
if (_error != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.danger.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.danger.withValues(alpha: 0.3)),
),
child: Text(
_error!,
style: const TextStyle(color: AppTheme.danger, fontSize: 13),
),
),
],
],
),
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('انصراف'),
),
ElevatedButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: const Text('ایجاد'),
),
],
);
}
}
// Users Table
class _UsersTable extends StatelessWidget {
final List<UserModel> users;
const _UsersTable({required this.users});
@override
Widget build(BuildContext context) {
return Card(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: const [
DataColumn(label: Text('نام کاربری')),
DataColumn(label: Text('نقش')),
DataColumn(label: Text('وضعیت')),
DataColumn(label: Text('تاریخ ایجاد')),
DataColumn(label: Text('عملیات')),
],
rows: users.map((user) => _buildRow(context, user)).toList(),
),
),
),
),
);
}
DataRow _buildRow(BuildContext context, UserModel user) {
return DataRow(
cells: [
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 16,
backgroundColor: AppTheme.primary.withValues(alpha: 0.12),
child: Text(
user.username[0].toUpperCase(),
style: const TextStyle(
color: AppTheme.primary,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 10),
Text(
user.username,
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
DataCell(_RoleBadge(role: user.role)),
DataCell(_StatusBadge(isActive: user.isActive)),
DataCell(
Text(
user.createdAt != null
? DateFormat('yyyy/MM/dd').format(user.createdAt!)
: '',
style: const TextStyle(color: AppTheme.textSecondary),
),
),
DataCell(
_ResetSecretButton(user: user),
),
],
);
}
}
class _ResetSecretButton extends StatefulWidget {
final UserModel user;
const _ResetSecretButton({required this.user});
@override
State<_ResetSecretButton> createState() => _ResetSecretButtonState();
}
class _ResetSecretButtonState extends State<_ResetSecretButton> {
bool _loading = false;
Future<void> _reset() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('تأیید ریست رمز'),
content: Text(
'آیا مطمئن هستید که می‌خواهید رمز «${widget.user.username}» را ریست کنید؟'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('انصراف'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.warning),
child: const Text('ریست'),
),
],
),
);
if (confirmed != true || !mounted) return;
setState(() => _loading = true);
final provider = context.read<UserProvider>();
final secret = await provider.resetSecret(widget.user.id);
if (!mounted) return;
setState(() => _loading = false);
if (secret != null) {
await SecretDialog.show(
context,
username: widget.user.username,
secret: secret,
isReset: true,
);
} else if (provider.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(provider.error!),
backgroundColor: AppTheme.danger,
),
);
}
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2));
}
return TextButton.icon(
onPressed: _reset,
icon: const Icon(Icons.lock_reset_rounded, size: 16),
label: const Text('ریست رمز'),
style: TextButton.styleFrom(foregroundColor: AppTheme.warning),
);
}
}
// Shared small widgets
class _RoleBadge extends StatelessWidget {
final UserRole role;
const _RoleBadge({required this.role});
@override
Widget build(BuildContext context) {
final color = AppTheme.roleColor(role.name);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Text(
role.label,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _StatusBadge extends StatelessWidget {
final bool isActive;
const _StatusBadge({required this.isActive});
@override
Widget build(BuildContext context) {
final color = isActive ? AppTheme.success : AppTheme.danger;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 5),
Text(
isActive ? 'فعال' : 'غیرفعال',
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
class _EmptyView extends StatelessWidget {
final IconData icon;
final String message;
const _EmptyView({required this.icon, required this.message});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 64, color: AppTheme.border),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
color: AppTheme.textSecondary, fontSize: 16),
),
],
),
);
}
}
class _ErrorView extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorView({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline_rounded,
size: 64, color: AppTheme.danger),
const SizedBox(height: 16),
Text(message,
style: const TextStyle(color: AppTheme.textSecondary)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh_rounded),
label: const Text('تلاش مجدد'),
),
],
),
);
}
}

View File

@ -0,0 +1,54 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiException implements Exception {
final int statusCode;
final String message;
const ApiException({required this.statusCode, required this.message});
@override
String toString() => 'ApiException($statusCode): $message';
}
class ApiClient {
final String baseUrl;
String? _token;
ApiClient({required this.baseUrl});
void setToken(String token) => _token = token;
void clearToken() => _token = null;
Map<String, String> get _headers => {
'Content-Type': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};
Future<dynamic> post(String path, Map<String, dynamic> body) async {
final response = await http.post(
Uri.parse('$baseUrl$path'),
headers: _headers,
body: jsonEncode(body),
);
return _handleResponse(response);
}
Future<dynamic> get(String path) async {
final response = await http.get(
Uri.parse('$baseUrl$path'),
headers: _headers,
);
return _handleResponse(response);
}
dynamic _handleResponse(http.Response response) {
if (response.body.isEmpty) return null;
final data = jsonDecode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
return data;
}
final detail = data is Map ? (data['detail'] ?? 'Unknown error') : 'Unknown error';
throw ApiException(statusCode: response.statusCode, message: detail.toString());
}
}

View File

@ -0,0 +1,17 @@
import '../interfaces/auth_service.dart';
import 'api_client.dart';
class AuthApiService implements AuthService {
final ApiClient _client;
AuthApiService(this._client);
@override
Future<String> login(String username, String secret) async {
final data = await _client.post('/auth/login', {
'username': username,
'secret': secret,
});
return data['access_token'] as String;
}
}

View File

@ -0,0 +1,61 @@
import '../../models/group_model.dart';
import '../../models/group_member_model.dart';
import '../interfaces/group_service.dart';
import 'api_client.dart';
/// Real API implementation.
/// NOTE: The backend has no list-groups or list-members endpoint.
/// Groups and members are tracked in memory per session.
class GroupApiService implements GroupService {
final ApiClient _client;
final List<GroupModel> _sessionGroups = [];
final Map<String, List<GroupMemberModel>> _sessionMembers = {};
GroupApiService(this._client);
@override
Future<List<GroupModel>> getGroups() async {
return List.unmodifiable(_sessionGroups);
}
@override
Future<GroupModel> createGroup(String name, String? description) async {
final body = <String, dynamic>{'name': name};
if (description != null && description.isNotEmpty) {
body['description'] = description;
}
final data = await _client.post('/groups/', body);
final group = GroupModel.fromJson(data as Map<String, dynamic>);
_sessionGroups.add(group);
_sessionMembers[group.id] = [];
return group;
}
@override
Future<GroupMemberModel> addMember(
String groupId,
String userId,
GroupRole role,
) async {
final data = await _client.post('/groups/$groupId/members', {
'user_id': userId,
'role': role.apiValue,
});
final member = GroupMemberModel.fromJson(data as Map<String, dynamic>);
_sessionMembers.putIfAbsent(groupId, () => []).add(member);
// Update member count on the cached group
final idx = _sessionGroups.indexWhere((g) => g.id == groupId);
if (idx != -1) {
_sessionGroups[idx] = _sessionGroups[idx].copyWith(
memberCount: (_sessionMembers[groupId]?.length ?? 1),
);
}
return member;
}
@override
Future<List<GroupMemberModel>> getGroupMembers(String groupId) async {
return List.unmodifiable(_sessionMembers[groupId] ?? []);
}
}

View File

@ -0,0 +1,39 @@
import '../../models/user_model.dart';
import '../interfaces/user_service.dart';
import 'api_client.dart';
/// Real API implementation.
/// NOTE: The backend has no list-users endpoint, so [getUsers] returns only
/// users created during the current session (stored in memory).
class UserApiService implements UserService {
final ApiClient _client;
final List<UserModel> _sessionUsers = [];
UserApiService(this._client);
@override
Future<List<UserModel>> getUsers() async {
return List.unmodifiable(_sessionUsers);
}
@override
Future<CreateUserResult> createUser(String username, UserRole role) async {
final data = await _client.post('/admin/users', {
'username': username,
'role': role.apiValue,
});
final user = UserModel.fromJson(data['user'] as Map<String, dynamic>);
final secret = data['secret'] as String;
_sessionUsers.add(user);
return (user: user, secret: secret);
}
@override
Future<String> resetSecret(String userId) async {
final data = await _client.post(
'/admin/users/$userId/reset-secret',
{},
);
return data['secret'] as String;
}
}

View File

@ -0,0 +1,4 @@
abstract class AuthService {
/// Authenticates the user and returns a JWT token.
Future<String> login(String username, String secret);
}

View File

@ -0,0 +1,20 @@
import '../../models/group_model.dart';
import '../../models/group_member_model.dart';
abstract class GroupService {
/// Returns all groups.
Future<List<GroupModel>> getGroups();
/// Creates a new group.
Future<GroupModel> createGroup(String name, String? description);
/// Adds a user to a group with the given role.
Future<GroupMemberModel> addMember(
String groupId,
String userId,
GroupRole role,
);
/// Returns all members of a group.
Future<List<GroupMemberModel>> getGroupMembers(String groupId);
}

View File

@ -0,0 +1,15 @@
import '../../models/user_model.dart';
typedef CreateUserResult = ({UserModel user, String secret});
abstract class UserService {
/// Returns all users.
/// In real-API mode, returns only in-session created users (no list endpoint).
Future<List<UserModel>> getUsers();
/// Creates a new user and returns the user along with the generated secret.
Future<CreateUserResult> createUser(String username, UserRole role);
/// Resets the secret for [userId] and returns the new secret.
Future<String> resetSecret(String userId);
}

View File

@ -0,0 +1,16 @@
import '../../config/app_config.dart';
import '../interfaces/auth_service.dart';
import '../api/api_client.dart';
class MockAuthService implements AuthService {
@override
Future<String> login(String username, String secret) async {
await Future.delayed(const Duration(milliseconds: 600));
if (username == AppConfig.mockAdminUsername &&
secret == AppConfig.mockAdminSecret) {
return 'mock_jwt_token_for_debug_mode';
}
throw const ApiException(statusCode: 401, message: 'نام کاربری یا رمز اشتباه است');
}
}

View File

@ -0,0 +1,171 @@
import '../../models/group_member_model.dart';
import '../../models/group_model.dart';
import '../../models/user_model.dart';
/// Static seed data used when [AppConfig.debugMode] is true.
class MockData {
static final List<UserModel> users = [
UserModel(
id: 'u-0001',
username: 'admin',
role: UserRole.admin,
isActive: true,
createdAt: DateTime(2025, 1, 10),
),
UserModel(
id: 'u-0002',
username: 'ali_karimi',
role: UserRole.group_manager,
isActive: true,
createdAt: DateTime(2025, 2, 3),
),
UserModel(
id: 'u-0003',
username: 'sara_mohammadi',
role: UserRole.member,
isActive: true,
createdAt: DateTime(2025, 2, 5),
),
UserModel(
id: 'u-0004',
username: 'reza_ahmadi',
role: UserRole.member,
isActive: true,
createdAt: DateTime(2025, 2, 10),
),
UserModel(
id: 'u-0005',
username: 'maryam_hosseini',
role: UserRole.group_manager,
isActive: true,
createdAt: DateTime(2025, 2, 15),
),
UserModel(
id: 'u-0006',
username: 'javad_rezaei',
role: UserRole.member,
isActive: false,
createdAt: DateTime(2025, 3, 1),
),
UserModel(
id: 'u-0007',
username: 'nasrin_bagheri',
role: UserRole.member,
isActive: true,
createdAt: DateTime(2025, 3, 5),
),
UserModel(
id: 'u-0008',
username: 'hamed_safari',
role: UserRole.member,
isActive: true,
createdAt: DateTime(2025, 3, 10),
),
UserModel(
id: 'u-0009',
username: 'leila_moradi',
role: UserRole.member,
isActive: true,
createdAt: DateTime(2025, 4, 2),
),
UserModel(
id: 'u-0010',
username: 'mehdi_tavakoli',
role: UserRole.group_manager,
isActive: false,
createdAt: DateTime(2025, 4, 8),
),
UserModel(
id: 'u-0011',
username: 'fatemeh_nazari',
role: UserRole.member,
isActive: true,
createdAt: DateTime(2025, 5, 1),
),
UserModel(
id: 'u-0012',
username: 'omid_shahidi',
role: UserRole.member,
isActive: true,
createdAt: DateTime(2025, 5, 15),
),
];
static final List<GroupModel> groups = [
GroupModel(
id: 'g-0001',
name: 'تیم آلفا',
description: 'واحد عملیاتی اصلی',
isActive: true,
type: GroupType.group,
createdAt: DateTime(2025, 1, 15),
memberCount: 4,
),
GroupModel(
id: 'g-0002',
name: 'تیم براوو',
description: 'واحد پشتیبانی و لجستیک',
isActive: true,
type: GroupType.group,
createdAt: DateTime(2025, 1, 20),
memberCount: 3,
),
GroupModel(
id: 'g-0003',
name: 'مرکز فرماندهی',
description: 'هماهنگی مرکزی تمام تیم‌ها',
isActive: true,
type: GroupType.group,
createdAt: DateTime(2025, 2, 1),
memberCount: 5,
),
GroupModel(
id: 'g-0004',
name: 'لجستیک',
description: 'مدیریت تجهیزات و منابع',
isActive: true,
type: GroupType.group,
createdAt: DateTime(2025, 2, 10),
memberCount: 3,
),
GroupModel(
id: 'g-0005',
name: 'واکنش اضطراری',
description: 'تیم پاسخ سریع به حوادث',
isActive: false,
type: GroupType.group,
createdAt: DateTime(2025, 3, 5),
memberCount: 2,
),
];
static final Map<String, List<GroupMemberModel>> memberships = {
'g-0001': [
GroupMemberModel(userId: 'u-0002', groupId: 'g-0001', role: GroupRole.manager, username: 'ali_karimi', joinedAt: DateTime(2025, 1, 15)),
GroupMemberModel(userId: 'u-0003', groupId: 'g-0001', role: GroupRole.member, username: 'sara_mohammadi', joinedAt: DateTime(2025, 1, 16)),
GroupMemberModel(userId: 'u-0004', groupId: 'g-0001', role: GroupRole.member, username: 'reza_ahmadi', joinedAt: DateTime(2025, 1, 17)),
GroupMemberModel(userId: 'u-0007', groupId: 'g-0001', role: GroupRole.member, username: 'nasrin_bagheri', joinedAt: DateTime(2025, 2, 1)),
],
'g-0002': [
GroupMemberModel(userId: 'u-0005', groupId: 'g-0002', role: GroupRole.manager, username: 'maryam_hosseini', joinedAt: DateTime(2025, 1, 20)),
GroupMemberModel(userId: 'u-0008', groupId: 'g-0002', role: GroupRole.member, username: 'hamed_safari', joinedAt: DateTime(2025, 1, 21)),
GroupMemberModel(userId: 'u-0009', groupId: 'g-0002', role: GroupRole.member, username: 'leila_moradi', joinedAt: DateTime(2025, 1, 22)),
],
'g-0003': [
GroupMemberModel(userId: 'u-0001', groupId: 'g-0003', role: GroupRole.manager, username: 'admin', joinedAt: DateTime(2025, 2, 1)),
GroupMemberModel(userId: 'u-0002', groupId: 'g-0003', role: GroupRole.member, username: 'ali_karimi', joinedAt: DateTime(2025, 2, 2)),
GroupMemberModel(userId: 'u-0005', groupId: 'g-0003', role: GroupRole.member, username: 'maryam_hosseini', joinedAt: DateTime(2025, 2, 3)),
GroupMemberModel(userId: 'u-0010', groupId: 'g-0003', role: GroupRole.member, username: 'mehdi_tavakoli', joinedAt: DateTime(2025, 2, 5)),
GroupMemberModel(userId: 'u-0011', groupId: 'g-0003', role: GroupRole.member, username: 'fatemeh_nazari', joinedAt: DateTime(2025, 2, 10)),
],
'g-0004': [
GroupMemberModel(userId: 'u-0010', groupId: 'g-0004', role: GroupRole.manager, username: 'mehdi_tavakoli', joinedAt: DateTime(2025, 2, 10)),
GroupMemberModel(userId: 'u-0011', groupId: 'g-0004', role: GroupRole.member, username: 'fatemeh_nazari', joinedAt: DateTime(2025, 2, 11)),
GroupMemberModel(userId: 'u-0012', groupId: 'g-0004', role: GroupRole.member, username: 'omid_shahidi', joinedAt: DateTime(2025, 2, 12)),
],
'g-0005': [
GroupMemberModel(userId: 'u-0002', groupId: 'g-0005', role: GroupRole.manager, username: 'ali_karimi', joinedAt: DateTime(2025, 3, 5)),
GroupMemberModel(userId: 'u-0006', groupId: 'g-0005', role: GroupRole.member, username: 'javad_rezaei', joinedAt: DateTime(2025, 3, 6)),
],
};
}

View File

@ -0,0 +1,78 @@
import 'package:uuid/uuid.dart';
import '../../models/group_model.dart';
import '../../models/group_member_model.dart';
import '../interfaces/group_service.dart';
import '../api/api_client.dart';
import 'mock_data.dart';
class MockGroupService implements GroupService {
final List<GroupModel> _groups = List.from(MockData.groups);
final Map<String, List<GroupMemberModel>> _members = {
for (final e in MockData.memberships.entries)
e.key: List.from(e.value),
};
final _uuid = const Uuid();
@override
Future<List<GroupModel>> getGroups() async {
await Future.delayed(const Duration(milliseconds: 400));
return List.unmodifiable(_groups);
}
@override
Future<GroupModel> createGroup(String name, String? description) async {
await Future.delayed(const Duration(milliseconds: 500));
final group = GroupModel(
id: 'g-${_uuid.v4().substring(0, 8)}',
name: name,
description: description,
isActive: true,
type: GroupType.group,
createdAt: DateTime.now(),
memberCount: 0,
);
_groups.add(group);
_members[group.id] = [];
return group;
}
@override
Future<GroupMemberModel> addMember(
String groupId,
String userId,
GroupRole role,
) async {
await Future.delayed(const Duration(milliseconds: 400));
final groupIdx = _groups.indexWhere((g) => g.id == groupId);
if (groupIdx == -1) {
throw const ApiException(statusCode: 404, message: 'گروه یافت نشد');
}
final existingMembers = _members.putIfAbsent(groupId, () => []);
if (existingMembers.any((m) => m.userId == userId)) {
throw const ApiException(statusCode: 400, message: 'کاربر قبلاً عضو این گروه است');
}
final member = GroupMemberModel(
userId: userId,
groupId: groupId,
role: role,
joinedAt: DateTime.now(),
);
existingMembers.add(member);
_groups[groupIdx] = _groups[groupIdx].copyWith(
memberCount: existingMembers.length,
);
return member;
}
@override
Future<List<GroupMemberModel>> getGroupMembers(String groupId) async {
await Future.delayed(const Duration(milliseconds: 300));
return List.unmodifiable(_members[groupId] ?? []);
}
}

View File

@ -0,0 +1,52 @@
import 'package:uuid/uuid.dart';
import '../../models/user_model.dart';
import '../interfaces/user_service.dart';
import '../api/api_client.dart';
import 'mock_data.dart';
class MockUserService implements UserService {
final List<UserModel> _users = List.from(MockData.users);
final _uuid = const Uuid();
@override
Future<List<UserModel>> getUsers() async {
await Future.delayed(const Duration(milliseconds: 400));
return List.unmodifiable(_users);
}
@override
Future<CreateUserResult> createUser(String username, UserRole role) async {
await Future.delayed(const Duration(milliseconds: 500));
if (_users.any((u) => u.username == username)) {
throw const ApiException(statusCode: 400, message: 'این نام کاربری قبلاً ثبت شده است');
}
final secret = _generateSecret();
final user = UserModel(
id: 'u-${_uuid.v4().substring(0, 8)}',
username: username,
role: role,
isActive: true,
createdAt: DateTime.now(),
);
_users.add(user);
return (user: user, secret: secret);
}
@override
Future<String> resetSecret(String userId) async {
await Future.delayed(const Duration(milliseconds: 400));
final idx = _users.indexWhere((u) => u.id == userId);
if (idx == -1) {
throw const ApiException(statusCode: 404, message: 'کاربر یافت نشد');
}
return _generateSecret();
}
String _generateSecret() {
final uuid = _uuid.v4().replaceAll('-', '');
return uuid.substring(0, 16);
}
}

View File

@ -0,0 +1,41 @@
import '../config/app_config.dart';
import 'interfaces/auth_service.dart';
import 'interfaces/user_service.dart';
import 'interfaces/group_service.dart';
import 'api/api_client.dart';
import 'api/auth_api_service.dart';
import 'api/user_api_service.dart';
import 'api/group_api_service.dart';
import 'mock/mock_auth_service.dart';
import 'mock/mock_user_service.dart';
import 'mock/mock_group_service.dart';
/// Singleton that wires together the correct service implementations
/// based on [AppConfig.debugMode].
class ServiceLocator {
static final ServiceLocator _instance = ServiceLocator._internal();
factory ServiceLocator() => _instance;
ServiceLocator._internal();
late final AuthService auth;
late final UserService users;
late final GroupService groups;
ApiClient? _apiClient;
void initialize() {
if (AppConfig.debugMode) {
auth = MockAuthService();
users = MockUserService();
groups = MockGroupService();
} else {
_apiClient = ApiClient(baseUrl: AppConfig.baseUrl);
auth = AuthApiService(_apiClient!);
users = UserApiService(_apiClient!);
groups = GroupApiService(_apiClient!);
}
}
void setToken(String token) => _apiClient?.setToken(token);
void clearToken() => _apiClient?.clearToken();
}

View File

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
class AppTheme {
// Color Palette
static const Color primary = Color(0xFF2563EB);
static const Color primaryDark = Color(0xFF1D4ED8);
static const Color sidebarBg = Color(0xFF0F172A);
static const Color sidebarSelected = Color(0xFF1E3A8A);
static const Color sidebarText = Color(0xFFCBD5E1);
static const Color sidebarTextActive = Color(0xFFFFFFFF);
static const Color background = Color(0xFFF1F5F9);
static const Color surface = Color(0xFFFFFFFF);
static const Color textPrimary = Color(0xFF0F172A);
static const Color textSecondary = Color(0xFF64748B);
static const Color success = Color(0xFF10B981);
static const Color warning = Color(0xFFF59E0B);
static const Color danger = Color(0xFFEF4444);
static const Color border = Color(0xFFE2E8F0);
// Role Colors
static Color roleColor(String role) {
switch (role) {
case 'admin':
return const Color(0xFF7C3AED);
case 'group_manager':
return const Color(0xFF0891B2);
default:
return const Color(0xFF64748B);
}
}
// Theme Data
static ThemeData get theme => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primary,
brightness: Brightness.light,
surface: surface,
),
scaffoldBackgroundColor: background,
fontFamily: 'Vazirmatn',
appBarTheme: const AppBarTheme(
backgroundColor: surface,
foregroundColor: textPrimary,
elevation: 0,
scrolledUnderElevation: 1,
shadowColor: border,
),
cardTheme: CardThemeData(
elevation: 0,
color: surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: border),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFFF8FAFC),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: danger),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
elevation: 0,
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
textStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: primary,
side: const BorderSide(color: primary),
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: primary,
),
),
dialogTheme: DialogThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 8,
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
dataTableTheme: DataTableThemeData(
headingRowColor: WidgetStateProperty.all(const Color(0xFFF8FAFC)),
dataRowColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return const Color(0xFFF1F5F9);
}
return surface;
}),
headingTextStyle: const TextStyle(
color: textSecondary,
fontWeight: FontWeight.w600,
fontSize: 13,
),
dataTextStyle: const TextStyle(
color: textPrimary,
fontSize: 14,
),
dividerThickness: 1,
columnSpacing: 24,
),
);
}

View File

@ -0,0 +1,239 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../config/app_config.dart';
import '../providers/auth_provider.dart';
import '../theme/app_theme.dart';
class AppSidebar extends StatelessWidget {
const AppSidebar({super.key});
static const _navItems = [
_NavItem(label: 'داشبورد', icon: Icons.dashboard_rounded, route: '/dashboard'),
_NavItem(label: 'کاربران', icon: Icons.people_rounded, route: '/users'),
_NavItem(label: 'گروه‌ها', icon: Icons.groups_rounded, route: '/groups'),
];
@override
Widget build(BuildContext context) {
final location = GoRouterState.of(context).matchedLocation;
return Container(
color: AppTheme.sidebarBg,
child: Column(
children: [
// Logo / Brand
Container(
padding: const EdgeInsets.fromLTRB(20, 32, 20, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.radio_rounded,
color: Colors.white,
size: 22,
),
),
const SizedBox(width: 12),
const Text(
'NEDA',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w800,
letterSpacing: 1.5,
),
),
],
),
const SizedBox(height: 6),
const Text(
'پنل مدیریت',
style: TextStyle(
color: AppTheme.sidebarText,
fontSize: 12,
),
),
],
),
),
// Debug badge
if (AppConfig.debugMode)
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: AppTheme.warning.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.warning.withValues(alpha: 0.4)),
),
child: const Row(
children: [
Icon(Icons.bug_report_rounded,
size: 14, color: AppTheme.warning),
SizedBox(width: 6),
Text(
'حالت آزمایشی',
style: TextStyle(
color: AppTheme.warning,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 12),
// Navigation items
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
children: _navItems.map((item) {
final isActive = location.startsWith(item.route) &&
(item.route != '/dashboard' || location == '/dashboard');
return _SidebarTile(item: item, isActive: isActive);
}).toList(),
),
),
// Admin info + Logout
const Divider(color: Color(0xFF1E293B), height: 1),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Consumer<AuthProvider>(
builder: (_, auth, __) => Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppTheme.primary.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.admin_panel_settings_rounded,
color: AppTheme.primary,
size: 20,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
auth.username ?? 'Admin',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
const Text(
'مدیر سیستم',
style: TextStyle(
color: AppTheme.sidebarText,
fontSize: 11,
),
),
],
),
),
],
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
context.read<AuthProvider>().logout();
context.go('/login');
},
icon: const Icon(Icons.logout_rounded, size: 16),
label: const Text('خروج'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.sidebarText,
side: const BorderSide(color: Color(0xFF334155)),
padding: const EdgeInsets.symmetric(vertical: 10),
),
),
),
],
),
),
],
),
);
}
}
class _SidebarTile extends StatelessWidget {
final _NavItem item;
final bool isActive;
const _SidebarTile({required this.item, required this.isActive});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isActive ? AppTheme.sidebarSelected : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
leading: Icon(
item.icon,
color: isActive ? Colors.white : AppTheme.sidebarText,
size: 20,
),
title: Text(
item.label,
style: TextStyle(
color: isActive ? Colors.white : AppTheme.sidebarText,
fontSize: 14,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
dense: true,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
onTap: () {
context.go(item.route);
// Close drawer if open
if (Scaffold.of(context).isDrawerOpen) {
Navigator.of(context).pop();
}
},
),
);
}
}
class _NavItem {
final String label;
final IconData icon;
final String route;
const _NavItem({
required this.label,
required this.icon,
required this.route,
});
}

View File

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
class ResponsiveLayout extends StatelessWidget {
final Widget sidebar;
final Widget body;
final String title;
const ResponsiveLayout({
super.key,
required this.sidebar,
required this.body,
required this.title,
});
static const double sidebarBreakpoint = 800;
static const double sidebarWidth = 240.0;
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final isWide = width >= sidebarBreakpoint;
if (isWide) {
return Scaffold(
body: Row(
children: [
SizedBox(width: sidebarWidth, child: sidebar),
Expanded(child: body),
],
),
);
}
// Narrow: use Drawer
return Scaffold(
appBar: AppBar(
title: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 18),
),
leading: Builder(
builder: (ctx) => IconButton(
icon: const Icon(Icons.menu_rounded),
onPressed: () => Scaffold.of(ctx).openDrawer(),
),
),
),
drawer: Drawer(
width: sidebarWidth,
child: sidebar,
),
body: body,
);
}
}

View File

@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_theme.dart';
/// Dialog shown after user creation or secret reset.
/// The secret is displayed only once user must copy it.
class SecretDialog extends StatefulWidget {
final String username;
final String secret;
final bool isReset;
const SecretDialog({
super.key,
required this.username,
required this.secret,
this.isReset = false,
});
static Future<void> show(
BuildContext context, {
required String username,
required String secret,
bool isReset = false,
}) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (_) => SecretDialog(
username: username,
secret: secret,
isReset: isReset,
),
);
}
@override
State<SecretDialog> createState() => _SecretDialogState();
}
class _SecretDialogState extends State<SecretDialog> {
bool _copied = false;
void _copy() async {
await Clipboard.setData(ClipboardData(text: widget.secret));
setState(() => _copied = true);
await Future.delayed(const Duration(seconds: 2));
if (mounted) setState(() => _copied = false);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.all(24),
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.success.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
widget.isReset
? Icons.lock_reset_rounded
: Icons.check_circle_rounded,
color: AppTheme.success,
size: 22,
),
),
const SizedBox(width: 12),
Text(
widget.isReset ? 'رمز ریست شد' : 'کاربر ایجاد شد',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Warning banner
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.warning.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.warning.withValues(alpha: 0.4)),
),
child: const Row(
children: [
Icon(Icons.warning_amber_rounded,
color: AppTheme.warning, size: 18),
SizedBox(width: 8),
Expanded(
child: Text(
'این رمز فقط یک‌بار نمایش داده می‌شود. حتماً آن را کپی کنید.',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
),
),
],
),
),
const SizedBox(height: 20),
// Username
const Text(
'نام کاربری:',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
widget.username,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 16),
// Secret
const Text(
'رمز عبور:',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.border),
),
child: Row(
children: [
Expanded(
child: Text(
widget.secret,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 16,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
color: AppTheme.textPrimary,
),
),
),
InkWell(
onTap: _copy,
borderRadius: BorderRadius.circular(6),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: _copied
? const Icon(Icons.check_rounded,
key: ValueKey('check'),
color: AppTheme.success,
size: 20)
: const Icon(Icons.copy_rounded,
key: ValueKey('copy'),
color: AppTheme.textSecondary,
size: 20),
),
),
],
),
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('متوجه شدم، بستن'),
),
],
);
}
}

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class StatCard extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color color;
final String? subtitle;
const StatCard({
super.key,
required this.title,
required this.value,
required this.icon,
required this.color,
this.subtitle,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 26),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary,
height: 1.1,
),
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
fontWeight: FontWeight.w500,
),
),
if (subtitle != null) ...[
const SizedBox(height: 2),
Text(
subtitle!,
style: TextStyle(
fontSize: 11,
color: color,
fontWeight: FontWeight.w600,
),
),
],
],
),
),
],
),
),
);
}
}

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

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

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 "admin_panel")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.admin_panel")
# 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,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

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,23 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<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)

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