progress in import persons

This commit is contained in:
Hesabix 2025-09-28 14:29:09 +03:30
parent b8ab020754
commit c31240846f
16 changed files with 2439 additions and 3 deletions

View file

@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Body
from fastapi import UploadFile, File
from sqlalchemy.orm import Session
from typing import Dict, Any, List, Optional
@ -493,6 +494,70 @@ async def export_persons_pdf(
)
@router.post("/businesses/{business_id}/persons/import/template",
summary="دانلود تمپلیت ایمپورت اشخاص",
description="فایل Excel تمپلیت برای ایمپورت اشخاص را برمی‌گرداند",
)
async def download_persons_import_template(
business_id: int,
request: Request,
auth_context: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
import io
import datetime
from fastapi.responses import Response
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
wb = Workbook()
ws = wb.active
ws.title = "Template"
headers = [
'code','alias_name','first_name','last_name','person_type','person_types','company_name','payment_id',
'national_id','registration_number','economic_id','country','province','city','address','postal_code',
'phone','mobile','fax','email','website','share_count','commission_sale_percent','commission_sales_return_percent',
'commission_sales_amount','commission_sales_return_amount'
]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal="center")
# Sample row
sample = [
'', 'نمونه نام مستعار', 'علی', 'احمدی', 'مشتری', 'مشتری, فروشنده', 'نمونه شرکت', 'PID123',
'0012345678', '12345', 'ECO-1', 'ایران', 'تهران', 'تهران', 'خیابان مثال ۱', '1234567890',
'02112345678', '09120000000', '', 'test@example.com', 'example.com', '', '5', '0', '0', '0'
]
for col, val in enumerate(sample, 1):
ws.cell(row=2, column=col, value=val)
# Auto width
for column in ws.columns:
try:
letter = column[0].column_letter
max_len = max(len(str(c.value)) if c.value is not None else 0 for c in column)
ws.column_dimensions[letter].width = min(max_len + 2, 50)
except Exception:
pass
buf = io.BytesIO()
wb.save(buf)
buf.seek(0)
filename = f"persons_import_template_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return Response(
content=buf.getvalue(),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename={filename}",
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@router.get("/persons/{person_id}",
summary="جزئیات شخص",
description="دریافت جزئیات یک شخص",
@ -626,3 +691,158 @@ async def get_persons_summary_endpoint(
request=request,
message="خلاصه اشخاص با موفقیت دریافت شد",
)
@router.post("/businesses/{business_id}/persons/import/excel",
summary="ایمپورت اشخاص از فایل Excel",
description="فایل اکسل را دریافت می‌کند و به‌صورت dry-run یا واقعی پردازش می‌کند",
)
async def import_persons_excel(
business_id: int,
request: Request,
file: UploadFile = File(...),
dry_run: bool = Body(default=True),
match_by: str = Body(default="code"),
conflict_policy: str = Body(default="upsert"),
auth_context: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
import io
import json
import re
from openpyxl import load_workbook
from fastapi import HTTPException
if not file.filename or not file.filename.lower().endswith('.xlsx'):
raise HTTPException(status_code=400, detail="فرمت فایل معتبر نیست. تنها xlsx پشتیبانی می‌شود")
content = await file.read()
try:
wb = load_workbook(filename=io.BytesIO(content), data_only=True)
except Exception:
raise HTTPException(status_code=400, detail="امکان خواندن فایل وجود ندارد")
ws = wb.active
rows = list(ws.iter_rows(values_only=True))
if not rows:
return success_response(data={"summary": {"total": 0}}, request=request, message="فایل خالی است")
headers = [str(h).strip() if h is not None else "" for h in rows[0]]
data_rows = rows[1:]
# helper to map enum strings (fa/en) to internal value
def normalize_person_type(value: str) -> Optional[str]:
if not value:
return None
value = str(value).strip()
mapping = {
'customer': 'مشتری', 'marketer': 'بازاریاب', 'employee': 'کارمند', 'supplier': 'تامین‌کننده',
'partner': 'همکار', 'seller': 'فروشنده', 'shareholder': 'سهامدار'
}
for en, fa in mapping.items():
if value.lower() == en or value == fa:
return fa
return value # assume already fa
errors: list[dict] = []
valid_items: list[dict] = []
for idx, row in enumerate(data_rows, start=2):
item: dict[str, Any] = {}
row_errors: list[str] = []
for ci, key in enumerate(headers):
if not key:
continue
val = row[ci] if ci < len(row) else None
if isinstance(val, str):
val = val.strip()
item[key] = val
# normalize types
if 'person_type' in item and item['person_type']:
item['person_type'] = normalize_person_type(item['person_type'])
if 'person_types' in item and item['person_types']:
# split by comma
parts = [normalize_person_type(p.strip()) for p in str(item['person_types']).split(',') if str(p).strip()]
item['person_types'] = parts
# alias_name required
if not item.get('alias_name'):
row_errors.append('alias_name الزامی است')
# shareholder rule
if (item.get('person_type') == 'سهامدار') or (isinstance(item.get('person_types'), list) and 'سهامدار' in item.get('person_types', [])):
sc = item.get('share_count')
try:
sc_val = int(sc) if sc is not None and str(sc).strip() != '' else None
except Exception:
sc_val = None
if sc_val is None or sc_val <= 0:
row_errors.append('برای سهامدار share_count باید > 0 باشد')
else:
item['share_count'] = sc_val
if row_errors:
errors.append({"row": idx, "errors": row_errors})
continue
valid_items.append(item)
inserted = 0
updated = 0
skipped = 0
if not dry_run and valid_items:
# apply import with conflict policy
from adapters.db.models.person import Person
from sqlalchemy import and_
def find_existing(session: Session, data: dict) -> Optional[Person]:
if match_by == 'national_id' and data.get('national_id'):
return session.query(Person).filter(and_(Person.business_id == business_id, Person.national_id == data['national_id'])).first()
if match_by == 'email' and data.get('email'):
return session.query(Person).filter(and_(Person.business_id == business_id, Person.email == data['email'])).first()
if match_by == 'code' and data.get('code'):
try:
code_int = int(data['code'])
return session.query(Person).filter(and_(Person.business_id == business_id, Person.code == code_int)).first()
except Exception:
return None
return None
for data in valid_items:
existing = find_existing(db, data)
if existing is None:
# create
try:
create_person(db, business_id, PersonCreateRequest(**data))
inserted += 1
except Exception:
skipped += 1
else:
if conflict_policy == 'insert':
skipped += 1
elif conflict_policy in ('update', 'upsert'):
try:
update_person(db, existing.id, business_id, PersonUpdateRequest(**data))
updated += 1
except Exception:
skipped += 1
summary = {
"total": len(data_rows),
"valid": len(valid_items),
"invalid": len(errors),
"inserted": inserted,
"updated": updated,
"skipped": skipped,
"dry_run": dry_run,
}
return success_response(
data={
"summary": summary,
"errors": errors,
},
request=request,
message="نتیجه ایمپورت اشخاص",
)

View file

@ -3,6 +3,7 @@ import 'package:hesabix_ui/l10n/app_localizations.dart';
import '../../widgets/data_table/data_table_widget.dart';
import '../../widgets/data_table/data_table_config.dart';
import '../../widgets/person/person_form_dialog.dart';
import '../../widgets/person/person_import_dialog.dart';
import '../../widgets/permission/permission_widgets.dart';
import '../../models/person_model.dart';
import '../../services/person_service.dart';
@ -273,6 +274,25 @@ class _PersonsPageState extends State<PersonsPage> {
),
),
),
Tooltip(
message: 'ایمپورت اشخاص از اکسل',
child: IconButton(
onPressed: () async {
final ok = await showDialog<bool>(
context: context,
builder: (context) => PersonImportDialog(businessId: widget.businessId),
);
if (ok == true) {
final state = _personsTableKey.currentState;
try {
// ignore: avoid_dynamic_calls
(state as dynamic)?.refresh();
} catch (_) {}
}
},
icon: const Icon(Icons.upload_file),
),
),
],
);
}

