added certificate page
This commit is contained in:
parent
8ab3df59d4
commit
1a41a99e20
@ -1,21 +1,40 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class ApiConfig {
|
class ApiConfig {
|
||||||
// Host handling:
|
/// Metti true quando vuoi puntare al server live
|
||||||
|
static const bool useLive = true;
|
||||||
|
|
||||||
|
/// Dominio produzione
|
||||||
|
static const String liveHost = 'app.yogiboook.com';
|
||||||
|
|
||||||
|
// Host handling DEV:
|
||||||
// - Web: localhost
|
// - Web: localhost
|
||||||
// - Android emulator: 10.0.2.2 (maps to host machine)
|
// - Android emulator: 10.0.2.2 (maps to host machine)
|
||||||
static String get host {
|
static String get _devHost {
|
||||||
if (kIsWeb) return 'localhost';
|
if (kIsWeb) return 'localhost';
|
||||||
if (defaultTargetPlatform == TargetPlatform.android) return '10.0.2.2';
|
if (defaultTargetPlatform == TargetPlatform.android) return '10.0.2.2';
|
||||||
return 'localhost';
|
return 'localhost';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Laravel/Vanguard API (login, password remind, social)
|
static String get host => useLive ? liveHost : _devHost;
|
||||||
static String get laravelApiBase => 'http://$host/yogiboook/public/api';
|
|
||||||
|
|
||||||
// Your custom PHP APIs under /public/userarea/api/
|
static String get scheme => useLive ? 'https' : 'http';
|
||||||
static String get phpApiBase => 'http://$host/yogiboook/public/userarea/api';
|
|
||||||
|
/// Laravel/Vanguard API (login, password remind, ecc)
|
||||||
|
/// DEV: http://10.0.2.2/yogiboook/public/api
|
||||||
|
/// LIVE: https://app.yogiboook.com/public/api
|
||||||
|
static String get laravelApiBase {
|
||||||
|
if (useLive) return '$scheme://$host/public/api';
|
||||||
|
return '$scheme://$host/yogiboook/public/api';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PHP custom APIs
|
||||||
|
/// DEV: http://10.0.2.2/yogiboook/public/userarea/api
|
||||||
|
/// LIVE: https://app.yogiboook.com/public/userarea/api
|
||||||
|
static String get phpApiBase {
|
||||||
|
if (useLive) return '$scheme://$host/public/userarea/api';
|
||||||
|
return '$scheme://$host/yogiboook/public/userarea/api';
|
||||||
|
}
|
||||||
|
|
||||||
static const String deviceName = 'FlutterApp';
|
static const String deviceName = 'FlutterApp';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import 'select_school_page.dart';
|
|||||||
import 'meditation_page.dart';
|
import 'meditation_page.dart';
|
||||||
import '../services/vanguard_api.dart';
|
import '../services/vanguard_api.dart';
|
||||||
import 'login_page.dart';
|
import 'login_page.dart';
|
||||||
|
import 'medical_certificates_page.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
final String token;
|
final String token;
|
||||||
@ -278,6 +279,20 @@ class _HomePageState extends State<HomePage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.medical_information),
|
||||||
|
title: const Text('Certificati medici'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop(); // chiude drawer
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) =>
|
||||||
|
MedicalCertificatesPage(token: widget.token),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.logout),
|
leading: const Icon(Icons.logout),
|
||||||
|
|||||||
613
lib/screens/medical_certificates_page.dart
Normal file
613
lib/screens/medical_certificates_page.dart
Normal file
@ -0,0 +1,613 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../services/medical_certificates_api.dart';
|
||||||
|
import '../config/api_config.dart';
|
||||||
|
|
||||||
|
class MedicalCertificatesPage extends StatefulWidget {
|
||||||
|
final String token;
|
||||||
|
|
||||||
|
const MedicalCertificatesPage({super.key, required this.token});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MedicalCertificatesPage> createState() =>
|
||||||
|
_MedicalCertificatesPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||||
|
bool loading = true;
|
||||||
|
String error = '';
|
||||||
|
List<Map<String, dynamic>> certs = [];
|
||||||
|
|
||||||
|
// upload flow
|
||||||
|
File? pickedFile;
|
||||||
|
String pickedLabel = '';
|
||||||
|
final docNameCtrl = TextEditingController(text: 'certificato');
|
||||||
|
final notesCtrl = TextEditingController();
|
||||||
|
DateTime? expiryDate;
|
||||||
|
bool uploading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
docNameCtrl.dispose();
|
||||||
|
notesCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reload() async {
|
||||||
|
setState(() {
|
||||||
|
loading = true;
|
||||||
|
error = '';
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final list = await MedicalCertificatesApi.list(token: widget.token);
|
||||||
|
setState(() => certs = list);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => error = e.toString());
|
||||||
|
} finally {
|
||||||
|
setState(() => loading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickFromChooser() async {
|
||||||
|
// 1 solo bottone -> bottom sheet scelta
|
||||||
|
final choice = await showModalBottomSheet<String>(
|
||||||
|
context: context,
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (_) => SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_camera),
|
||||||
|
title: const Text('Fotocamera'),
|
||||||
|
onTap: () => Navigator.pop(context, 'camera'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.photo_library),
|
||||||
|
title: const Text('Galleria'),
|
||||||
|
onTap: () => Navigator.pop(context, 'gallery'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.picture_as_pdf),
|
||||||
|
title: const Text('PDF'),
|
||||||
|
onTap: () => Navigator.pop(context, 'pdf'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (choice == null) return;
|
||||||
|
|
||||||
|
File? f;
|
||||||
|
String label = '';
|
||||||
|
|
||||||
|
if (choice == 'camera' || choice == 'gallery') {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final x = await picker.pickImage(
|
||||||
|
source: choice == 'camera' ? ImageSource.camera : ImageSource.gallery,
|
||||||
|
imageQuality: 85,
|
||||||
|
);
|
||||||
|
if (x == null) return;
|
||||||
|
f = File(x.path);
|
||||||
|
label = x.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choice == 'pdf') {
|
||||||
|
final res = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: const ['pdf'],
|
||||||
|
withData: false,
|
||||||
|
);
|
||||||
|
if (res == null || res.files.isEmpty) return;
|
||||||
|
final path = res.files.single.path;
|
||||||
|
if (path == null) return;
|
||||||
|
f = File(path);
|
||||||
|
label = res.files.single.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
pickedFile = f;
|
||||||
|
pickedLabel = label;
|
||||||
|
// reset campi per nuovo upload
|
||||||
|
docNameCtrl.text = 'certificato';
|
||||||
|
notesCtrl.clear();
|
||||||
|
expiryDate = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickExpiry() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final d = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
firstDate: DateTime(now.year, now.month, now.day),
|
||||||
|
lastDate: DateTime(now.year + 10),
|
||||||
|
initialDate: expiryDate ?? now,
|
||||||
|
);
|
||||||
|
if (d == null) return;
|
||||||
|
setState(() => expiryDate = d);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fmtYYYYMMDD(DateTime d) {
|
||||||
|
final mm = d.month.toString().padLeft(2, '0');
|
||||||
|
final dd = d.day.toString().padLeft(2, '0');
|
||||||
|
return '${d.year}-$mm-$dd';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fmtDDMMYYYY(DateTime d) {
|
||||||
|
final dd = d.day.toString().padLeft(2, '0');
|
||||||
|
final mm = d.month.toString().padLeft(2, '0');
|
||||||
|
return '$dd/$mm/${d.year}';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _doUpload() async {
|
||||||
|
if (pickedFile == null) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('Seleziona prima un file.')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (expiryDate == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Seleziona la data di scadenza.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
uploading = true;
|
||||||
|
error = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MedicalCertificatesApi.upload(
|
||||||
|
token: widget.token,
|
||||||
|
file: pickedFile!,
|
||||||
|
documentName: docNameCtrl.text,
|
||||||
|
expiryDateYYYYMMDD: _fmtYYYYMMDD(expiryDate!),
|
||||||
|
notes: notesCtrl.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
// reset form
|
||||||
|
setState(() {
|
||||||
|
pickedFile = null;
|
||||||
|
pickedLabel = '';
|
||||||
|
docNameCtrl.text = 'certificato';
|
||||||
|
notesCtrl.clear();
|
||||||
|
expiryDate = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await _reload();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Certificato caricato correttamente.')),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => error = e.toString());
|
||||||
|
} finally {
|
||||||
|
setState(() => uploading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openFileUrl(String url) async {
|
||||||
|
// url arriva tipo "/userarea/certificate/xxx"
|
||||||
|
// per aprirlo serve url assoluto
|
||||||
|
final abs = '${ApiConfig.scheme}://${ApiConfig.host}$url';
|
||||||
|
final uri = Uri.parse(abs);
|
||||||
|
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
if (!ok && mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Impossibile aprire il file.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteCert(int id) async {
|
||||||
|
final ok = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('Elimina certificato'),
|
||||||
|
content: const Text('Vuoi davvero eliminare questo certificato?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Annulla'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: const Text('Elimina'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (ok != true) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await MedicalCertificatesApi.delete(token: widget.token, certId: id);
|
||||||
|
await _reload();
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => error = e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isImagePath(String p) {
|
||||||
|
final x = p.toLowerCase();
|
||||||
|
return x.endsWith('.jpg') ||
|
||||||
|
x.endsWith('.jpeg') ||
|
||||||
|
x.endsWith('.png') ||
|
||||||
|
x.endsWith('.heic') ||
|
||||||
|
x.endsWith('.heif');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isPdfPath(String p) => p.toLowerCase().endsWith('.pdf');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
'Certificati medici',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w900),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: loading ? null : _reload,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: RefreshIndicator(
|
||||||
|
onRefresh: _reload,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 22),
|
||||||
|
children: [
|
||||||
|
// 1 SOLO bottone in alto
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: uploading ? null : _pickFromChooser,
|
||||||
|
icon: const Icon(Icons.cloud_upload),
|
||||||
|
label: const Text('Carica certificato'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Preview + form (solo se file selezionato)
|
||||||
|
if (pickedFile != null) ...[
|
||||||
|
_UploadCard(
|
||||||
|
filename: pickedLabel.isNotEmpty
|
||||||
|
? pickedLabel
|
||||||
|
: pickedFile!.path.split(Platform.pathSeparator).last,
|
||||||
|
isImage: _isImagePath(pickedFile!.path),
|
||||||
|
isPdf: _isPdfPath(pickedFile!.path),
|
||||||
|
file: pickedFile!,
|
||||||
|
documentNameController: docNameCtrl,
|
||||||
|
notesController: notesCtrl,
|
||||||
|
expiryDate: expiryDate,
|
||||||
|
onPickExpiry: _pickExpiry,
|
||||||
|
onUpload: uploading ? null : _doUpload,
|
||||||
|
uploading: uploading,
|
||||||
|
expiryLabel: expiryDate == null
|
||||||
|
? 'Seleziona data'
|
||||||
|
: _fmtDDMMYYYY(expiryDate!),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (error.isNotEmpty) ...[
|
||||||
|
Text(error, style: const TextStyle(color: Colors.red)),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Certificati caricati (${certs.length})',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 30),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
)
|
||||||
|
else if (certs.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 26),
|
||||||
|
child: Center(child: Text('Nessun certificato caricato')),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
...certs.map((c) {
|
||||||
|
final expired = c['is_expired'] == true;
|
||||||
|
final docName = (c['document_name'] ?? '').toString();
|
||||||
|
final filename = (c['filename'] ?? '').toString();
|
||||||
|
final uploadedAt = (c['uploaded_at'] ?? '').toString();
|
||||||
|
final expiry = (c['expiry_date'] ?? '').toString();
|
||||||
|
final notes = (c['notes'] ?? '').toString();
|
||||||
|
final fileUrl = (c['file_url'] ?? '').toString();
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
color: expired ? const Color(0xFFFFEBEE) : Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
docName.isNotEmpty ? docName : 'certificato',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (expired)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFDC3545),
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'SCADUTO',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
filename,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black54,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.event,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Caricato: $uploadedAt',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black54,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.timer,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Scadenza: ${expiry.isEmpty ? '—' : expiry}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: expired
|
||||||
|
? const Color(0xFFDC3545)
|
||||||
|
: Colors.black54,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (notes.trim().isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(notes, style: const TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () => _openFileUrl(fileUrl),
|
||||||
|
icon: const Icon(Icons.open_in_new),
|
||||||
|
label: const Text('Apri'),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () =>
|
||||||
|
_deleteCert((c['id'] as num).toInt()),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete_outline,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
'Elimina',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UploadCard extends StatelessWidget {
|
||||||
|
final String filename;
|
||||||
|
final bool isImage;
|
||||||
|
final bool isPdf;
|
||||||
|
final File file;
|
||||||
|
final TextEditingController documentNameController;
|
||||||
|
final TextEditingController notesController;
|
||||||
|
final DateTime? expiryDate;
|
||||||
|
final String expiryLabel;
|
||||||
|
final VoidCallback onPickExpiry;
|
||||||
|
final VoidCallback? onUpload;
|
||||||
|
final bool uploading;
|
||||||
|
|
||||||
|
const _UploadCard({
|
||||||
|
required this.filename,
|
||||||
|
required this.isImage,
|
||||||
|
required this.isPdf,
|
||||||
|
required this.file,
|
||||||
|
required this.documentNameController,
|
||||||
|
required this.notesController,
|
||||||
|
required this.expiryDate,
|
||||||
|
required this.expiryLabel,
|
||||||
|
required this.onPickExpiry,
|
||||||
|
required this.onUpload,
|
||||||
|
required this.uploading,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Anteprima',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w900, fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
if (isImage)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
child: Image.file(
|
||||||
|
file,
|
||||||
|
height: 160,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF6F6FB),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isPdf ? Icons.picture_as_pdf : Icons.insert_drive_file,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
filename,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
controller: documentNameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Nome documento',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: onPickExpiry,
|
||||||
|
icon: const Icon(Icons.date_range),
|
||||||
|
label: Text(expiryLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
controller: notesController,
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 4,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Note (opzionale)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: onUpload,
|
||||||
|
icon: uploading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.check),
|
||||||
|
label: Text(uploading ? 'Caricamento...' : 'Conferma upload'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
lib/services/medical_certificates_api.dart
Normal file
113
lib/services/medical_certificates_api.dart
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
|
import '../config/api_config.dart';
|
||||||
|
|
||||||
|
class MedicalCertificatesApi {
|
||||||
|
static Uri _u(String path, [Map<String, String>? q]) =>
|
||||||
|
Uri.parse('${ApiConfig.phpApiBase}/$path').replace(queryParameters: q);
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>> list({
|
||||||
|
required String token,
|
||||||
|
}) async {
|
||||||
|
final res = await http.get(
|
||||||
|
_u('api_medical_certificates_list.php'),
|
||||||
|
headers: {'Accept': 'application/json', 'Authorization': 'Bearer $token'},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception('List failed (${res.statusCode}): ${res.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
||||||
|
if (data['success'] != true) {
|
||||||
|
throw Exception(data['message'] ?? 'List error');
|
||||||
|
}
|
||||||
|
|
||||||
|
final certs = (data['certificates'] as List).cast<Map<String, dynamic>>();
|
||||||
|
return certs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> upload({
|
||||||
|
required String token,
|
||||||
|
required File file,
|
||||||
|
required String documentName,
|
||||||
|
required String expiryDateYYYYMMDD,
|
||||||
|
String? notes,
|
||||||
|
}) async {
|
||||||
|
final req = http.MultipartRequest(
|
||||||
|
'POST',
|
||||||
|
_u('api_medical_certificates_upload.php'),
|
||||||
|
);
|
||||||
|
|
||||||
|
req.headers.addAll({
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
});
|
||||||
|
|
||||||
|
req.fields['document_name'] = documentName.trim().isEmpty
|
||||||
|
? 'certificato'
|
||||||
|
: documentName.trim();
|
||||||
|
req.fields['expiry_date'] = expiryDateYYYYMMDD;
|
||||||
|
if (notes != null && notes.trim().isNotEmpty)
|
||||||
|
req.fields['notes'] = notes.trim();
|
||||||
|
|
||||||
|
final mimeType = lookupMimeType(file.path) ?? 'application/octet-stream';
|
||||||
|
|
||||||
|
req.files.add(
|
||||||
|
await http.MultipartFile.fromPath(
|
||||||
|
'certificate',
|
||||||
|
file.path,
|
||||||
|
contentType: _parseMediaType(mimeType),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final streamed = await req.send();
|
||||||
|
final body = await streamed.stream.bytesToString();
|
||||||
|
|
||||||
|
if (streamed.statusCode != 200) {
|
||||||
|
throw Exception('Upload failed (${streamed.statusCode}): $body');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(body) as Map<String, dynamic>;
|
||||||
|
if (data['success'] != true) {
|
||||||
|
throw Exception(data['message'] ?? 'Upload error');
|
||||||
|
}
|
||||||
|
return (data['certificate'] as Map).cast<String, dynamic>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> delete({
|
||||||
|
required String token,
|
||||||
|
required int certId,
|
||||||
|
}) async {
|
||||||
|
// usiamo POST fallback (più compatibile)
|
||||||
|
final res = await http.post(
|
||||||
|
_u('api_medical_certificates_delete.php'),
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: 'cert_id=$certId',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception('Delete failed (${res.statusCode}): ${res.body}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
||||||
|
if (data['success'] != true) {
|
||||||
|
throw Exception(data['message'] ?? 'Delete error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper per MediaType senza dipendenze extra
|
||||||
|
http.MediaType? _parseMediaType(String mime) {
|
||||||
|
final parts = mime.split('/');
|
||||||
|
if (parts.length != 2) return null;
|
||||||
|
return http.MediaType(parts[0], parts[1]);
|
||||||
|
}
|
||||||
@ -7,12 +7,16 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
|
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
|
||||||
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
|
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
|
||||||
|
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) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
audioplayers_linux
|
audioplayers_linux
|
||||||
|
file_selector_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,16 @@ import FlutterMacOS
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import audioplayers_darwin
|
import audioplayers_darwin
|
||||||
|
import file_picker
|
||||||
|
import file_selector_macos
|
||||||
import google_sign_in_ios
|
import google_sign_in_ios
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
|
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
|
||||||
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
|||||||
136
pubspec.lock
136
pubspec.lock
@ -97,6 +97,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.5+1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -137,6 +145,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.3.7"
|
||||||
|
file_selector_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_linux
|
||||||
|
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.4"
|
||||||
|
file_selector_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_macos
|
||||||
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.5"
|
||||||
|
file_selector_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_platform_interface
|
||||||
|
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
|
file_selector_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file_selector_windows
|
||||||
|
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.3+5"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -163,6 +211,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: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.33"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -237,6 +293,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+10"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.13+3"
|
||||||
|
image_picker_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_linux
|
||||||
|
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
image_picker_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_macos
|
||||||
|
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2+1"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
image_picker_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_windows
|
||||||
|
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -301,6 +421,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
|
mime:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -538,6 +666,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.15.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -39,6 +39,9 @@ dependencies:
|
|||||||
google_sign_in: ^7.0.0
|
google_sign_in: ^7.0.0
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
audioplayers: ^6.5.1
|
audioplayers: ^6.5.1
|
||||||
|
file_picker: ^8.0.7
|
||||||
|
image_picker: ^1.1.2
|
||||||
|
mime: ^2.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@ -7,11 +7,14 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
AudioplayersWindowsPluginRegisterWithRegistrar(
|
AudioplayersWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
audioplayers_windows
|
audioplayers_windows
|
||||||
|
file_selector_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user