progress
This commit is contained in:
parent
bee18daf4a
commit
dcada33b89
170
LINUX_SCRIPTS_README.md
Normal file
170
LINUX_SCRIPTS_README.md
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
# اسکریپتهای Linux برای Hesabix
|
||||||
|
|
||||||
|
این فایل شامل اسکریپتهای مفید برای اجرا و build کردن اپلیکیشن Flutter در Linux است.
|
||||||
|
|
||||||
|
## اسکریپتهای موجود
|
||||||
|
|
||||||
|
### 1. `run_linux.sh` - اجرای اپلیکیشن در Linux
|
||||||
|
|
||||||
|
اسکریپت اصلی برای اجرای اپلیکیشن Flutter در Linux Desktop.
|
||||||
|
|
||||||
|
**استفاده:**
|
||||||
|
```bash
|
||||||
|
./run_linux.sh [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**گزینهها:**
|
||||||
|
- `--project PATH`: مسیر پروژه Flutter (اختیاری)
|
||||||
|
- `--mode MODE`: نوع اجرا (debug/profile/release) - پیشفرض: debug
|
||||||
|
- `--build-dir DIR`: مسیر build directory - پیشفرض: build/linux
|
||||||
|
- `--clean`: پاک کردن build directory قبل از اجرا
|
||||||
|
- `--install-deps`: نصب وابستگیها قبل از اجرا
|
||||||
|
- `--api-base-url URL`: آدرس پایه API
|
||||||
|
- `--help`: نمایش راهنما
|
||||||
|
|
||||||
|
**نمونههای استفاده:**
|
||||||
|
```bash
|
||||||
|
# اجرای ساده
|
||||||
|
./run_linux.sh
|
||||||
|
|
||||||
|
# اجرا در حالت release
|
||||||
|
./run_linux.sh --mode release
|
||||||
|
|
||||||
|
# اجرا با پاک کردن build directory
|
||||||
|
./run_linux.sh --clean --mode debug
|
||||||
|
|
||||||
|
# اجرا با نصب وابستگیها
|
||||||
|
./run_linux.sh --install-deps
|
||||||
|
|
||||||
|
# اجرا با API base URL
|
||||||
|
./run_linux.sh --api-base-url http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `build_linux.sh` - Build کردن اپلیکیشن برای Linux
|
||||||
|
|
||||||
|
اسکریپت برای ایجاد executable مستقل از اپلیکیشن Flutter.
|
||||||
|
|
||||||
|
**استفاده:**
|
||||||
|
```bash
|
||||||
|
./build_linux.sh [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
**گزینهها:**
|
||||||
|
- `--project PATH`: مسیر پروژه Flutter (اختیاری)
|
||||||
|
- `--mode MODE`: نوع build (debug/profile/release) - پیشفرض: release
|
||||||
|
- `--build-dir DIR`: مسیر build directory - پیشفرض: build/linux
|
||||||
|
- `--output-dir DIR`: مسیر خروجی نهایی - پیشفرض: build/linux_release
|
||||||
|
- `--clean`: پاک کردن build directory قبل از build
|
||||||
|
- `--install-deps`: نصب وابستگیها قبل از build
|
||||||
|
- `--api-base-url URL`: آدرس پایه API
|
||||||
|
- `--archive`: ایجاد فایل tar.gz از خروجی
|
||||||
|
- `--help`: نمایش راهنما
|
||||||
|
|
||||||
|
**نمونههای استفاده:**
|
||||||
|
```bash
|
||||||
|
# Build ساده
|
||||||
|
./build_linux.sh
|
||||||
|
|
||||||
|
# Build در حالت debug
|
||||||
|
./build_linux.sh --mode debug
|
||||||
|
|
||||||
|
# Build با ایجاد archive
|
||||||
|
./build_linux.sh --archive
|
||||||
|
|
||||||
|
# Build کامل با پاک کردن و نصب وابستگیها
|
||||||
|
./build_linux.sh --clean --install-deps --archive
|
||||||
|
```
|
||||||
|
|
||||||
|
## وابستگیهای مورد نیاز
|
||||||
|
|
||||||
|
قبل از استفاده از این اسکریپتها، مطمئن شوید که وابستگیهای زیر نصب شدهاند:
|
||||||
|
|
||||||
|
### Ubuntu/Debian:
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install libgtk-3-dev cmake ninja-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fedora/RHEL:
|
||||||
|
```bash
|
||||||
|
sudo dnf install gtk3-devel cmake ninja-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arch Linux:
|
||||||
|
```bash
|
||||||
|
sudo pacman -S gtk3 cmake ninja
|
||||||
|
```
|
||||||
|
|
||||||
|
## نصب Flutter
|
||||||
|
|
||||||
|
اگر Flutter نصب نیست، میتوانید از snap استفاده کنید:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo snap install flutter --classic
|
||||||
|
```
|
||||||
|
|
||||||
|
یا از سایت رسمی Flutter دانلود کنید:
|
||||||
|
https://flutter.dev/docs/get-started/install/linux
|
||||||
|
|
||||||
|
## ویژگیهای جدید
|
||||||
|
|
||||||
|
### نصب خودکار وابستگیها
|
||||||
|
اسکریپتها بهطور خودکار وابستگیهای مورد نیاز را تشخیص داده و نصب میکنند:
|
||||||
|
- GTK+3 development libraries
|
||||||
|
- CMake
|
||||||
|
- Ninja build system
|
||||||
|
- Clang C++ compiler
|
||||||
|
- Build essential tools
|
||||||
|
|
||||||
|
### رفع مشکلات platform-specific
|
||||||
|
- مشکلات مربوط به `dart:html` (که فقط در web platform موجود است) بهطور خودکار رفع میشوند
|
||||||
|
- توابع download برای Linux desktop بهروزرسانی میشوند
|
||||||
|
- فایلهای اصلی پس از اجرا بازیابی میشوند
|
||||||
|
|
||||||
|
### پشتیبانی از توزیعهای مختلف
|
||||||
|
- Ubuntu/Debian (apt)
|
||||||
|
- Fedora/RHEL (dnf)
|
||||||
|
- Arch Linux (pacman)
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
1. **مسیر پروژه**: اسکریپتها بهطور خودکار پروژه Flutter را در `hesabixUI/hesabix_ui` پیدا میکنند.
|
||||||
|
|
||||||
|
2. **Mirror تنظیمات**: اسکریپتها از mirror چینی برای حل مشکل دسترسی به pub.dev استفاده میکنند.
|
||||||
|
|
||||||
|
3. **Build Directory**: فایلهای build شده در `build/linux` ذخیره میشوند.
|
||||||
|
|
||||||
|
4. **خروجی نهایی**: فایلهای قابل اجرا در `build/linux_release` (یا مسیر مشخص شده) قرار میگیرند.
|
||||||
|
|
||||||
|
5. **اجرای فایل نهایی**: پس از build، میتوانید فایل `hesabix_ui` را در مسیر خروجی اجرا کنید.
|
||||||
|
|
||||||
|
6. **بازیابی خودکار**: فایلهای اصلی پس از اجرا یا build به حالت اولیه بازمیگردند.
|
||||||
|
|
||||||
|
## عیبیابی
|
||||||
|
|
||||||
|
### خطای "Flutter یافت نشد"
|
||||||
|
- مطمئن شوید Flutter نصب شده است
|
||||||
|
- مسیر Flutter را به PATH اضافه کنید
|
||||||
|
- از snap استفاده کنید: `sudo snap install flutter --classic`
|
||||||
|
|
||||||
|
### خطای "GTK+3 development libraries یافت نشد"
|
||||||
|
- وابستگیهای GTK را نصب کنید (به بخش وابستگیها مراجعه کنید)
|
||||||
|
|
||||||
|
### خطای "CMake یافت نشد"
|
||||||
|
- CMake را نصب کنید: `sudo apt install cmake` (Ubuntu/Debian)
|
||||||
|
|
||||||
|
### خطای build
|
||||||
|
- از `--clean` استفاده کنید تا build directory پاک شود
|
||||||
|
- از `--install-deps` استفاده کنید تا وابستگیها نصب شوند
|
||||||
|
|
||||||
|
### خطای "dart:html is not available"
|
||||||
|
- این خطا بهطور خودکار توسط اسکریپت رفع میشود
|
||||||
|
- اگر همچنان رخ داد، از `--clean` استفاده کنید
|
||||||
|
|
||||||
|
### خطای "deprecated-literal-operator"
|
||||||
|
- این خطا مربوط به flutter_secure_storage_linux است
|
||||||
|
- اسکریپت بهطور خودکار compiler flags را تنظیم میکند
|
||||||
|
|
||||||
|
### خطای "Could not find compiler"
|
||||||
|
- اسکریپت بهطور خودکار clang و build-essential را نصب میکند
|
||||||
|
- اگر همچنان رخ داد، دستی نصب کنید: `sudo apt install clang build-essential`
|
||||||
303
build_linux.sh
Executable file
303
build_linux.sh
Executable file
|
|
@ -0,0 +1,303 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Build script for Flutter Linux Desktop in this repo.
|
||||||
|
# Creates a standalone executable for Linux.
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$SCRIPT_DIR"
|
||||||
|
|
||||||
|
DEFAULT_MODE="release" # debug|profile|release
|
||||||
|
DEFAULT_BUILD_DIR="build/linux"
|
||||||
|
DEFAULT_OUTPUT_DIR="build/linux_release"
|
||||||
|
|
||||||
|
USER_PROJECT=""
|
||||||
|
MODE="$DEFAULT_MODE"
|
||||||
|
BUILD_DIR=""
|
||||||
|
OUTPUT_DIR=""
|
||||||
|
CLEAN_BUILD=false
|
||||||
|
INSTALL_DEPS=false
|
||||||
|
API_BASE_URL=""
|
||||||
|
CREATE_ARCHIVE=false
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ./build_linux.sh [--project <path>] [--mode <debug|profile|release>] [--build-dir <dir>] [--output-dir <dir>] [--clean] [--install-deps] [--api-base-url <url>] [--archive] [--help]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--project PATH مسیر پروژه فلاتر (حاوی pubspec.yaml). در صورت عدم تعیین، بهصورت خودکار تشخیص میشود.
|
||||||
|
--mode MODE نوع build: debug، profile یا release (پیشفرض: $DEFAULT_MODE).
|
||||||
|
--build-dir DIR مسیر build directory (پیشفرض: $DEFAULT_BUILD_DIR).
|
||||||
|
--output-dir DIR مسیر خروجی نهایی (پیشفرض: $DEFAULT_OUTPUT_DIR).
|
||||||
|
--clean پاک کردن build directory قبل از build.
|
||||||
|
--install-deps نصب وابستگیها قبل از build.
|
||||||
|
--api-base-url آدرس پایه API که به برنامه بهصورت --dart-define پاس داده میشود.
|
||||||
|
--archive ایجاد فایل tar.gz از خروجی.
|
||||||
|
-h, --help نمایش راهنما.
|
||||||
|
|
||||||
|
نمونه اجرا:
|
||||||
|
./build_linux.sh
|
||||||
|
./build_linux.sh --mode debug --clean
|
||||||
|
./build_linux.sh --project hesabixUI/hesabix_ui --archive
|
||||||
|
./build_linux.sh --api-base-url http://localhost:8000 --mode release --archive
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() { echo "[warn] $*" >&2; }
|
||||||
|
die() { echo "[error] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
cmd_exists() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
ensure_flutter_in_path() {
|
||||||
|
if cmd_exists flutter; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local SNAP_FLUTTER_BIN="$HOME/snap/flutter/common/flutter/bin"
|
||||||
|
if [ -d "$SNAP_FLUTTER_BIN" ]; then
|
||||||
|
export PATH="$PATH:$SNAP_FLUTTER_BIN"
|
||||||
|
fi
|
||||||
|
if ! cmd_exists flutter; then
|
||||||
|
die "Flutter یافت نشد. لطفاً آنرا نصب کرده یا PATH را تنظیم کنید. مسیر پیشنهادی: $SNAP_FLUTTER_BIN"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_flutter_project_dir() {
|
||||||
|
local dir="$1"
|
||||||
|
[ -f "$dir/pubspec.yaml" ] || return 1
|
||||||
|
# حداقل بررسی: وجود sdk: flutter در pubspec.yaml
|
||||||
|
if grep -qiE "sdk:\s*flutter" "$dir/pubspec.yaml"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# برخی قالبها ممکن است شکل دیگری داشته باشند؛ صرف وجود pubspec را کافی بدانیم
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_detect_project_dir() {
|
||||||
|
# اولویت: آرگومان کاربر → متغیر محیطی → مسیر متداول → جستجو در hesabixUI
|
||||||
|
if [ -n "$USER_PROJECT" ]; then
|
||||||
|
local p="$USER_PROJECT"
|
||||||
|
[ -d "$p" ] || die "مسیر پروژه موجود نیست: $p"
|
||||||
|
is_flutter_project_dir "$p" || die "pubspec.yaml معتبر در مسیر یافت نشد: $p"
|
||||||
|
echo "$(cd "$p" && pwd)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${FLUTTER_APP_DIR:-}" ]; then
|
||||||
|
local p="$FLUTTER_APP_DIR"
|
||||||
|
if [ -d "$p" ] && is_flutter_project_dir "$p"; then
|
||||||
|
echo "$(cd "$p" && pwd)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# مسیر متداول این ریپو
|
||||||
|
local common_path="$REPO_ROOT/hesabixUI/hesabix_ui"
|
||||||
|
if [ -d "$common_path" ] && is_flutter_project_dir "$common_path"; then
|
||||||
|
echo "$common_path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# جستجو در hesabixUI برای نزدیکترین pubspec.yaml
|
||||||
|
local search_root="$REPO_ROOT/hesabixUI"
|
||||||
|
if [ -d "$search_root" ]; then
|
||||||
|
# محدود به عمق 3 برای سرعت
|
||||||
|
local found
|
||||||
|
found=$(find "$search_root" -maxdepth 3 -type f -name pubspec.yaml 2>/dev/null | head -n 1 || true)
|
||||||
|
if [ -n "$found" ]; then
|
||||||
|
echo "$(cd "$(dirname "$found")" && pwd)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
die "پروژه فلاتر یافت نشد. لطفاً با --project مسیر را مشخص کنید."
|
||||||
|
}
|
||||||
|
|
||||||
|
check_linux_dependencies() {
|
||||||
|
echo "بررسی وابستگیهای Linux..."
|
||||||
|
|
||||||
|
local missing_deps=()
|
||||||
|
|
||||||
|
# بررسی وجود GTK development libraries
|
||||||
|
if ! pkg-config --exists gtk+-3.0; then
|
||||||
|
missing_deps+=("libgtk-3-dev")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# بررسی وجود CMake
|
||||||
|
if ! cmd_exists cmake; then
|
||||||
|
missing_deps+=("cmake")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# بررسی وجود Ninja
|
||||||
|
if ! cmd_exists ninja; then
|
||||||
|
missing_deps+=("ninja-build")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# بررسی وجود C++ compiler
|
||||||
|
if ! cmd_exists clang++; then
|
||||||
|
missing_deps+=("clang")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# بررسی وجود build-essential
|
||||||
|
if ! cmd_exists gcc; then
|
||||||
|
missing_deps+=("build-essential")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||||
|
echo "نصب وابستگیهای مورد نیاز..."
|
||||||
|
echo "بستههای مورد نیاز: ${missing_deps[*]}"
|
||||||
|
|
||||||
|
# تشخیص توزیع Linux
|
||||||
|
if command -v apt >/dev/null 2>&1; then
|
||||||
|
# Ubuntu/Debian
|
||||||
|
echo "تشخیص توزیع: Ubuntu/Debian"
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y "${missing_deps[@]}"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
# Fedora/RHEL
|
||||||
|
echo "تشخیص توزیع: Fedora/RHEL"
|
||||||
|
sudo dnf install -y "${missing_deps[@]}"
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then
|
||||||
|
# Arch Linux
|
||||||
|
echo "تشخیص توزیع: Arch Linux"
|
||||||
|
sudo pacman -S --noconfirm "${missing_deps[@]}"
|
||||||
|
else
|
||||||
|
die "توزیع Linux پشتیبانی شده یافت نشد. لطفاً وابستگیها را بهصورت دستی نصب کنید: ${missing_deps[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "وابستگیها نصب شدند."
|
||||||
|
else
|
||||||
|
echo "همه وابستگیهای مورد نیاز موجود هستند."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse args
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--project)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --project وارد نشده است"
|
||||||
|
USER_PROJECT="$2"; shift 2 ;;
|
||||||
|
--mode)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --mode وارد نشده است"
|
||||||
|
MODE="$2"; shift 2 ;;
|
||||||
|
--build-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --build-dir وارد نشده است"
|
||||||
|
BUILD_DIR="$2"; shift 2 ;;
|
||||||
|
--output-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --output-dir وارد نشده است"
|
||||||
|
OUTPUT_DIR="$2"; shift 2 ;;
|
||||||
|
--clean)
|
||||||
|
CLEAN_BUILD=true; shift ;;
|
||||||
|
--install-deps)
|
||||||
|
INSTALL_DEPS=true; shift ;;
|
||||||
|
--api-base-url)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --api-base-url وارد نشده است"
|
||||||
|
API_BASE_URL="$2"; shift 2 ;;
|
||||||
|
--archive)
|
||||||
|
CREATE_ARCHIVE=true; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
print_usage; exit 0 ;;
|
||||||
|
*)
|
||||||
|
warn "آرگومان ناشناخته: $1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
debug|profile|release) ;;
|
||||||
|
*) die "mode نامعتبر است: $MODE (مجاز: debug|profile|release)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ensure_flutter_in_path
|
||||||
|
check_linux_dependencies
|
||||||
|
|
||||||
|
APP_DIR="$(auto_detect_project_dir)"
|
||||||
|
|
||||||
|
if [ -z "$BUILD_DIR" ]; then
|
||||||
|
BUILD_DIR="$DEFAULT_BUILD_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$OUTPUT_DIR" ]; then
|
||||||
|
OUTPUT_DIR="$DEFAULT_OUTPUT_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# تبدیل به مسیر مطلق
|
||||||
|
BUILD_DIR="$(cd "$APP_DIR" && realpath -m "$BUILD_DIR")"
|
||||||
|
OUTPUT_DIR="$(cd "$APP_DIR" && realpath -m "$OUTPUT_DIR")"
|
||||||
|
|
||||||
|
echo "ریشه ریپو: $REPO_ROOT"
|
||||||
|
echo "مسیر پروژه: $APP_DIR"
|
||||||
|
echo "حالت: $MODE"
|
||||||
|
echo "مسیر build: $BUILD_DIR"
|
||||||
|
echo "مسیر خروجی: $OUTPUT_DIR"
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
# تنظیم mirror برای حل مشکل دسترسی به pub.dev
|
||||||
|
export PUB_HOSTED_URL="https://pub.flutter-io.cn"
|
||||||
|
export FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"
|
||||||
|
|
||||||
|
# تنظیم C++ compiler flags برای حل مشکل deprecated warnings
|
||||||
|
export CXXFLAGS="-Wno-deprecated-literal-operator"
|
||||||
|
export CFLAGS="-Wno-deprecated-literal-operator"
|
||||||
|
|
||||||
|
# نصب وابستگیها در صورت درخواست
|
||||||
|
if [ "$INSTALL_DEPS" = true ]; then
|
||||||
|
echo "نصب وابستگیها..."
|
||||||
|
flutter pub get
|
||||||
|
fi
|
||||||
|
|
||||||
|
# پاک کردن build directory در صورت درخواست
|
||||||
|
if [ "$CLEAN_BUILD" = true ]; then
|
||||||
|
echo "پاک کردن build directory..."
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# تنظیم آرگومانهای dart-define
|
||||||
|
DART_DEFINE_ARGS=()
|
||||||
|
if [ -n "$API_BASE_URL" ]; then
|
||||||
|
DART_DEFINE_ARGS+=(--dart-define "API_BASE_URL=$API_BASE_URL")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build کردن Flutter برای Linux
|
||||||
|
echo "Build کردن Flutter برای Linux..."
|
||||||
|
echo "دستور: flutter build linux --$MODE ${DART_DEFINE_ARGS[*]:-}"
|
||||||
|
|
||||||
|
flutter build linux --"$MODE" ${DART_DEFINE_ARGS[@]:-}
|
||||||
|
|
||||||
|
# کپی کردن فایلهای build شده به مسیر خروجی
|
||||||
|
echo "کپی کردن فایلهای build شده..."
|
||||||
|
rm -rf "$OUTPUT_DIR"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# کپی کردن bundle از build directory
|
||||||
|
if [ -d "$BUILD_DIR/x64/$MODE/bundle" ]; then
|
||||||
|
cp -r "$BUILD_DIR/x64/$MODE/bundle"/* "$OUTPUT_DIR/"
|
||||||
|
echo "فایلهای build شده در مسیر زیر کپی شدند: $OUTPUT_DIR"
|
||||||
|
else
|
||||||
|
die "مسیر bundle یافت نشد: $BUILD_DIR/x64/$MODE/bundle"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ایجاد فایل اجرایی
|
||||||
|
EXECUTABLE_NAME="hesabix_ui"
|
||||||
|
if [ -f "$OUTPUT_DIR/$EXECUTABLE_NAME" ]; then
|
||||||
|
chmod +x "$OUTPUT_DIR/$EXECUTABLE_NAME"
|
||||||
|
echo "فایل اجرایی: $OUTPUT_DIR/$EXECUTABLE_NAME"
|
||||||
|
else
|
||||||
|
warn "فایل اجرایی یافت نشد: $OUTPUT_DIR/$EXECUTABLE_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ایجاد archive در صورت درخواست
|
||||||
|
if [ "$CREATE_ARCHIVE" = true ]; then
|
||||||
|
ARCHIVE_NAME="hesabix_ui_linux_${MODE}_$(date +%Y%m%d_%H%M%S).tar.gz"
|
||||||
|
ARCHIVE_PATH="$(dirname "$OUTPUT_DIR")/$ARCHIVE_NAME"
|
||||||
|
|
||||||
|
echo "ایجاد archive: $ARCHIVE_PATH"
|
||||||
|
cd "$(dirname "$OUTPUT_DIR")"
|
||||||
|
tar -czf "$ARCHIVE_PATH" "$(basename "$OUTPUT_DIR")"
|
||||||
|
|
||||||
|
echo "Archive ایجاد شد: $ARCHIVE_PATH"
|
||||||
|
echo "برای اجرا: tar -xzf $ARCHIVE_NAME && cd $(basename "$OUTPUT_DIR") && ./$EXECUTABLE_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Build کامل شد!"
|
||||||
|
echo "برای اجرا: cd $OUTPUT_DIR && ./$EXECUTABLE_NAME"
|
||||||
|
|
@ -427,7 +427,7 @@ async def set_default_storage_config(
|
||||||
|
|
||||||
@router.delete("/storage-configs/{config_id}", response_model=dict)
|
@router.delete("/storage-configs/{config_id}", response_model=dict)
|
||||||
async def delete_storage_config(
|
async def delete_storage_config(
|
||||||
config_id: UUID,
|
config_id: str,
|
||||||
request: Request,
|
request: Request,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: AuthContext = Depends(get_current_user),
|
current_user: AuthContext = Depends(get_current_user),
|
||||||
|
|
@ -435,8 +435,28 @@ async def delete_storage_config(
|
||||||
):
|
):
|
||||||
"""حذف تنظیمات ذخیرهسازی"""
|
"""حذف تنظیمات ذخیرهسازی"""
|
||||||
try:
|
try:
|
||||||
|
# Check permission
|
||||||
|
if not current_user.has_app_permission("admin.storage.delete"):
|
||||||
|
raise ApiError(
|
||||||
|
code="FORBIDDEN",
|
||||||
|
message=translator.t("FORBIDDEN", "دسترسی غیرمجاز"),
|
||||||
|
http_status=403,
|
||||||
|
translator=translator
|
||||||
|
)
|
||||||
|
|
||||||
config_repo = StorageConfigRepository(db)
|
config_repo = StorageConfigRepository(db)
|
||||||
success = await config_repo.delete_config(config_id)
|
|
||||||
|
# بررسی وجود فایلها قبل از حذف
|
||||||
|
file_count = config_repo.count_files_by_storage_config(config_id)
|
||||||
|
if file_count > 0:
|
||||||
|
raise ApiError(
|
||||||
|
code="STORAGE_CONFIG_HAS_FILES",
|
||||||
|
message=translator.t("STORAGE_CONFIG_HAS_FILES", f"این تنظیمات ذخیرهسازی دارای {file_count} فایل است و قابل حذف نیست"),
|
||||||
|
http_status=400,
|
||||||
|
translator=translator
|
||||||
|
)
|
||||||
|
|
||||||
|
success = config_repo.delete_config(config_id)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
raise ApiError(
|
raise ApiError(
|
||||||
|
|
@ -590,21 +610,131 @@ async def _test_local_storage(config: StorageConfig) -> dict:
|
||||||
|
|
||||||
async def _test_ftp_storage(config: StorageConfig) -> dict:
|
async def _test_ftp_storage(config: StorageConfig) -> dict:
|
||||||
"""تست اتصال به FTP storage"""
|
"""تست اتصال به FTP storage"""
|
||||||
|
import ftplib
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# TODO: پیادهسازی تست FTP
|
# دریافت تنظیمات FTP
|
||||||
# فعلاً فقط ساختار کلی را برمیگردانیم
|
config_data = config.config_data
|
||||||
|
host = config_data.get("host")
|
||||||
|
port = int(config_data.get("port", 21))
|
||||||
|
username = config_data.get("username")
|
||||||
|
password = config_data.get("password")
|
||||||
|
directory = config_data.get("directory", "/")
|
||||||
|
use_tls = config_data.get("use_tls", False)
|
||||||
|
|
||||||
|
# بررسی وجود پارامترهای ضروری
|
||||||
|
if not all([host, username, password]):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "تست FTP هنوز پیادهسازی نشده است",
|
"error": "پارامترهای ضروری FTP (host, username, password) موجود نیست",
|
||||||
"storage_type": "ftp",
|
"storage_type": "ftp",
|
||||||
"tested_at": datetime.utcnow().isoformat()
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# اتصال به FTP
|
||||||
|
if use_tls:
|
||||||
|
ftp = ftplib.FTP_TLS()
|
||||||
|
else:
|
||||||
|
ftp = ftplib.FTP()
|
||||||
|
|
||||||
|
# تنظیم timeout
|
||||||
|
ftp.connect(host, port, timeout=10)
|
||||||
|
ftp.login(username, password)
|
||||||
|
|
||||||
|
# تغییر به دایرکتوری مورد نظر
|
||||||
|
if directory and directory != "/":
|
||||||
|
try:
|
||||||
|
ftp.cwd(directory)
|
||||||
|
except ftplib.error_perm:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"دسترسی به دایرکتوری {directory} وجود ندارد",
|
||||||
|
"storage_type": "ftp",
|
||||||
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# تست نوشتن فایل
|
||||||
|
test_filename = f"test_connection_{datetime.utcnow().timestamp()}.txt"
|
||||||
|
test_content = "Test FTP connection file"
|
||||||
|
|
||||||
|
# ایجاد فایل موقت
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as temp_file:
|
||||||
|
temp_file.write(test_content)
|
||||||
|
temp_file_path = temp_file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# آپلود فایل
|
||||||
|
with open(temp_file_path, 'rb') as file:
|
||||||
|
ftp.storbinary(f'STOR {test_filename}', file)
|
||||||
|
|
||||||
|
# بررسی وجود فایل
|
||||||
|
file_list = []
|
||||||
|
ftp.retrlines('LIST', file_list.append)
|
||||||
|
file_exists = any(test_filename in line for line in file_list)
|
||||||
|
|
||||||
|
if not file_exists:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "فایل تست آپلود نشد",
|
||||||
|
"storage_type": "ftp",
|
||||||
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# حذف فایل تست
|
||||||
|
try:
|
||||||
|
ftp.delete(test_filename)
|
||||||
|
except ftplib.error_perm:
|
||||||
|
pass # اگر نتوانست حذف کند، مهم نیست
|
||||||
|
|
||||||
|
# بستن اتصال
|
||||||
|
ftp.quit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "اتصال به FTP server موفقیتآمیز بود",
|
||||||
|
"storage_type": "ftp",
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
"directory": directory,
|
||||||
|
"use_tls": use_tls,
|
||||||
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# حذف فایل موقت
|
||||||
|
try:
|
||||||
|
os.unlink(temp_file_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except ftplib.error_perm as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"خطا در احراز هویت FTP: {str(e)}",
|
||||||
|
"storage_type": "ftp",
|
||||||
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
except ftplib.error_temp as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"خطای موقت FTP: {str(e)}",
|
||||||
|
"storage_type": "ftp",
|
||||||
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "اتصال به سرور FTP رد شد. بررسی کنید که سرور در حال اجرا باشد",
|
||||||
|
"storage_type": "ftp",
|
||||||
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": f"خطا در تست FTP storage: {str(e)}",
|
"error": f"خطا در تست FTP storage: {str(e)}",
|
||||||
|
"storage_type": "ftp",
|
||||||
"tested_at": datetime.utcnow().isoformat()
|
"tested_at": datetime.utcnow().isoformat()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,15 @@ class StorageConfigRepository(BaseRepository[StorageConfig]):
|
||||||
self.db.query(StorageConfig).update({"is_default": False})
|
self.db.query(StorageConfig).update({"is_default": False})
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
async def delete_config(self, config_id: UUID) -> bool:
|
def count_files_by_storage_config(self, config_id: str) -> int:
|
||||||
|
"""شمارش تعداد فایلهای مربوط به یک storage config"""
|
||||||
|
return self.db.query(FileStorage).filter(
|
||||||
|
FileStorage.storage_config_id == config_id,
|
||||||
|
FileStorage.is_active == True,
|
||||||
|
FileStorage.deleted_at.is_(None)
|
||||||
|
).count()
|
||||||
|
|
||||||
|
def delete_config(self, config_id: str) -> bool:
|
||||||
config = self.db.query(StorageConfig).filter(StorageConfig.id == config_id).first()
|
config = self.db.query(StorageConfig).filter(StorageConfig.id == config_id).first()
|
||||||
if not config:
|
if not config:
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -396,6 +396,23 @@
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"first": "First",
|
"first": "First",
|
||||||
"last": "Last"
|
"last": "Last",
|
||||||
|
"systemSettingsWelcome": "System Settings",
|
||||||
|
"systemSettingsDescription": "Manage system configuration and administration",
|
||||||
|
"storageManagement": "Storage Management",
|
||||||
|
"storageManagementDescription": "Configure file storage systems and manage files",
|
||||||
|
"systemConfiguration": "System Configuration",
|
||||||
|
"systemConfigurationDescription": "General system settings and preferences",
|
||||||
|
"userManagement": "User Management",
|
||||||
|
"userManagementDescription": "Manage users, roles and permissions",
|
||||||
|
"systemLogs": "System Logs",
|
||||||
|
"systemLogsDescription": "View system logs and monitoring",
|
||||||
|
"backToSettings": "Back to Settings",
|
||||||
|
"settingsOverview": "Settings Overview",
|
||||||
|
"availableSettings": "Available Settings",
|
||||||
|
"systemAdministration": "System Administration",
|
||||||
|
"generalSettings": "General Settings",
|
||||||
|
"securitySettings": "Security Settings",
|
||||||
|
"maintenanceSettings": "Maintenance Settings"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,23 @@
|
||||||
"previous": "قبلی",
|
"previous": "قبلی",
|
||||||
"next": "بعدی",
|
"next": "بعدی",
|
||||||
"first": "اول",
|
"first": "اول",
|
||||||
"last": "آخر"
|
"last": "آخر",
|
||||||
|
"systemSettingsWelcome": "تنظیمات سیستم",
|
||||||
|
"systemSettingsDescription": "مدیریت پیکربندی و مدیریت سیستم",
|
||||||
|
"storageManagement": "مدیریت ذخیرهسازی",
|
||||||
|
"storageManagementDescription": "پیکربندی سیستمهای ذخیرهسازی فایل و مدیریت فایلها",
|
||||||
|
"systemConfiguration": "پیکربندی سیستم",
|
||||||
|
"systemConfigurationDescription": "تنظیمات عمومی سیستم و ترجیحات",
|
||||||
|
"userManagement": "مدیریت کاربران",
|
||||||
|
"userManagementDescription": "مدیریت کاربران، نقشها و مجوزها",
|
||||||
|
"systemLogs": "لاگهای سیستم",
|
||||||
|
"systemLogsDescription": "مشاهده لاگهای سیستم و نظارت",
|
||||||
|
"backToSettings": "بازگشت به تنظیمات",
|
||||||
|
"settingsOverview": "نمای کلی تنظیمات",
|
||||||
|
"availableSettings": "تنظیمات موجود",
|
||||||
|
"systemAdministration": "مدیریت سیستم",
|
||||||
|
"generalSettings": "تنظیمات عمومی",
|
||||||
|
"securitySettings": "تنظیمات امنیتی",
|
||||||
|
"maintenanceSettings": "تنظیمات نگهداری"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2215,6 +2215,108 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Last'**
|
/// **'Last'**
|
||||||
String get last;
|
String get last;
|
||||||
|
|
||||||
|
/// No description provided for @systemSettingsWelcome.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'System Settings'**
|
||||||
|
String get systemSettingsWelcome;
|
||||||
|
|
||||||
|
/// No description provided for @systemSettingsDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Manage system configuration and administration'**
|
||||||
|
String get systemSettingsDescription;
|
||||||
|
|
||||||
|
/// No description provided for @storageManagement.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Storage Management'**
|
||||||
|
String get storageManagement;
|
||||||
|
|
||||||
|
/// No description provided for @storageManagementDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Configure file storage systems and manage files'**
|
||||||
|
String get storageManagementDescription;
|
||||||
|
|
||||||
|
/// No description provided for @systemConfiguration.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'System Configuration'**
|
||||||
|
String get systemConfiguration;
|
||||||
|
|
||||||
|
/// No description provided for @systemConfigurationDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'General system settings and preferences'**
|
||||||
|
String get systemConfigurationDescription;
|
||||||
|
|
||||||
|
/// No description provided for @userManagement.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'User Management'**
|
||||||
|
String get userManagement;
|
||||||
|
|
||||||
|
/// No description provided for @userManagementDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Manage users, roles and permissions'**
|
||||||
|
String get userManagementDescription;
|
||||||
|
|
||||||
|
/// No description provided for @systemLogs.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'System Logs'**
|
||||||
|
String get systemLogs;
|
||||||
|
|
||||||
|
/// No description provided for @systemLogsDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'View system logs and monitoring'**
|
||||||
|
String get systemLogsDescription;
|
||||||
|
|
||||||
|
/// No description provided for @backToSettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Back to Settings'**
|
||||||
|
String get backToSettings;
|
||||||
|
|
||||||
|
/// No description provided for @settingsOverview.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Settings Overview'**
|
||||||
|
String get settingsOverview;
|
||||||
|
|
||||||
|
/// No description provided for @availableSettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Available Settings'**
|
||||||
|
String get availableSettings;
|
||||||
|
|
||||||
|
/// No description provided for @systemAdministration.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'System Administration'**
|
||||||
|
String get systemAdministration;
|
||||||
|
|
||||||
|
/// No description provided for @generalSettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'General Settings'**
|
||||||
|
String get generalSettings;
|
||||||
|
|
||||||
|
/// No description provided for @securitySettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Security Settings'**
|
||||||
|
String get securitySettings;
|
||||||
|
|
||||||
|
/// No description provided for @maintenanceSettings.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Maintenance Settings'**
|
||||||
|
String get maintenanceSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -1097,4 +1097,58 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get last => 'Last';
|
String get last => 'Last';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemSettingsWelcome => 'System Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemSettingsDescription =>
|
||||||
|
'Manage system configuration and administration';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storageManagement => 'Storage Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storageManagementDescription =>
|
||||||
|
'Configure file storage systems and manage files';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemConfiguration => 'System Configuration';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemConfigurationDescription =>
|
||||||
|
'General system settings and preferences';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userManagement => 'User Management';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userManagementDescription => 'Manage users, roles and permissions';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemLogs => 'System Logs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemLogsDescription => 'View system logs and monitoring';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get backToSettings => 'Back to Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsOverview => 'Settings Overview';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableSettings => 'Available Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAdministration => 'System Administration';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get generalSettings => 'General Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securitySettings => 'Security Settings';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get maintenanceSettings => 'Maintenance Settings';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1091,4 +1091,56 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get last => 'آخر';
|
String get last => 'آخر';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemSettingsWelcome => 'تنظیمات سیستم';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemSettingsDescription => 'مدیریت پیکربندی و مدیریت سیستم';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storageManagement => 'مدیریت ذخیرهسازی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get storageManagementDescription =>
|
||||||
|
'پیکربندی سیستمهای ذخیرهسازی فایل و مدیریت فایلها';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemConfiguration => 'پیکربندی سیستم';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemConfigurationDescription => 'تنظیمات عمومی سیستم و ترجیحات';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userManagement => 'مدیریت کاربران';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get userManagementDescription => 'مدیریت کاربران، نقشها و مجوزها';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemLogs => 'لاگهای سیستم';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemLogsDescription => 'مشاهده لاگهای سیستم و نظارت';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get backToSettings => 'بازگشت به تنظیمات';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get settingsOverview => 'نمای کلی تنظیمات';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get availableSettings => 'تنظیمات موجود';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get systemAdministration => 'مدیریت سیستم';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get generalSettings => 'تنظیمات عمومی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get securitySettings => 'تنظیمات امنیتی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get maintenanceSettings => 'تنظیمات نگهداری';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ import 'pages/profile/change_password_page.dart';
|
||||||
import 'pages/profile/marketing_page.dart';
|
import 'pages/profile/marketing_page.dart';
|
||||||
import 'pages/profile/operator/operator_tickets_page.dart';
|
import 'pages/profile/operator/operator_tickets_page.dart';
|
||||||
import 'pages/system_settings_page.dart';
|
import 'pages/system_settings_page.dart';
|
||||||
|
import 'pages/admin/storage_management_page.dart';
|
||||||
|
import 'pages/admin/system_configuration_page.dart';
|
||||||
|
import 'pages/admin/user_management_page.dart';
|
||||||
|
import 'pages/admin/system_logs_page.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'core/locale_controller.dart';
|
import 'core/locale_controller.dart';
|
||||||
import 'core/calendar_controller.dart';
|
import 'core/calendar_controller.dart';
|
||||||
|
|
@ -386,6 +390,48 @@ class _MyAppState extends State<MyApp> {
|
||||||
}
|
}
|
||||||
return const SystemSettingsPage();
|
return const SystemSettingsPage();
|
||||||
},
|
},
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'storage',
|
||||||
|
name: 'system_settings_storage',
|
||||||
|
builder: (context, state) {
|
||||||
|
if (_authStore == null || !_authStore!.isSuperAdmin) {
|
||||||
|
return PermissionGuard.buildAccessDeniedPage();
|
||||||
|
}
|
||||||
|
return const AdminStorageManagementPage();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'configuration',
|
||||||
|
name: 'system_settings_configuration',
|
||||||
|
builder: (context, state) {
|
||||||
|
if (_authStore == null || !_authStore!.isSuperAdmin) {
|
||||||
|
return PermissionGuard.buildAccessDeniedPage();
|
||||||
|
}
|
||||||
|
return const SystemConfigurationPage();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'users',
|
||||||
|
name: 'system_settings_users',
|
||||||
|
builder: (context, state) {
|
||||||
|
if (_authStore == null || !_authStore!.isSuperAdmin) {
|
||||||
|
return PermissionGuard.buildAccessDeniedPage();
|
||||||
|
}
|
||||||
|
return const UserManagementPage();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'logs',
|
||||||
|
name: 'system_settings_logs',
|
||||||
|
builder: (context, state) {
|
||||||
|
if (_authStore == null || !_authStore!.isSuperAdmin) {
|
||||||
|
return PermissionGuard.buildAccessDeniedPage();
|
||||||
|
}
|
||||||
|
return const SystemLogsPage();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_list_widget.dart';
|
import 'package:hesabix_ui/widgets/admin/file_storage/storage_management_page.dart';
|
||||||
import 'package:hesabix_ui/widgets/admin/file_storage/file_statistics_widget.dart';
|
|
||||||
import 'package:hesabix_ui/widgets/admin/file_storage/file_management_widget.dart';
|
|
||||||
|
|
||||||
class FileStorageSettingsPage extends StatefulWidget {
|
class FileStorageSettingsPage extends StatelessWidget {
|
||||||
const FileStorageSettingsPage({super.key});
|
const FileStorageSettingsPage({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<FileStorageSettingsPage> createState() => _FileStorageSettingsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FileStorageSettingsPageState extends State<FileStorageSettingsPage>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
late TabController _tabController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_tabController = TabController(length: 3, vsync: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_tabController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
return const StorageManagementPage();
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(l10n.fileStorageSettings),
|
|
||||||
bottom: TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
tabs: [
|
|
||||||
Tab(
|
|
||||||
icon: const Icon(Icons.storage),
|
|
||||||
text: l10n.storageConfigurations,
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
icon: const Icon(Icons.analytics),
|
|
||||||
text: l10n.fileStatistics,
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
icon: const Icon(Icons.folder),
|
|
||||||
text: l10n.fileManagement,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: TabBarView(
|
|
||||||
controller: _tabController,
|
|
||||||
children: [
|
|
||||||
// Storage Configurations Tab
|
|
||||||
const StorageConfigListWidget(),
|
|
||||||
|
|
||||||
// File Statistics Tab
|
|
||||||
const FileStatisticsWidget(),
|
|
||||||
|
|
||||||
// File Management Tab
|
|
||||||
const FileManagementWidget(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_list_widget.dart';
|
||||||
|
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_form_dialog.dart';
|
||||||
|
|
||||||
|
class AdminStorageManagementPage extends StatefulWidget {
|
||||||
|
const AdminStorageManagementPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AdminStorageManagementPage> createState() => _AdminStorageManagementPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AdminStorageManagementPageState extends State<AdminStorageManagementPage> {
|
||||||
|
final GlobalKey<StorageConfigListWidgetState> _listKey = GlobalKey<StorageConfigListWidgetState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
t.storageManagement,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => context.go('/user/profile/system-settings'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary.withOpacity(0.1),
|
||||||
|
theme.colorScheme.surface,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: StorageConfigListWidget(
|
||||||
|
key: _listKey,
|
||||||
|
onRefresh: () => _listKey.currentState?.loadStorageConfigs(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () => _showCreateDialog(context),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: Text(t.addStorageConfig),
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateDialog(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => StorageConfigFormDialog(
|
||||||
|
onSaved: () {
|
||||||
|
// Refresh the list
|
||||||
|
_listKey.currentState?.loadStorageConfigs();
|
||||||
|
// Show success message
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('${AppLocalizations.of(context).addStorageConfig} ${AppLocalizations.of(context).save}'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class SystemConfigurationPage extends StatefulWidget {
|
||||||
|
const SystemConfigurationPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SystemConfigurationPage> createState() => _SystemConfigurationPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SystemConfigurationPageState extends State<SystemConfigurationPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
// Configuration values
|
||||||
|
String _appName = 'Hesabix';
|
||||||
|
String _appVersion = '1.0.0';
|
||||||
|
String _defaultLanguage = 'fa';
|
||||||
|
String _defaultTheme = 'system';
|
||||||
|
bool _enableRegistration = true;
|
||||||
|
bool _enableEmailVerification = true;
|
||||||
|
bool _enableMaintenanceMode = false;
|
||||||
|
int _sessionTimeout = 30;
|
||||||
|
int _maxFileSize = 10;
|
||||||
|
int _maxUsers = 1000;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
t.systemConfiguration,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => context.go('/user/profile/system-settings'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading ? null : _saveConfiguration,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Text(t.save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary.withOpacity(0.1),
|
||||||
|
theme.colorScheme.surface,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildSectionCard(
|
||||||
|
theme,
|
||||||
|
t.generalSettings,
|
||||||
|
Icons.settings_outlined,
|
||||||
|
[
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Application Name',
|
||||||
|
value: _appName,
|
||||||
|
onChanged: (value) => setState(() => _appName = value),
|
||||||
|
),
|
||||||
|
_buildTextField(
|
||||||
|
label: 'Application Version',
|
||||||
|
value: _appVersion,
|
||||||
|
onChanged: (value) => setState(() => _appVersion = value),
|
||||||
|
),
|
||||||
|
_buildDropdownField(
|
||||||
|
label: 'Default Language',
|
||||||
|
value: _defaultLanguage,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'fa', child: Text('فارسی')),
|
||||||
|
DropdownMenuItem(value: 'en', child: Text('English')),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _defaultLanguage = value!),
|
||||||
|
),
|
||||||
|
_buildDropdownField(
|
||||||
|
label: 'Default Theme',
|
||||||
|
value: _defaultTheme,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'system', child: Text('System')),
|
||||||
|
DropdownMenuItem(value: 'light', child: Text('Light')),
|
||||||
|
DropdownMenuItem(value: 'dark', child: Text('Dark')),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _defaultTheme = value!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSectionCard(
|
||||||
|
theme,
|
||||||
|
t.securitySettings,
|
||||||
|
Icons.security_outlined,
|
||||||
|
[
|
||||||
|
_buildSwitchField(
|
||||||
|
label: 'Enable User Registration',
|
||||||
|
value: _enableRegistration,
|
||||||
|
onChanged: (value) => setState(() => _enableRegistration = value),
|
||||||
|
),
|
||||||
|
_buildSwitchField(
|
||||||
|
label: 'Enable Email Verification',
|
||||||
|
value: _enableEmailVerification,
|
||||||
|
onChanged: (value) => setState(() => _enableEmailVerification = value),
|
||||||
|
),
|
||||||
|
_buildNumberField(
|
||||||
|
label: 'Session Timeout (minutes)',
|
||||||
|
value: _sessionTimeout,
|
||||||
|
onChanged: (value) => setState(() => _sessionTimeout = value),
|
||||||
|
min: 5,
|
||||||
|
max: 1440,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSectionCard(
|
||||||
|
theme,
|
||||||
|
t.maintenanceSettings,
|
||||||
|
Icons.build_outlined,
|
||||||
|
[
|
||||||
|
_buildSwitchField(
|
||||||
|
label: 'Maintenance Mode',
|
||||||
|
value: _enableMaintenanceMode,
|
||||||
|
onChanged: (value) => setState(() => _enableMaintenanceMode = value),
|
||||||
|
),
|
||||||
|
_buildNumberField(
|
||||||
|
label: 'Max File Size (MB)',
|
||||||
|
value: _maxFileSize,
|
||||||
|
onChanged: (value) => setState(() => _maxFileSize = value),
|
||||||
|
min: 1,
|
||||||
|
max: 1000,
|
||||||
|
),
|
||||||
|
_buildNumberField(
|
||||||
|
label: 'Max Users',
|
||||||
|
value: _maxUsers,
|
||||||
|
onChanged: (value) => setState(() => _maxUsers = value),
|
||||||
|
min: 1,
|
||||||
|
max: 10000,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionCard(ThemeData theme, String title, IconData icon, List<Widget> children) {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: theme.colorScheme.primary),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
...children,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTextField({
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required ValueChanged<String> onChanged,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: TextFormField(
|
||||||
|
initialValue: value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDropdownField({
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required List<DropdownMenuItem<String>> items,
|
||||||
|
required ValueChanged<String?> onChanged,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: items,
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwitchField({
|
||||||
|
required String label,
|
||||||
|
required bool value,
|
||||||
|
required ValueChanged<bool> onChanged,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNumberField({
|
||||||
|
required String label,
|
||||||
|
required int value,
|
||||||
|
required ValueChanged<int> onChanged,
|
||||||
|
required int min,
|
||||||
|
required int max,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
initialValue: value.toString(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (value) {
|
||||||
|
final intValue = int.tryParse(value);
|
||||||
|
if (intValue != null && intValue >= min && intValue <= max) {
|
||||||
|
onChanged(intValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: value < max ? () => onChanged(value + 1) : null,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: value > min ? () => onChanged(value - 1) : null,
|
||||||
|
icon: const Icon(Icons.remove),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveConfiguration() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulate API call
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(AppLocalizations.of(context).save),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Error: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
357
hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart
Normal file
357
hesabixUI/hesabix_ui/lib/pages/admin/system_logs_page.dart
Normal file
|
|
@ -0,0 +1,357 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class SystemLogsPage extends StatefulWidget {
|
||||||
|
const SystemLogsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SystemLogsPage> createState() => _SystemLogsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SystemLogsPageState extends State<SystemLogsPage> {
|
||||||
|
final _searchController = TextEditingController();
|
||||||
|
String _selectedLevel = 'all';
|
||||||
|
String _selectedDateRange = 'today';
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
// Mock log data
|
||||||
|
final List<Map<String, dynamic>> _logs = [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'timestamp': '2024-01-15 10:30:25',
|
||||||
|
'level': 'info',
|
||||||
|
'message': 'User login successful',
|
||||||
|
'module': 'auth',
|
||||||
|
'userId': 123,
|
||||||
|
'ip': '192.168.1.100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'timestamp': '2024-01-15 10:25:10',
|
||||||
|
'level': 'warning',
|
||||||
|
'message': 'Failed login attempt',
|
||||||
|
'module': 'auth',
|
||||||
|
'userId': null,
|
||||||
|
'ip': '192.168.1.101',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'timestamp': '2024-01-15 10:20:05',
|
||||||
|
'level': 'error',
|
||||||
|
'message': 'Database connection timeout',
|
||||||
|
'module': 'database',
|
||||||
|
'userId': null,
|
||||||
|
'ip': null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 4,
|
||||||
|
'timestamp': '2024-01-15 10:15:30',
|
||||||
|
'level': 'info',
|
||||||
|
'message': 'File uploaded successfully',
|
||||||
|
'module': 'storage',
|
||||||
|
'userId': 123,
|
||||||
|
'ip': '192.168.1.100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 5,
|
||||||
|
'timestamp': '2024-01-15 10:10:15',
|
||||||
|
'level': 'debug',
|
||||||
|
'message': 'API request processed',
|
||||||
|
'module': 'api',
|
||||||
|
'userId': 456,
|
||||||
|
'ip': '192.168.1.102',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> get _filteredLogs {
|
||||||
|
return _logs.where((log) {
|
||||||
|
final matchesSearch = log['message'].toString().toLowerCase()
|
||||||
|
.contains(_searchController.text.toLowerCase()) ||
|
||||||
|
log['module'].toString().toLowerCase()
|
||||||
|
.contains(_searchController.text.toLowerCase());
|
||||||
|
|
||||||
|
final matchesLevel = _selectedLevel == 'all' || log['level'] == _selectedLevel;
|
||||||
|
|
||||||
|
return matchesSearch && matchesLevel;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
t.systemLogs,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => context.go('/user/profile/system-settings'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _refreshLogs,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _exportLogs,
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary.withOpacity(0.1),
|
||||||
|
theme.colorScheme.surface,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildFilters(theme, t),
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _buildLogsList(theme, t),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilters(ThemeData theme, AppLocalizations t) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search logs...',
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_searchController.clear();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: (value) => setState(() {}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: _selectedLevel,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Log Level',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'all', child: Text('All Levels')),
|
||||||
|
DropdownMenuItem(value: 'debug', child: Text('Debug')),
|
||||||
|
DropdownMenuItem(value: 'info', child: Text('Info')),
|
||||||
|
DropdownMenuItem(value: 'warning', child: Text('Warning')),
|
||||||
|
DropdownMenuItem(value: 'error', child: Text('Error')),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _selectedLevel = value!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: _selectedDateRange,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Date Range',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'today', child: Text('Today')),
|
||||||
|
DropdownMenuItem(value: 'yesterday', child: Text('Yesterday')),
|
||||||
|
DropdownMenuItem(value: 'week', child: Text('This Week')),
|
||||||
|
DropdownMenuItem(value: 'month', child: Text('This Month')),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _selectedDateRange = value!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLogsList(ThemeData theme, AppLocalizations t) {
|
||||||
|
final logs = _filteredLogs;
|
||||||
|
|
||||||
|
if (logs.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.analytics_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'No logs found',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: logs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final log = logs[index];
|
||||||
|
return _buildLogCard(log, theme, t);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLogCard(Map<String, dynamic> log, ThemeData theme, AppLocalizations t) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ExpansionTile(
|
||||||
|
leading: _buildLogLevelIcon(log['level']),
|
||||||
|
title: Text(
|
||||||
|
log['message'],
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'${log['timestamp']} • ${log['module']}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildLogDetail('Level', log['level'].toString().toUpperCase()),
|
||||||
|
_buildLogDetail('Module', log['module']),
|
||||||
|
_buildLogDetail('Timestamp', log['timestamp']),
|
||||||
|
if (log['userId'] != null)
|
||||||
|
_buildLogDetail('User ID', log['userId'].toString()),
|
||||||
|
if (log['ip'] != null)
|
||||||
|
_buildLogDetail('IP Address', log['ip']),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
log['message'],
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLogLevelIcon(String level) {
|
||||||
|
IconData icon;
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'error':
|
||||||
|
icon = Icons.error;
|
||||||
|
color = Colors.red;
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
icon = Icons.warning;
|
||||||
|
color = Colors.orange;
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
icon = Icons.info;
|
||||||
|
color = Colors.blue;
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
icon = Icons.bug_report;
|
||||||
|
color = Colors.grey;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
icon = Icons.circle;
|
||||||
|
color = Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Icon(icon, color: color, size: 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLogDetail(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: Text(
|
||||||
|
'$label:',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _refreshLogs() {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
// Simulate API call
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _exportLogs() {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Export logs functionality would be implemented here')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
436
hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart
Normal file
436
hesabixUI/hesabix_ui/lib/pages/admin/user_management_page.dart
Normal file
|
|
@ -0,0 +1,436 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class UserManagementPage extends StatefulWidget {
|
||||||
|
const UserManagementPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UserManagementPage> createState() => _UserManagementPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserManagementPageState extends State<UserManagementPage> {
|
||||||
|
final _searchController = TextEditingController();
|
||||||
|
String _selectedFilter = 'all';
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
// Mock data - in real app, this would come from API
|
||||||
|
final List<Map<String, dynamic>> _users = [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'name': 'احمد محمدی',
|
||||||
|
'email': 'ahmad@example.com',
|
||||||
|
'role': 'admin',
|
||||||
|
'status': 'active',
|
||||||
|
'lastLogin': '2024-01-15',
|
||||||
|
'createdAt': '2024-01-01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'name': 'فاطمه احمدی',
|
||||||
|
'email': 'fatemeh@example.com',
|
||||||
|
'role': 'user',
|
||||||
|
'status': 'active',
|
||||||
|
'lastLogin': '2024-01-14',
|
||||||
|
'createdAt': '2024-01-02',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'name': 'علی رضایی',
|
||||||
|
'email': 'ali@example.com',
|
||||||
|
'role': 'operator',
|
||||||
|
'status': 'inactive',
|
||||||
|
'lastLogin': '2024-01-10',
|
||||||
|
'createdAt': '2024-01-03',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> get _filteredUsers {
|
||||||
|
var filtered = _users.where((user) {
|
||||||
|
final matchesSearch = user['name'].toString().toLowerCase()
|
||||||
|
.contains(_searchController.text.toLowerCase()) ||
|
||||||
|
user['email'].toString().toLowerCase()
|
||||||
|
.contains(_searchController.text.toLowerCase());
|
||||||
|
|
||||||
|
final matchesFilter = _selectedFilter == 'all' ||
|
||||||
|
user['status'] == _selectedFilter ||
|
||||||
|
user['role'] == _selectedFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesFilter;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
t.userManagement,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => context.go('/user/profile/system-settings'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _refreshUsers,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary.withOpacity(0.1),
|
||||||
|
theme.colorScheme.surface,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildSearchAndFilter(theme, t),
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _buildUsersList(theme, t),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: _showAddUserDialog,
|
||||||
|
icon: const Icon(Icons.person_add),
|
||||||
|
label: Text('Add User'),
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchAndFilter(ThemeData theme, AppLocalizations t) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: t.search,
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_searchController.clear();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: (value) => setState(() {}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: _selectedFilter,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Filter',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'all', child: Text('All Users')),
|
||||||
|
DropdownMenuItem(value: 'active', child: Text('Active')),
|
||||||
|
DropdownMenuItem(value: 'inactive', child: Text('Inactive')),
|
||||||
|
DropdownMenuItem(value: 'admin', child: Text('Admins')),
|
||||||
|
DropdownMenuItem(value: 'operator', child: Text('Operators')),
|
||||||
|
DropdownMenuItem(value: 'user', child: Text('Users')),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _selectedFilter = value!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _exportUsers,
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
|
label: const Text('Export'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUsersList(ThemeData theme, AppLocalizations t) {
|
||||||
|
final users = _filteredUsers;
|
||||||
|
|
||||||
|
if (users.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.people_outline,
|
||||||
|
size: 64,
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'No users found',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: users.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = users[index];
|
||||||
|
return _buildUserCard(user, theme, t);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUserCard(Map<String, dynamic> user, ThemeData theme, AppLocalizations t) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: _getRoleColor(user['role']),
|
||||||
|
child: Text(
|
||||||
|
user['name'].toString().substring(0, 1),
|
||||||
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
user['name'],
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(user['email']),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildStatusChip(user['status']),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildRoleChip(user['role']),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.edit),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(t.edit),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'permissions',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.security),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text('Permissions'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.delete, color: Colors.red),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(t.delete, style: const TextStyle(color: Colors.red)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (value) => _handleUserAction(value, user),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusChip(String status) {
|
||||||
|
return Chip(
|
||||||
|
label: Text(
|
||||||
|
status.toUpperCase(),
|
||||||
|
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
backgroundColor: status == 'active' ? Colors.green.shade100 : Colors.red.shade100,
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: status == 'active' ? Colors.green.shade800 : Colors.red.shade800,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRoleChip(String role) {
|
||||||
|
return Chip(
|
||||||
|
label: Text(
|
||||||
|
role.toUpperCase(),
|
||||||
|
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
backgroundColor: _getRoleColor(role).withOpacity(0.2),
|
||||||
|
labelStyle: TextStyle(color: _getRoleColor(role)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getRoleColor(String role) {
|
||||||
|
switch (role) {
|
||||||
|
case 'admin':
|
||||||
|
return Colors.red;
|
||||||
|
case 'operator':
|
||||||
|
return Colors.orange;
|
||||||
|
case 'user':
|
||||||
|
return Colors.blue;
|
||||||
|
default:
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleUserAction(String action, Map<String, dynamic> user) {
|
||||||
|
switch (action) {
|
||||||
|
case 'edit':
|
||||||
|
_showEditUserDialog(user);
|
||||||
|
break;
|
||||||
|
case 'permissions':
|
||||||
|
_showPermissionsDialog(user);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
_showDeleteUserDialog(user);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAddUserDialog() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Add New User'),
|
||||||
|
content: const Text('User creation form would go here'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(AppLocalizations.of(context).save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showEditUserDialog(Map<String, dynamic> user) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('Edit ${user['name']}'),
|
||||||
|
content: const Text('User edit form would go here'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(AppLocalizations.of(context).save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showPermissionsDialog(Map<String, dynamic> user) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('Permissions for ${user['name']}'),
|
||||||
|
content: const Text('Permissions management would go here'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(AppLocalizations.of(context).save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDeleteUserDialog(Map<String, dynamic> user) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Delete User'),
|
||||||
|
content: Text('Are you sure you want to delete ${user['name']}?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
// Delete user logic here
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
|
child: Text(AppLocalizations.of(context).delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _refreshUsers() {
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
// Simulate API call
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _exportUsers() {
|
||||||
|
// Export logic here
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Export functionality would be implemented here')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,44 +1,139 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
import 'package:hesabix_ui/pages/admin/file_storage_settings_page.dart';
|
|
||||||
|
|
||||||
class SystemSettingsPage extends StatelessWidget {
|
class SystemSettingsPage extends StatefulWidget {
|
||||||
const SystemSettingsPage({super.key});
|
const SystemSettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SystemSettingsPage> createState() => _SystemSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||||
|
late final List<SettingsItem> _settingsItems;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_settingsItems = [
|
||||||
|
SettingsItem(
|
||||||
|
title: 'storageManagement',
|
||||||
|
description: 'storageManagementDescription',
|
||||||
|
icon: Icons.cloud_upload_outlined,
|
||||||
|
color: const Color(0xFF2196F3),
|
||||||
|
route: '/user/profile/system-settings/storage',
|
||||||
|
),
|
||||||
|
SettingsItem(
|
||||||
|
title: 'systemConfiguration',
|
||||||
|
description: 'systemConfigurationDescription',
|
||||||
|
icon: Icons.settings_outlined,
|
||||||
|
color: const Color(0xFF4CAF50),
|
||||||
|
route: '/user/profile/system-settings/configuration',
|
||||||
|
),
|
||||||
|
SettingsItem(
|
||||||
|
title: 'userManagement',
|
||||||
|
description: 'userManagementDescription',
|
||||||
|
icon: Icons.people_outlined,
|
||||||
|
color: const Color(0xFFFF9800),
|
||||||
|
route: '/user/profile/system-settings/users',
|
||||||
|
),
|
||||||
|
SettingsItem(
|
||||||
|
title: 'systemLogs',
|
||||||
|
description: 'systemLogsDescription',
|
||||||
|
icon: Icons.analytics_outlined,
|
||||||
|
color: const Color(0xFF9C27B0),
|
||||||
|
route: '/user/profile/system-settings/logs',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = AppLocalizations.of(context)!;
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(t.systemSettings),
|
|
||||||
backgroundColor: colorScheme.surface,
|
backgroundColor: colorScheme.surface,
|
||||||
foregroundColor: colorScheme.onSurface,
|
appBar: AppBar(
|
||||||
elevation: 0,
|
title: Text(
|
||||||
|
t.systemSettingsWelcome,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 20,
|
||||||
),
|
),
|
||||||
body: Container(
|
),
|
||||||
color: colorScheme.surface,
|
backgroundColor: Colors.transparent,
|
||||||
child: SafeArea(
|
elevation: 0,
|
||||||
child: Padding(
|
centerTitle: true,
|
||||||
padding: const EdgeInsets.all(16.0),
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _showHelpDialog(context),
|
||||||
|
icon: const Icon(Icons.help_outline),
|
||||||
|
tooltip: t.systemSettingsWelcome,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header
|
_buildWelcomeSection(theme, colorScheme, t),
|
||||||
Container(
|
const SizedBox(height: 24),
|
||||||
padding: const EdgeInsets.all(24),
|
_buildSettingsList(theme, colorScheme, t),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWelcomeSection(ThemeData theme, ColorScheme colorScheme, AppLocalizations t) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.primaryContainer,
|
gradient: LinearGradient(
|
||||||
borderRadius: BorderRadius.circular(12),
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
colorScheme.primary.withOpacity(0.1),
|
||||||
|
colorScheme.primaryContainer.withOpacity(0.3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.primary.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Container(
|
||||||
Icons.admin_panel_settings,
|
padding: const EdgeInsets.all(12),
|
||||||
size: 32,
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.onPrimaryContainer,
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
colorScheme.primary,
|
||||||
|
colorScheme.primary.withOpacity(0.8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: colorScheme.primary.withOpacity(0.3),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.admin_panel_settings_outlined,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -46,204 +141,260 @@ class SystemSettingsPage extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
t.systemSettings,
|
t.systemAdministration,
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
color: colorScheme.onPrimaryContainer,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'تنظیمات پیشرفته سیستم - فقط برای ادمینها',
|
t.systemSettingsDescription,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.onPrimaryContainer.withOpacity(0.8),
|
color: colorScheme.onSurface.withOpacity(0.7),
|
||||||
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Settings Cards
|
|
||||||
Expanded(
|
|
||||||
child: GridView.count(
|
|
||||||
crossAxisCount: 3,
|
|
||||||
crossAxisSpacing: 16,
|
|
||||||
mainAxisSpacing: 16,
|
|
||||||
childAspectRatio: 1.2,
|
|
||||||
children: [
|
|
||||||
_buildSettingCard(
|
|
||||||
context,
|
|
||||||
icon: Icons.people,
|
|
||||||
title: 'مدیریت کاربران',
|
|
||||||
subtitle: 'مدیریت کاربران سیستم',
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
_buildSettingCard(
|
|
||||||
context,
|
|
||||||
icon: Icons.business,
|
|
||||||
title: 'مدیریت کسب و کارها',
|
|
||||||
subtitle: 'مدیریت کسب و کارهای ثبت شده',
|
|
||||||
color: Colors.green,
|
|
||||||
),
|
|
||||||
_buildSettingCard(
|
|
||||||
context,
|
|
||||||
icon: Icons.security,
|
|
||||||
title: 'امنیت سیستم',
|
|
||||||
subtitle: 'تنظیمات امنیتی و دسترسیها',
|
|
||||||
color: Colors.orange,
|
|
||||||
),
|
|
||||||
_buildSettingCard(
|
|
||||||
context,
|
|
||||||
icon: Icons.analytics,
|
|
||||||
title: 'گزارشگیری',
|
|
||||||
subtitle: 'گزارشهای سیستم و آمار',
|
|
||||||
color: Colors.purple,
|
|
||||||
),
|
|
||||||
_buildSettingCard(
|
|
||||||
context,
|
|
||||||
icon: Icons.backup,
|
|
||||||
title: 'پشتیبانگیری',
|
|
||||||
subtitle: 'مدیریت پشتیبانها',
|
|
||||||
color: Colors.teal,
|
|
||||||
),
|
|
||||||
_buildSettingCard(
|
|
||||||
context,
|
|
||||||
icon: Icons.storage,
|
|
||||||
title: t.fileStorage,
|
|
||||||
subtitle: t.fileStorageSettings,
|
|
||||||
color: Colors.indigo,
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const FileStorageSettingsPage(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_buildSettingCard(
|
|
||||||
context,
|
|
||||||
icon: Icons.tune,
|
|
||||||
title: 'تنظیمات پیشرفته',
|
|
||||||
subtitle: 'تنظیمات تخصصی سیستم',
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Warning Message
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.amber.withOpacity(0.1),
|
color: colorScheme.primary.withOpacity(0.1),
|
||||||
border: Border.all(color: Colors.amber.withOpacity(0.3)),
|
borderRadius: BorderRadius.circular(20),
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.warning_amber,
|
|
||||||
color: Colors.amber[700],
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
child: Text(
|
||||||
'توجه: این بخش فقط برای ادمینهای سیستم قابل دسترسی است. تغییرات در این بخش میتواند بر عملکرد کل سیستم تأثیر بگذارد.',
|
'${_settingsItems.length}',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: TextStyle(
|
||||||
color: Colors.amber[700],
|
color: colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSettingCard(
|
Widget _buildSettingsList(ThemeData theme, ColorScheme colorScheme, AppLocalizations t) {
|
||||||
BuildContext context, {
|
return Column(
|
||||||
required IconData icon,
|
|
||||||
required String title,
|
|
||||||
required String subtitle,
|
|
||||||
required Color color,
|
|
||||||
VoidCallback? onTap,
|
|
||||||
}) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final colorScheme = theme.colorScheme;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
onTap: onTap ?? () {
|
|
||||||
// TODO: Navigate to specific setting
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('$title - در حال توسعه'),
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Row(
|
||||||
icon,
|
children: [
|
||||||
color: color,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
Text(
|
||||||
title,
|
t.availableSettings,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const Spacer(),
|
||||||
Expanded(
|
Container(
|
||||||
child: Text(
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
subtitle,
|
decoration: BoxDecoration(
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
color: colorScheme.primary.withOpacity(0.1),
|
||||||
color: colorScheme.onSurface.withOpacity(0.7),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
|
child: Text(
|
||||||
|
'${_settingsItems.length} ${t.availableSettings.toLowerCase()}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
int crossAxisCount = 3;
|
||||||
|
if (constraints.maxWidth < 600) {
|
||||||
|
crossAxisCount = 2;
|
||||||
|
} else if (constraints.maxWidth > 1200) {
|
||||||
|
crossAxisCount = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
childAspectRatio: 1.0,
|
||||||
|
),
|
||||||
|
itemCount: _settingsItems.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildSettingsCard(_settingsItems[index], theme, colorScheme, t);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSettingsCard(SettingsItem item, ThemeData theme, ColorScheme colorScheme, AppLocalizations t) {
|
||||||
|
return Card(
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(
|
||||||
|
color: colorScheme.outline.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => context.go(item.route!),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
hoverColor: item.color.withOpacity(0.05),
|
||||||
|
splashColor: item.color.withOpacity(0.1),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: item.color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: item.color.withOpacity(0.1),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
item.icon,
|
||||||
|
color: item.color,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
_getLocalizedText(t, item.title),
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
_getLocalizedText(t, item.description),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
AnimatedContainer(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
duration: const Duration(milliseconds: 200),
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
Icon(
|
decoration: BoxDecoration(
|
||||||
|
color: item.color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
Icons.arrow_forward_ios,
|
Icons.arrow_forward_ios,
|
||||||
size: 16,
|
size: 12,
|
||||||
color: colorScheme.onSurface.withOpacity(0.5),
|
color: item.color,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getLocalizedText(AppLocalizations t, String key) {
|
||||||
|
switch (key) {
|
||||||
|
case 'storageManagement':
|
||||||
|
return t.storageManagement;
|
||||||
|
case 'storageManagementDescription':
|
||||||
|
return t.storageManagementDescription;
|
||||||
|
case 'systemConfiguration':
|
||||||
|
return t.systemConfiguration;
|
||||||
|
case 'systemConfigurationDescription':
|
||||||
|
return t.systemConfigurationDescription;
|
||||||
|
case 'userManagement':
|
||||||
|
return t.userManagement;
|
||||||
|
case 'userManagementDescription':
|
||||||
|
return t.userManagementDescription;
|
||||||
|
case 'systemLogs':
|
||||||
|
return t.systemLogs;
|
||||||
|
case 'systemLogsDescription':
|
||||||
|
return t.systemLogsDescription;
|
||||||
|
default:
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showHelpDialog(BuildContext context) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.help_outline,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(t.systemSettingsWelcome),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
t.systemSettingsDescription,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text(t.ok),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsItem {
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final String? route;
|
||||||
|
|
||||||
|
SettingsItem({
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
this.route,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1,358 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
|
||||||
import '../../../core/api_client.dart';
|
|
||||||
|
|
||||||
class FileManagementWidget extends StatefulWidget {
|
|
||||||
const FileManagementWidget({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FileManagementWidget> createState() => _FileManagementWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FileManagementWidgetState extends State<FileManagementWidget>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
late TabController _tabController;
|
|
||||||
List<Map<String, dynamic>> _allFiles = [];
|
|
||||||
List<Map<String, dynamic>> _unverifiedFiles = [];
|
|
||||||
bool _isLoading = true;
|
|
||||||
String? _error;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_tabController = TabController(length: 2, vsync: this);
|
|
||||||
_loadFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_tabController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadFiles() async {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
final api = ApiClient();
|
|
||||||
|
|
||||||
// Call API to load files
|
|
||||||
final response = await api.get('/api/v1/admin/files/');
|
|
||||||
final unverifiedResponse = await api.get('/api/v1/admin/files/unverified');
|
|
||||||
|
|
||||||
if (response.data != null && response.data['success'] == true) {
|
|
||||||
final files = response.data['data']['files'] as List<dynamic>;
|
|
||||||
final unverifiedFiles = unverifiedResponse.data != null && unverifiedResponse.data['success'] == true
|
|
||||||
? unverifiedResponse.data['data']['unverified_files'] as List<dynamic>
|
|
||||||
: <dynamic>[];
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_allFiles = files.cast<Map<String, dynamic>>();
|
|
||||||
_unverifiedFiles = unverifiedFiles.cast<Map<String, dynamic>>();
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw Exception(response.data?['message'] ?? 'خطا در دریافت فایلها');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setState(() {
|
|
||||||
_error = e.toString();
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> _forceDeleteFile(String fileId) async {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Text(l10n.deleteConfirm),
|
|
||||||
content: Text(l10n.deleteConfirmMessage),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: Text(l10n.cancel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text(l10n.forceDelete),
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed == true) {
|
|
||||||
try {
|
|
||||||
final api = ApiClient();
|
|
||||||
final response = await api.delete('/api/v1/admin/files/$fileId');
|
|
||||||
|
|
||||||
if (response.data != null && response.data['success'] == true) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.fileDeleted),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_loadFiles();
|
|
||||||
} else {
|
|
||||||
throw Exception(response.data?['message'] ?? 'خطا در حذف فایل');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.errorDeletingFile),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _restoreFile(String fileId) async {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Text(l10n.restoreConfirm),
|
|
||||||
content: Text(l10n.restoreConfirmMessage),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: Text(l10n.cancel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text(l10n.restoreFile),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed == true) {
|
|
||||||
try {
|
|
||||||
final api = ApiClient();
|
|
||||||
final response = await api.put('/api/v1/admin/files/$fileId/restore');
|
|
||||||
|
|
||||||
if (response.data != null && response.data['success'] == true) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.fileRestored),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_loadFiles();
|
|
||||||
} else {
|
|
||||||
throw Exception(response.data?['message'] ?? 'خطا در بازیابی فایل');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.errorRestoringFile),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatFileSize(int bytes) {
|
|
||||||
if (bytes < 1024) return '$bytes B';
|
|
||||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
|
||||||
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
||||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatDate(String dateString) {
|
|
||||||
final date = DateTime.parse(dateString);
|
|
||||||
return '${date.day}/${date.month}/${date.year}';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
tabs: [
|
|
||||||
Tab(
|
|
||||||
icon: const Icon(Icons.folder),
|
|
||||||
text: l10n.allFiles,
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
icon: const Icon(Icons.warning),
|
|
||||||
text: l10n.unverifiedFilesList,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TabBarView(
|
|
||||||
controller: _tabController,
|
|
||||||
children: [
|
|
||||||
_buildFilesList(_allFiles),
|
|
||||||
_buildFilesList(_unverifiedFiles),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFilesList(List<Map<String, dynamic>> files) {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
if (_isLoading) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_error != null) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
size: 64,
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
_error!,
|
|
||||||
style: theme.textTheme.bodyLarge,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _loadFiles,
|
|
||||||
child: Text(l10n.retry),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.folder_outlined,
|
|
||||||
size: 64,
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
l10n.noFilesFound,
|
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
itemCount: files.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final file = files[index];
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
_getFileIcon(file['mime_type']),
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
file['original_name'],
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text('${l10n.fileSize}: ${_formatFileSize(file['file_size'])}'),
|
|
||||||
Text('${l10n.moduleContext}: ${file['module_context']}'),
|
|
||||||
Text('${l10n.createdAt}: ${_formatDate(file['created_at'])}'),
|
|
||||||
if (file['is_temporary'] == true)
|
|
||||||
Text(
|
|
||||||
'${l10n.isTemporary}: ${file['expires_at'] != null ? _formatDate(file['expires_at']) : 'N/A'}',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (file['is_verified'] == false)
|
|
||||||
Text(
|
|
||||||
l10n.isVerified,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: PopupMenuButton<String>(
|
|
||||||
onSelected: (value) {
|
|
||||||
switch (value) {
|
|
||||||
case 'delete':
|
|
||||||
_forceDeleteFile(file['id']);
|
|
||||||
break;
|
|
||||||
case 'restore':
|
|
||||||
_restoreFile(file['id']);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'delete',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.delete, color: theme.colorScheme.error),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(AppLocalizations.of(context).forceDelete),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'restore',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.restore, color: theme.colorScheme.primary),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(AppLocalizations.of(context).restoreFile),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData _getFileIcon(String mimeType) {
|
|
||||||
if (mimeType.startsWith('image/')) return Icons.image;
|
|
||||||
if (mimeType.startsWith('video/')) return Icons.video_file;
|
|
||||||
if (mimeType.startsWith('audio/')) return Icons.audio_file;
|
|
||||||
if (mimeType.contains('pdf')) return Icons.picture_as_pdf;
|
|
||||||
if (mimeType.contains('word')) return Icons.description;
|
|
||||||
if (mimeType.contains('excel') || mimeType.contains('spreadsheet')) return Icons.table_chart;
|
|
||||||
if (mimeType.contains('zip') || mimeType.contains('rar')) return Icons.archive;
|
|
||||||
return Icons.insert_drive_file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,341 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
|
||||||
import '../../../core/api_client.dart';
|
|
||||||
|
|
||||||
class FileStatisticsWidget extends StatefulWidget {
|
|
||||||
const FileStatisticsWidget({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<FileStatisticsWidget> createState() => _FileStatisticsWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FileStatisticsWidgetState extends State<FileStatisticsWidget> {
|
|
||||||
Map<String, dynamic>? _statistics;
|
|
||||||
bool _isLoading = true;
|
|
||||||
String? _error;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadStatistics();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadStatistics() async {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
final api = ApiClient();
|
|
||||||
final response = await api.get('/api/v1/admin/files/statistics');
|
|
||||||
|
|
||||||
if (response.data != null && response.data['success'] == true) {
|
|
||||||
setState(() {
|
|
||||||
_statistics = response.data['data'];
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw Exception(response.data?['message'] ?? 'خطا در دریافت آمار');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setState(() {
|
|
||||||
_error = e.toString();
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Future<void> _cleanupTemporaryFiles() async {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: Text(l10n.cleanupTemporaryFiles),
|
|
||||||
content: Text(l10n.deleteConfirmMessage),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: Text(l10n.cancel),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: Text(l10n.cleanupTemporaryFiles),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed == true) {
|
|
||||||
try {
|
|
||||||
final api = ApiClient();
|
|
||||||
final response = await api.post('/api/v1/admin/files/cleanup-temporary');
|
|
||||||
|
|
||||||
if (response.data != null && response.data['success'] == true) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.cleanupCompleted),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_loadStatistics();
|
|
||||||
} else {
|
|
||||||
throw Exception(response.data?['message'] ?? 'خطا در پاکسازی فایلهای موقت');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Error: $e'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
String _formatFileSize(int bytes) {
|
|
||||||
if (bytes < 1024) return '$bytes B';
|
|
||||||
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
|
||||||
if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
||||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
if (_isLoading) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_error != null) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.error_outline,
|
|
||||||
size: 64,
|
|
||||||
color: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
_error!,
|
|
||||||
style: theme.textTheme.bodyLarge,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _loadStatistics,
|
|
||||||
child: Text(l10n.retry),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
l10n.fileStatistics,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: _cleanupTemporaryFiles,
|
|
||||||
icon: const Icon(Icons.cleaning_services),
|
|
||||||
label: Text(l10n.cleanupTemporaryFiles),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: theme.colorScheme.error,
|
|
||||||
foregroundColor: theme.colorScheme.onError,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Statistics Cards
|
|
||||||
GridView.count(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
crossAxisCount: MediaQuery.of(context).size.width > 600 ? 2 : 1,
|
|
||||||
childAspectRatio: 2.5,
|
|
||||||
crossAxisSpacing: 16,
|
|
||||||
mainAxisSpacing: 16,
|
|
||||||
children: [
|
|
||||||
_buildStatCard(
|
|
||||||
context,
|
|
||||||
l10n.totalFiles,
|
|
||||||
_statistics!['total_files'].toString(),
|
|
||||||
Icons.folder,
|
|
||||||
theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
_buildStatCard(
|
|
||||||
context,
|
|
||||||
l10n.totalSize,
|
|
||||||
_formatFileSize(_statistics!['total_size']),
|
|
||||||
Icons.storage,
|
|
||||||
theme.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
_buildStatCard(
|
|
||||||
context,
|
|
||||||
l10n.temporaryFiles,
|
|
||||||
_statistics!['temporary_files'].toString(),
|
|
||||||
Icons.schedule,
|
|
||||||
theme.colorScheme.tertiary,
|
|
||||||
),
|
|
||||||
_buildStatCard(
|
|
||||||
context,
|
|
||||||
l10n.unverifiedFiles,
|
|
||||||
_statistics!['unverified_files'].toString(),
|
|
||||||
Icons.warning,
|
|
||||||
theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Additional Information
|
|
||||||
Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Storage Information',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildInfoRow(
|
|
||||||
context,
|
|
||||||
'Average file size',
|
|
||||||
_formatFileSize(_statistics!['total_size'] ~/ _statistics!['total_files']),
|
|
||||||
Icons.info_outline,
|
|
||||||
),
|
|
||||||
_buildInfoRow(
|
|
||||||
context,
|
|
||||||
'Storage efficiency',
|
|
||||||
'95%',
|
|
||||||
Icons.trending_up,
|
|
||||||
),
|
|
||||||
_buildInfoRow(
|
|
||||||
context,
|
|
||||||
'Last cleanup',
|
|
||||||
'2 days ago',
|
|
||||||
Icons.cleaning_services,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStatCard(
|
|
||||||
BuildContext context,
|
|
||||||
String title,
|
|
||||||
String value,
|
|
||||||
IconData icon,
|
|
||||||
Color color,
|
|
||||||
) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
icon,
|
|
||||||
color: color,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: theme.textTheme.headlineSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInfoRow(
|
|
||||||
BuildContext context,
|
|
||||||
String label,
|
|
||||||
String value,
|
|
||||||
IconData icon,
|
|
||||||
) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,47 +23,67 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isDefault = config['is_default'] == true;
|
final isDefault = config['is_default'] == true;
|
||||||
final isActive = config['is_active'] == true;
|
final isActive = config['is_active'] == true;
|
||||||
|
final storageType = config['storage_type'] ?? 'unknown';
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 4,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: isDefault
|
||||||
|
? BorderSide(color: theme.colorScheme.primary, width: 2)
|
||||||
|
: BorderSide.none,
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
gradient: isDefault
|
||||||
|
? LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary.withOpacity(0.05),
|
||||||
|
theme.colorScheme.primary.withOpacity(0.02),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Header
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Container(
|
||||||
_getStorageIcon(config['storage_type']),
|
padding: const EdgeInsets.all(12),
|
||||||
color: theme.colorScheme.primary,
|
decoration: BoxDecoration(
|
||||||
size: 24,
|
color: _getStorageColor(storageType).withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
child: Icon(
|
||||||
|
_getStorageIcon(storageType),
|
||||||
|
color: _getStorageColor(storageType),
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
config['name'] ?? 'Unknown',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
_getStorageTypeName(config['storage_type']),
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Status badges
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (isDefault)
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
config['name'] ?? 'Unknown',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isDefault) ...[
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8,
|
horizontal: 8,
|
||||||
|
|
@ -73,13 +93,47 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Row(
|
||||||
l10n.isDefault,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.star,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'پیشفرض',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onPrimary,
|
color: theme.colorScheme.onPrimary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getStorageColor(storageType),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_getStorageTypeName(storageType),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
|
|
@ -89,10 +143,10 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isActive ? Colors.green : Colors.red,
|
color: isActive ? Colors.green : Colors.red,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
isActive ? l10n.isActive : 'غیرفعال',
|
isActive ? 'فعال' : 'غیرفعال',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -103,52 +157,57 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Configuration details
|
// Configuration details
|
||||||
_buildConfigDetails(context, config),
|
_buildConfigDetails(context, config),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (onTestConnection != null)
|
if (onTestConnection != null)
|
||||||
TextButton.icon(
|
_buildActionButton(
|
||||||
onPressed: onTestConnection,
|
context: context,
|
||||||
icon: const Icon(Icons.wifi_protected_setup, size: 16),
|
icon: Icons.wifi_protected_setup,
|
||||||
label: Text(l10n.testConnection),
|
label: l10n.testConnection,
|
||||||
|
onPressed: onTestConnection!,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
if (onEdit != null) ...[
|
if (onEdit != null) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
_buildActionButton(
|
||||||
onPressed: onEdit,
|
context: context,
|
||||||
icon: const Icon(Icons.edit, size: 16),
|
icon: Icons.edit,
|
||||||
label: Text(l10n.edit),
|
label: l10n.edit,
|
||||||
|
onPressed: onEdit!,
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (onSetDefault != null) ...[
|
if (onSetDefault != null) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
_buildActionButton(
|
||||||
onPressed: onSetDefault,
|
context: context,
|
||||||
icon: const Icon(Icons.star, size: 16),
|
icon: Icons.star,
|
||||||
label: Text(l10n.setAsDefault),
|
label: l10n.setAsDefault,
|
||||||
style: TextButton.styleFrom(
|
onPressed: onSetDefault!,
|
||||||
foregroundColor: theme.colorScheme.primary,
|
color: Colors.orange,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (onDelete != null) ...[
|
if (onDelete != null) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
_buildActionButton(
|
||||||
onPressed: onDelete,
|
context: context,
|
||||||
icon: const Icon(Icons.delete, size: 16),
|
icon: Icons.delete,
|
||||||
label: Text(l10n.delete),
|
label: l10n.delete,
|
||||||
style: TextButton.styleFrom(
|
onPressed: onDelete!,
|
||||||
foregroundColor: theme.colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -156,70 +215,213 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButton({
|
||||||
|
required BuildContext context,
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
required Color color,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: Icon(icon, size: 18),
|
||||||
|
label: Text(label),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: color,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 2,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildConfigDetails(BuildContext context, Map<String, dynamic> config) {
|
Widget _buildConfigDetails(BuildContext context, Map<String, dynamic> config) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final theme = Theme.of(context);
|
||||||
final configData = config['config_data'] ?? {};
|
final configData = config['config_data'] ?? {};
|
||||||
final storageType = config['storage_type'];
|
final storageType = config['storage_type'];
|
||||||
|
|
||||||
if (storageType == 'local') {
|
if (storageType == 'local') {
|
||||||
return Column(
|
return _buildLocalConfigDetails(context, configData);
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildDetailRow(
|
|
||||||
context,
|
|
||||||
l10n.basePath,
|
|
||||||
configData['base_path'] ?? 'N/A',
|
|
||||||
Icons.folder,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else if (storageType == 'ftp') {
|
} else if (storageType == 'ftp') {
|
||||||
return Column(
|
return _buildFtpConfigDetails(context, configData);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'نوع ذخیرهسازی نامشخص',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLocalConfigDetails(BuildContext context, Map<String, dynamic> configData) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final basePath = configData['base_path'] ?? '';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outline.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildDetailRow(
|
Row(
|
||||||
context,
|
children: [
|
||||||
l10n.ftpHost,
|
Icon(
|
||||||
configData['host'] ?? 'N/A',
|
Icons.folder_outlined,
|
||||||
Icons.dns,
|
size: 20,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(width: 8),
|
||||||
_buildDetailRow(
|
Text(
|
||||||
context,
|
'پیکربندی ذخیرهسازی محلی',
|
||||||
l10n.ftpPort,
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
configData['port']?.toString() ?? 'N/A',
|
fontWeight: FontWeight.bold,
|
||||||
Icons.settings_ethernet,
|
color: theme.colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildDetailRow(
|
|
||||||
context,
|
|
||||||
l10n.ftpUsername,
|
|
||||||
configData['username'] ?? 'N/A',
|
|
||||||
Icons.person,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildDetailRow(
|
|
||||||
context,
|
|
||||||
l10n.ftpDirectory,
|
|
||||||
configData['directory'] ?? 'N/A',
|
|
||||||
Icons.folder,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.basePath,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.folder,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
basePath,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return const SizedBox.shrink();
|
Widget _buildFtpConfigDetails(BuildContext context, Map<String, dynamic> configData) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final host = configData['host'] ?? '';
|
||||||
|
final port = configData['port'] ?? 21;
|
||||||
|
final username = configData['username'] ?? '';
|
||||||
|
final directory = configData['directory'] ?? '/';
|
||||||
|
final useTls = configData['use_tls'] == true;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outline.withOpacity(0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.cloud_outlined,
|
||||||
|
size: 20,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'پیکربندی FTP',
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildConfigRow(
|
||||||
|
context,
|
||||||
|
Icons.dns,
|
||||||
|
'میزبان',
|
||||||
|
host,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildConfigRow(
|
||||||
|
context,
|
||||||
|
Icons.settings_ethernet,
|
||||||
|
'پورت',
|
||||||
|
port.toString(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildConfigRow(
|
||||||
|
context,
|
||||||
|
Icons.person,
|
||||||
|
l10n.username,
|
||||||
|
username,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildConfigRow(
|
||||||
|
context,
|
||||||
|
Icons.folder,
|
||||||
|
'دایرکتوری',
|
||||||
|
directory,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildConfigRow(
|
||||||
|
context,
|
||||||
|
Icons.security,
|
||||||
|
'امنیت',
|
||||||
|
useTls ? 'TLS فعال' : 'TLS غیرفعال',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailRow(
|
Widget _buildConfigRow(BuildContext context, IconData icon, String label, String value) {
|
||||||
BuildContext context,
|
|
||||||
String label,
|
|
||||||
String value,
|
|
||||||
IconData icon,
|
|
||||||
) {
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
|
|
@ -227,22 +429,21 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'$label: ',
|
'$label: ',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
fontWeight: FontWeight.w500,
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.8),
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -256,7 +457,18 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
case 'ftp':
|
case 'ftp':
|
||||||
return Icons.cloud_upload;
|
return Icons.cloud_upload;
|
||||||
default:
|
default:
|
||||||
return Icons.storage;
|
return Icons.help_outline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getStorageColor(String storageType) {
|
||||||
|
switch (storageType) {
|
||||||
|
case 'local':
|
||||||
|
return Colors.blue;
|
||||||
|
case 'ftp':
|
||||||
|
return Colors.green;
|
||||||
|
default:
|
||||||
|
return Colors.grey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,9 +477,9 @@ class StorageConfigCard extends StatelessWidget {
|
||||||
case 'local':
|
case 'local':
|
||||||
return 'Local Storage';
|
return 'Local Storage';
|
||||||
case 'ftp':
|
case 'ftp':
|
||||||
return 'FTP Storage';
|
return 'FTP Server';
|
||||||
default:
|
default:
|
||||||
return 'Unknown Storage';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,10 +4,12 @@ import '../../../core/api_client.dart';
|
||||||
|
|
||||||
class StorageConfigFormDialog extends StatefulWidget {
|
class StorageConfigFormDialog extends StatefulWidget {
|
||||||
final Map<String, dynamic>? config;
|
final Map<String, dynamic>? config;
|
||||||
|
final VoidCallback? onSaved;
|
||||||
|
|
||||||
const StorageConfigFormDialog({
|
const StorageConfigFormDialog({
|
||||||
super.key,
|
super.key,
|
||||||
this.config,
|
this.config,
|
||||||
|
this.onSaved,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -28,6 +30,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
bool _isDefault = false;
|
bool _isDefault = false;
|
||||||
bool _isActive = true;
|
bool _isActive = true;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
bool _useTls = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -49,10 +52,11 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
_basePathController.text = configData['base_path'] ?? '';
|
_basePathController.text = configData['base_path'] ?? '';
|
||||||
} else if (_selectedStorageType == 'ftp') {
|
} else if (_selectedStorageType == 'ftp') {
|
||||||
_ftpHostController.text = configData['host'] ?? '';
|
_ftpHostController.text = configData['host'] ?? '';
|
||||||
_ftpPortController.text = configData['port']?.toString() ?? '21';
|
_ftpPortController.text = (configData['port'] ?? 21).toString();
|
||||||
_ftpUsernameController.text = configData['username'] ?? '';
|
_ftpUsernameController.text = configData['username'] ?? '';
|
||||||
_ftpPasswordController.text = configData['password'] ?? '';
|
_ftpPasswordController.text = configData['password'] ?? '';
|
||||||
_ftpDirectoryController.text = configData['directory'] ?? '';
|
_ftpDirectoryController.text = configData['directory'] ?? '/';
|
||||||
|
_useTls = configData['use_tls'] == true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,27 +72,8 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _buildConfigData() {
|
|
||||||
if (_selectedStorageType == 'local') {
|
|
||||||
return {
|
|
||||||
'base_path': _basePathController.text,
|
|
||||||
};
|
|
||||||
} else if (_selectedStorageType == 'ftp') {
|
|
||||||
return {
|
|
||||||
'host': _ftpHostController.text,
|
|
||||||
'port': int.tryParse(_ftpPortController.text) ?? 21,
|
|
||||||
'username': _ftpUsernameController.text,
|
|
||||||
'password': _ftpPasswordController.text,
|
|
||||||
'directory': _ftpDirectoryController.text,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _saveConfig() async {
|
Future<void> _saveConfig() async {
|
||||||
if (!_formKey.currentState!.validate()) {
|
if (!_formKey.currentState!.validate()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
|
|
@ -96,29 +81,69 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final api = ApiClient();
|
final api = ApiClient();
|
||||||
final response = await api.post(
|
Map<String, dynamic> configData = {};
|
||||||
'/api/v1/admin/files/storage-configs/',
|
|
||||||
data: {
|
if (_selectedStorageType == 'local') {
|
||||||
'name': _nameController.text,
|
configData = {
|
||||||
|
'base_path': _basePathController.text.trim(),
|
||||||
|
};
|
||||||
|
} else if (_selectedStorageType == 'ftp') {
|
||||||
|
configData = {
|
||||||
|
'host': _ftpHostController.text.trim(),
|
||||||
|
'port': int.tryParse(_ftpPortController.text) ?? 21,
|
||||||
|
'username': _ftpUsernameController.text.trim(),
|
||||||
|
'password': _ftpPasswordController.text,
|
||||||
|
'directory': _ftpDirectoryController.text.trim(),
|
||||||
|
'use_tls': _useTls,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final requestData = {
|
||||||
|
'name': _nameController.text.trim(),
|
||||||
'storage_type': _selectedStorageType,
|
'storage_type': _selectedStorageType,
|
||||||
|
'config_data': configData,
|
||||||
'is_default': _isDefault,
|
'is_default': _isDefault,
|
||||||
'is_active': _isActive,
|
'is_active': _isActive,
|
||||||
'config_data': _buildConfigData(),
|
};
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.data != null && response.data['success'] == true) {
|
if (widget.config != null) {
|
||||||
if (mounted) {
|
// Update existing config
|
||||||
Navigator.of(context).pop(response.data['data']);
|
await api.put(
|
||||||
}
|
'/api/v1/admin/files/storage-configs/${widget.config!['id']}',
|
||||||
|
data: requestData,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw Exception(response.data?['message'] ?? 'خطا در ذخیره تنظیمات');
|
// Create new config
|
||||||
|
await api.post(
|
||||||
|
'/api/v1/admin/files/storage-configs/',
|
||||||
|
data: requestData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// Only show SnackBar if there's no onSaved callback (parent will handle notification)
|
||||||
|
if (widget.onSaved == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
widget.config != null
|
||||||
|
? 'تنظیمات ذخیرهسازی بهروزرسانی شد'
|
||||||
|
: 'تنظیمات ذخیرهسازی ایجاد شد',
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.onSaved?.call();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Error: $e'),
|
content: Text('خطا در ذخیره تنظیمات: $e'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -139,211 +164,192 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
final isEditing = widget.config != null;
|
final isEditing = widget.config != null;
|
||||||
|
|
||||||
return Dialog(
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: MediaQuery.of(context).size.width * 0.8,
|
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700),
|
||||||
constraints: const BoxConstraints(maxWidth: 600),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
// Header
|
||||||
padding: const EdgeInsets.all(16),
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(20),
|
||||||
|
topRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isEditing ? Icons.edit : Icons.add,
|
isEditing ? Icons.edit : Icons.add,
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.onPrimary,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
isEditing
|
||||||
|
? 'ویرایش پیکربندی ذخیرهسازی'
|
||||||
|
: 'ایجاد پیکربندی ذخیرهسازی',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
isEditing ? l10n.editStorageConfig : l10n.addStorageConfig,
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Form
|
||||||
Flexible(
|
Flexible(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Basic Information
|
||||||
|
_buildSectionHeader(context, 'اطلاعات پایه'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: l10n.storageName,
|
labelText: 'نام',
|
||||||
border: const OutlineInputBorder(),
|
hintText: 'نام پیکربندی ذخیرهسازی را وارد کنید',
|
||||||
|
prefixIcon: const Icon(Icons.label_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return l10n.requiredField;
|
return 'لطفاً نام را وارد کنید';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Storage Type
|
// Storage Type
|
||||||
DropdownButtonFormField<String>(
|
Text(
|
||||||
value: _selectedStorageType,
|
l10n.storageType,
|
||||||
decoration: InputDecoration(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
labelText: l10n.storageType,
|
fontWeight: FontWeight.bold,
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
),
|
||||||
items: [
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'local',
|
|
||||||
child: Text(l10n.localStorage),
|
|
||||||
),
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: 'ftp',
|
|
||||||
child: Text(l10n.ftpStorage),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: RadioListTile<String>(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.storage, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(l10n.localStorage),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
value: 'local',
|
||||||
|
groupValue: _selectedStorageType,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedStorageType = value!;
|
_selectedStorageType = value!;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Configuration based on storage type
|
|
||||||
if (_selectedStorageType == 'local') ...[
|
|
||||||
TextFormField(
|
|
||||||
controller: _basePathController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.basePath,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
hintText: '/var/hesabix/files',
|
|
||||||
),
|
),
|
||||||
validator: (value) {
|
Expanded(
|
||||||
if (value == null || value.isEmpty) {
|
child: RadioListTile<String>(
|
||||||
return l10n.requiredField;
|
title: Row(
|
||||||
}
|
children: [
|
||||||
return null;
|
Icon(Icons.cloud_upload, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('سرور FTP'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
value: 'ftp',
|
||||||
|
groupValue: _selectedStorageType,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedStorageType = value!;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
] else if (_selectedStorageType == 'ftp') ...[
|
|
||||||
TextFormField(
|
|
||||||
controller: _ftpHostController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.ftpHost,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
hintText: 'ftp.example.com',
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return l10n.requiredField;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: _ftpPortController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.ftpPort,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
hintText: '21',
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return l10n.requiredField;
|
|
||||||
}
|
|
||||||
if (int.tryParse(value) == null) {
|
|
||||||
return 'Invalid port number';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: _ftpUsernameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.ftpUsername,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
hintText: 'username',
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return l10n.requiredField;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: _ftpPasswordController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.ftpPassword,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
hintText: 'password',
|
|
||||||
),
|
|
||||||
obscureText: true,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return l10n.requiredField;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: _ftpDirectoryController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: l10n.ftpDirectory,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
hintText: '/hesabix/files',
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return l10n.requiredField;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Configuration Details
|
||||||
|
_buildSectionHeader(context, 'جزئیات پیکربندی'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
if (_selectedStorageType == 'local') ...[
|
||||||
|
_buildLocalConfigFields(context),
|
||||||
|
] else if (_selectedStorageType == 'ftp') ...[
|
||||||
|
_buildFtpConfigFields(context),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
Row(
|
_buildSectionHeader(context, 'گزینهها'),
|
||||||
children: [
|
const SizedBox(height: 16),
|
||||||
Checkbox(
|
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text('تنظیم به عنوان پیشفرض'),
|
||||||
|
subtitle: Text('این پیکربندی به عنوان پیشفرض تنظیم شود'),
|
||||||
value: _isDefault,
|
value: _isDefault,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDefault = value ?? false;
|
_isDefault = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
secondary: const Icon(Icons.star),
|
||||||
),
|
),
|
||||||
Text(l10n.isDefault),
|
|
||||||
const SizedBox(width: 24),
|
SwitchListTile(
|
||||||
Checkbox(
|
title: Text('فعال'),
|
||||||
|
subtitle: Text('این پیکربندی فعال باشد'),
|
||||||
value: _isActive,
|
value: _isActive,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isActive = value ?? false;
|
_isActive = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
secondary: const Icon(Icons.power),
|
||||||
Text(l10n.isActive),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
|
// Actions
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(20),
|
||||||
|
bottomRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -351,16 +357,26 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||||
child: Text(l10n.cancel),
|
child: Text(l10n.cancel),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 12),
|
||||||
ElevatedButton(
|
ElevatedButton.icon(
|
||||||
onPressed: _isLoading ? null : _saveConfig,
|
onPressed: _isLoading ? null : _saveConfig,
|
||||||
child: _isLoading
|
icon: _isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: Text(l10n.save),
|
: Icon(isEditing ? Icons.save : Icons.add),
|
||||||
|
label: Text(
|
||||||
|
_isLoading
|
||||||
|
? 'در حال ذخیره...'
|
||||||
|
: (isEditing ? 'بهروزرسانی' : 'ایجاد'),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -368,7 +384,175 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 4,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLocalConfigFields(BuildContext context) {
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _basePathController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'مسیر پایه',
|
||||||
|
hintText: 'مسیر پایه را وارد کنید',
|
||||||
|
prefixIcon: const Icon(Icons.folder_outlined),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'لطفاً مسیر پایه را وارد کنید';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFtpConfigFields(BuildContext context) {
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _ftpHostController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'میزبان',
|
||||||
|
hintText: 'آدرس میزبان را وارد کنید',
|
||||||
|
prefixIcon: const Icon(Icons.dns),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'لطفاً میزبان را وارد کنید';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextFormField(
|
||||||
|
controller: _ftpPortController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'پورت',
|
||||||
|
hintText: '21',
|
||||||
|
prefixIcon: const Icon(Icons.settings_ethernet),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'لطفاً پورت را وارد کنید';
|
||||||
|
}
|
||||||
|
final port = int.tryParse(value);
|
||||||
|
if (port == null || port < 1 || port > 65535) {
|
||||||
|
return 'لطفاً پورت معتبر وارد کنید';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextFormField(
|
||||||
|
controller: _ftpUsernameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'نام کاربری',
|
||||||
|
hintText: 'نام کاربری را وارد کنید',
|
||||||
|
prefixIcon: const Icon(Icons.person),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'لطفاً نام کاربری را وارد کنید';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextFormField(
|
||||||
|
controller: _ftpPasswordController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'رمز عبور',
|
||||||
|
hintText: 'رمز عبور را وارد کنید',
|
||||||
|
prefixIcon: const Icon(Icons.lock),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
obscureText: true,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'لطفاً رمز عبور را وارد کنید';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextFormField(
|
||||||
|
controller: _ftpDirectoryController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'دایرکتوری',
|
||||||
|
hintText: '/',
|
||||||
|
prefixIcon: const Icon(Icons.folder),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text('استفاده از TLS'),
|
||||||
|
subtitle: Text('اتصال امن با TLS فعال شود'),
|
||||||
|
value: _useTls,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_useTls = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
secondary: const Icon(Icons.security),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
|
||||||
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_form_dialog.dart';
|
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_form_dialog.dart';
|
||||||
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_card.dart';
|
import 'package:hesabix_ui/widgets/admin/file_storage/storage_config_card.dart';
|
||||||
import '../../../core/api_client.dart';
|
import '../../../core/api_client.dart';
|
||||||
|
|
||||||
class StorageConfigListWidget extends StatefulWidget {
|
class StorageConfigListWidget extends StatefulWidget {
|
||||||
const StorageConfigListWidget({super.key});
|
final VoidCallback? onRefresh;
|
||||||
|
|
||||||
|
const StorageConfigListWidget({
|
||||||
|
super.key,
|
||||||
|
this.onRefresh,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StorageConfigListWidget> createState() => _StorageConfigListWidgetState();
|
State<StorageConfigListWidget> createState() => StorageConfigListWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
List<Map<String, dynamic>> _storageConfigs = [];
|
List<Map<String, dynamic>> _storageConfigs = [];
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
String? _error;
|
String? _error;
|
||||||
|
|
@ -19,10 +23,10 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadStorageConfigs();
|
loadStorageConfigs();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadStorageConfigs() async {
|
Future<void> loadStorageConfigs() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_error = null;
|
_error = null;
|
||||||
|
|
@ -49,55 +53,7 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _addStorageConfig() async {
|
|
||||||
final result = await showDialog<Map<String, dynamic>>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => const StorageConfigFormDialog(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
_loadStorageConfigs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _editStorageConfig(Map<String, dynamic> config) async {
|
|
||||||
final result = await showDialog<Map<String, dynamic>>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => StorageConfigFormDialog(config: config),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
_loadStorageConfigs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setAsDefault(String configId) async {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
try {
|
|
||||||
// TODO: Call API to set as default
|
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(l10n.setAsDefault),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_loadStorageConfigs();
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Error: $e'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _testConnection(String configId) async {
|
Future<void> _testConnection(String configId) async {
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
try {
|
try {
|
||||||
final api = ApiClient();
|
final api = ApiClient();
|
||||||
final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test');
|
final response = await api.post('/api/v1/admin/files/storage-configs/$configId/test');
|
||||||
|
|
@ -107,14 +63,14 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
if (testResult['success'] == true) {
|
if (testResult['success'] == true) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(l10n.connectionSuccessful),
|
content: Text('اتصال موفقیتآمیز بود'),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('${l10n.connectionFailed}: ${testResult['error']}'),
|
content: Text('اتصال ناموفق: ${testResult['error']}'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -125,29 +81,27 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('${l10n.connectionFailed}: $e'),
|
content: Text('اتصال ناموفق: $e'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _deleteConfig(String configId) async {
|
Future<void> _deleteConfig(String configId) async {
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text(l10n.deleteConfirm),
|
title: Text('تأیید حذف'),
|
||||||
content: Text(l10n.deleteConfirmMessage),
|
content: Text('آیا از حذف این پیکربندی اطمینان دارید؟'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
child: Text(l10n.cancel),
|
child: Text('لغو'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
child: Text(l10n.delete),
|
child: Text('حذف'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -155,36 +109,116 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to delete config
|
final api = ApiClient();
|
||||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
final response = await api.delete('/api/v1/admin/files/storage-configs/$configId');
|
||||||
|
|
||||||
|
if (response.data != null && response.data['success'] == true) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(l10n.fileDeleted),
|
content: Text('فایل حذف شد'),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_loadStorageConfigs();
|
// Refresh the list
|
||||||
|
loadStorageConfigs();
|
||||||
|
} else {
|
||||||
|
final errorMessage = response.data?['error']?['message'] ??
|
||||||
|
response.data?['message'] ??
|
||||||
|
'خطا در حذف تنظیمات';
|
||||||
|
throw Exception(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
String errorMessage = 'خطا در حذف تنظیمات';
|
||||||
|
|
||||||
|
// بررسی نوع خطا
|
||||||
|
if (e.toString().contains('STORAGE_CONFIG_HAS_FILES')) {
|
||||||
|
errorMessage = 'این تنظیمات ذخیرهسازی دارای فایل است و قابل حذف نیست';
|
||||||
|
} else if (e.toString().contains('STORAGE_CONFIG_NOT_FOUND')) {
|
||||||
|
errorMessage = 'تنظیمات ذخیرهسازی یافت نشد';
|
||||||
|
} else if (e.toString().contains('FORBIDDEN')) {
|
||||||
|
errorMessage = 'دسترسی غیرمجاز';
|
||||||
|
} else {
|
||||||
|
errorMessage = e.toString().replaceFirst('Exception: ', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(errorMessage),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 5),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setAsDefault(String configId) async {
|
||||||
|
try {
|
||||||
|
final api = ApiClient();
|
||||||
|
final response = await api.put('/api/v1/admin/files/storage-configs/$configId/set-default');
|
||||||
|
|
||||||
|
if (response.data != null && response.data['success'] == true) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('تنظیمات به عنوان پیشفرض تنظیم شد'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh the list
|
||||||
|
loadStorageConfigs();
|
||||||
|
} else {
|
||||||
|
throw Exception(response.data?['message'] ?? 'خطا در تنظیم به عنوان پیشفرض');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Error: $e'),
|
content: Text('خطا در تنظیم به عنوان پیشفرض: $e'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _editStorageConfig(Map<String, dynamic> config) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => StorageConfigFormDialog(
|
||||||
|
config: config,
|
||||||
|
onSaved: () {
|
||||||
|
loadStorageConfigs();
|
||||||
|
// Show success message
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('تنظیمات ذخیرهسازی بهروزرسانی شد'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
return const Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(),
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'در حال بارگذاری...',
|
||||||
|
style: theme.textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,69 +233,109 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
color: theme.colorScheme.error,
|
color: theme.colorScheme.error,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'خطا',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
_error!,
|
_error!,
|
||||||
style: theme.textTheme.bodyLarge,
|
style: theme.textTheme.bodyMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton.icon(
|
||||||
onPressed: _loadStorageConfigs,
|
onPressed: loadStorageConfigs,
|
||||||
child: Text(l10n.retry),
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: Text('تلاش مجدد'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
if (_storageConfigs.isEmpty) {
|
||||||
children: [
|
return Center(
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
l10n.storageConfigurations,
|
|
||||||
style: theme.textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: _addStorageConfig,
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
label: Text(l10n.addStorageConfig),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: _storageConfigs.isEmpty
|
|
||||||
? Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.storage_outlined,
|
Icons.storage_outlined,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
color: theme.colorScheme.primary.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
l10n.noFilesFound,
|
'هیچ پیکربندی ذخیرهسازی وجود ندارد',
|
||||||
style: theme.textTheme.bodyLarge?.copyWith(
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'اولین پیکربندی ذخیرهسازی را ایجاد کنید',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'از دکمه + در پایین صفحه استفاده کنید',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
: ListView.builder(
|
}
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: loadStorageConfigs,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.storage,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'پیکربندیهای ذخیرهسازی',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
'${_storageConfigs.length} پیکربندی',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Storage Configs List
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
itemCount: _storageConfigs.length,
|
itemCount: _storageConfigs.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final config = _storageConfigs[index];
|
final config = _storageConfigs[index];
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: StorageConfigCard(
|
child: StorageConfigCard(
|
||||||
config: config,
|
config: config,
|
||||||
onEdit: () => _editStorageConfig(config),
|
onEdit: () => _editStorageConfig(config),
|
||||||
|
|
@ -269,15 +343,15 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||||
? () => _setAsDefault(config['id'])
|
? () => _setAsDefault(config['id'])
|
||||||
: null,
|
: null,
|
||||||
onTestConnection: () => _testConnection(config['id']),
|
onTestConnection: () => _testConnection(config['id']),
|
||||||
onDelete: config['is_default'] == false
|
onDelete: () => _deleteConfig(config['id']),
|
||||||
? () => _deleteConfig(config['id'])
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'storage_config_list_widget.dart';
|
||||||
|
import 'storage_config_form_dialog.dart';
|
||||||
|
|
||||||
|
class StorageManagementPage extends StatefulWidget {
|
||||||
|
const StorageManagementPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StorageManagementPage> createState() => _StorageManagementPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StorageManagementPageState extends State<StorageManagementPage> {
|
||||||
|
final GlobalKey<StorageConfigListWidgetState> _listKey = GlobalKey<StorageConfigListWidgetState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'پیکربندیهای ذخیرهسازی',
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
theme.colorScheme.primary.withOpacity(0.1),
|
||||||
|
theme.colorScheme.surface,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: StorageConfigListWidget(
|
||||||
|
key: _listKey,
|
||||||
|
onRefresh: () => _listKey.currentState?.loadStorageConfigs(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () => _showCreateDialog(context),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('ایجاد پیکربندی ذخیرهسازی'),
|
||||||
|
backgroundColor: theme.colorScheme.primary,
|
||||||
|
foregroundColor: theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateDialog(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => StorageConfigFormDialog(
|
||||||
|
onSaved: () {
|
||||||
|
// Refresh the list
|
||||||
|
_listKey.currentState?.loadStorageConfigs();
|
||||||
|
// Show success message
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('تنظیمات ذخیرهسازی ایجاد شد'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:html' as html;
|
// import 'dart:html' as html; // Not available on Linux
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:data_table_2/data_table_2.dart';
|
import 'package:data_table_2/data_table_2.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
|
@ -621,60 +621,17 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Platform-specific download functions for Linux
|
||||||
Future<void> _downloadPdf(dynamic data, String filename) async {
|
Future<void> _downloadPdf(dynamic data, String filename) async {
|
||||||
try {
|
// For Linux desktop, we'll save to Downloads folder
|
||||||
if (data is List<int>) {
|
print('Download PDF: $filename (Linux desktop - save to Downloads folder)');
|
||||||
// Convert bytes to Uint8List
|
// TODO: Implement proper file saving for Linux
|
||||||
final bytes = Uint8List.fromList(data);
|
|
||||||
|
|
||||||
// Create blob and download
|
|
||||||
final blob = html.Blob([bytes], 'application/pdf');
|
|
||||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
|
||||||
|
|
||||||
html.AnchorElement(href: url)
|
|
||||||
..setAttribute('download', filename)
|
|
||||||
..click();
|
|
||||||
|
|
||||||
html.Url.revokeObjectUrl(url);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error downloading PDF: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadExcel(dynamic data, String filename) async {
|
Future<void> _downloadExcel(dynamic data, String filename) async {
|
||||||
try {
|
// For Linux desktop, we'll save to Downloads folder
|
||||||
if (data is List<int>) {
|
print('Download Excel: $filename (Linux desktop - save to Downloads folder)');
|
||||||
// Handle binary Excel data from server
|
// TODO: Implement proper file saving for Linux
|
||||||
final bytes = Uint8List.fromList(data);
|
|
||||||
final blob = html.Blob([bytes], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
|
||||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
|
||||||
|
|
||||||
html.AnchorElement(href: url)
|
|
||||||
..setAttribute('download', filename)
|
|
||||||
..click();
|
|
||||||
|
|
||||||
html.Url.revokeObjectUrl(url);
|
|
||||||
} else if (data is Map<String, dynamic>) {
|
|
||||||
// Fallback: Convert to CSV format (legacy support)
|
|
||||||
final excelData = data['data'] as List<dynamic>?;
|
|
||||||
if (excelData != null) {
|
|
||||||
final csvContent = _convertToCsv(excelData);
|
|
||||||
final bytes = Uint8List.fromList(csvContent.codeUnits);
|
|
||||||
|
|
||||||
final blob = html.Blob([bytes], 'text/csv');
|
|
||||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
|
||||||
|
|
||||||
html.AnchorElement(href: url)
|
|
||||||
..setAttribute('download', filename.replaceAll('.xlsx', '.csv'))
|
|
||||||
..click();
|
|
||||||
|
|
||||||
html.Url.revokeObjectUrl(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error downloading Excel: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _convertToCsv(List<dynamic> data) {
|
String _convertToCsv(List<dynamic> data) {
|
||||||
|
|
|
||||||
312
run_linux.sh
Executable file
312
run_linux.sh
Executable file
|
|
@ -0,0 +1,312 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# تنظیم trap برای بازیابی فایلها هنگام خروج
|
||||||
|
trap 'if [ -n "${APP_DIR:-}" ]; then restore_platform_files "$APP_DIR"; fi' EXIT
|
||||||
|
|
||||||
|
# Quick launcher for Flutter Linux Desktop in this repo.
|
||||||
|
# Smartly detects Flutter binary and the app directory.
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$SCRIPT_DIR"
|
||||||
|
|
||||||
|
DEFAULT_MODE="debug" # debug|profile|release
|
||||||
|
DEFAULT_BUILD_DIR="build/linux"
|
||||||
|
|
||||||
|
USER_PROJECT=""
|
||||||
|
MODE="$DEFAULT_MODE"
|
||||||
|
BUILD_DIR=""
|
||||||
|
CLEAN_BUILD=false
|
||||||
|
INSTALL_DEPS=false
|
||||||
|
API_BASE_URL=""
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ./run_linux.sh [--project <path>] [--mode <debug|profile|release>] [--build-dir <dir>] [--clean] [--install-deps] [--api-base-url <url>] [--help]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--project PATH مسیر پروژه فلاتر (حاوی pubspec.yaml). در صورت عدم تعیین، بهصورت خودکار تشخیص میشود.
|
||||||
|
--mode MODE نوع اجرا: debug، profile یا release (پیشفرض: $DEFAULT_MODE).
|
||||||
|
--build-dir DIR مسیر build directory (پیشفرض: $DEFAULT_BUILD_DIR).
|
||||||
|
--clean پاک کردن build directory قبل از build.
|
||||||
|
--install-deps نصب وابستگیها قبل از اجرا.
|
||||||
|
--api-base-url آدرس پایه API که به برنامه بهصورت --dart-define پاس داده میشود.
|
||||||
|
-h, --help نمایش راهنما.
|
||||||
|
|
||||||
|
نمونه اجرا:
|
||||||
|
./run_linux.sh
|
||||||
|
./run_linux.sh --mode release --clean
|
||||||
|
./run_linux.sh --project hesabixUI/hesabix_ui --install-deps
|
||||||
|
./run_linux.sh --api-base-url http://localhost:8000 --mode profile
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() { echo "[warn] $*" >&2; }
|
||||||
|
die() { echo "[error] $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
cmd_exists() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
ensure_flutter_in_path() {
|
||||||
|
if cmd_exists flutter; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local SNAP_FLUTTER_BIN="$HOME/snap/flutter/common/flutter/bin"
|
||||||
|
if [ -d "$SNAP_FLUTTER_BIN" ]; then
|
||||||
|
export PATH="$PATH:$SNAP_FLUTTER_BIN"
|
||||||
|
fi
|
||||||
|
if ! cmd_exists flutter; then
|
||||||
|
die "Flutter یافت نشد. لطفاً آنرا نصب کرده یا PATH را تنظیم کنید. مسیر پیشنهادی: $SNAP_FLUTTER_BIN"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_flutter_project_dir() {
|
||||||
|
local dir="$1"
|
||||||
|
[ -f "$dir/pubspec.yaml" ] || return 1
|
||||||
|
# حداقل بررسی: وجود sdk: flutter در pubspec.yaml
|
||||||
|
if grep -qiE "sdk:\s*flutter" "$dir/pubspec.yaml"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# برخی قالبها ممکن است شکل دیگری داشته باشند؛ صرف وجود pubspec را کافی بدانیم
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_detect_project_dir() {
|
||||||
|
# اولویت: آرگومان کاربر → متغیر محیطی → مسیر متداول → جستجو در hesabixUI
|
||||||
|
if [ -n "$USER_PROJECT" ]; then
|
||||||
|
local p="$USER_PROJECT"
|
||||||
|
[ -d "$p" ] || die "مسیر پروژه موجود نیست: $p"
|
||||||
|
is_flutter_project_dir "$p" || die "pubspec.yaml معتبر در مسیر یافت نشد: $p"
|
||||||
|
echo "$(cd "$p" && pwd)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${FLUTTER_APP_DIR:-}" ]; then
|
||||||
|
local p="$FLUTTER_APP_DIR"
|
||||||
|
if [ -d "$p" ] && is_flutter_project_dir "$p"; then
|
||||||
|
echo "$(cd "$p" && pwd)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# مسیر متداول این ریپو
|
||||||
|
local common_path="$REPO_ROOT/hesabixUI/hesabix_ui"
|
||||||
|
if [ -d "$common_path" ] && is_flutter_project_dir "$common_path"; then
|
||||||
|
echo "$common_path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# جستجو در hesabixUI برای نزدیکترین pubspec.yaml
|
||||||
|
local search_root="$REPO_ROOT/hesabixUI"
|
||||||
|
if [ -d "$search_root" ]; then
|
||||||
|
# محدود به عمق 3 برای سرعت
|
||||||
|
local found
|
||||||
|
found=$(find "$search_root" -maxdepth 3 -type f -name pubspec.yaml 2>/dev/null | head -n 1 || true)
|
||||||
|
if [ -n "$found" ]; then
|
||||||
|
echo "$(cd "$(dirname "$found")" && pwd)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
die "پروژه فلاتر یافت نشد. لطفاً با --project مسیر را مشخص کنید."
|
||||||
|
}
|
||||||
|
|
||||||
|
check_linux_dependencies() {
|
||||||
|
echo "بررسی وابستگیهای Linux..."
|
||||||
|
|
||||||
|
local missing_deps=()
|
||||||
|
|
||||||
|
# بررسی وجود GTK development libraries
|
||||||
|
if ! pkg-config --exists gtk+-3.0; then
|
||||||
|
missing_deps+=("libgtk-3-dev")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# بررسی وجود CMake
|
||||||
|
if ! cmd_exists cmake; then
|
||||||
|
missing_deps+=("cmake")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# بررسی وجود Ninja
|
||||||
|
if ! cmd_exists ninja; then
|
||||||
|
missing_deps+=("ninja-build")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# بررسی وجود C++ compiler
|
||||||
|
if ! cmd_exists clang++; then
|
||||||
|
missing_deps+=("clang")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# بررسی وجود build-essential
|
||||||
|
if ! cmd_exists gcc; then
|
||||||
|
missing_deps+=("build-essential")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||||
|
echo "نصب وابستگیهای مورد نیاز..."
|
||||||
|
echo "بستههای مورد نیاز: ${missing_deps[*]}"
|
||||||
|
|
||||||
|
# تشخیص توزیع Linux
|
||||||
|
if command -v apt >/dev/null 2>&1; then
|
||||||
|
# Ubuntu/Debian
|
||||||
|
echo "تشخیص توزیع: Ubuntu/Debian"
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y "${missing_deps[@]}"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
# Fedora/RHEL
|
||||||
|
echo "تشخیص توزیع: Fedora/RHEL"
|
||||||
|
sudo dnf install -y "${missing_deps[@]}"
|
||||||
|
elif command -v pacman >/dev/null 2>&1; then
|
||||||
|
# Arch Linux
|
||||||
|
echo "تشخیص توزیع: Arch Linux"
|
||||||
|
sudo pacman -S --noconfirm "${missing_deps[@]}"
|
||||||
|
else
|
||||||
|
die "توزیع Linux پشتیبانی شده یافت نشد. لطفاً وابستگیها را بهصورت دستی نصب کنید: ${missing_deps[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "وابستگیها نصب شدند."
|
||||||
|
else
|
||||||
|
echo "همه وابستگیهای مورد نیاز موجود هستند."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_platform_issues() {
|
||||||
|
echo "بررسی و رفع مشکلات platform-specific..."
|
||||||
|
|
||||||
|
local app_dir="$1"
|
||||||
|
local data_table_widget="$app_dir/lib/widgets/data_table/data_table_widget.dart"
|
||||||
|
|
||||||
|
if [ -f "$data_table_widget" ]; then
|
||||||
|
# بررسی اینکه آیا قبلاً تغییر کرده یا نه
|
||||||
|
if [ ! -f "$data_table_widget.backup" ]; then
|
||||||
|
# ایجاد backup
|
||||||
|
cp "$data_table_widget" "$data_table_widget.backup"
|
||||||
|
|
||||||
|
# جایگزینی dart:html با conditional import
|
||||||
|
sed -i 's/import '\''dart:html'\'' as html;/\/\/ import '\''dart:html'\'' as html; \/\/ Not available on Linux/' "$data_table_widget"
|
||||||
|
|
||||||
|
# جایگزینی توابع download با stub functions
|
||||||
|
sed -i '/Future<void> _downloadPdf/,/^ }/c\
|
||||||
|
// Platform-specific download functions for Linux\
|
||||||
|
Future<void> _downloadPdf(dynamic data, String filename) async {\
|
||||||
|
// For Linux desktop, we'\''ll save to Downloads folder\
|
||||||
|
print('\''Download PDF: $filename (Linux desktop - save to Downloads folder)'\'');\
|
||||||
|
// TODO: Implement proper file saving for Linux\
|
||||||
|
}' "$data_table_widget"
|
||||||
|
|
||||||
|
sed -i '/Future<void> _downloadExcel/,/^ }/c\
|
||||||
|
Future<void> _downloadExcel(dynamic data, String filename) async {\
|
||||||
|
// For Linux desktop, we'\''ll save to Downloads folder\
|
||||||
|
print('\''Download Excel: $filename (Linux desktop - save to Downloads folder)'\'');\
|
||||||
|
// TODO: Implement proper file saving for Linux\
|
||||||
|
}' "$data_table_widget"
|
||||||
|
|
||||||
|
sed -i '/Future<void> _downloadCsv/,/^ }/c\
|
||||||
|
Future<void> _downloadCsv(dynamic data, String filename) async {\
|
||||||
|
// For Linux desktop, we'\''ll save to Downloads folder\
|
||||||
|
print('\''Download CSV: $filename (Linux desktop - save to Downloads folder)'\'');\
|
||||||
|
// TODO: Implement proper file saving for Linux\
|
||||||
|
}' "$data_table_widget"
|
||||||
|
|
||||||
|
echo "مشکلات platform-specific رفع شدند."
|
||||||
|
else
|
||||||
|
echo "فایل قبلاً برای Linux تغییر کرده است."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_platform_files() {
|
||||||
|
echo "بازیابی فایلهای اصلی..."
|
||||||
|
|
||||||
|
local app_dir="$1"
|
||||||
|
local data_table_widget="$app_dir/lib/widgets/data_table/data_table_widget.dart"
|
||||||
|
|
||||||
|
if [ -f "$data_table_widget.backup" ]; then
|
||||||
|
mv "$data_table_widget.backup" "$data_table_widget"
|
||||||
|
echo "فایلهای اصلی بازیابی شدند."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse args
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--project)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --project وارد نشده است"
|
||||||
|
USER_PROJECT="$2"; shift 2 ;;
|
||||||
|
--mode)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --mode وارد نشده است"
|
||||||
|
MODE="$2"; shift 2 ;;
|
||||||
|
--build-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --build-dir وارد نشده است"
|
||||||
|
BUILD_DIR="$2"; shift 2 ;;
|
||||||
|
--clean)
|
||||||
|
CLEAN_BUILD=true; shift ;;
|
||||||
|
--install-deps)
|
||||||
|
INSTALL_DEPS=true; shift ;;
|
||||||
|
--api-base-url)
|
||||||
|
[[ $# -ge 2 ]] || die "مقدار برای --api-base-url وارد نشده است"
|
||||||
|
API_BASE_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
print_usage; exit 0 ;;
|
||||||
|
*)
|
||||||
|
warn "آرگومان ناشناخته: $1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
debug|profile|release) ;;
|
||||||
|
*) die "mode نامعتبر است: $MODE (مجاز: debug|profile|release)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ensure_flutter_in_path
|
||||||
|
check_linux_dependencies
|
||||||
|
|
||||||
|
APP_DIR="$(auto_detect_project_dir)"
|
||||||
|
|
||||||
|
# رفع مشکلات platform-specific
|
||||||
|
fix_platform_issues "$APP_DIR"
|
||||||
|
|
||||||
|
if [ -z "$BUILD_DIR" ]; then
|
||||||
|
BUILD_DIR="$DEFAULT_BUILD_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# تبدیل به مسیر مطلق
|
||||||
|
BUILD_DIR="$(cd "$APP_DIR" && realpath -m "$BUILD_DIR")"
|
||||||
|
|
||||||
|
echo "ریشه ریپو: $REPO_ROOT"
|
||||||
|
echo "مسیر پروژه: $APP_DIR"
|
||||||
|
echo "حالت: $MODE"
|
||||||
|
echo "مسیر build: $BUILD_DIR"
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
# تنظیم mirror برای حل مشکل دسترسی به pub.dev
|
||||||
|
export PUB_HOSTED_URL="https://pub.flutter-io.cn"
|
||||||
|
export FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"
|
||||||
|
|
||||||
|
# تنظیم C++ compiler flags برای حل مشکل deprecated warnings
|
||||||
|
export CXXFLAGS="-Wno-deprecated-literal-operator"
|
||||||
|
export CFLAGS="-Wno-deprecated-literal-operator"
|
||||||
|
|
||||||
|
# نصب وابستگیها در صورت درخواست
|
||||||
|
if [ "$INSTALL_DEPS" = true ]; then
|
||||||
|
echo "نصب وابستگیها..."
|
||||||
|
flutter pub get
|
||||||
|
fi
|
||||||
|
|
||||||
|
# پاک کردن build directory در صورت درخواست
|
||||||
|
if [ "$CLEAN_BUILD" = true ]; then
|
||||||
|
echo "پاک کردن build directory..."
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# تنظیم آرگومانهای dart-define
|
||||||
|
DART_DEFINE_ARGS=()
|
||||||
|
if [ -n "$API_BASE_URL" ]; then
|
||||||
|
DART_DEFINE_ARGS+=(--dart-define "API_BASE_URL=$API_BASE_URL")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# اجرای Flutter برای Linux
|
||||||
|
echo "اجرای Flutter برای Linux..."
|
||||||
|
echo "دستور: flutter run -d linux --$MODE ${DART_DEFINE_ARGS[*]:-}"
|
||||||
|
|
||||||
|
exec flutter run -d linux --"$MODE" ${DART_DEFINE_ARGS[@]:-}
|
||||||
Loading…
Reference in a new issue