View file

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'helpers/file_saver.dart';
// import 'dart:html' as html; // Not available on Linux
// // import 'dart:html' as html; // Not available on Linux // Not available on Linux
import 'package:flutter/material.dart';
import 'package:data_table_2/data_table_2.dart';
import 'package:dio/dio.dart';
@ -670,12 +670,17 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
await FileSaver.saveBytes(bytes, filename);
}
// Platform-specific download functions for Linux
Future<void> _downloadPdf(dynamic data, String filename) async {
await _saveBytesToDownloads(data, filename);
// 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 {
await _saveBytesToDownloads(data, filename);
// 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
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
export 'file_picker_bridge_io.dart' if (dart.library.html) 'file_picker_bridge_web.dart';

View file

@ -0,0 +1,25 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
class PickedFileData {
final String name;
final List<int> bytes;
PickedFileData(this.name, this.bytes);
}
class FilePickerBridge {
static Future<PickedFileData?> pickExcel() async {
final res = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: const ['xlsx'],
withData: false,
);
if (res == null || res.files.isEmpty) return null;
final pf = res.files.first;
if (pf.path == null) return null;
final bytes = await File(pf.path!).readAsBytes();
return PickedFileData(pf.name, bytes);
}
}

View file

@ -0,0 +1,50 @@
import 'dart:async';
import 'dart:html' as html;
import 'dart:typed_data';
class PickedFileData {
final String name;
final List<int> bytes;
PickedFileData(this.name, this.bytes);
}
class FilePickerBridge {
static Future<PickedFileData?> pickExcel() async {
try {
final input = html.FileUploadInputElement()
..accept = '.xlsx'
..multiple = false;
input.click();
final completer = Completer<PickedFileData?>();
input.onChange.listen((e) {
final files = input.files;
if (files != null && files.isNotEmpty) {
final file = files.first;
final reader = html.FileReader();
reader.onLoad.listen((e) {
final bytes = reader.result as Uint8List;
completer.complete(PickedFileData(file.name, bytes.toList()));
});
reader.onError.listen((e) {
completer.complete(null);
});
reader.readAsArrayBuffer(file);
} else {
completer.complete(null);
}
});
return await completer.future;
} catch (e) {
return null;
}
}
}

