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 APIRouter, Depends, HTTPException, Query, Request, Body
from fastapi import UploadFile, File
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Dict, Any, List, Optional 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}", @router.get("/persons/{person_id}",
summary="جزئیات شخص", summary="جزئیات شخص",
description="دریافت جزئیات یک شخص", description="دریافت جزئیات یک شخص",
@ -626,3 +691,158 @@ async def get_persons_summary_endpoint(
request=request, request=request,
message="خلاصه اشخاص با موفقیت دریافت شد", 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_widget.dart';
import '../../widgets/data_table/data_table_config.dart'; import '../../widgets/data_table/data_table_config.dart';
import '../../widgets/person/person_form_dialog.dart'; import '../../widgets/person/person_form_dialog.dart';
import '../../widgets/person/person_import_dialog.dart';
import '../../widgets/permission/permission_widgets.dart'; import '../../widgets/permission/permission_widgets.dart';
import '../../models/person_model.dart'; import '../../models/person_model.dart';
import '../../services/person_service.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 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'helpers/file_saver.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:flutter/material.dart';
import 'package:data_table_2/data_table_2.dart'; import 'package:data_table_2/data_table_2.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@ -670,12 +670,17 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
await FileSaver.saveBytes(bytes, filename); await FileSaver.saveBytes(bytes, filename);
} }
// Platform-specific download functions for Linux
Future<void> _downloadPdf(dynamic data, String filename) async { 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 { 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) function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror) 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_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>") target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction() endfunction()

View file

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

View file

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

View file

@ -5,11 +5,15 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import file_picker
import file_selector_macos
import flutter_secure_storage_macos import flutter_secure_storage_macos
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 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")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View file

@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -41,6 +49,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.19.1" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@ -65,6 +81,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.6.0" 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: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@ -105,6 +129,78 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.0.1" 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: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -131,6 +227,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -197,6 +301,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "16.2.2" 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: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -357,6 +469,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.2.0" 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: platform:
dependency: transitive dependency: transitive
description: description:
@ -554,6 +674,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" 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: sdks:
dart: ">=3.9.2 <4.0.0" dart: ">=3.9.2 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"

View file

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

View file

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

View file

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