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)
|
||||
async def delete_storage_config(
|
||||
config_id: UUID,
|
||||
config_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AuthContext = Depends(get_current_user),
|
||||
|
|
@ -435,8 +435,28 @@ async def delete_storage_config(
|
|||
):
|
||||
"""حذف تنظیمات ذخیرهسازی"""
|
||||
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)
|
||||
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:
|
||||
raise ApiError(
|
||||
|
|
@ -590,21 +610,131 @@ async def _test_local_storage(config: StorageConfig) -> dict:
|
|||
|
||||
async def _test_ftp_storage(config: StorageConfig) -> dict:
|
||||
"""تست اتصال به FTP storage"""
|
||||
import ftplib
|
||||
import tempfile
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
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 {
|
||||
"success": False,
|
||||
"error": "پارامترهای ضروری FTP (host, username, password) موجود نیست",
|
||||
"storage_type": "ftp",
|
||||
"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": "تست FTP هنوز پیادهسازی نشده است",
|
||||
"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:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"خطا در تست FTP storage: {str(e)}",
|
||||
"storage_type": "ftp",
|
||||
"tested_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,15 @@ class StorageConfigRepository(BaseRepository[StorageConfig]):
|
|||
self.db.query(StorageConfig).update({"is_default": False})
|
||||
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()
|
||||
if not config:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -396,6 +396,23 @@
|
|||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"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": "قبلی",
|
||||
"next": "بعدی",
|
||||
"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:
|
||||
/// **'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
|
||||
|
|
|
|||
|
|
@ -1097,4 +1097,58 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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
|
||||
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/operator/operator_tickets_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 'core/locale_controller.dart';
|
||||
import 'core/calendar_controller.dart';
|
||||
|
|
@ -386,6 +390,48 @@ class _MyAppState extends State<MyApp> {
|
|||
}
|
||||
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: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/file_statistics_widget.dart';
|
||||
import 'package:hesabix_ui/widgets/admin/file_storage/file_management_widget.dart';
|
||||
import 'package:hesabix_ui/widgets/admin/file_storage/storage_management_page.dart';
|
||||
|
||||
class FileStorageSettingsPage extends StatefulWidget {
|
||||
class FileStorageSettingsPage extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
return const StorageManagementPage();
|
||||
}
|
||||
}
|
||||
|
|
@ -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,168 +1,323 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.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});
|
||||
|
||||
@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
|
||||
Widget build(BuildContext context) {
|
||||
final t = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final t = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.surface,
|
||||
appBar: AppBar(
|
||||
title: Text(t.systemSettings),
|
||||
backgroundColor: colorScheme.surface,
|
||||
foregroundColor: colorScheme.onSurface,
|
||||
title: Text(
|
||||
t.systemSettingsWelcome,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _showHelpDialog(context),
|
||||
icon: const Icon(Icons.help_outline),
|
||||
tooltip: t.systemSettingsWelcome,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Container(
|
||||
color: colorScheme.surface,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWelcomeSection(theme, colorScheme, t),
|
||||
const SizedBox(height: 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(
|
||||
gradient: LinearGradient(
|
||||
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(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
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),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
Text(
|
||||
t.systemAdministration,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
t.systemSettingsDescription,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${_settingsItems.length}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsList(ThemeData theme, ColorScheme colorScheme, AppLocalizations t) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
t.availableSettings,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
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: colorScheme.primaryContainer,
|
||||
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,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: item.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.admin_panel_settings,
|
||||
size: 32,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
t.systemSettings,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'تنظیمات پیشرفته سیستم - فقط برای ادمینها',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onPrimaryContainer.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withOpacity(0.1),
|
||||
border: Border.all(color: Colors.amber.withOpacity(0.3)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber,
|
||||
color: Colors.amber[700],
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'توجه: این بخش فقط برای ادمینهای سیستم قابل دسترسی است. تغییرات در این بخش میتواند بر عملکرد کل سیستم تأثیر بگذارد.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.amber[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: item.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -173,77 +328,73 @@ class SystemSettingsPage extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingCard(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required Color color,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
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,203 +23,405 @@ class StorageConfigCard extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
final isDefault = config['is_default'] == true;
|
||||
final isActive = config['is_active'] == true;
|
||||
final storageType = config['storage_type'] ?? 'unknown';
|
||||
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStorageColor(storageType).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
_getStorageIcon(storageType),
|
||||
color: _getStorageColor(storageType),
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
config['name'] ?? 'Unknown',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isDefault) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_getStorageIcon(config['storage_type']),
|
||||
color: theme.colorScheme.primary,
|
||||
size: 24,
|
||||
Icons.star,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'پیشفرض',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Status badges
|
||||
Row(
|
||||
children: [
|
||||
if (isDefault)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.green : Colors.red,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
isActive ? 'فعال' : 'غیرفعال',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
l10n.isDefault,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Configuration details
|
||||
_buildConfigDetails(context, config),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Actions
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (onTestConnection != null)
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
icon: Icons.wifi_protected_setup,
|
||||
label: l10n.testConnection,
|
||||
onPressed: onTestConnection!,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
if (onEdit != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.green : Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
isActive ? l10n.isActive : 'غیرفعال',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
icon: Icons.edit,
|
||||
label: l10n.edit,
|
||||
onPressed: onEdit!,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Configuration details
|
||||
_buildConfigDetails(context, config),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Actions
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (onTestConnection != null)
|
||||
TextButton.icon(
|
||||
onPressed: onTestConnection,
|
||||
icon: const Icon(Icons.wifi_protected_setup, size: 16),
|
||||
label: Text(l10n.testConnection),
|
||||
),
|
||||
if (onEdit != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
onPressed: onEdit,
|
||||
icon: const Icon(Icons.edit, size: 16),
|
||||
label: Text(l10n.edit),
|
||||
),
|
||||
],
|
||||
if (onSetDefault != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
onPressed: onSetDefault,
|
||||
icon: const Icon(Icons.star, size: 16),
|
||||
label: Text(l10n.setAsDefault),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
if (onSetDefault != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
icon: Icons.star,
|
||||
label: l10n.setAsDefault,
|
||||
onPressed: onSetDefault!,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (onDelete != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
onPressed: onDelete,
|
||||
icon: const Icon(Icons.delete, size: 16),
|
||||
label: Text(l10n.delete),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
],
|
||||
if (onDelete != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButton(
|
||||
context: context,
|
||||
icon: Icons.delete,
|
||||
label: l10n.delete,
|
||||
onPressed: onDelete!,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final configData = config['config_data'] ?? {};
|
||||
final storageType = config['storage_type'];
|
||||
|
||||
if (storageType == 'local') {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
context,
|
||||
l10n.basePath,
|
||||
configData['base_path'] ?? 'N/A',
|
||||
Icons.folder,
|
||||
),
|
||||
],
|
||||
);
|
||||
return _buildLocalConfigDetails(context, configData);
|
||||
} else if (storageType == 'ftp') {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailRow(
|
||||
context,
|
||||
l10n.ftpHost,
|
||||
configData['host'] ?? 'N/A',
|
||||
Icons.dns,
|
||||
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),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildDetailRow(
|
||||
context,
|
||||
l10n.ftpPort,
|
||||
configData['port']?.toString() ?? 'N/A',
|
||||
Icons.settings_ethernet,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(
|
||||
BuildContext context,
|
||||
String label,
|
||||
String value,
|
||||
IconData icon,
|
||||
) {
|
||||
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,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.folder_outlined,
|
||||
size: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'پیکربندی ذخیرهسازی محلی',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 _buildConfigRow(BuildContext context, IconData icon, String label, String value) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Row(
|
||||
|
|
@ -227,22 +429,21 @@ class StorageConfigCard extends StatelessWidget {
|
|||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$label: ',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.8),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -256,7 +457,18 @@ class StorageConfigCard extends StatelessWidget {
|
|||
case 'ftp':
|
||||
return Icons.cloud_upload;
|
||||
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':
|
||||
return 'Local Storage';
|
||||
case 'ftp':
|
||||
return 'FTP Storage';
|
||||
return 'FTP Server';
|
||||
default:
|
||||
return 'Unknown Storage';
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,12 @@ import '../../../core/api_client.dart';
|
|||
|
||||
class StorageConfigFormDialog extends StatefulWidget {
|
||||
final Map<String, dynamic>? config;
|
||||
final VoidCallback? onSaved;
|
||||
|
||||
const StorageConfigFormDialog({
|
||||
super.key,
|
||||
this.config,
|
||||
this.onSaved,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -28,6 +30,7 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
|||
bool _isDefault = false;
|
||||
bool _isActive = true;
|
||||
bool _isLoading = false;
|
||||
bool _useTls = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -49,10 +52,11 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
|||
_basePathController.text = configData['base_path'] ?? '';
|
||||
} else if (_selectedStorageType == 'ftp') {
|
||||
_ftpHostController.text = configData['host'] ?? '';
|
||||
_ftpPortController.text = configData['port']?.toString() ?? '21';
|
||||
_ftpPortController.text = (configData['port'] ?? 21).toString();
|
||||
_ftpUsernameController.text = configData['username'] ?? '';
|
||||
_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();
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
|
|
@ -96,29 +81,69 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
|||
|
||||
try {
|
||||
final api = ApiClient();
|
||||
final response = await api.post(
|
||||
'/api/v1/admin/files/storage-configs/',
|
||||
data: {
|
||||
'name': _nameController.text,
|
||||
'storage_type': _selectedStorageType,
|
||||
'is_default': _isDefault,
|
||||
'is_active': _isActive,
|
||||
'config_data': _buildConfigData(),
|
||||
},
|
||||
);
|
||||
Map<String, dynamic> configData = {};
|
||||
|
||||
if (response.data != null && response.data['success'] == true) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(response.data['data']);
|
||||
}
|
||||
if (_selectedStorageType == 'local') {
|
||||
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,
|
||||
'config_data': configData,
|
||||
'is_default': _isDefault,
|
||||
'is_active': _isActive,
|
||||
};
|
||||
|
||||
if (widget.config != null) {
|
||||
// Update existing config
|
||||
await api.put(
|
||||
'/api/v1/admin/files/storage-configs/${widget.config!['id']}',
|
||||
data: requestData,
|
||||
);
|
||||
} 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) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: $e'),
|
||||
content: Text('خطا در ذخیره تنظیمات: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
|
|
@ -139,211 +164,192 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
|||
final isEditing = widget.config != null;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
// Header
|
||||
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(
|
||||
children: [
|
||||
Icon(
|
||||
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(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Form
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Basic Information
|
||||
_buildSectionHeader(context, 'اطلاعات پایه'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Name
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.storageName,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'نام',
|
||||
hintText: 'نام پیکربندی ذخیرهسازی را وارد کنید',
|
||||
prefixIcon: const Icon(Icons.label_outline),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return l10n.requiredField;
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'لطفاً نام را وارد کنید';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Storage Type
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedStorageType,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.storageType,
|
||||
border: const OutlineInputBorder(),
|
||||
Text(
|
||||
l10n.storageType,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
),
|
||||
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',
|
||||
child: Text(l10n.localStorage),
|
||||
groupValue: _selectedStorageType,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedStorageType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'ftp',
|
||||
child: Text(l10n.ftpStorage),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: RadioListTile<String>(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.cloud_upload, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text('سرور FTP'),
|
||||
],
|
||||
),
|
||||
value: 'ftp',
|
||||
groupValue: _selectedStorageType,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedStorageType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Configuration Details
|
||||
_buildSectionHeader(context, 'جزئیات پیکربندی'),
|
||||
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) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return l10n.requiredField;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
_buildLocalConfigFields(context),
|
||||
] 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;
|
||||
},
|
||||
),
|
||||
_buildFtpConfigFields(context),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Options
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
_buildSectionHeader(context, 'گزینهها'),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
SwitchListTile(
|
||||
title: Text('تنظیم به عنوان پیشفرض'),
|
||||
subtitle: Text('این پیکربندی به عنوان پیشفرض تنظیم شود'),
|
||||
value: _isDefault,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isDefault = value ?? false;
|
||||
_isDefault = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(l10n.isDefault),
|
||||
const SizedBox(width: 24),
|
||||
Checkbox(
|
||||
secondary: const Icon(Icons.star),
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: Text('فعال'),
|
||||
subtitle: Text('این پیکربندی فعال باشد'),
|
||||
value: _isActive,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isActive = value ?? false;
|
||||
_isActive = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(l10n.isActive),
|
||||
],
|
||||
secondary: const Icon(Icons.power),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
|
|
@ -351,16 +357,26 @@ class _StorageConfigFormDialogState extends State<StorageConfigFormDialog> {
|
|||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isLoading ? null : _saveConfig,
|
||||
child: _isLoading
|
||||
icon: _isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
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: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_card.dart';
|
||||
import '../../../core/api_client.dart';
|
||||
|
||||
class StorageConfigListWidget extends StatefulWidget {
|
||||
const StorageConfigListWidget({super.key});
|
||||
final VoidCallback? onRefresh;
|
||||
|
||||
const StorageConfigListWidget({
|
||||
super.key,
|
||||
this.onRefresh,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StorageConfigListWidget> createState() => _StorageConfigListWidgetState();
|
||||
State<StorageConfigListWidget> createState() => StorageConfigListWidgetState();
|
||||
}
|
||||
|
||||
class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||
class StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
||||
List<Map<String, dynamic>> _storageConfigs = [];
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
|
@ -19,10 +23,10 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadStorageConfigs();
|
||||
loadStorageConfigs();
|
||||
}
|
||||
|
||||
Future<void> _loadStorageConfigs() async {
|
||||
Future<void> loadStorageConfigs() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_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 {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
try {
|
||||
final api = ApiClient();
|
||||
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) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.connectionSuccessful),
|
||||
content: Text('اتصال موفقیتآمیز بود'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${l10n.connectionFailed}: ${testResult['error']}'),
|
||||
content: Text('اتصال ناموفق: ${testResult['error']}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
|
|
@ -125,29 +81,27 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
|||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${l10n.connectionFailed}: $e'),
|
||||
content: Text('اتصال ناموفق: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> _deleteConfig(String configId) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.deleteConfirm),
|
||||
content: Text(l10n.deleteConfirmMessage),
|
||||
title: Text('تأیید حذف'),
|
||||
content: Text('آیا از حذف این پیکربندی اطمینان دارید؟'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(l10n.cancel),
|
||||
child: Text('لغو'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(l10n.delete),
|
||||
child: Text('حذف'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -155,36 +109,116 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
|||
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
// TODO: Call API to delete config
|
||||
await Future.delayed(const Duration(seconds: 1)); // Simulate API call
|
||||
final api = ApiClient();
|
||||
final response = await api.delete('/api/v1/admin/files/storage-configs/$configId');
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.fileDeleted),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
if (response.data != null && response.data['success'] == true) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('فایل حذف شد'),
|
||||
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('Error: $e'),
|
||||
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) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('خطا در تنظیم به عنوان پیشفرض: $e'),
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'در حال بارگذاری...',
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -199,85 +233,125 @@ class _StorageConfigListWidgetState extends State<StorageConfigListWidget> {
|
|||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'خطا',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_error!,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadStorageConfigs,
|
||||
child: Text(l10n.retry),
|
||||
ElevatedButton.icon(
|
||||
onPressed: loadStorageConfigs,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text('تلاش مجدد'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.storageConfigurations,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
),
|
||||
if (_storageConfigs.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.storage_outlined,
|
||||
size: 64,
|
||||
color: theme.colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'هیچ پیکربندی ذخیرهسازی وجود ندارد',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _addStorageConfig,
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(l10n.addStorageConfig),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: _storageConfigs.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.storage_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 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,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: _storageConfigs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final config = _storageConfigs[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: StorageConfigCard(
|
||||
config: config,
|
||||
onEdit: () => _editStorageConfig(config),
|
||||
onSetDefault: config['is_default'] == false
|
||||
? () => _setAsDefault(config['id'])
|
||||
: null,
|
||||
onTestConnection: () => _testConnection(config['id']),
|
||||
onDelete: config['is_default'] == false
|
||||
? () => _deleteConfig(config['id'])
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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,
|
||||
itemBuilder: (context, index) {
|
||||
final config = _storageConfigs[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: StorageConfigCard(
|
||||
config: config,
|
||||
onEdit: () => _editStorageConfig(config),
|
||||
onSetDefault: config['is_default'] == false
|
||||
? () => _setAsDefault(config['id'])
|
||||
: null,
|
||||
onTestConnection: () => _testConnection(config['id']),
|
||||
onDelete: () => _deleteConfig(config['id']),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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:typed_data';
|
||||
import 'dart:html' as html;
|
||||
// import 'dart:html' as html; // Not available on Linux
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:data_table_2/data_table_2.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 {
|
||||
try {
|
||||
if (data is List<int>) {
|
||||
// Convert bytes to Uint8List
|
||||
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');
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
Future<void> _downloadExcel(dynamic data, String filename) async {
|
||||
try {
|
||||
if (data is List<int>) {
|
||||
// Handle binary Excel data from server
|
||||
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');
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
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