View file

@ -0,0 +1,328 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'file_picker_bridge.dart';
import '../../core/api_client.dart';
import '../data_table/helpers/file_saver.dart';
class PersonImportDialog extends StatefulWidget {
final int businessId;
const PersonImportDialog({super.key, required this.businessId});
@override
State<PersonImportDialog> createState() => _PersonImportDialogState();
}
class _PersonImportDialogState extends State<PersonImportDialog> {
final TextEditingController _pathCtrl = TextEditingController();
bool _dryRun = true;
String _matchBy = 'code';
String _conflictPolicy = 'upsert';
bool _loading = false;
Map<String, dynamic>? _result;
PickedFileData? _selectedFile;
bool _isInitialized = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isInitialized = true;
});
}
});
}
@override
void dispose() {
_pathCtrl.dispose();
super.dispose();
}
Future<void> _pickFile() async {
if (!_isInitialized) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('لطفاً صبر کنید تا دیالوگ کاملاً بارگذاری شود')),
);
}
return;
}
try {
final picked = await FilePickerBridge.pickExcel();
if (picked != null) {
setState(() {
_selectedFile = picked;
_pathCtrl.text = picked.name;
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('خطا در انتخاب فایل: $e')),
);
}
}
}
Future<void> _downloadTemplate() async {
try {
setState(() => _loading = true);
final api = ApiClient();
final res = await api.post(
'/persons/businesses/${widget.businessId}/persons/import/template',
responseType: ResponseType.bytes,
);
String filename = 'persons_import_template.xlsx';
final cd = res.headers.value('content-disposition');
if (cd != null) {
try {
final parts = cd.split(';').map((e) => e.trim());
for (final p in parts) {
if (p.toLowerCase().startsWith('filename=')) {
var name = p.substring('filename='.length).trim();
if (name.startsWith('"') && name.endsWith('"') && name.length >= 2) {
name = name.substring(1, name.length - 1);
}
if (name.isNotEmpty) filename = name;
break;
}
}
} catch (_) {}
}
await FileSaver.saveBytes((res.data as List<int>), filename);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('تمپلیت دانلود شد: $filename')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('خطا در دانلود تمپلیت: $e')),
);
}
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _runImport({required bool dryRun}) async {
// Ensure file is selected
if (_selectedFile == null) {
await _pickFile();
if (_selectedFile == null) return;
}
final filename = _selectedFile!.name;
final bytes = _selectedFile!.bytes;
try {
setState(() {
_loading = true;
_result = null;
});
final form = FormData.fromMap({
'file': MultipartFile.fromBytes(bytes, filename: filename),
'dry_run': dryRun,
'match_by': _matchBy,
'conflict_policy': _conflictPolicy,
});
final api = ApiClient();
final res = await api.post<Map<String, dynamic>>(
'/persons/businesses/${widget.businessId}/persons/import/excel',
data: form,
options: Options(contentType: 'multipart/form-data'),
);
setState(() {
_result = res.data;
});
if (!dryRun) {
if (mounted) Navigator.of(context).pop(true);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('خطا در ایمپورت: $e')),
);
}
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('ایمپورت اشخاص از اکسل'),
content: SizedBox(
width: 560,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(children: [
Expanded(
child: TextField(
controller: _pathCtrl,
readOnly: true,
decoration: const InputDecoration(
labelText: 'فایل انتخاب‌شده',
hintText: 'هیچ فایلی انتخاب نشده',
isDense: true,
),
),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: (_loading || !_isInitialized) ? null : _pickFile,
icon: const Icon(Icons.attach_file),
label: const Text('انتخاب فایل'),
),
]),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _matchBy,
isDense: true,
items: const [
DropdownMenuItem(value: 'code', child: Text('match by: code')),
DropdownMenuItem(value: 'national_id', child: Text('match by: national_id')),
DropdownMenuItem(value: 'email', child: Text('match by: email')),
],
onChanged: (v) => setState(() => _matchBy = v ?? 'code'),
decoration: const InputDecoration(isDense: true),
),
),
const SizedBox(width: 8),
Expanded(
child: DropdownButtonFormField<String>(
value: _conflictPolicy,
isDense: true,
items: const [
DropdownMenuItem(value: 'insert', child: Text('policy: insert-only')),
DropdownMenuItem(value: 'update', child: Text('policy: update-existing')),
DropdownMenuItem(value: 'upsert', child: Text('policy: upsert')),
],
onChanged: (v) => setState(() => _conflictPolicy = v ?? 'upsert'),
decoration: const InputDecoration(isDense: true),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Checkbox(
value: _dryRun,
onChanged: (v) => setState(() => _dryRun = v ?? true),
),
const Text('Dry run (فقط اعتبارسنجی)')
],
),
const SizedBox(height: 8),
Row(
children: [
OutlinedButton.icon(
onPressed: _loading ? null : _downloadTemplate,
icon: const Icon(Icons.download),
label: const Text('دانلود تمپلیت'),
),
const SizedBox(width: 8),
FilledButton.icon(
onPressed: _loading ? null : () => _runImport(dryRun: _dryRun),
icon: _loading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.play_arrow),
label: Text(_dryRun ? 'بررسی (Dry run)' : 'ایمپورت'),
),
const SizedBox(width: 8),
if (_dryRun)
FilledButton.tonalIcon(
onPressed: _loading ? null : () async {
// اجرای ایمپورت واقعی
setState(() => _dryRun = false);
await _runImport(dryRun: false);
},
icon: const Icon(Icons.cloud_upload),
label: const Text('ایمپورت واقعی'),
)
],
),
if (_result != null) ...[
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: Text('نتیجه:', style: Theme.of(context).textTheme.titleSmall),
),
const SizedBox(height: 8),
_ResultSummary(result: _result!),
],
],
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(false),
child: const Text('بستن'),
),
],
);
}
}
class _ResultSummary extends StatelessWidget {
final Map<String, dynamic> result;
const _ResultSummary({required this.result});
@override
Widget build(BuildContext context) {
final data = result['data'] as Map<String, dynamic>?;
final summary = (data?['summary'] as Map<String, dynamic>?) ?? {};
final errors = (data?['errors'] as List?)?.cast<Map<String, dynamic>>() ?? const [];
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Wrap(
spacing: 12,
runSpacing: 4,
children: [
_chip('کل', summary['total']),
_chip('معتبر', summary['valid']),
_chip('نامعتبر', summary['invalid']),
_chip('ایجاد شده', summary['inserted']),
_chip('به‌روزرسانی', summary['updated']),
_chip('رد شده', summary['skipped']),
_chip('Dry run', summary['dry_run'] == true ? 'بله' : 'خیر'),
],
),
const SizedBox(height: 8),
if (errors.isNotEmpty)
SizedBox(
height: 160,
child: ListView.builder(
itemCount: errors.length,
itemBuilder: (context, i) {
final e = errors[i];
return ListTile(
dense: true,
leading: const Icon(Icons.error_outline, color: Colors.red),
title: Text('ردیف ${e['row']}'),
subtitle: Text(((e['errors'] as List?)?.join('، ')) ?? ''),
);
},
),
),
],
);
}
Widget _chip(String label, Object? value) {
return Chip(label: Text('$label: ${value ?? '-'}'));
}
}

View file

@ -42,6 +42,7 @@ endif()
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE -Wno-deprecated-literal-operator)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()

View file

@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);

View file

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
)

View file

@ -5,11 +5,15 @@
import FlutterMacOS
import Foundation
import file_picker
import file_selector_macos
import flutter_secure_storage_macos
import path_provider_foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View file

@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@ -41,6 +49,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
description:
@ -65,6 +81,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.0"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.11"
dio:
dependency: "direct main"
description:
@ -105,6 +129,78 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.3.3"
file_selector:
dependency: "direct main"
description:
name: file_selector
sha256: "5f1d15a7f17115038f433d1b0ea57513cc9e29a9d5338d166cb0bef3fa90a7a0"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
file_selector_android:
dependency: transitive
description:
name: file_selector_android
sha256: "4be8ae7374c81daf88e49084a1d68dfe68466ef38a6a3d711cc0b83d53e22465"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.1+16"
file_selector_ios:
dependency: transitive
description:
name: file_selector_ios
sha256: fe9f52123af16bba4ad65bd7e03defbbb4b172a38a8e6aaa2a869a0c56a5f5fb
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.3+2"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.4+4"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.2"
file_selector_web:
dependency: transitive
description:
name: file_selector_web
sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.4+2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.3+4"
fixnum:
dependency: transitive
description:
@ -131,6 +227,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.30"
flutter_secure_storage:
dependency: "direct main"
description:
@ -197,6 +301,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "16.2.2"
http:
dependency: transitive
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
http_parser:
dependency: transitive
description:
@ -357,6 +469,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
platform:
dependency: transitive
description:
@ -554,6 +674,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.6.1"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.29.0"

View file

@ -47,6 +47,8 @@ dependencies:
shamsi_date: ^1.1.1
intl: ^0.20.0
data_table_2: ^2.5.12
file_picker: ^10.3.3
file_selector: ^1.0.4
dev_dependencies:
flutter_test:

View file

@ -6,9 +6,12 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
}

View file

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
)