Merge api service
This commit is contained in:
commit
9746318f2c
BIN
assets/images/screenshot_1.png
Normal file
BIN
assets/images/screenshot_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 381 KiB |
BIN
assets/images/screenshot_2.png
Normal file
BIN
assets/images/screenshot_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
BIN
assets/images/screenshot_3.png
Normal file
BIN
assets/images/screenshot_3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
BIN
assets/images/screenshot_4.png
Normal file
BIN
assets/images/screenshot_4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 292 KiB |
@ -1,33 +1,266 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:dropdown_search/dropdown_search.dart';
|
import 'package:dropdown_search/dropdown_search.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
import '../../config/apiUrl.dart';
|
||||||
|
import '../../services/apiService.dart';
|
||||||
|
import '../../utils/auth_utils.dart';
|
||||||
import '../../widgets/custom_text_forex.dart';
|
import '../../widgets/custom_text_forex.dart';
|
||||||
|
import 'forex_list.dart';
|
||||||
|
|
||||||
class ForexData extends StatefulWidget {
|
class ForexData extends StatefulWidget {
|
||||||
|
final Future<List<dynamic>> Function() fetchGetForex;
|
||||||
final bool isDesktop;
|
final bool isDesktop;
|
||||||
final Color? layoutColor;
|
final Color? layoutColor;
|
||||||
const ForexData({super.key, required this.isDesktop, this.layoutColor});
|
|
||||||
|
final int? forexId; // <-- Add this
|
||||||
|
final Map<String, dynamic>? forexData;
|
||||||
|
|
||||||
|
const ForexData(
|
||||||
|
{super.key,
|
||||||
|
required this.isDesktop,
|
||||||
|
this.layoutColor,
|
||||||
|
required this.fetchGetForex,
|
||||||
|
this.forexId,
|
||||||
|
this.forexData});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ForexDataState createState() => ForexDataState();
|
ForexDataState createState() => ForexDataState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ForexDataState extends State<ForexData> {
|
class ForexDataState extends State<ForexData> {
|
||||||
|
final ApiService apiService = ApiService();
|
||||||
|
Map<String, String> countryMap = {};
|
||||||
|
late List<dynamic>? apiCountryData;
|
||||||
|
late List<dynamic>? apiAirlineCountryData;
|
||||||
|
Map<String, dynamic>? apiData;
|
||||||
|
final Map<String, TextEditingController> controllers = {};
|
||||||
|
Map<String, String> errorMessages = {};
|
||||||
|
|
||||||
List<dynamic> countryList = [];
|
List<dynamic> countryList = [];
|
||||||
String? selectedCountry;
|
String? selectedCountry;
|
||||||
|
String? selectedCountryName;
|
||||||
String? selectedCurrency;
|
String? selectedCurrency;
|
||||||
String? selectedDuration;
|
String? selectedDuration;
|
||||||
String? selectedPerdiemAmount;
|
String? selectedPerdiemAmount;
|
||||||
|
String? userId;
|
||||||
|
int? forexDataId;
|
||||||
|
late String isActive = "1";
|
||||||
|
|
||||||
|
List<String> dataHeader = [
|
||||||
|
"country_code",
|
||||||
|
"country",
|
||||||
|
"currency",
|
||||||
|
"perdiemAmount"
|
||||||
|
];
|
||||||
|
|
||||||
|
Map<String, dynamic> forex_Detials() {
|
||||||
|
final data = {
|
||||||
|
// "forex_perdiem_id": int.parse(forexId),
|
||||||
|
"country_code": selectedCountry,
|
||||||
|
"country_name": selectedCountryName,
|
||||||
|
"currency": controllers["currency"]?.text,
|
||||||
|
"perdiem_amount": controllers["perdiemAmount"]?.text,
|
||||||
|
"is_active": 1,
|
||||||
|
"created_by": userId,
|
||||||
|
"is_active": isActive,
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
apiCountryData = null;
|
||||||
|
apiData = null;
|
||||||
|
for (var field in dataHeader) {
|
||||||
|
controllers[field] = TextEditingController();
|
||||||
|
}
|
||||||
|
fetchCountries();
|
||||||
|
if (widget.forexId != null) {
|
||||||
|
print('Editing Forex ID: ${widget.forexId}');
|
||||||
|
updateForexDetails();
|
||||||
|
}
|
||||||
|
_clearError();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearError() {
|
||||||
|
setState(() {
|
||||||
|
errorMessages.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
for (var controller in controllers.values) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateForexDetails() {
|
||||||
|
print("Updateeee - ${widget.forexData}");
|
||||||
|
|
||||||
|
final data = widget.forexData;
|
||||||
|
|
||||||
|
if (data == null) return;
|
||||||
|
setState(() {
|
||||||
|
selectedCountry = data['country_code']; // For dropdown
|
||||||
|
selectedCountryName =
|
||||||
|
data['country_name']; // For dropdown label or display
|
||||||
|
selectedCurrency = data['currency']; // Optional if used elsewhere
|
||||||
|
|
||||||
|
controllers['currency']?.text = data['currency'] ?? '';
|
||||||
|
controllers['perdiemAmount']?.text = data['perdiem_amount'].toString();
|
||||||
|
isActive = data["is_active"];
|
||||||
|
final forexId = int.tryParse(data['forex_perdiem_id'].toString());
|
||||||
|
forexDataId = forexId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchCountries() async {
|
||||||
|
try {
|
||||||
|
List<dynamic> countries = await apiService.fetchCountryList();
|
||||||
|
setState(() {
|
||||||
|
apiCountryData = countries;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching country list: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleStatus() {
|
||||||
|
setState(() {
|
||||||
|
isActive = isActive == "1" ? "0" : "1";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validateData() {
|
||||||
|
errorMessages.clear();
|
||||||
|
|
||||||
|
final data = {
|
||||||
|
"country_code": selectedCountry,
|
||||||
|
"country": selectedCountryName,
|
||||||
|
"currency": controllers["currency"]?.text,
|
||||||
|
"perdiemAmount": controllers["perdiemAmount"]?.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
final requiredFields = [
|
||||||
|
"country_code",
|
||||||
|
"country",
|
||||||
|
"currency",
|
||||||
|
"perdiemAmount"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check validation for each field
|
||||||
|
for (String field in requiredFields) {
|
||||||
|
if (data[field] == null || data[field].toString().trim().isEmpty) {
|
||||||
|
errorMessages[field] = "Required";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorMessages.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleSubmit() async {
|
||||||
|
userId = await getUserId();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
// This triggers UI rebuild with error messages
|
||||||
|
if (validateData()) {
|
||||||
|
postForexData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final forexData1 = forex_Detials();
|
||||||
|
print("ForexDAta - $forexData1");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> postForexData({int isActive = 1}) async {
|
||||||
|
// final remarksData = getData();
|
||||||
|
|
||||||
|
final forexData = forex_Detials();
|
||||||
|
|
||||||
|
print("forexDataPOSDf - $forexData");
|
||||||
|
|
||||||
|
final String apiUrldata;
|
||||||
|
if (forexDataId != null) {
|
||||||
|
print("feforexDataId - $forexDataId");
|
||||||
|
|
||||||
|
apiUrldata = '$apiUrl/api/updateForexPerdiem/$forexDataId';
|
||||||
|
forexData["id"] = forexDataId;
|
||||||
|
forexData["updated_by"] = userId;
|
||||||
|
} else {
|
||||||
|
apiUrldata = '$apiUrl/api/createForexPerdiem';
|
||||||
|
forexData["created_by"] = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Remarks Data - remarksData");
|
||||||
|
|
||||||
|
// final String apiUrldata = '$apiUrl/api/createForexPerdiem';
|
||||||
|
// api/updateForexPerdiem/39
|
||||||
|
|
||||||
|
final token = await getToken(); // Fetch token
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
throw Exception('Token not found. Please log in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (selectedPlanId != null && selectedPlanId!.isNotEmpty) {
|
||||||
|
// planData['plan_id'] = selectedPlanId; // Add plan_id for update
|
||||||
|
// }
|
||||||
|
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(apiUrldata);
|
||||||
|
final headers = {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
final body = jsonEncode(forexData);
|
||||||
|
|
||||||
|
final response = forexDataId != null
|
||||||
|
? await http.put(uri, headers: headers, body: body)
|
||||||
|
: await http.post(uri, headers: headers, body: body);
|
||||||
|
|
||||||
|
// final response = await http.post(
|
||||||
|
// Uri.parse(apiUrldata),
|
||||||
|
// headers: {
|
||||||
|
// 'Authorization': 'Bearer $token',
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// },
|
||||||
|
// body: jsonEncode(forexData), // Convert map to JSON
|
||||||
|
// );
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
print("Forex Details Created successfully!");
|
||||||
|
print("Response: ${response.body}");
|
||||||
|
// _clearError();
|
||||||
|
_clearError();
|
||||||
|
await widget.fetchGetForex();
|
||||||
|
|
||||||
|
// dispose();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else {
|
||||||
|
print("Failed to submit plan. Status: ${response.statusCode}");
|
||||||
|
print("Error: ${response.body}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(" Error submitting plan: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
late Map<String, String> countryMap; // Mapping country_code -> country_name
|
late Map<String, String> countryMap; // Mapping country_code -> country_name
|
||||||
late List<String> countryCodes; // List of country codes
|
late List<String> countryCodes; // List of country codes
|
||||||
|
|
||||||
countryList = [];
|
// countryList = [];
|
||||||
// countryList = widget.apiCountryData ?? [];
|
countryList = apiCountryData ?? [];
|
||||||
|
|
||||||
// Map country codes to country names
|
// Map country codes to country names
|
||||||
countryMap = {
|
countryMap = {
|
||||||
@ -52,13 +285,18 @@ class ForexDataState extends State<ForexData> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Create Forex Details',
|
'Create Perdiem Amount',
|
||||||
style: GoogleFonts.poppins(fontSize: 18, color: Colors.black),
|
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 2),
|
||||||
|
Divider(
|
||||||
|
thickness: 0.2,
|
||||||
|
color: Colors.blueGrey.shade100,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -80,10 +318,23 @@ class ForexDataState extends State<ForexData> {
|
|||||||
selectedItem: countryMap[selectedCountry],
|
selectedItem: countryMap[selectedCountry],
|
||||||
popupProps: PopupProps.menu(
|
popupProps: PopupProps.menu(
|
||||||
showSearchBox: true, // Enables search functionality
|
showSearchBox: true, // Enables search functionality
|
||||||
|
menuProps: const MenuProps(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(maxHeight: 250),
|
||||||
|
itemBuilder: (context, item, isSelected) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8.0, vertical: 6.0),
|
||||||
|
child: Text(
|
||||||
|
item,
|
||||||
|
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
searchFieldProps: TextFieldProps(
|
searchFieldProps: TextFieldProps(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Search Country...",
|
hintText: "Search Country...",
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
hintStyle: GoogleFonts.poppins(fontSize: 11),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -101,7 +352,7 @@ class ForexDataState extends State<ForexData> {
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
selectedItem ?? "Select Country",
|
selectedItem ?? "Select Country",
|
||||||
style: TextStyle(fontSize: 12),
|
style: GoogleFonts.poppins(fontSize: 11),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: (String? newValue) {
|
onChanged: (String? newValue) {
|
||||||
@ -110,18 +361,19 @@ class ForexDataState extends State<ForexData> {
|
|||||||
selectedCountry = countryMap.entries
|
selectedCountry = countryMap.entries
|
||||||
.firstWhere((entry) => entry.value == newValue)
|
.firstWhere((entry) => entry.value == newValue)
|
||||||
.key;
|
.key;
|
||||||
|
selectedCountryName = newValue;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// if (errorMessages["country_code"] != null) ...[
|
if (errorMessages["country_code"] != null) ...[
|
||||||
// SizedBox(height: 5), // Space before error message
|
SizedBox(height: 5), // Space before error message
|
||||||
// Text(
|
Text(
|
||||||
// "Select Country",
|
errorMessages["country_code"]!,
|
||||||
// style: TextStyle(color: Colors.red, fontSize: 12),
|
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||||
// ),
|
),
|
||||||
// ],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@ -129,7 +381,7 @@ class ForexDataState extends State<ForexData> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Currency *",
|
"Currency",
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -145,24 +397,26 @@ class ForexDataState extends State<ForexData> {
|
|||||||
// : MediaQuery.of(context).size.width * 0.66,
|
// : MediaQuery.of(context).size.width * 0.66,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: Padding(
|
child: TextField(
|
||||||
padding: const EdgeInsets.all(8.0),
|
controller: controllers["currency"],
|
||||||
child: Center(
|
style: const TextStyle(fontSize: 12),
|
||||||
child: Text(
|
decoration: const InputDecoration(
|
||||||
// "cur",
|
labelText: "Currency",
|
||||||
// "${selectedCurrency}",
|
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
|
||||||
selectedCurrency ?? "Currency",
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
// selectedCurrency?.isNotEmpty == true ? selectedCurrency! : "Currency",
|
border: InputBorder.none,
|
||||||
style: const TextStyle(
|
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFF575A74)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
|
if (errorMessages["currency"] != null) ...[
|
||||||
|
SizedBox(height: 5), // Space before error message
|
||||||
|
Text(
|
||||||
|
errorMessages["currency"]!,
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
// if (isDesktop)SizedBox(width: 8,) else SizedBox(height: 8,),
|
// if (isDesktop)SizedBox(width: 8,) else SizedBox(height: 8,),
|
||||||
|
|
||||||
@ -185,30 +439,61 @@ class ForexDataState extends State<ForexData> {
|
|||||||
isDesktop: widget.isDesktop,
|
isDesktop: widget.isDesktop,
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 35,
|
height: 40,
|
||||||
child: Padding(
|
child: TextField(
|
||||||
padding: const EdgeInsets.all(8.0),
|
controller: controllers["perdiemAmount"],
|
||||||
child: Text(
|
style: const TextStyle(fontSize: 12),
|
||||||
// "amo",
|
decoration: const InputDecoration(
|
||||||
selectedPerdiemAmount ?? "Amount",
|
labelText: "Perdiem Amount",
|
||||||
// selectedPerdiemAmount?.isNotEmpty == true ? selectedPerdiemAmount! : "Amount",
|
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
|
||||||
style: const TextStyle(
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
if (errorMessages["perdiemAmount"] != null) ...[
|
||||||
|
SizedBox(height: 5), // Space before error message
|
||||||
|
Text(
|
||||||
|
errorMessages["perdiemAmount"]!,
|
||||||
|
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
|
||||||
|
if (forexDataId != null)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Change Status ",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF575A74)),
|
color: Color(0xFF575A74)),
|
||||||
// decoration: const InputDecoration(
|
),
|
||||||
// labelText: "To",
|
Tooltip(
|
||||||
// labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
|
message:
|
||||||
// floatingLabelBehavior: FloatingLabelBehavior.never,
|
isActive == "1" ? "Tap to deactivate" : "Tap to activate",
|
||||||
// border: InputBorder.none,
|
child: GestureDetector(
|
||||||
// contentPadding: EdgeInsets.symmetric(vertical: 16),
|
onTap: toggleStatus,
|
||||||
// ),
|
child: Text(
|
||||||
),
|
isActive == "1" ? "Active" : "Inactive",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: "Inter",
|
||||||
|
color: isActive == "1" ? Colors.green : Colors.red,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (forexDataId != null)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 15,
|
height: 15,
|
||||||
),
|
),
|
||||||
@ -238,8 +523,9 @@ class ForexDataState extends State<ForexData> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
handleSubmit();
|
||||||
// You can get text from commentController.text
|
// You can get text from commentController.text
|
||||||
Navigator.of(context).pop(); // Close the modal
|
// Navigator.of(context).pop(); // Close the modal
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: widget.layoutColor,
|
backgroundColor: widget.layoutColor,
|
||||||
@ -247,7 +533,7 @@ class ForexDataState extends State<ForexData> {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text('Submit',
|
child: Text('Save',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 11, color: Colors.white)),
|
fontSize: 11, color: Colors.white)),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -25,6 +25,9 @@ class ForexDataList extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ForexDataListState extends State<ForexDataList> {
|
class ForexDataListState extends State<ForexDataList> {
|
||||||
|
final GlobalKey<ForexDataListState> forexListKey =
|
||||||
|
GlobalKey<ForexDataListState>();
|
||||||
|
|
||||||
final ApiService apiService = ApiService();
|
final ApiService apiService = ApiService();
|
||||||
late Future<List<dynamic>> futureForex;
|
late Future<List<dynamic>> futureForex;
|
||||||
|
|
||||||
@ -62,6 +65,19 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
// futurePlans = fetchPlans();
|
// futurePlans = fetchPlans();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> refreshData() {
|
||||||
|
print("Calling Refresh Data");
|
||||||
|
|
||||||
|
futureForex = fetchGetForex();
|
||||||
|
|
||||||
|
return futureForex.then((users) {
|
||||||
|
setState(() {
|
||||||
|
allForex = users;
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void loadInitialData() async {
|
void loadInitialData() async {
|
||||||
String? layoutString = await getLayoutColor();
|
String? layoutString = await getLayoutColor();
|
||||||
String? bodyStringColor = await getBodyColor();
|
String? bodyStringColor = await getBodyColor();
|
||||||
@ -266,21 +282,43 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void filterUsers(String query) {
|
void filterForex1(String query) {
|
||||||
print("allUsers before filtering: $query");
|
print("allUsers before filtering: $query");
|
||||||
final lowerQuery = query.toLowerCase();
|
final lowerQuery = query.toLowerCase();
|
||||||
setState(() {
|
setState(() {
|
||||||
filteredForex = allForex.where((user) {
|
filteredForex = allForex.where((forex) {
|
||||||
return (user['first_name']?.toLowerCase().contains(lowerQuery) ??
|
return (forex['country_code']?.toLowerCase().contains(lowerQuery) ??
|
||||||
false) ||
|
false) ||
|
||||||
(user['last_name']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
(forex['country_name']?.toLowerCase().contains(lowerQuery) ??
|
||||||
(user['email']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
false) ||
|
||||||
(user['role_value']?.toLowerCase().contains(lowerQuery) ?? false);
|
(forex['currency']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||||
|
(forex['perdiem_amount']?.toLowerCase().contains(lowerQuery) ??
|
||||||
|
false);
|
||||||
}).toList();
|
}).toList();
|
||||||
});
|
});
|
||||||
print("filteredPlans: $filteredForex");
|
print("filteredPlans: $filteredForex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void filterForex(String query) {
|
||||||
|
print("allForex before filtering: $query");
|
||||||
|
final lowerQuery = query.toLowerCase();
|
||||||
|
setState(() {
|
||||||
|
filteredForex = allForex.where((forex) {
|
||||||
|
final isActiveStatus =
|
||||||
|
forex['is_active'] == "1" ? "active" : "inactive";
|
||||||
|
return (forex['country_code']?.toLowerCase().contains(lowerQuery) ??
|
||||||
|
false) ||
|
||||||
|
(forex['country_name']?.toLowerCase().contains(lowerQuery) ??
|
||||||
|
false) ||
|
||||||
|
(forex['currency']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||||
|
(forex['perdiem_amount']?.toLowerCase().contains(lowerQuery) ??
|
||||||
|
false) ||
|
||||||
|
(isActiveStatus.contains(lowerQuery));
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
print("filteredForex: $filteredForex");
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||||
@ -360,7 +398,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Forex Details',
|
'Perdiem Amount Details',
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: isDesktop ? 16 : 14,
|
fontSize: isDesktop ? 16 : 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@ -371,7 +409,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
),
|
),
|
||||||
if (isDesktop)
|
if (isDesktop)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.22,
|
width: MediaQuery.of(context).size.width * 0.16,
|
||||||
),
|
),
|
||||||
|
|
||||||
if (isDesktop)
|
if (isDesktop)
|
||||||
@ -380,7 +418,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
height: 40,
|
height: 40,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: searchController,
|
controller: searchController,
|
||||||
onChanged: filterUsers,
|
onChanged: filterForex,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Search ...",
|
hintText: "Search ...",
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
@ -431,11 +469,8 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ForexData(
|
builder: (context) => ForexData(
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
// planId: plan.planId,
|
|
||||||
// planId: plan
|
|
||||||
// .planId
|
|
||||||
// .toString(),
|
|
||||||
layoutColor: layoutColor!,
|
layoutColor: layoutColor!,
|
||||||
|
fetchGetForex: refreshData,
|
||||||
// role:
|
// role:
|
||||||
// "Travel Agent"
|
// "Travel Agent"
|
||||||
),
|
),
|
||||||
@ -446,7 +481,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
MainAxisSize.min, // Ensures content fits nicely
|
MainAxisSize.min, // Ensures content fits nicely
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"Add Forex",
|
"Add Perdiem",
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: isDesktop ? 13 : 11,
|
fontSize: isDesktop ? 13 : 11,
|
||||||
),
|
),
|
||||||
@ -477,7 +512,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
height: 35,
|
height: 35,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: searchController,
|
controller: searchController,
|
||||||
onChanged: filterUsers,
|
onChanged: filterForex,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Search ...",
|
hintText: "Search ...",
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
@ -537,7 +572,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
// ),
|
// ),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
"No Plans Available For This User",
|
"No Perdiem Available ",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
@ -546,7 +581,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
"Please Create Plan",
|
"Please Create Perdiem Amount",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 16, color: Colors.grey),
|
fontSize: 16, color: Colors.grey),
|
||||||
@ -558,10 +593,10 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<dynamic> users =
|
List<dynamic> forex =
|
||||||
filteredForex.isNotEmpty ? filteredForex : allForex;
|
filteredForex.isNotEmpty ? filteredForex : allForex;
|
||||||
|
|
||||||
users.sort((a, b) {
|
forex.sort((a, b) {
|
||||||
DateTime dateA = DateTime.parse(a['created_on']);
|
DateTime dateA = DateTime.parse(a['created_on']);
|
||||||
DateTime dateB = DateTime.parse(b['created_on']);
|
DateTime dateB = DateTime.parse(b['created_on']);
|
||||||
|
|
||||||
@ -569,7 +604,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
.compareTo(dateA); // Descending: newest first
|
.compareTo(dateA); // Descending: newest first
|
||||||
});
|
});
|
||||||
|
|
||||||
List paginatedUser = users
|
List paginatedForex = forex
|
||||||
.skip(currentPage * itemsPerPage)
|
.skip(currentPage * itemsPerPage)
|
||||||
.take(itemsPerPage)
|
.take(itemsPerPage)
|
||||||
.toList();
|
.toList();
|
||||||
@ -617,6 +652,13 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w600),
|
||||||
)),
|
)),
|
||||||
|
DataColumn(
|
||||||
|
label: Text(
|
||||||
|
'Status',
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
)),
|
||||||
DataColumn(
|
DataColumn(
|
||||||
label: Text(
|
label: Text(
|
||||||
'Actions',
|
'Actions',
|
||||||
@ -625,7 +667,7 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w600),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
rows: paginatedUser.map((forex) {
|
rows: paginatedForex.map((forex) {
|
||||||
String forexId = forex['forex_perdiem_id']
|
String forexId = forex['forex_perdiem_id']
|
||||||
.toString(); // Get user ID
|
.toString(); // Get user ID
|
||||||
bool isSelected = selectedUserId == forexId;
|
bool isSelected = selectedUserId == forexId;
|
||||||
@ -658,186 +700,64 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
softWrap: true,
|
softWrap: true,
|
||||||
overflow: TextOverflow.ellipsis)),
|
overflow: TextOverflow.ellipsis)),
|
||||||
DataCell(
|
DataCell(
|
||||||
UserActionsMenu(
|
Text(
|
||||||
user: forex,
|
forex['is_active'] == "1"
|
||||||
getUserDetails: (id) =>
|
? 'Active'
|
||||||
apiService.getSingleUser(id),
|
: 'Inactive',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: "Inter",
|
||||||
|
// color: forex['is_active'] == "1"
|
||||||
|
// ? Colors.green
|
||||||
|
// : Colors.grey,
|
||||||
),
|
),
|
||||||
// PopupMenuButton<int>(
|
softWrap: true,
|
||||||
// color: Colors.white,
|
overflow: TextOverflow.ellipsis,
|
||||||
// padding: EdgeInsets.zero,
|
),
|
||||||
// offset: Offset(0, 30),
|
),
|
||||||
// icon: Icon(
|
DataCell(
|
||||||
// Icons.more_vert,
|
// UserActionsMenu(
|
||||||
// color: Color(0xFF475569),
|
// user: forex,
|
||||||
// size: 14,
|
// getUserDetails: (id) =>
|
||||||
|
// apiService.getSingleUser(id),
|
||||||
// ),
|
// ),
|
||||||
// itemBuilder: (context) => [
|
GestureDetector(
|
||||||
// CustomPopupMenuEntry(
|
child: Image.asset(
|
||||||
// child: Container(
|
'assets/images/IconsImg/edit.png',
|
||||||
// padding: EdgeInsets.symmetric(
|
width: 20,
|
||||||
// horizontal: 8, vertical: 8),
|
height: 15),
|
||||||
// child: Row(
|
onTap: () async {
|
||||||
// mainAxisSize: MainAxisSize.min,
|
// final userId = getUserId(user['user_id']);
|
||||||
// mainAxisAlignment:
|
// final usersData = await getUserDetails(userId);
|
||||||
// MainAxisAlignment.center,
|
|
||||||
// children: [
|
|
||||||
// IconButton(
|
|
||||||
// icon: Icon(
|
|
||||||
// Icons.remove_red_eye,
|
|
||||||
// color:
|
|
||||||
// Color(0xFF475569),
|
|
||||||
// size: 18),
|
|
||||||
// onPressed: () async {
|
|
||||||
// print(
|
|
||||||
// "USerDAta1 - $user");
|
|
||||||
// // Fetch the user data properly with await
|
|
||||||
// Map<String, dynamic>
|
|
||||||
// usersData =
|
|
||||||
// await apiService
|
|
||||||
// .getSingleUser(user[
|
|
||||||
// 'user_id']
|
|
||||||
// is String
|
|
||||||
// ? int.parse(user[
|
|
||||||
// 'user_id'])
|
|
||||||
// : user[
|
|
||||||
// 'user_id']);
|
|
||||||
//
|
//
|
||||||
// print(
|
final forexId = int.tryParse(
|
||||||
// "USerDAta2 - $usersData");
|
forex['forex_perdiem_id']
|
||||||
//
|
.toString());
|
||||||
// // userSingleData =
|
|
||||||
// // await apiService
|
if (forexId != null) {
|
||||||
// // .getSingleUser(user[
|
print("ForexId -- $forexId");
|
||||||
// // 'user_id']);
|
final data = await apiService
|
||||||
//
|
.getForexDetailsFind(forexId);
|
||||||
// context.go(
|
print("ForexId -- $data");
|
||||||
// "/CreateUserDetails",
|
|
||||||
// extra: {
|
showDialog(
|
||||||
// "selectedUser":
|
context: context,
|
||||||
// usersData,
|
builder: (context) => ForexData(
|
||||||
// "isViewMode": true
|
isDesktop: isDesktop,
|
||||||
// },
|
forexId: forexId, // Pass the ID
|
||||||
// );
|
forexData: data,
|
||||||
// }),
|
layoutColor: layoutColor!,
|
||||||
// IconButton(
|
// fetchGetForex: fetchGetForex,
|
||||||
// icon: Image.asset(
|
fetchGetForex: refreshData,
|
||||||
// 'assets/images/IconsImg/edit.png',
|
// role:
|
||||||
// width: 20,
|
// "Travel Agent"
|
||||||
// height: 15),
|
),
|
||||||
// onPressed: () async {
|
);
|
||||||
// // Fetch the user data properly with await
|
} else {
|
||||||
// Map<String, dynamic>
|
print("Invalid Forex ID");
|
||||||
// usersData =
|
}
|
||||||
// await apiService
|
},
|
||||||
// .getSingleUser(user[
|
),
|
||||||
// 'user_id']
|
|
||||||
// is String
|
|
||||||
// ? int.parse(user[
|
|
||||||
// 'user_id'])
|
|
||||||
// : user[
|
|
||||||
// 'user_id']);
|
|
||||||
//
|
|
||||||
// print(
|
|
||||||
// "USerDAta2 - $usersData");
|
|
||||||
// context.go(
|
|
||||||
// "/CreateUserDetails",
|
|
||||||
// extra: {
|
|
||||||
// "selectedUser":
|
|
||||||
// usersData,
|
|
||||||
// "isViewMode": false
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// Row(
|
|
||||||
// children: [
|
|
||||||
// MouseRegion(
|
|
||||||
// cursor: user['is_active'] == "0"
|
|
||||||
// ? SystemMouseCursors.forbidden
|
|
||||||
// : SystemMouseCursors.click,
|
|
||||||
// child: IconButton(
|
|
||||||
// icon: Icon(Icons.remove_red_eye,
|
|
||||||
// size: 18,
|
|
||||||
// color: user['is_active'] == "0"
|
|
||||||
// ? Colors.grey
|
|
||||||
// : Color(0xFF475569)),
|
|
||||||
// onPressed: user['is_active'] == "0"
|
|
||||||
// ? null
|
|
||||||
// : () {
|
|
||||||
// context.go(
|
|
||||||
// "/CreateUserDetails",
|
|
||||||
// extra: {
|
|
||||||
// "selectedUser": user,
|
|
||||||
// "isViewMode": true
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
//
|
|
||||||
// MouseRegion(
|
|
||||||
// cursor: user['is_active'] == "0"
|
|
||||||
// ? SystemMouseCursors.forbidden
|
|
||||||
// : SystemMouseCursors.click,
|
|
||||||
// child: GestureDetector(
|
|
||||||
// onTap: user['is_active'] == "0"
|
|
||||||
// ? null
|
|
||||||
// : () {
|
|
||||||
//
|
|
||||||
// },
|
|
||||||
// child: Image.asset(
|
|
||||||
// 'assets/images/IconsImg/edit.png',
|
|
||||||
// width: 20,
|
|
||||||
// height: 15),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
//
|
|
||||||
// // MouseRegion(
|
|
||||||
// // cursor: user['is_active'] == "0"
|
|
||||||
// // ? SystemMouseCursors
|
|
||||||
// // .forbidden
|
|
||||||
// // : SystemMouseCursors.click,
|
|
||||||
// // child: IconButton(
|
|
||||||
// // icon: Icon(Icons.edit,
|
|
||||||
// // color:
|
|
||||||
// // user['is_active'] ==
|
|
||||||
// // "0"
|
|
||||||
// // ? Colors.grey
|
|
||||||
// // : Colors.green),
|
|
||||||
// // onPressed:
|
|
||||||
// // user['is_active'] == "0"
|
|
||||||
// // ? null
|
|
||||||
// // : () {
|
|
||||||
// // print(
|
|
||||||
// // "USER: $user");
|
|
||||||
// //
|
|
||||||
// // // final userJson = jsonEncode(
|
|
||||||
// // // user); // Convert user map to string
|
|
||||||
// // // final encodedUser =
|
|
||||||
// // // Uri.encodeComponent(
|
|
||||||
// // // userJson);
|
|
||||||
// //
|
|
||||||
// // context.go(
|
|
||||||
// // "/CreateUserDetails",
|
|
||||||
// // extra: {
|
|
||||||
// // "selectedUser":
|
|
||||||
// // user,
|
|
||||||
// // "isViewMode":
|
|
||||||
// // false
|
|
||||||
// // },
|
|
||||||
// // );
|
|
||||||
// // },
|
|
||||||
// // ),
|
|
||||||
// // ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@ -877,10 +797,43 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
fontWeight: FontWeight.w700),
|
fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
|
|
||||||
UserActionsMenu(
|
GestureDetector(
|
||||||
user: forex,
|
child: Image.asset(
|
||||||
getUserDetails: (id) =>
|
'assets/images/IconsImg/edit.png',
|
||||||
apiService.getSingleUser(id),
|
width: 20,
|
||||||
|
height: 15),
|
||||||
|
onTap: () async {
|
||||||
|
// final userId = getUserId(user['user_id']);
|
||||||
|
// final usersData = await getUserDetails(userId);
|
||||||
|
//
|
||||||
|
final forexId = int.tryParse(
|
||||||
|
forex['forex_perdiem_id']
|
||||||
|
.toString());
|
||||||
|
|
||||||
|
if (forexId != null) {
|
||||||
|
print("ForexId -- $forexId");
|
||||||
|
final data = await apiService
|
||||||
|
.getForexDetailsFind(forexId);
|
||||||
|
print("ForexId -- $data");
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ForexData(
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
forexId:
|
||||||
|
forexId, // Pass the ID
|
||||||
|
forexData: data,
|
||||||
|
layoutColor: layoutColor!,
|
||||||
|
// fetchGetForex: fetchGetForex,
|
||||||
|
fetchGetForex: refreshData,
|
||||||
|
// role:
|
||||||
|
// "Travel Agent"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
print("Invalid Forex ID");
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
// PopupMenuButton<int>(
|
// PopupMenuButton<int>(
|
||||||
// color: Colors.white,
|
// color: Colors.white,
|
||||||
@ -1027,16 +980,44 @@ class ForexDataListState extends State<ForexDataList> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: isDesktop
|
child: isDesktop
|
||||||
? SingleChildScrollView(
|
? (searchController.text.isNotEmpty &&
|
||||||
scrollDirection: Axis.vertical,
|
filteredForex.isEmpty
|
||||||
child: table, // <-- your existing table
|
? Center(
|
||||||
)
|
child: Text(
|
||||||
: buildMobileCardView(paginatedUser),
|
"No matches found",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: table,
|
||||||
|
))
|
||||||
|
: (searchController.text.isNotEmpty &&
|
||||||
|
filteredForex.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
"No matches found",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: buildMobileCardView(paginatedForex)),
|
||||||
|
),
|
||||||
|
// Expanded(
|
||||||
|
// child: isDesktop
|
||||||
|
// ? SingleChildScrollView(
|
||||||
|
// scrollDirection: Axis.vertical,
|
||||||
|
// child: table, // <-- your existing table
|
||||||
|
// )
|
||||||
|
// : buildMobileCardView(paginatedUser),
|
||||||
|
// ),
|
||||||
PaginationControls(
|
PaginationControls(
|
||||||
currentPage: currentPage,
|
currentPage: currentPage,
|
||||||
itemsPerPage: itemsPerPage,
|
itemsPerPage: itemsPerPage,
|
||||||
totalItems: users.length,
|
totalItems: forex.length,
|
||||||
activeColor: layoutColor, // your theme color
|
activeColor: layoutColor, // your theme color
|
||||||
onPageChanged: (page) {
|
onPageChanged: (page) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
4
lib/Screens/myTemplates/assets.dart
Normal file
4
lib/Screens/myTemplates/assets.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
const kScreenshot1 = 'assets/images/screenshot_1.png';
|
||||||
|
const kScreenshot2 = 'assets/images/screenshot_2.png';
|
||||||
|
const kScreenshot3 = 'assets/images/screenshot_3.png';
|
||||||
|
const kScreenshot4 = 'assets/images/screenshot_4.png';
|
||||||
105
lib/Screens/myTemplates/custom_toolbar.dart
Normal file
105
lib/Screens/myTemplates/custom_toolbar.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||||
|
|
||||||
|
/// Custom toolbar that uses the buttons of [`flutter_quill`](https://pub.dev/packages/flutter_quill).
|
||||||
|
///
|
||||||
|
/// See also: [Custom toolbar](https://github.com/singerdmx/flutter-quill/blob/master/doc/custom_toolbar.md).
|
||||||
|
class CustomToolbar extends StatelessWidget {
|
||||||
|
const CustomToolbar({super.key, required this.controller});
|
||||||
|
|
||||||
|
final QuillController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Wrap(
|
||||||
|
children: [
|
||||||
|
QuillToolbarHistoryButton(
|
||||||
|
isUndo: true,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
QuillToolbarHistoryButton(
|
||||||
|
isUndo: false,
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
QuillToolbarToggleStyleButton(
|
||||||
|
options: const QuillToolbarToggleStyleButtonOptions(),
|
||||||
|
controller: controller,
|
||||||
|
attribute: Attribute.bold,
|
||||||
|
),
|
||||||
|
QuillToolbarToggleStyleButton(
|
||||||
|
options: const QuillToolbarToggleStyleButtonOptions(),
|
||||||
|
controller: controller,
|
||||||
|
attribute: Attribute.italic,
|
||||||
|
),
|
||||||
|
QuillToolbarToggleStyleButton(
|
||||||
|
controller: controller,
|
||||||
|
attribute: Attribute.underline,
|
||||||
|
),
|
||||||
|
QuillToolbarClearFormatButton(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
QuillToolbarImageButton(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
QuillToolbarCameraButton(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
QuillToolbarVideoButton(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
QuillToolbarColorButton(
|
||||||
|
controller: controller,
|
||||||
|
isBackground: false,
|
||||||
|
),
|
||||||
|
QuillToolbarColorButton(
|
||||||
|
controller: controller,
|
||||||
|
isBackground: true,
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
QuillToolbarSelectHeaderStyleDropdownButton(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
QuillToolbarSelectLineHeightStyleDropdownButton(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
QuillToolbarToggleCheckListButton(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
QuillToolbarToggleStyleButton(
|
||||||
|
controller: controller,
|
||||||
|
attribute: Attribute.ol,
|
||||||
|
),
|
||||||
|
QuillToolbarToggleStyleButton(
|
||||||
|
controller: controller,
|
||||||
|
attribute: Attribute.ul,
|
||||||
|
),
|
||||||
|
QuillToolbarToggleStyleButton(
|
||||||
|
controller: controller,
|
||||||
|
attribute: Attribute.inlineCode,
|
||||||
|
),
|
||||||
|
QuillToolbarToggleStyleButton(
|
||||||
|
controller: controller,
|
||||||
|
attribute: Attribute.blockQuote,
|
||||||
|
),
|
||||||
|
QuillToolbarIndentButton(
|
||||||
|
controller: controller,
|
||||||
|
isIncrease: true,
|
||||||
|
),
|
||||||
|
QuillToolbarIndentButton(
|
||||||
|
controller: controller,
|
||||||
|
isIncrease: false,
|
||||||
|
),
|
||||||
|
const VerticalDivider(),
|
||||||
|
QuillToolbarLinkStyleButton(controller: controller),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/Screens/myTemplates/flutter_quill_extensions.dart
Normal file
21
lib/Screens/myTemplates/flutter_quill_extensions.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
library;
|
||||||
|
|
||||||
|
export 'src/common/extensions/controller_ext.dart';
|
||||||
|
export 'src/editor/image/config/image_config.dart';
|
||||||
|
export 'src/editor/image/config/image_web_config.dart';
|
||||||
|
export 'src/editor/image/image_embed.dart';
|
||||||
|
export 'src/editor/image/image_embed_types.dart';
|
||||||
|
export 'src/editor/image/image_web_embed.dart';
|
||||||
|
export 'src/editor/video/config/video_config.dart';
|
||||||
|
export 'src/editor/video/config/video_web_config.dart';
|
||||||
|
export 'src/editor/video/video_embed.dart';
|
||||||
|
export 'src/editor/video/video_web_embed.dart';
|
||||||
|
export 'src/flutter_quill_embeds.dart';
|
||||||
|
export 'src/toolbar/camera/camera_button.dart';
|
||||||
|
export 'src/toolbar/camera/camera_types.dart';
|
||||||
|
export 'src/toolbar/camera/config/camera_config.dart';
|
||||||
|
export 'src/toolbar/image/config/image_config.dart';
|
||||||
|
export 'src/toolbar/image/image_button.dart';
|
||||||
|
export 'src/toolbar/video/config/video.dart';
|
||||||
|
export 'src/toolbar/video/config/video_config.dart';
|
||||||
|
export 'src/toolbar/video/video_button.dart';
|
||||||
295
lib/Screens/myTemplates/quill_delta_sample.dart
Normal file
295
lib/Screens/myTemplates/quill_delta_sample.dart
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import 'assets.dart';
|
||||||
|
|
||||||
|
const kQuillDefaultSample = [
|
||||||
|
{
|
||||||
|
'insert': {'image': kScreenshot2},
|
||||||
|
'attributes': {
|
||||||
|
'width': '100',
|
||||||
|
'height': '100',
|
||||||
|
'style': 'width:500px; height:350px;'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{'insert': 'Flutter Quill'},
|
||||||
|
{
|
||||||
|
'attributes': {'header': 1},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'insert': {
|
||||||
|
'video':
|
||||||
|
'https://www.youtube.com/watch?v=V4hgdKhIqtc&list=PLbhaS_83B97s78HsDTtplRTEhcFsqSqIK&index=1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'insert': {
|
||||||
|
'video':
|
||||||
|
'https://user-images.githubusercontent.com/122956/126238875-22e42501-ad41-4266-b1d6-3f89b5e3b79b.mp4'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{'insert': '\nRich text editor for Flutter'},
|
||||||
|
{
|
||||||
|
'attributes': {'header': 2},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Quill component for Flutter'},
|
||||||
|
{
|
||||||
|
'attributes': {'header': 3},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'link': 'https://bulletjournal.us/home/index.html'},
|
||||||
|
'insert': 'Bullet Journal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'insert':
|
||||||
|
':\nTrack personal and group journals (ToDo, Note, Ledger) from multiple views with timely reminders'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'insert':
|
||||||
|
'Share your tasks and notes with teammates, and see changes as they happen in real-time, across all devices'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Check out what you and your teammates are working on each day'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': '\nSplitting bills with friends can never be easier.'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Start creating a group and invite your friends to join.'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Create a BuJo of Ledger type to see expense or balance summary.'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'insert':
|
||||||
|
'\nAttach one or multiple labels to tasks, notes or transactions. Later you can track them just using the label(s).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'blockquote': true},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': "\nvar BuJo = 'Bullet' + 'Journal'"},
|
||||||
|
{
|
||||||
|
'attributes': {'code-block': true},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': '\nStart tracking in your browser'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Stop the timer on your phone'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'All your time entries are synced'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 2},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'between the phone apps'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 2},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'and the website.'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 3},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': '\n'},
|
||||||
|
{'insert': '\nCenter Align'},
|
||||||
|
{
|
||||||
|
'attributes': {'align': 'center'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Right Align'},
|
||||||
|
{
|
||||||
|
'attributes': {'align': 'right'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Justify Align'},
|
||||||
|
{
|
||||||
|
'attributes': {'align': 'justify'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Have trouble finding things? '},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Just type in the search bar'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1, 'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'and easily find contents'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 2, 'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'across projects or folders.'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 2, 'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'It matches text in your note or task.'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1, 'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Enable reminders so that you will get notified by'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'email'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1, 'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'message on your phone'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1, 'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'popup on the web site'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1, 'list': 'ordered'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Create a BuJo serving as project or folder'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Organize your'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1, 'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'tasks'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 2, 'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'notes'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 2, 'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'transactions'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 2, 'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'under BuJo '},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 3, 'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'See them in Calendar'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'or hierarchical view'},
|
||||||
|
{
|
||||||
|
'attributes': {'indent': 1, 'list': 'bullet'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'this is a check list'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'checked'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'this is a uncheck list'},
|
||||||
|
{
|
||||||
|
'attributes': {'list': 'unchecked'},
|
||||||
|
'insert': '\n'
|
||||||
|
},
|
||||||
|
{'insert': 'Font '},
|
||||||
|
{
|
||||||
|
'attributes': {'font': 'sans-serif'},
|
||||||
|
'insert': 'Sans Serif'
|
||||||
|
},
|
||||||
|
{'insert': ' '},
|
||||||
|
{
|
||||||
|
'attributes': {'font': 'serif'},
|
||||||
|
'insert': 'Serif'
|
||||||
|
},
|
||||||
|
{'insert': ' '},
|
||||||
|
{
|
||||||
|
'attributes': {'font': 'monospace'},
|
||||||
|
'insert': 'Monospace'
|
||||||
|
},
|
||||||
|
{'insert': ' Size '},
|
||||||
|
{
|
||||||
|
'attributes': {'size': 'small'},
|
||||||
|
'insert': 'Small'
|
||||||
|
},
|
||||||
|
{'insert': ' '},
|
||||||
|
{
|
||||||
|
'attributes': {'size': 'large'},
|
||||||
|
'insert': 'Large'
|
||||||
|
},
|
||||||
|
{'insert': ' '},
|
||||||
|
{
|
||||||
|
'attributes': {'size': 'huge'},
|
||||||
|
'insert': 'Huge'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'size': '15.0'},
|
||||||
|
'insert': 'font size 15'
|
||||||
|
},
|
||||||
|
{'insert': ' '},
|
||||||
|
{
|
||||||
|
'attributes': {'size': '35'},
|
||||||
|
'insert': 'font size 35'
|
||||||
|
},
|
||||||
|
{'insert': ' '},
|
||||||
|
{
|
||||||
|
'attributes': {'size': '20'},
|
||||||
|
'insert': 'font size 20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'token': 'built_in'},
|
||||||
|
'insert': ' diff'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'token': 'operator'},
|
||||||
|
'insert': '-match'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'attributes': {'token': 'literal'},
|
||||||
|
'insert': '-patch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'insert': {
|
||||||
|
'image':
|
||||||
|
'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'
|
||||||
|
},
|
||||||
|
'attributes': {
|
||||||
|
'width': '230',
|
||||||
|
'style': 'display: block; margin: auto; width: 500px;'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{'insert': '\n'}
|
||||||
|
];
|
||||||
30
lib/Screens/myTemplates/src/common/default_image_insert.dart
Normal file
30
lib/Screens/myTemplates/src/common/default_image_insert.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import '../editor/image/image_embed_types.dart';
|
||||||
|
import 'extensions/controller_ext.dart';
|
||||||
|
|
||||||
|
OnImageInsertCallback _defaultOnImageInsert() {
|
||||||
|
return (imageUrl, controller) async {
|
||||||
|
controller
|
||||||
|
..skipRequestKeyboard = true
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
..insertImageBlock(imageSource: imageUrl);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@internal
|
||||||
|
Future<void> handleImageInsert(
|
||||||
|
String imageUrl, {
|
||||||
|
required QuillController controller,
|
||||||
|
required OnImageInsertCallback? onImageInsertCallback,
|
||||||
|
required OnImageInsertedCallback? onImageInsertedCallback,
|
||||||
|
}) async {
|
||||||
|
final customOnImageInsert = onImageInsertCallback;
|
||||||
|
if (customOnImageInsert != null) {
|
||||||
|
await customOnImageInsert.call(imageUrl, controller);
|
||||||
|
} else {
|
||||||
|
await _defaultOnImageInsert().call(imageUrl, controller);
|
||||||
|
}
|
||||||
|
await onImageInsertedCallback?.call(imageUrl);
|
||||||
|
}
|
||||||
30
lib/Screens/myTemplates/src/common/default_video_insert.dart
Normal file
30
lib/Screens/myTemplates/src/common/default_video_insert.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import '../toolbar/video/config/video.dart';
|
||||||
|
import 'extensions/controller_ext.dart';
|
||||||
|
|
||||||
|
OnVideoInsertCallback _defaultOnVideoInsert() {
|
||||||
|
return (imageUrl, controller) async {
|
||||||
|
controller
|
||||||
|
..skipRequestKeyboard = true
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
..insertVideoBlock(videoUrl: imageUrl);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@internal
|
||||||
|
Future<void> handleVideoInsert(
|
||||||
|
String videoUrl, {
|
||||||
|
required QuillController controller,
|
||||||
|
required OnVideoInsertCallback? onVideoInsertCallback,
|
||||||
|
required OnVideoInsertedCallback? onVideoInsertedCallback,
|
||||||
|
}) async {
|
||||||
|
final customOnVideoInsert = onVideoInsertCallback;
|
||||||
|
if (customOnVideoInsert != null) {
|
||||||
|
await customOnVideoInsert.call(videoUrl, controller);
|
||||||
|
} else {
|
||||||
|
await _defaultOnVideoInsert().call(videoUrl, controller);
|
||||||
|
}
|
||||||
|
await onVideoInsertedCallback?.call(videoUrl);
|
||||||
|
}
|
||||||
12
lib/Screens/myTemplates/src/common/extensions/attribute.dart
Normal file
12
lib/Screens/myTemplates/src/common/extensions/attribute.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart'
|
||||||
|
show Attribute, AttributeScope;
|
||||||
|
|
||||||
|
class FlutterAlignmentAttribute extends Attribute<String?> {
|
||||||
|
const FlutterAlignmentAttribute(String? val)
|
||||||
|
: super('flutterAlignment', AttributeScope.ignore, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AttributeExt on Attribute {
|
||||||
|
static const FlutterAlignmentAttribute flutterAlignment =
|
||||||
|
FlutterAlignmentAttribute(null);
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
|
@Deprecated('Invalid extension')
|
||||||
|
extension QuillControllerExt on QuillController {
|
||||||
|
@Deprecated(
|
||||||
|
'Invalid extension property and will be removed, use selection.baseOffset instead')
|
||||||
|
int get index => selection.baseOffset;
|
||||||
|
@Deprecated(
|
||||||
|
'Invalid extension property and will be removed, use selection.extentOffset - selection.baseOffset instead')
|
||||||
|
int get length => selection.extentOffset - index;
|
||||||
|
|
||||||
|
@Deprecated('Invalid extension method and will be removed.')
|
||||||
|
void insertImageBlock({
|
||||||
|
required String imageSource,
|
||||||
|
}) {
|
||||||
|
this
|
||||||
|
..skipRequestKeyboard = true
|
||||||
|
..replaceText(
|
||||||
|
index,
|
||||||
|
length,
|
||||||
|
BlockEmbed.image(imageSource),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
..moveCursorToPosition(index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Invalid extension method and will be removed.')
|
||||||
|
void insertVideoBlock({
|
||||||
|
required String videoUrl,
|
||||||
|
}) {
|
||||||
|
this
|
||||||
|
..skipRequestKeyboard = true
|
||||||
|
..replaceText(index, length, BlockEmbed.video(videoUrl), null)
|
||||||
|
..moveCursorToPosition(index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
lib/Screens/myTemplates/src/common/image_video_utils.dart
Normal file
122
lib/Screens/myTemplates/src/common/image_video_utils.dart
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart' show QuillDialogTheme;
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
import 'utils/patterns.dart';
|
||||||
|
|
||||||
|
enum LinkType {
|
||||||
|
video,
|
||||||
|
image,
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeLinkDialog extends StatefulWidget {
|
||||||
|
const TypeLinkDialog({
|
||||||
|
required this.linkType,
|
||||||
|
this.dialogTheme,
|
||||||
|
this.link,
|
||||||
|
this.linkRegExp,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final QuillDialogTheme? dialogTheme;
|
||||||
|
final String? link;
|
||||||
|
final RegExp? linkRegExp;
|
||||||
|
final LinkType linkType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TypeLinkDialogState createState() => TypeLinkDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeLinkDialogState extends State<TypeLinkDialog> {
|
||||||
|
late String _link;
|
||||||
|
late TextEditingController _controller;
|
||||||
|
RegExp? _linkRegExp;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_link = widget.link ?? '';
|
||||||
|
_controller = TextEditingController(text: _link);
|
||||||
|
|
||||||
|
_linkRegExp = widget.linkRegExp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
|
||||||
|
content: TextField(
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
maxLines: null,
|
||||||
|
style: widget.dialogTheme?.inputTextStyle,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: context.loc.pasteLink,
|
||||||
|
hintText: widget.linkType == LinkType.image
|
||||||
|
? context.loc.pleaseEnterAValidImageURL
|
||||||
|
: context.loc.pleaseEnterAValidVideoURL,
|
||||||
|
labelStyle: widget.dialogTheme?.labelTextStyle,
|
||||||
|
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
|
||||||
|
),
|
||||||
|
autofocus: true,
|
||||||
|
onChanged: _linkChanged,
|
||||||
|
controller: _controller,
|
||||||
|
onEditingComplete: () {
|
||||||
|
if (!_canPress()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_applyLink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _canPress() ? _applyLink : null,
|
||||||
|
child: Text(
|
||||||
|
context.loc.ok,
|
||||||
|
style: widget.dialogTheme?.labelTextStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _linkChanged(String value) {
|
||||||
|
setState(() {
|
||||||
|
_link = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyLink() {
|
||||||
|
Navigator.pop(context, _link.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
RegExp get linkRegExp {
|
||||||
|
final customRegExp = _linkRegExp;
|
||||||
|
if (customRegExp != null) {
|
||||||
|
return customRegExp;
|
||||||
|
}
|
||||||
|
switch (widget.linkType) {
|
||||||
|
case LinkType.video:
|
||||||
|
if (youtubeRegExp.hasMatch(_link)) {
|
||||||
|
return youtubeRegExp;
|
||||||
|
}
|
||||||
|
return videoRegExp;
|
||||||
|
case LinkType.image:
|
||||||
|
return imageRegExp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _canPress() {
|
||||||
|
if (_link.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (widget.linkType == LinkType.image) {}
|
||||||
|
return _link.isNotEmpty && linkRegExp.hasMatch(_link);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
// import 'package:universal_html/html.dart' as html;
|
||||||
|
|
||||||
|
// Fake interface for the logic that this package needs from (web-only) dart:ui.
|
||||||
|
// This is conditionally exported so the analyzer sees these methods as
|
||||||
|
// available.
|
||||||
|
|
||||||
|
// typedef PlatroformViewFactory = html.Element Function(int viewId);
|
||||||
|
|
||||||
|
// /// Shim for web_ui engine.PlatformViewRegistry
|
||||||
|
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62
|
||||||
|
// class PlatformViewRegistry {
|
||||||
|
// /// Shim for registerViewFactory
|
||||||
|
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
|
||||||
|
// static dynamic registerViewFactory(
|
||||||
|
// String viewTypeId, PlatroformViewFactory viewFactory) {}
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /// Shim for web_ui engine.AssetManager
|
||||||
|
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12
|
||||||
|
// class WebOnlyAssetManager {
|
||||||
|
// static dynamic getAssetUrl(String asset) {}
|
||||||
|
// }
|
||||||
|
|
||||||
|
class PlatformViewRegistry {
|
||||||
|
/// Register [viewType] as being created by the given [viewFactory].
|
||||||
|
///
|
||||||
|
/// [viewFactory] can be any function that takes an integer and optional
|
||||||
|
/// `params` and returns an `HTMLElement` DOM object.
|
||||||
|
bool registerViewFactory(
|
||||||
|
String viewType,
|
||||||
|
Function viewFactory, {
|
||||||
|
bool isVisible = true,
|
||||||
|
}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the view previously created for [viewId].
|
||||||
|
///
|
||||||
|
/// Throws if no view has been created for [viewId].
|
||||||
|
Object getViewById(int viewId) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export 'dart:ui' if (dart.library.js_interop) 'dart:ui_web';
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:flutter/widgets.dart' show BuildContext, MediaQuery;
|
||||||
|
|
||||||
|
Map<String, String> parseCssString(String cssString) {
|
||||||
|
final result = <String, String>{};
|
||||||
|
final declarations = cssString.split(';');
|
||||||
|
|
||||||
|
for (final declaration in declarations) {
|
||||||
|
final parts = declaration.split(':');
|
||||||
|
if (parts.length == 2) {
|
||||||
|
final property = parts[0].trim();
|
||||||
|
final value = parts[1].trim();
|
||||||
|
result[property] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _CssUnit {
|
||||||
|
px('px'),
|
||||||
|
percentage('%'),
|
||||||
|
viewportWidth('vw'),
|
||||||
|
viewportHeight('vh'),
|
||||||
|
em('em'),
|
||||||
|
rem('rem'),
|
||||||
|
invalid('invalid');
|
||||||
|
|
||||||
|
const _CssUnit(this.cssName);
|
||||||
|
|
||||||
|
final String cssName;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? parseCssPropertyAsDouble(
|
||||||
|
String value, {
|
||||||
|
required BuildContext context,
|
||||||
|
}) {
|
||||||
|
if (value.trim().isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse it in case it's a valid double already
|
||||||
|
var doubleValue = double.tryParse(value);
|
||||||
|
|
||||||
|
if (doubleValue != null) {
|
||||||
|
return doubleValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not then if it's a css numberic value then we will try to parse it
|
||||||
|
final unit = _CssUnit.values
|
||||||
|
.where((element) => value.endsWith(element.cssName))
|
||||||
|
.firstOrNull;
|
||||||
|
if (unit == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
value = value.replaceFirst(unit.cssName, '');
|
||||||
|
doubleValue = double.tryParse(value);
|
||||||
|
if (doubleValue != null) {
|
||||||
|
switch (unit) {
|
||||||
|
case _CssUnit.px:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
case _CssUnit.percentage:
|
||||||
|
// Not supported yet
|
||||||
|
doubleValue = null;
|
||||||
|
break;
|
||||||
|
case _CssUnit.viewportWidth:
|
||||||
|
doubleValue = (doubleValue / 100) * MediaQuery.sizeOf(context).width;
|
||||||
|
break;
|
||||||
|
case _CssUnit.viewportHeight:
|
||||||
|
doubleValue = (doubleValue / 100) * MediaQuery.sizeOf(context).height;
|
||||||
|
break;
|
||||||
|
case _CssUnit.em:
|
||||||
|
doubleValue = MediaQuery.textScalerOf(context).scale(doubleValue);
|
||||||
|
break;
|
||||||
|
case _CssUnit.rem:
|
||||||
|
doubleValue = MediaQuery.textScalerOf(context).scale(doubleValue);
|
||||||
|
break;
|
||||||
|
case _CssUnit.invalid:
|
||||||
|
doubleValue = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doubleValue;
|
||||||
|
}
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show immutable;
|
||||||
|
import 'package:flutter/widgets.dart' show Alignment, BuildContext;
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
import 'element_shared_utils.dart';
|
||||||
|
|
||||||
|
/// Theses properties are not officialy supported by quill js
|
||||||
|
/// but they are only used in all platforms other than web
|
||||||
|
/// and they will be stored in css style property so quill js ignore them
|
||||||
|
enum ExtraElementProperties {
|
||||||
|
deletable,
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
ElementSize elementSize,
|
||||||
|
double? margin,
|
||||||
|
Alignment alignment,
|
||||||
|
) getElementAttributes(
|
||||||
|
Node node,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
|
var elementSize = const ElementSize(null, null);
|
||||||
|
var elementAlignment = Alignment.center;
|
||||||
|
double? elementMargin;
|
||||||
|
|
||||||
|
final heightValue = parseCssPropertyAsDouble(
|
||||||
|
node.style.attributes[Attribute.height.key]?.value.toString() ?? '',
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
final widthValue = parseCssPropertyAsDouble(
|
||||||
|
node.style.attributes[Attribute.width.key]?.value.toString() ?? '',
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (heightValue != null) {
|
||||||
|
elementSize = elementSize.copyWith(
|
||||||
|
height: heightValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (widthValue != null) {
|
||||||
|
elementSize = elementSize.copyWith(
|
||||||
|
width: widthValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final cssStyle = node.style.attributes['style'];
|
||||||
|
|
||||||
|
if (cssStyle != null) {
|
||||||
|
// It css value as string but we will try to support it anyway
|
||||||
|
|
||||||
|
final cssAttrs = parseCssString(cssStyle.value.toString());
|
||||||
|
|
||||||
|
final cssHeightValue = parseCssPropertyAsDouble(
|
||||||
|
(cssAttrs[Attribute.height.key]) ?? '',
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
final cssWidthValue = parseCssPropertyAsDouble(
|
||||||
|
(cssAttrs[Attribute.width.key]) ?? '',
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
|
||||||
|
// cssHeightValue != null && elementSize.height == null
|
||||||
|
if (cssHeightValue != null) {
|
||||||
|
elementSize = elementSize.copyWith(height: cssHeightValue);
|
||||||
|
}
|
||||||
|
if (cssWidthValue != null) {
|
||||||
|
elementSize = elementSize.copyWith(width: cssWidthValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
elementAlignment = getAlignment(cssAttrs['alignment']);
|
||||||
|
|
||||||
|
final margin = double.tryParse('margin');
|
||||||
|
if (margin != null) {
|
||||||
|
elementMargin = margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (elementSize, elementMargin, elementAlignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class ElementSize {
|
||||||
|
const ElementSize(
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// If non-null, requires the child to have exactly this width.
|
||||||
|
/// If null, the child is free to choose its own width.
|
||||||
|
final double? width;
|
||||||
|
|
||||||
|
/// If non-null, requires the child to have exactly this height.
|
||||||
|
/// If null, the child is free to choose its own height.
|
||||||
|
final double? height;
|
||||||
|
|
||||||
|
ElementSize copyWith({
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
}) {
|
||||||
|
return ElementSize(
|
||||||
|
width ?? this.width,
|
||||||
|
height ?? this.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
|
||||||
|
|
||||||
|
import 'element_shared_utils.dart';
|
||||||
|
|
||||||
|
/// Prefer the width, and height from the css style attribute if exits
|
||||||
|
/// it can be `auto` or `100px` so it's specific to HTML && CSS
|
||||||
|
/// if not, we will use the one from attributes which is usually just an double
|
||||||
|
(
|
||||||
|
String height,
|
||||||
|
String width,
|
||||||
|
String margin,
|
||||||
|
String alignment,
|
||||||
|
) getWebElementAttributes(
|
||||||
|
Node node,
|
||||||
|
) {
|
||||||
|
var height = 'auto';
|
||||||
|
var width = 'auto';
|
||||||
|
// TODO(): Add support for margin and alignment
|
||||||
|
var margin = 'auto';
|
||||||
|
const alignment = 'center';
|
||||||
|
|
||||||
|
final cssStyle = node.style.attributes['style'];
|
||||||
|
|
||||||
|
final heightValue = node.style.attributes[Attribute.height.key]?.value;
|
||||||
|
final widthValue = node.style.attributes[Attribute.width.key]?.value;
|
||||||
|
|
||||||
|
if (cssStyle != null) {
|
||||||
|
final attrs = parseCssString(cssStyle.value.toString());
|
||||||
|
|
||||||
|
final cssHeightValue = attrs[Attribute.height.key];
|
||||||
|
|
||||||
|
if (cssHeightValue != null) {
|
||||||
|
height = cssHeightValue;
|
||||||
|
} else {
|
||||||
|
height = '${heightValue}px';
|
||||||
|
}
|
||||||
|
final cssWidthValue = attrs[Attribute.width.key];
|
||||||
|
if (cssWidthValue != null) {
|
||||||
|
width = cssWidthValue;
|
||||||
|
} else if (widthValue != null) {
|
||||||
|
width = '${widthValue}px';
|
||||||
|
}
|
||||||
|
|
||||||
|
final cssMarginValue = attrs['margin'];
|
||||||
|
if (cssMarginValue != null) {
|
||||||
|
margin = cssMarginValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (height, width, margin, alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heightValue != null) {
|
||||||
|
height = '${heightValue}px';
|
||||||
|
}
|
||||||
|
if (widthValue != null) {
|
||||||
|
width = '${widthValue}px';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (height, width, margin, alignment);
|
||||||
|
}
|
||||||
17
lib/Screens/myTemplates/src/common/utils/patterns.dart
Normal file
17
lib/Screens/myTemplates/src/common/utils/patterns.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
RegExp base64RegExp = RegExp(
|
||||||
|
r'^(?:[A-Za-z0-9+\/][A-Za-z0-9+\/][A-Za-z0-9+\/][A-Za-z0-9+\/])*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$',
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageRegExp = RegExp(
|
||||||
|
r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final videoRegExp = RegExp(
|
||||||
|
r'\bhttps?://\S+\.(mp4|mov|avi|mkv|flv|wmv|webm)\b',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
|
final youtubeRegExp = RegExp(
|
||||||
|
r'^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]+)(\S+)?$',
|
||||||
|
caseSensitive: false,
|
||||||
|
);
|
||||||
30
lib/Screens/myTemplates/src/common/utils/string.dart
Normal file
30
lib/Screens/myTemplates/src/common/utils/string.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart' show Attribute;
|
||||||
|
|
||||||
|
String replaceStyleStringWithSize(
|
||||||
|
String cssStyle, {
|
||||||
|
required double width,
|
||||||
|
required double height,
|
||||||
|
}) {
|
||||||
|
final result = <String, String>{};
|
||||||
|
final pairs = cssStyle.split(';');
|
||||||
|
for (final pair in pairs) {
|
||||||
|
final index = pair.indexOf(':');
|
||||||
|
if (index < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final key = pair.substring(0, index).trim();
|
||||||
|
result[key] = pair.substring(index + 1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
result[Attribute.width.key] = width.toString();
|
||||||
|
result[Attribute.height.key] = height.toString();
|
||||||
|
final sb = StringBuffer();
|
||||||
|
for (final pair in result.entries) {
|
||||||
|
sb
|
||||||
|
..write(pair.key)
|
||||||
|
..write(': ')
|
||||||
|
..write(pair.value)
|
||||||
|
..write('; ');
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
30
lib/Screens/myTemplates/src/common/utils/utils.dart
Normal file
30
lib/Screens/myTemplates/src/common/utils/utils.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'patterns.dart';
|
||||||
|
|
||||||
|
bool isBase64(String str) {
|
||||||
|
return base64RegExp.hasMatch(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isHttpUrl(String url) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(url.trim());
|
||||||
|
return uri.isScheme('HTTP') || uri.isScheme('HTTPS');
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isImageBase64(String imageUrl) {
|
||||||
|
return !isHttpUrl(imageUrl) && isBase64(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isYouTubeUrl(String videoUrl) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(videoUrl);
|
||||||
|
return uri.host == 'www.youtube.com' ||
|
||||||
|
uri.host == 'youtube.com' ||
|
||||||
|
uri.host == 'youtu.be' ||
|
||||||
|
uri.host == 'www.youtu.be';
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/Screens/myTemplates/src/common/utils/web/web.dart
Normal file
1
lib/Screens/myTemplates/src/common/utils/web/web.dart
Normal file
@ -0,0 +1 @@
|
|||||||
|
export './web_stub.dart' if (dart.library.js_interop) './web_real.dart';
|
||||||
46
lib/Screens/myTemplates/src/common/utils/web/web_real.dart
Normal file
46
lib/Screens/myTemplates/src/common/utils/web/web_real.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:web/web.dart';
|
||||||
|
import '../dart_ui/dart_ui_fake.dart'
|
||||||
|
if (dart.library.js_interop) '../dart_ui/dart_ui_real.dart' as ui;
|
||||||
|
|
||||||
|
void main(List<String> args) {
|
||||||
|
HTMLImageElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
void createHtmlImageElement({
|
||||||
|
required String src,
|
||||||
|
required String height,
|
||||||
|
required String width,
|
||||||
|
required String margin,
|
||||||
|
required String alignSelf,
|
||||||
|
}) {
|
||||||
|
ui.PlatformViewRegistry().registerViewFactory(src, (viewId) {
|
||||||
|
return createHtmlImageElement(
|
||||||
|
src: src,
|
||||||
|
alignSelf: alignSelf,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
margin: margin,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void createHtmlIFrameElement({
|
||||||
|
required String src,
|
||||||
|
required String height,
|
||||||
|
required String width,
|
||||||
|
required String margin,
|
||||||
|
required String alignSelf,
|
||||||
|
}) {
|
||||||
|
ui.PlatformViewRegistry().registerViewFactory(
|
||||||
|
src,
|
||||||
|
(id) {
|
||||||
|
return HTMLIFrameElement()
|
||||||
|
..style.width = width
|
||||||
|
..style.height = height
|
||||||
|
..src = src
|
||||||
|
..style.border = 'none'
|
||||||
|
..style.margin = margin
|
||||||
|
..style.alignSelf = alignSelf;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
19
lib/Screens/myTemplates/src/common/utils/web/web_stub.dart
Normal file
19
lib/Screens/myTemplates/src/common/utils/web/web_stub.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
void createHtmlImageElement({
|
||||||
|
required String src,
|
||||||
|
required String height,
|
||||||
|
required String width,
|
||||||
|
required String margin,
|
||||||
|
required String alignSelf,
|
||||||
|
}) =>
|
||||||
|
throw UnimplementedError(
|
||||||
|
'A stub method is called, createHtmlImageElement is for web platforms only.');
|
||||||
|
|
||||||
|
void createHtmlIFrameElement({
|
||||||
|
required String src,
|
||||||
|
required String height,
|
||||||
|
required String width,
|
||||||
|
required String margin,
|
||||||
|
required String alignSelf,
|
||||||
|
}) =>
|
||||||
|
throw UnimplementedError(
|
||||||
|
'A stub method is called, createHtmlIFrameElement is for web platforms only.');
|
||||||
@ -0,0 +1,165 @@
|
|||||||
|
import 'dart:io' show File;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
import '../image_embed_types.dart';
|
||||||
|
|
||||||
|
/// [QuillEditorImageEmbedConfig] for desktop, mobile and
|
||||||
|
/// other platforms
|
||||||
|
/// excluding web, it's configurations that is needed for the editor
|
||||||
|
///
|
||||||
|
@immutable
|
||||||
|
class QuillEditorImageEmbedConfig {
|
||||||
|
const QuillEditorImageEmbedConfig({
|
||||||
|
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
|
||||||
|
this.shouldRemoveImageCallback,
|
||||||
|
this.imageProviderBuilder,
|
||||||
|
this.imageErrorWidgetBuilder,
|
||||||
|
this.onImageClicked,
|
||||||
|
}) : _onImageRemovedCallback = onImageRemovedCallback;
|
||||||
|
|
||||||
|
/// [onImageRemovedCallback] is called when an image is
|
||||||
|
/// removed from the editor.
|
||||||
|
/// By default, [onImageRemovedCallback] deletes the
|
||||||
|
/// temporary image file if
|
||||||
|
/// the platform is mobile and if it still exists. You
|
||||||
|
/// can customize this behavior
|
||||||
|
/// by passing your own function that handles the removal process.
|
||||||
|
///
|
||||||
|
/// Example of [onImageRemovedCallback] customization:
|
||||||
|
/// ```dart
|
||||||
|
/// afterRemoveImageFromEditor: (imageFile) async {
|
||||||
|
/// // Your custom logic here
|
||||||
|
/// // or leave it empty to do nothing
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Default value if the passed value is null:
|
||||||
|
/// [QuillEditorImageEmbedConfig.defaultOnImageRemovedCallback]
|
||||||
|
///
|
||||||
|
/// so if you want to do nothing make sure to pass a empty callback
|
||||||
|
/// instead of passing null as value
|
||||||
|
final ImageEmbedBuilderOnRemovedCallback? _onImageRemovedCallback;
|
||||||
|
|
||||||
|
ImageEmbedBuilderOnRemovedCallback get onImageRemovedCallback {
|
||||||
|
return _onImageRemovedCallback ??
|
||||||
|
QuillEditorImageEmbedConfig.defaultOnImageRemovedCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [shouldRemoveImageCallback] is a callback
|
||||||
|
/// function that is invoked when the
|
||||||
|
/// user attempts to remove an image from the editor. It allows you to control
|
||||||
|
/// whether the image should be removed based on your custom logic.
|
||||||
|
///
|
||||||
|
/// Example of [shouldRemoveImageCallback] customization:
|
||||||
|
/// ```dart
|
||||||
|
/// shouldRemoveImageFromEditor: (imageFile) async {
|
||||||
|
/// // Show a confirmation dialog before removing the image
|
||||||
|
/// final isShouldRemove = await showYesCancelDialog(
|
||||||
|
/// context: context,
|
||||||
|
/// options: const YesOrCancelDialogOptions(
|
||||||
|
/// title: 'Deleting an image',
|
||||||
|
/// message: 'Are you sure you want' ' to delete this
|
||||||
|
/// image from the editor?',
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// // Return `true` to allow image removal if the user confirms, otherwise
|
||||||
|
/// `false`
|
||||||
|
/// return isShouldRemove;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback;
|
||||||
|
|
||||||
|
/// Allows to override the default handling and fallback to the default if `null` was returned.
|
||||||
|
///
|
||||||
|
/// Example of [imageProviderBuilder] customization:
|
||||||
|
/// ```dart
|
||||||
|
/// imageProviderBuilder: (imageUrl) async {
|
||||||
|
/// if (imageUrl.startsWith('assets/')) {
|
||||||
|
/// // Supports Image assets
|
||||||
|
/// return AssetImage(imageUrl);
|
||||||
|
/// }
|
||||||
|
/// if (imageUrl.startsWith('http')) {
|
||||||
|
/// // Use https://pub.dev/packages/cached_network_image
|
||||||
|
/// // for network images to cache them.
|
||||||
|
/// return CachedNetworkImageProvider(imageUrl);
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Return null to fallback to default handling
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
final ImageEmbedBuilderProviderBuilder? imageProviderBuilder;
|
||||||
|
|
||||||
|
/// [imageErrorWidgetBuilder] if you want to show a custom widget based on the
|
||||||
|
/// exception that happen while loading the image, if it network image or
|
||||||
|
/// local one, and it will get called on all the images even in the photo
|
||||||
|
/// preview widget and not just in the quill editor
|
||||||
|
/// by default the default error from flutter framework will thrown
|
||||||
|
///
|
||||||
|
final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder;
|
||||||
|
|
||||||
|
/// What should happen when the image is pressed?
|
||||||
|
///
|
||||||
|
/// By default will show `ImageOptionsMenu` dialog. If you want to handle what happens
|
||||||
|
/// to the image when it's clicked, you can pass a callback to this property.
|
||||||
|
final void Function(String imageSource)? onImageClicked;
|
||||||
|
|
||||||
|
static ImageEmbedBuilderOnRemovedCallback get defaultOnImageRemovedCallback {
|
||||||
|
return (imageUrl) async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final mobile = isMobileApp;
|
||||||
|
// If the platform is not mobile, return void;
|
||||||
|
// Since the mobile OS gives us a copy of the image
|
||||||
|
|
||||||
|
// Note: We should remove the image on Flutter web
|
||||||
|
// since the behavior is similar to how it is on mobile,
|
||||||
|
// but since this builder is not for web, we will ignore it
|
||||||
|
if (!mobile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On mobile OS (Android, iOS), the system will not give us
|
||||||
|
// direct access to the image; instead,
|
||||||
|
// it will give us the image
|
||||||
|
// in the temp directory of the application. So, we want to
|
||||||
|
// remove it when we no longer need it.
|
||||||
|
|
||||||
|
// but on desktop we don't want to touch user files
|
||||||
|
// especially on macOS, where we can't even delete
|
||||||
|
// it without
|
||||||
|
// permission
|
||||||
|
|
||||||
|
final dartIoImageFile = File(imageUrl);
|
||||||
|
|
||||||
|
final isFileExists = await dartIoImageFile.exists();
|
||||||
|
if (isFileExists) {
|
||||||
|
await dartIoImageFile.delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QuillEditorImageEmbedConfig copyWith({
|
||||||
|
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
|
||||||
|
ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback,
|
||||||
|
ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||||
|
ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder,
|
||||||
|
bool? forceUseMobileOptionMenuForImageClick,
|
||||||
|
}) {
|
||||||
|
return QuillEditorImageEmbedConfig(
|
||||||
|
onImageRemovedCallback: onImageRemovedCallback ?? _onImageRemovedCallback,
|
||||||
|
shouldRemoveImageCallback:
|
||||||
|
shouldRemoveImageCallback ?? this.shouldRemoveImageCallback,
|
||||||
|
imageProviderBuilder: imageProviderBuilder ?? this.imageProviderBuilder,
|
||||||
|
imageErrorWidgetBuilder:
|
||||||
|
imageErrorWidgetBuilder ?? this.imageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:flutter/widgets.dart' show BoxConstraints;
|
||||||
|
import 'package:meta/meta.dart' show immutable;
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class QuillEditorWebImageEmbedConfig {
|
||||||
|
const QuillEditorWebImageEmbedConfig({
|
||||||
|
this.constraints,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BoxConstraints? constraints;
|
||||||
|
}
|
||||||
77
lib/Screens/myTemplates/src/editor/image/image_embed.dart
Normal file
77
lib/Screens/myTemplates/src/editor/image/image_embed.dart
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
|
import '../../common/utils/element_utils/element_utils.dart';
|
||||||
|
import 'config/image_config.dart';
|
||||||
|
import 'image_menu.dart';
|
||||||
|
import 'widgets/image.dart';
|
||||||
|
|
||||||
|
class QuillEditorImageEmbedBuilder extends EmbedBuilder {
|
||||||
|
QuillEditorImageEmbedBuilder({
|
||||||
|
required this.config,
|
||||||
|
});
|
||||||
|
final QuillEditorImageEmbedConfig config;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => BlockEmbed.imageType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get expanded => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
EmbedContext embedContext,
|
||||||
|
) {
|
||||||
|
final imageSource = standardizeImageUrl(embedContext.node.value.data);
|
||||||
|
final ((imageSize), margin, alignment) = getElementAttributes(
|
||||||
|
embedContext.node,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
final width = imageSize.width;
|
||||||
|
final height = imageSize.height;
|
||||||
|
|
||||||
|
final imageWidget = getImageWidgetByImageSource(
|
||||||
|
context: context,
|
||||||
|
imageSource,
|
||||||
|
imageProviderBuilder: config.imageProviderBuilder,
|
||||||
|
imageErrorWidgetBuilder: config.imageErrorWidgetBuilder,
|
||||||
|
alignment: alignment,
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
final onImageClicked = config.onImageClicked;
|
||||||
|
if (onImageClicked != null) {
|
||||||
|
onImageClicked(imageSource);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => ImageOptionsMenu(
|
||||||
|
controller: embedContext.controller,
|
||||||
|
config: config,
|
||||||
|
imageSource: imageSource,
|
||||||
|
imageSize: imageSize,
|
||||||
|
readOnly: embedContext.readOnly,
|
||||||
|
imageProvider: imageWidget.image,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (margin != null) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.all(margin),
|
||||||
|
child: imageWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return imageWidget;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import 'package:flutter/widgets.dart'
|
||||||
|
show ImageErrorWidgetBuilder, ImageProvider;
|
||||||
|
import 'package:flutter/widgets.dart' show BuildContext;
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:meta/meta.dart' show immutable;
|
||||||
|
|
||||||
|
/// When request picking an image, for example when the image button toolbar
|
||||||
|
/// clicked, it should be null in case the user didn't choose any image or
|
||||||
|
/// any other reasons, and it should be the image file path as string that is
|
||||||
|
/// exists in case the user picked the image successfully
|
||||||
|
///
|
||||||
|
/// by default we already have a default implementation that show a dialog
|
||||||
|
/// request the source for picking the image, from gallery, link or camera
|
||||||
|
typedef OnRequestPickImage = Future<String?> Function(
|
||||||
|
BuildContext context,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A callback will called when inserting a image in the editor
|
||||||
|
/// it have the logic that will insert the image block using the controller
|
||||||
|
typedef OnImageInsertCallback = Future<void> Function(
|
||||||
|
String image,
|
||||||
|
QuillController controller,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// When a new image picked this callback will called and you might want to
|
||||||
|
/// do some logic depending on your use case
|
||||||
|
typedef OnImageInsertedCallback = Future<void> Function(
|
||||||
|
String image,
|
||||||
|
);
|
||||||
|
|
||||||
|
enum InsertImageSource {
|
||||||
|
gallery,
|
||||||
|
camera,
|
||||||
|
link,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configurations for dealing with images, on insert a image
|
||||||
|
/// on request picking a image
|
||||||
|
@immutable
|
||||||
|
class QuillToolbarImageConfig {
|
||||||
|
const QuillToolbarImageConfig({
|
||||||
|
this.onRequestPickImage,
|
||||||
|
this.onImageInsertedCallback,
|
||||||
|
this.onImageInsertCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
final OnRequestPickImage? onRequestPickImage;
|
||||||
|
|
||||||
|
final OnImageInsertedCallback? onImageInsertedCallback;
|
||||||
|
|
||||||
|
final OnImageInsertCallback? onImageInsertCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef ImageEmbedBuilderWillRemoveCallback = Future<bool> Function(
|
||||||
|
String imageUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef ImageEmbedBuilderOnRemovedCallback = Future<void> Function(
|
||||||
|
String imageUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef ImageEmbedBuilderProviderBuilder = ImageProvider? Function(
|
||||||
|
BuildContext context,
|
||||||
|
String imageUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef ImageEmbedBuilderErrorWidgetBuilder = ImageErrorWidgetBuilder;
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import 'dart:async' show Completer;
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class ImageLoader {
|
||||||
|
static ImageLoader _instance = ImageLoader();
|
||||||
|
|
||||||
|
static ImageLoader get instance => _instance;
|
||||||
|
|
||||||
|
/// Allows overriding the instance for testing
|
||||||
|
@visibleForTesting
|
||||||
|
static set instance(ImageLoader newInstance) => _instance = newInstance;
|
||||||
|
|
||||||
|
// TODO(performance): This will load the image again. In case
|
||||||
|
// this is a network image, then this will be inefficient.
|
||||||
|
Future<Uint8List?> loadImageBytesFromImageProvider({
|
||||||
|
required ImageProvider imageProvider,
|
||||||
|
}) async {
|
||||||
|
final stream = imageProvider.resolve(ImageConfiguration.empty);
|
||||||
|
final completer = Completer<ui.Image>();
|
||||||
|
|
||||||
|
ImageStreamListener? listener;
|
||||||
|
listener = ImageStreamListener((info, _) {
|
||||||
|
completer.complete(info.image);
|
||||||
|
stream.removeListener(listener!);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.addListener(listener);
|
||||||
|
|
||||||
|
final image = await completer.future;
|
||||||
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
return byteData?.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
}
|
||||||
246
lib/Screens/myTemplates/src/editor/image/image_menu.dart
Normal file
246
lib/Screens/myTemplates/src/editor/image/image_menu.dart
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import 'package:flutter/cupertino.dart' show showCupertinoModalPopup;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart'
|
||||||
|
show ImageUrl, QuillController, StyleAttribute, getEmbedNode;
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
import '../../common/utils/element_utils/element_utils.dart';
|
||||||
|
import '../../common/utils/string.dart';
|
||||||
|
import 'config/image_config.dart';
|
||||||
|
import 'image_load_utils.dart';
|
||||||
|
import 'image_save_utils.dart';
|
||||||
|
import 'widgets/image.dart' show ImageTapWrapper, getImageStyleString;
|
||||||
|
import 'widgets/image_resizer.dart' show ImageResizer;
|
||||||
|
|
||||||
|
class ImageOptionsMenu extends StatelessWidget {
|
||||||
|
const ImageOptionsMenu({
|
||||||
|
required this.controller,
|
||||||
|
required this.config,
|
||||||
|
required this.imageSource,
|
||||||
|
required this.imageSize,
|
||||||
|
required this.readOnly,
|
||||||
|
required this.imageProvider,
|
||||||
|
this.prefersGallerySave = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final QuillController controller;
|
||||||
|
final QuillEditorImageEmbedConfig config;
|
||||||
|
final String imageSource;
|
||||||
|
final ElementSize imageSize;
|
||||||
|
final bool readOnly;
|
||||||
|
final ImageProvider imageProvider;
|
||||||
|
|
||||||
|
// TODO(quill_native_bridge): Update this doc comment once saveImageToGallery()
|
||||||
|
// is supported on Windows too (will be applicable like macOS). See https://pub.dev/packages/quill_native_bridge#-features
|
||||||
|
/// Determines if the image should be saved to the gallery instead of using the
|
||||||
|
/// system file save dialog for platforms that support both.
|
||||||
|
///
|
||||||
|
/// Currently, the only platform where this applies is macOS.
|
||||||
|
///
|
||||||
|
/// This is silently ignored on platforms that only support gallery save (Android and iOS)
|
||||||
|
/// or only image save.
|
||||||
|
///
|
||||||
|
/// For more details, refer to [quill_native_bridge Saving images](https://pub.dev/packages/quill_native_bridge#-saving-images).
|
||||||
|
final bool prefersGallerySave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final materialTheme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
|
||||||
|
child: SimpleDialog(
|
||||||
|
title: Text(context.loc.image),
|
||||||
|
children: [
|
||||||
|
if (!readOnly)
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.resize),
|
||||||
|
leading: const Icon(Icons.settings_outlined),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
showCupertinoModalPopup<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (modalContext) {
|
||||||
|
final screenSize = MediaQuery.sizeOf(modalContext);
|
||||||
|
return ImageResizer(
|
||||||
|
onImageResize: (width, height) {
|
||||||
|
final res = getEmbedNode(
|
||||||
|
controller,
|
||||||
|
controller.selection.start,
|
||||||
|
);
|
||||||
|
|
||||||
|
final attr = replaceStyleStringWithSize(
|
||||||
|
getImageStyleString(controller),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
);
|
||||||
|
controller
|
||||||
|
..skipRequestKeyboard = true
|
||||||
|
..formatText(
|
||||||
|
res.offset,
|
||||||
|
1,
|
||||||
|
StyleAttribute(attr),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
imageWidth: imageSize.width,
|
||||||
|
imageHeight: imageSize.height,
|
||||||
|
maxWidth: screenSize.width,
|
||||||
|
maxHeight: screenSize.height,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.copy_all_outlined),
|
||||||
|
title: Text(context.loc.copy),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
controller.copiedImageUrl = ImageUrl(
|
||||||
|
imageSource,
|
||||||
|
getImageStyleString(controller),
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageBytes = await ImageLoader.instance
|
||||||
|
.loadImageBytesFromImageProvider(
|
||||||
|
imageProvider: imageProvider);
|
||||||
|
if (imageBytes != null) {
|
||||||
|
await ClipboardServiceProvider.instance.copyImage(imageBytes);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!readOnly)
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.delete_forever_outlined,
|
||||||
|
color: materialTheme.colorScheme.error,
|
||||||
|
),
|
||||||
|
title: Text(context.loc.remove),
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// Call the remove check callback if set
|
||||||
|
if (await config.shouldRemoveImageCallback?.call(imageSource) ==
|
||||||
|
false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final offset = getEmbedNode(
|
||||||
|
controller,
|
||||||
|
controller.selection.start,
|
||||||
|
).offset;
|
||||||
|
controller.replaceText(
|
||||||
|
offset,
|
||||||
|
1,
|
||||||
|
'',
|
||||||
|
TextSelection.collapsed(offset: offset),
|
||||||
|
);
|
||||||
|
// Call the post remove callback if set
|
||||||
|
await config.onImageRemovedCallback.call(imageSource);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.save),
|
||||||
|
title: Text(context.loc.save),
|
||||||
|
onTap: () async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final localizations = context.loc;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
SaveImageResult? result;
|
||||||
|
try {
|
||||||
|
result = await ImageSaver.instance.saveImage(
|
||||||
|
imageUrl: imageSource,
|
||||||
|
imageProvider: imageProvider,
|
||||||
|
prefersGallerySave: prefersGallerySave,
|
||||||
|
);
|
||||||
|
} on GalleryImageSaveAccessDeniedException {
|
||||||
|
messenger.showSnackBar(SnackBar(
|
||||||
|
content: Text(
|
||||||
|
localizations.saveImagePermissionDenied,
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
messenger.showSnackBar(SnackBar(
|
||||||
|
content: Text(
|
||||||
|
localizations.errorUnexpectedSavingImage,
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kIsWeb) {
|
||||||
|
messenger.showSnackBar(SnackBar(
|
||||||
|
content: Text(localizations.successImageDownloaded)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isGallerySave) {
|
||||||
|
messenger.showSnackBar(SnackBar(
|
||||||
|
content: Text(localizations.successImageSavedGallery),
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: localizations.openGallery,
|
||||||
|
onPressed: () =>
|
||||||
|
QuillNativeProvider.instance.openGalleryApp(),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDesktopApp) {
|
||||||
|
final imageFilePath = result.imageFilePath;
|
||||||
|
if (imageFilePath == null) {
|
||||||
|
// User canceled the system save dialog.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(localizations.successImageSaved),
|
||||||
|
// On macOS the app only has access to the picked file from the system save
|
||||||
|
// dialog and not the directory where it was saved.
|
||||||
|
// Opening the directory of that file requires entitlements on macOS
|
||||||
|
// See https://pub.dev/packages/url_launcher#macos-file-access-configuration
|
||||||
|
// Open the saved image file instead of the directory
|
||||||
|
action: defaultTargetPlatform == TargetPlatform.macOS
|
||||||
|
? SnackBarAction(
|
||||||
|
label: localizations.openFile,
|
||||||
|
onPressed: () => launchUrl(Uri.file(imageFilePath)),
|
||||||
|
)
|
||||||
|
: SnackBarAction(
|
||||||
|
label: localizations.openFileLocation,
|
||||||
|
onPressed: () => launchUrl(
|
||||||
|
Uri.directory(p.dirname(imageFilePath))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw StateError(
|
||||||
|
'Image save result is not handled on $defaultTargetPlatform');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.zoom_in),
|
||||||
|
title: Text(context.loc.zoom),
|
||||||
|
onTap: () => Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ImageTapWrapper(
|
||||||
|
imageUrl: imageSource,
|
||||||
|
config: config,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
254
lib/Screens/myTemplates/src/editor/image/image_save_utils.dart
Normal file
254
lib/Screens/myTemplates/src/editor/image/image_save_utils.dart
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
@internal
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'image_load_utils.dart';
|
||||||
|
|
||||||
|
const defaultImageFileExtension = 'png';
|
||||||
|
|
||||||
|
// The [imageSourcePath] could be file, asset path or HTTP image URL.
|
||||||
|
String extractImageFileExtensionFromImageSource(String? imageSourcePath) {
|
||||||
|
if (imageSourcePath == null || imageSourcePath.isEmpty) {
|
||||||
|
return defaultImageFileExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageSourcePath.contains('.')) {
|
||||||
|
return defaultImageFileExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.extension(imageSourcePath).replaceFirst('.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The [imageSourcePath] could be file, asset path or HTTP image URL.
|
||||||
|
String? extractImageNameFromImageSource(String? imageSourcePath) {
|
||||||
|
if (imageSourcePath == null || imageSourcePath.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final uri = Uri.parse(imageSourcePath);
|
||||||
|
final pathWithoutQuery = uri.path;
|
||||||
|
|
||||||
|
final imageName = p.basenameWithoutExtension(pathWithoutQuery);
|
||||||
|
if (imageName.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return imageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SaveImageResult {
|
||||||
|
const SaveImageResult({
|
||||||
|
required this.imageFilePath,
|
||||||
|
required this.isGallerySave,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Returns `null` on web platforms, if [isGallerySave] is `true`
|
||||||
|
/// or in case the user cancels the save operation on desktop platforms.
|
||||||
|
final String? imageFilePath;
|
||||||
|
|
||||||
|
final bool isGallerySave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(other, this)) return true;
|
||||||
|
if (other is! SaveImageResult) return false;
|
||||||
|
return other.imageFilePath == imageFilePath &&
|
||||||
|
other.isGallerySave == isGallerySave;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(imageFilePath, isGallerySave);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'SaveImageResult(imageFilePath: $imageFilePath, isGallerySave: $isGallerySave)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const String defaultImageFileNamePrefix = 'IMG';
|
||||||
|
|
||||||
|
String getDefaultImageFileName({required bool isGallerySave}) {
|
||||||
|
if (kIsWeb) {
|
||||||
|
// The browser handles name conflicts.
|
||||||
|
return defaultImageFileNamePrefix;
|
||||||
|
}
|
||||||
|
if (isGallerySave) {
|
||||||
|
// The gallery app handles name conflicts.
|
||||||
|
return defaultImageFileNamePrefix;
|
||||||
|
}
|
||||||
|
if (defaultTargetPlatform == TargetPlatform.macOS ||
|
||||||
|
defaultTargetPlatform == TargetPlatform.windows) {
|
||||||
|
// Windows and macOS system native save dialog prompts the user to confirm file overwrite.
|
||||||
|
return defaultImageFileNamePrefix;
|
||||||
|
}
|
||||||
|
final uniqueFileName =
|
||||||
|
'${defaultImageFileNamePrefix}_${DateTime.now().toIso8601String()}';
|
||||||
|
if (defaultTargetPlatform == TargetPlatform.linux) {
|
||||||
|
// IMPORTANT: On Linux, it depends on the desktop environment
|
||||||
|
// and name conflicts may not be handled. Always provide a unique image file name.
|
||||||
|
return uniqueFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> shouldSaveToGallery({required bool prefersGallerySave}) async {
|
||||||
|
final supportsGallerySave = await QuillNativeProvider.instance
|
||||||
|
.isSupported(QuillNativeBridgeFeature.saveImageToGallery);
|
||||||
|
if (!supportsGallerySave) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final supportsImageSave = await QuillNativeProvider.instance
|
||||||
|
.isSupported(QuillNativeBridgeFeature.saveImage);
|
||||||
|
if (!supportsImageSave) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return supportsGallerySave && prefersGallerySave;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Thrown when the gallery image save operation is denied
|
||||||
|
/// due to insufficient or denied permissions.
|
||||||
|
class GalleryImageSaveAccessDeniedException implements Exception {
|
||||||
|
GalleryImageSaveAccessDeniedException([this.message]);
|
||||||
|
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
message ??
|
||||||
|
'Permission to save the image to the gallery was denied or insufficient.';
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageSaver {
|
||||||
|
ImageSaver._();
|
||||||
|
|
||||||
|
static ImageSaver _instance = ImageSaver._();
|
||||||
|
|
||||||
|
static ImageSaver get instance => _instance;
|
||||||
|
|
||||||
|
/// Allows overriding the instance for testing
|
||||||
|
@visibleForTesting
|
||||||
|
static set instance(ImageSaver newInstance) => _instance = newInstance;
|
||||||
|
|
||||||
|
/// Saves an image to the user's device based on the platform:
|
||||||
|
///
|
||||||
|
/// - **Web**: Downloads the image using the browser's download functionality.
|
||||||
|
/// - **Desktop**: Prompts the user to choose a location for the image using
|
||||||
|
/// native save dialog, defaulting to the user's `Pictures` directory. Or
|
||||||
|
/// saves the image to the gallery in case [prefersGallerySave] is `true` and
|
||||||
|
// TODO(quill_native_bridge): Update this doc comment once saveImageToGallery()
|
||||||
|
// is supported on Windows too (will be applicable like macOS). See https://pub.dev/packages/quill_native_bridge#-features
|
||||||
|
/// the gallery is supported (currently only macOS is applicable).
|
||||||
|
/// - **Mobile**: Saves the image to the gallery, requesting permission if needed.
|
||||||
|
///
|
||||||
|
/// The [imageUrl] could be file or network image URL and is used to extract
|
||||||
|
/// image file extension and the image name.
|
||||||
|
///
|
||||||
|
/// The [imageProvider] is used to load the image bytes from using [ImageLoader].
|
||||||
|
///
|
||||||
|
/// Returns `null` on failure.
|
||||||
|
///
|
||||||
|
/// Throws [GalleryImageSaveAccessDeniedException] in case permission was denied or insuffeicnet.
|
||||||
|
Future<SaveImageResult?> saveImage({
|
||||||
|
required String imageUrl,
|
||||||
|
required ImageProvider imageProvider,
|
||||||
|
required bool prefersGallerySave,
|
||||||
|
}) async {
|
||||||
|
assert(() {
|
||||||
|
if (imageUrl.isEmpty) {
|
||||||
|
throw ArgumentError.value(imageUrl, 'imageUrl', 'cannot be empty');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
final imageFileExtension =
|
||||||
|
extractImageFileExtensionFromImageSource(imageUrl);
|
||||||
|
final imageName = extractImageNameFromImageSource(imageUrl);
|
||||||
|
|
||||||
|
final imageBytes = await ImageLoader.instance
|
||||||
|
.loadImageBytesFromImageProvider(imageProvider: imageProvider);
|
||||||
|
if (imageBytes == null || imageBytes.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kIsWeb) {
|
||||||
|
await QuillNativeProvider.instance.saveImage(
|
||||||
|
imageBytes,
|
||||||
|
options: ImageSaveOptions(
|
||||||
|
name: imageName ?? getDefaultImageFileName(isGallerySave: false),
|
||||||
|
fileExtension: imageFileExtension),
|
||||||
|
);
|
||||||
|
return const SaveImageResult(
|
||||||
|
imageFilePath: null,
|
||||||
|
isGallerySave: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await shouldSaveToGallery(prefersGallerySave: prefersGallerySave)) {
|
||||||
|
try {
|
||||||
|
await QuillNativeProvider.instance.saveImageToGallery(
|
||||||
|
imageBytes,
|
||||||
|
options: GalleryImageSaveOptions(
|
||||||
|
name: imageName ?? getDefaultImageFileName(isGallerySave: true),
|
||||||
|
fileExtension: imageFileExtension,
|
||||||
|
// Specifying the album name requires read-write permission
|
||||||
|
// on iOS and macOS on all versions. Pass null to request add-only on
|
||||||
|
// supported versions (previous versions still use read-write).
|
||||||
|
albumName: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return const SaveImageResult(
|
||||||
|
imageFilePath: null,
|
||||||
|
isGallerySave: true,
|
||||||
|
);
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
// TODO(save-image): Part of https://github.com/FlutterQuill/quill-native-bridge/issues/2
|
||||||
|
|
||||||
|
// Permission request is required only on iOS, macOS and Android API 28 and earlier.
|
||||||
|
if (e.code == 'PERMISSION_DENIED') {
|
||||||
|
// macOS imposes security restrictions when running the app
|
||||||
|
// on sources other than Xcode or the macOS terminal, such as Android Studio or VS Code.
|
||||||
|
// This is not an issue in production. Throwing [GalleryImageSaveAccessDeniedException] will indicate
|
||||||
|
// that the user denied the permission, even though it will always deny the permission even if granted.
|
||||||
|
// Make sure we don't handle that error (it has details) during development to avoid confusion.
|
||||||
|
// For more details, see https://github.com/flutter/flutter/issues/134191#issuecomment-2506248266
|
||||||
|
// and https://pub.dev/packages/quill_native_bridge#-saving-images-to-the-gallery
|
||||||
|
|
||||||
|
final possiblePermissionIssueDuringDevelopmentOnMacOS =
|
||||||
|
kDebugMode && defaultTargetPlatform == TargetPlatform.macOS;
|
||||||
|
if (possiblePermissionIssueDuringDevelopmentOnMacOS) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw GalleryImageSaveAccessDeniedException(e.toString());
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await QuillNativeProvider.instance
|
||||||
|
.isSupported(QuillNativeBridgeFeature.saveImage)) {
|
||||||
|
assert(!isMobileApp,
|
||||||
|
'Mobile platforms support saving images to the gallery only');
|
||||||
|
|
||||||
|
final result = await QuillNativeProvider.instance.saveImage(
|
||||||
|
imageBytes,
|
||||||
|
options: ImageSaveOptions(
|
||||||
|
name: imageName ?? getDefaultImageFileName(isGallerySave: false),
|
||||||
|
fileExtension: imageFileExtension,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return SaveImageResult(
|
||||||
|
imageFilePath: result.filePath,
|
||||||
|
isGallerySave: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw StateError('Image save is not handled on $defaultTargetPlatform');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
|
import '../../common/utils/element_utils/element_web_utils.dart';
|
||||||
|
import '../../common/utils/utils.dart';
|
||||||
|
import '../../common/utils/web/web.dart';
|
||||||
|
import 'config/image_web_config.dart';
|
||||||
|
|
||||||
|
class QuillEditorWebImageEmbedBuilder extends EmbedBuilder {
|
||||||
|
const QuillEditorWebImageEmbedBuilder({
|
||||||
|
required this.config,
|
||||||
|
});
|
||||||
|
|
||||||
|
final QuillEditorWebImageEmbedConfig config;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => BlockEmbed.imageType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get expanded => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
EmbedContext embedContext,
|
||||||
|
) {
|
||||||
|
assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform');
|
||||||
|
|
||||||
|
final (height, width, margin, alignment) =
|
||||||
|
getWebElementAttributes(embedContext.node);
|
||||||
|
|
||||||
|
var imageSource = embedContext.node.value.data.toString();
|
||||||
|
|
||||||
|
// This logic make sure if the image is imageBase64 then
|
||||||
|
// it make sure if the pattern is like
|
||||||
|
// data:image/png;base64, [base64 encoded image string here]
|
||||||
|
// if not then it will add the data:image/png;base64, at the first
|
||||||
|
if (isImageBase64(imageSource)) {
|
||||||
|
// Sometimes the image base 64 for some reasons
|
||||||
|
// doesn't displayed with the 'data:image/png;base64'
|
||||||
|
if (!(imageSource.startsWith('data:image/') &&
|
||||||
|
imageSource.contains('base64'))) {
|
||||||
|
imageSource = 'data:image/png;base64, $imageSource';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createHtmlImageElement(
|
||||||
|
src: imageSource,
|
||||||
|
alignSelf: alignment,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
margin: margin,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints:
|
||||||
|
config.constraints ?? BoxConstraints.loose(const Size(200, 200)),
|
||||||
|
child: HtmlElementView(
|
||||||
|
viewType: imageSource,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
186
lib/Screens/myTemplates/src/editor/image/widgets/image.dart
Normal file
186
lib/Screens/myTemplates/src/editor/image/widgets/image.dart
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import 'dart:convert' show base64;
|
||||||
|
import 'dart:io' show File;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
|
import '../../../common/utils/utils.dart';
|
||||||
|
import '../config/image_config.dart';
|
||||||
|
import '../image_embed_types.dart';
|
||||||
|
|
||||||
|
String getImageStyleString(QuillController controller) {
|
||||||
|
final String? s = controller
|
||||||
|
.getAllSelectionStyles()
|
||||||
|
.firstWhere((s) => s.attributes.containsKey(Attribute.style.key),
|
||||||
|
orElse: Style.new)
|
||||||
|
.attributes[Attribute.style.key]
|
||||||
|
?.value;
|
||||||
|
return s ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [imageProviderBuilder] To override the return value pass value to it
|
||||||
|
/// [imageSource] The source of the image in the quill delta json document
|
||||||
|
/// It could be http, file, network, asset, or base 64 image
|
||||||
|
ImageProvider getImageProviderByImageSource(
|
||||||
|
String imageSource, {
|
||||||
|
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||||
|
required BuildContext context,
|
||||||
|
}) {
|
||||||
|
if (imageProviderBuilder != null) {
|
||||||
|
final imageProvider = imageProviderBuilder(context, imageSource);
|
||||||
|
if (imageProvider != null) {
|
||||||
|
return imageProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isImageBase64(imageSource)) {
|
||||||
|
return MemoryImage(base64.decode(imageSource));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHttpUrl(imageSource)) {
|
||||||
|
return NetworkImage(imageSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File image
|
||||||
|
if (kIsWeb) {
|
||||||
|
return NetworkImage(imageSource);
|
||||||
|
}
|
||||||
|
return FileImage(File(imageSource));
|
||||||
|
}
|
||||||
|
|
||||||
|
Image getImageWidgetByImageSource(
|
||||||
|
String imageSource, {
|
||||||
|
required BuildContext context,
|
||||||
|
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||||
|
required ImageErrorWidgetBuilder? imageErrorWidgetBuilder,
|
||||||
|
double? width,
|
||||||
|
double? height,
|
||||||
|
AlignmentGeometry alignment = Alignment.center,
|
||||||
|
}) {
|
||||||
|
return Image(
|
||||||
|
image: getImageProviderByImageSource(
|
||||||
|
context: context,
|
||||||
|
imageSource,
|
||||||
|
imageProviderBuilder: imageProviderBuilder,
|
||||||
|
),
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
alignment: alignment,
|
||||||
|
errorBuilder: imageErrorWidgetBuilder,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String standardizeImageUrl(String url) {
|
||||||
|
if (url.contains('base64')) {
|
||||||
|
return url.split(',')[1];
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const List<String> _imageFileExtensions = [
|
||||||
|
'.jpeg',
|
||||||
|
'.png',
|
||||||
|
'.jpg',
|
||||||
|
'.gif',
|
||||||
|
'.webp',
|
||||||
|
'.tif',
|
||||||
|
'.heic'
|
||||||
|
];
|
||||||
|
|
||||||
|
/// This is a bug of Gallery Saver Package.
|
||||||
|
/// It can not save image that's filename does not end with it's file extension
|
||||||
|
/// like below.
|
||||||
|
// "https://firebasestorage.googleapis.com/v0/b/eventat-4ba96.appspot.com/o/2019-Metrology-Events.jpg?alt=media&token=bfc47032-5173-4b3f-86bb-9659f46b362a"
|
||||||
|
/// If imageUrl does not end with it's file extension,
|
||||||
|
/// file extension is added to image url for saving.
|
||||||
|
String appendFileExtensionToImageUrl(String url) {
|
||||||
|
final endsWithImageFileExtension = _imageFileExtensions
|
||||||
|
.firstWhere((s) => url.toLowerCase().endsWith(s), orElse: () => '');
|
||||||
|
if (endsWithImageFileExtension.isNotEmpty) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
final imageFileExtension = _imageFileExtensions
|
||||||
|
.firstWhere((s) => url.toLowerCase().contains(s), orElse: () => '');
|
||||||
|
|
||||||
|
return url + imageFileExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageTapWrapper extends StatelessWidget {
|
||||||
|
const ImageTapWrapper({
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.config,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String imageUrl;
|
||||||
|
final QuillEditorImageEmbedConfig config;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
constraints: BoxConstraints.expand(
|
||||||
|
height: MediaQuery.sizeOf(context).height,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
PhotoView(
|
||||||
|
imageProvider: getImageProviderByImageSource(
|
||||||
|
context: context,
|
||||||
|
imageUrl,
|
||||||
|
imageProviderBuilder: config.imageProviderBuilder,
|
||||||
|
),
|
||||||
|
errorBuilder: config.imageErrorWidgetBuilder,
|
||||||
|
loadingBuilder: (context, event) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.black,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 10,
|
||||||
|
top: MediaQuery.paddingOf(context).top + 10.0,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Opacity(
|
||||||
|
opacity: 0.2,
|
||||||
|
child: Container(
|
||||||
|
height: 30,
|
||||||
|
width: 30,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,126 @@
|
|||||||
|
import 'package:flutter/cupertino.dart'
|
||||||
|
show CupertinoActionSheet, CupertinoActionSheetAction;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart' show SchedulerBinding;
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
class ImageResizer extends StatefulWidget {
|
||||||
|
const ImageResizer({
|
||||||
|
required this.imageWidth,
|
||||||
|
required this.imageHeight,
|
||||||
|
required this.maxWidth,
|
||||||
|
required this.maxHeight,
|
||||||
|
required this.onImageResize,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double? imageWidth;
|
||||||
|
final double? imageHeight;
|
||||||
|
final double maxWidth;
|
||||||
|
final double maxHeight;
|
||||||
|
final Function(double width, double height) onImageResize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageResizerState createState() => ImageResizerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageResizerState extends State<ImageResizer> {
|
||||||
|
late double _width;
|
||||||
|
late double _height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_width = widget.imageWidth ?? widget.maxWidth;
|
||||||
|
_height = widget.imageHeight ?? widget.maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (Theme.of(context).isCupertino) {
|
||||||
|
return _showCupertinoMenu();
|
||||||
|
}
|
||||||
|
return _showMaterialMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _showMaterialMenu() {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_widthSlider(),
|
||||||
|
_heightSlider(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _showCupertinoMenu() {
|
||||||
|
return CupertinoActionSheet(
|
||||||
|
actions: [
|
||||||
|
CupertinoActionSheetAction(
|
||||||
|
onPressed: () {},
|
||||||
|
child: _widthSlider(),
|
||||||
|
),
|
||||||
|
CupertinoActionSheetAction(
|
||||||
|
onPressed: () {},
|
||||||
|
child: _heightSlider(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _slider({
|
||||||
|
required bool isWidth,
|
||||||
|
required ValueChanged<double> onChanged,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Card(
|
||||||
|
child: Slider.adaptive(
|
||||||
|
value: isWidth ? _width : _height,
|
||||||
|
max: isWidth ? widget.maxWidth : widget.maxHeight,
|
||||||
|
divisions: 1000,
|
||||||
|
// Might need to be changed
|
||||||
|
label: isWidth ? context.loc.width : context.loc.height,
|
||||||
|
onChanged: (val) {
|
||||||
|
setState(() {
|
||||||
|
onChanged(val);
|
||||||
|
_resizeImage();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _heightSlider() {
|
||||||
|
return _slider(
|
||||||
|
isWidth: false,
|
||||||
|
onChanged: (value) {
|
||||||
|
_height = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _widthSlider() {
|
||||||
|
return _slider(
|
||||||
|
isWidth: true,
|
||||||
|
onChanged: (value) {
|
||||||
|
_width = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _scheduled = false;
|
||||||
|
|
||||||
|
void _resizeImage() {
|
||||||
|
if (_scheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduled = true;
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
widget.onImageResize(_width, _height);
|
||||||
|
_scheduled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/widgets.dart' show GlobalKey, Widget;
|
||||||
|
import 'package:meta/meta.dart' show experimental, immutable;
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class QuillEditorVideoEmbedConfig {
|
||||||
|
const QuillEditorVideoEmbedConfig({
|
||||||
|
this.onVideoInit,
|
||||||
|
this.customVideoBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// [onVideoInit] is a callback function that gets triggered when
|
||||||
|
/// a video is initialized.
|
||||||
|
/// You can use this to perform actions or setup configurations related
|
||||||
|
/// to video embedding.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```dart
|
||||||
|
/// onVideoInit: (videoContainerKey) {
|
||||||
|
/// // Custom video initialization logic
|
||||||
|
/// },
|
||||||
|
/// // Customize other callback functions as needed
|
||||||
|
/// ```
|
||||||
|
final void Function(GlobalKey videoContainerKey)? onVideoInit;
|
||||||
|
|
||||||
|
/// [customVideoBuilder] is a callback function that receives the
|
||||||
|
/// video URL and a read-only flag. This allows users to define
|
||||||
|
/// their own logic for rendering video widgets, enabling support
|
||||||
|
/// for various video platforms, such as YouTube.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```dart
|
||||||
|
/// customVideoBuilder: (videoUrl, readOnly) {
|
||||||
|
/// // Return `null` to fallback to defualt logic of QuillEditorVideoEmbedBuilder
|
||||||
|
///
|
||||||
|
/// // Return a custom video widget based on the videoUrl
|
||||||
|
/// return CustomVideoWidget(videoUrl: videoUrl, readOnly: readOnly);
|
||||||
|
/// },
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// It's a quick solution as response to https://github.com/singerdmx/flutter-quill/issues/2284
|
||||||
|
///
|
||||||
|
/// **Might be removed or changed in future releases.**
|
||||||
|
@experimental
|
||||||
|
final Widget? Function(String videoUrl, bool readOnly)? customVideoBuilder;
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import 'package:meta/meta.dart' show immutable;
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class QuillEditorWebVideoEmbedConfig {
|
||||||
|
const QuillEditorWebVideoEmbedConfig();
|
||||||
|
}
|
||||||
55
lib/Screens/myTemplates/src/editor/video/video_embed.dart
Normal file
55
lib/Screens/myTemplates/src/editor/video/video_embed.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
|
import '../../common/utils/element_utils/element_utils.dart';
|
||||||
|
import 'config/video_config.dart';
|
||||||
|
import 'widgets/video_app.dart';
|
||||||
|
|
||||||
|
class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
|
||||||
|
const QuillEditorVideoEmbedBuilder({
|
||||||
|
required this.config,
|
||||||
|
});
|
||||||
|
|
||||||
|
final QuillEditorVideoEmbedConfig config;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => BlockEmbed.videoType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get expanded => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
EmbedContext embedContext,
|
||||||
|
) {
|
||||||
|
final videoUrl = embedContext.node.value.data;
|
||||||
|
|
||||||
|
final customVideoBuilder = config.customVideoBuilder;
|
||||||
|
if (customVideoBuilder != null) {
|
||||||
|
final videoWidget = customVideoBuilder(videoUrl, embedContext.readOnly);
|
||||||
|
if (videoWidget != null) {
|
||||||
|
return videoWidget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final ((elementSize), margin, alignment) = getElementAttributes(
|
||||||
|
embedContext.node,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
final width = elementSize.width;
|
||||||
|
final height = elementSize.height;
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
margin: EdgeInsets.all(margin ?? 0.0),
|
||||||
|
alignment: alignment,
|
||||||
|
child: VideoApp(
|
||||||
|
videoUrl: videoUrl,
|
||||||
|
readOnly: embedContext.readOnly,
|
||||||
|
onVideoInit: config.onVideoInit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
|
import '../../common/utils/element_utils/element_web_utils.dart';
|
||||||
|
import '../../common/utils/utils.dart';
|
||||||
|
import '../../common/utils/web/web.dart';
|
||||||
|
import 'config/video_web_config.dart';
|
||||||
|
import 'youtube_video_url.dart';
|
||||||
|
|
||||||
|
class QuillEditorWebVideoEmbedBuilder extends EmbedBuilder {
|
||||||
|
const QuillEditorWebVideoEmbedBuilder({
|
||||||
|
required this.config,
|
||||||
|
});
|
||||||
|
|
||||||
|
final QuillEditorWebVideoEmbedConfig config;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get key => BlockEmbed.videoType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get expanded => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
EmbedContext embedContext,
|
||||||
|
) {
|
||||||
|
var videoUrl = embedContext.node.value.data;
|
||||||
|
if (isYouTubeUrl(videoUrl)) {
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
final youtubeID = convertVideoUrlToId(videoUrl);
|
||||||
|
if (youtubeID != null) {
|
||||||
|
videoUrl = 'https://www.youtube.com/embed/$youtubeID';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final (height, width, margin, alignment) =
|
||||||
|
getWebElementAttributes(embedContext.node);
|
||||||
|
|
||||||
|
createHtmlIFrameElement(
|
||||||
|
src: videoUrl,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
margin: margin,
|
||||||
|
alignSelf: alignment,
|
||||||
|
);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 500,
|
||||||
|
child: HtmlElementView(
|
||||||
|
viewType: videoUrl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
122
lib/Screens/myTemplates/src/editor/video/widgets/video_app.dart
Normal file
122
lib/Screens/myTemplates/src/editor/video/widgets/video_app.dart
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import 'dart:io' show File;
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
import '../../../common/utils/utils.dart';
|
||||||
|
|
||||||
|
/// Widget for playing back video
|
||||||
|
/// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
|
||||||
|
class VideoApp extends StatefulWidget {
|
||||||
|
const VideoApp({
|
||||||
|
required this.videoUrl,
|
||||||
|
required this.readOnly,
|
||||||
|
super.key,
|
||||||
|
this.onVideoInit,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String videoUrl;
|
||||||
|
final bool readOnly;
|
||||||
|
final void Function(GlobalKey videoContainerKey)? onVideoInit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
VideoAppState createState() => VideoAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoAppState extends State<VideoApp> {
|
||||||
|
late VideoPlayerController _controller;
|
||||||
|
GlobalKey videoContainerKey = GlobalKey();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = isHttpUrl(widget.videoUrl)
|
||||||
|
? VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl))
|
||||||
|
: VideoPlayerController.file(File(widget.videoUrl))
|
||||||
|
..initialize().then((_) {
|
||||||
|
// Ensure the first frame is shown after the video is initialized,
|
||||||
|
// even before the play button has been pressed.
|
||||||
|
setState(() {});
|
||||||
|
if (widget.onVideoInit != null) {
|
||||||
|
widget.onVideoInit?.call(videoContainerKey);
|
||||||
|
}
|
||||||
|
}).catchError((error) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final defaultStyles = DefaultStyles.getInstance(context);
|
||||||
|
if (_controller.value.hasError) {
|
||||||
|
if (widget.readOnly) {
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: widget.videoUrl,
|
||||||
|
style: defaultStyles.link,
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => launchUrl(
|
||||||
|
Uri.parse(widget.videoUrl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
text: widget.videoUrl,
|
||||||
|
style: defaultStyles.link,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (!_controller.value.isInitialized) {
|
||||||
|
return VideoProgressIndicator(
|
||||||
|
_controller,
|
||||||
|
allowScrubbing: true,
|
||||||
|
colors: const VideoProgressColors(playedColor: Colors.blue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
key: videoContainerKey,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_controller.value.isPlaying
|
||||||
|
? _controller.pause()
|
||||||
|
: _controller.play();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: _controller.value.aspectRatio,
|
||||||
|
child: VideoPlayer(_controller),
|
||||||
|
)),
|
||||||
|
_controller.value.isPlaying
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Container(
|
||||||
|
color: const Color(0xfff5f5f5),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.play_arrow,
|
||||||
|
size: 60,
|
||||||
|
color: Colors.blueGrey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// Function copied from https://github.com/sarbagyastha/youtube_player_flutter/blob/f8e1e79991066bcc70f0a7c93941ca0d54b7370e/packages/youtube_player_flutter/lib/src/player/youtube_player.dart#L154
|
||||||
|
/// and is not written as part of this project.
|
||||||
|
///
|
||||||
|
/// Used as quick response for https://github.com/singerdmx/flutter-quill/issues/2284
|
||||||
|
@experimental
|
||||||
|
@internal
|
||||||
|
@Deprecated(
|
||||||
|
'Will be removed in future releases, for now included as quick response to https://github.com/singerdmx/flutter-quill/issues/2284',
|
||||||
|
)
|
||||||
|
String? convertVideoUrlToId(String url, {bool trimWhitespaces = true}) {
|
||||||
|
if (!url.contains('http') && (url.length == 11)) return url;
|
||||||
|
if (trimWhitespaces) url = url.trim();
|
||||||
|
|
||||||
|
for (final exp in [
|
||||||
|
RegExp(
|
||||||
|
r'^https:\/\/(?:www\.|m\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$'),
|
||||||
|
RegExp(
|
||||||
|
r'^https:\/\/(?:music\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$'),
|
||||||
|
RegExp(
|
||||||
|
r'^https:\/\/(?:www\.|m\.)?youtube\.com\/shorts\/([_\-a-zA-Z0-9]{11}).*$'),
|
||||||
|
RegExp(
|
||||||
|
r'^https:\/\/(?:www\.|m\.)?youtube(?:-nocookie)?\.com\/embed\/([_\-a-zA-Z0-9]{11}).*$'),
|
||||||
|
RegExp(r'^https:\/\/youtu\.be\/([_\-a-zA-Z0-9]{11}).*$')
|
||||||
|
]) {
|
||||||
|
final Match? match = exp.firstMatch(url);
|
||||||
|
if (match != null && match.groupCount >= 1) return match.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
106
lib/Screens/myTemplates/src/flutter_quill_embeds.dart
Normal file
106
lib/Screens/myTemplates/src/flutter_quill_embeds.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
|
import 'editor/image/config/image_config.dart';
|
||||||
|
import 'editor/image/image_embed.dart';
|
||||||
|
import 'editor/video/config/video_config.dart';
|
||||||
|
import 'editor/video/config/video_web_config.dart';
|
||||||
|
import 'editor/video/video_embed.dart';
|
||||||
|
import 'editor/video/video_web_embed.dart';
|
||||||
|
import 'toolbar/camera/camera_button.dart';
|
||||||
|
import 'toolbar/camera/config/camera_config.dart';
|
||||||
|
import 'toolbar/image/config/image_config.dart';
|
||||||
|
import 'toolbar/image/image_button.dart';
|
||||||
|
import 'toolbar/video/config/video_config.dart';
|
||||||
|
import 'toolbar/video/video_button.dart';
|
||||||
|
|
||||||
|
abstract final class FlutterQuillEmbeds {
|
||||||
|
/// Returns a list of embed builders for [QuillEditor]
|
||||||
|
/// to provide basic support for loading images and videos.
|
||||||
|
///
|
||||||
|
static List<EmbedBuilder> editorBuilders({
|
||||||
|
QuillEditorImageEmbedConfig? imageEmbedConfig =
|
||||||
|
const QuillEditorImageEmbedConfig(),
|
||||||
|
QuillEditorVideoEmbedConfig? videoEmbedConfig =
|
||||||
|
const QuillEditorVideoEmbedConfig(),
|
||||||
|
}) {
|
||||||
|
return [
|
||||||
|
if (imageEmbedConfig != null)
|
||||||
|
QuillEditorImageEmbedBuilder(
|
||||||
|
config: imageEmbedConfig,
|
||||||
|
),
|
||||||
|
if (videoEmbedConfig != null)
|
||||||
|
QuillEditorVideoEmbedBuilder(
|
||||||
|
config: videoEmbedConfig,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of embed builders specifically designed for web support
|
||||||
|
/// to load images and videos.
|
||||||
|
///
|
||||||
|
static List<EmbedBuilder> editorWebBuilders({
|
||||||
|
QuillEditorImageEmbedConfig? imageEmbedConfig =
|
||||||
|
const QuillEditorImageEmbedConfig(),
|
||||||
|
QuillEditorWebVideoEmbedConfig? videoEmbedConfig =
|
||||||
|
const QuillEditorWebVideoEmbedConfig(),
|
||||||
|
}) {
|
||||||
|
if (!kIsWeb) {
|
||||||
|
throw UnsupportedError(
|
||||||
|
'The ${FlutterQuillEmbeds.editorWebBuilders} is for web, use ${FlutterQuillEmbeds.editorBuilders} '
|
||||||
|
'instead for non-web platforms',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
if (imageEmbedConfig != null)
|
||||||
|
QuillEditorImageEmbedBuilder(
|
||||||
|
config: imageEmbedConfig,
|
||||||
|
),
|
||||||
|
if (videoEmbedConfig != null)
|
||||||
|
QuillEditorWebVideoEmbedBuilder(
|
||||||
|
config: videoEmbedConfig,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of embed builders for [QuillEditor].
|
||||||
|
///
|
||||||
|
/// It will use [editorWebBuilders] for web and [editorBuilders] for non-web platforms.
|
||||||
|
static List<EmbedBuilder> defaultEditorBuilders() {
|
||||||
|
return kIsWeb ? editorWebBuilders() : editorBuilders();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of embed button builders to support images and videos.
|
||||||
|
///
|
||||||
|
/// Pass `null` to options of a button to not show it.
|
||||||
|
static List<EmbedButtonBuilder> toolbarButtons({
|
||||||
|
QuillToolbarImageButtonOptions? imageButtonOptions =
|
||||||
|
const QuillToolbarImageButtonOptions(),
|
||||||
|
QuillToolbarVideoButtonOptions? videoButtonOptions =
|
||||||
|
const QuillToolbarVideoButtonOptions(),
|
||||||
|
QuillToolbarCameraButtonOptions? cameraButtonOptions,
|
||||||
|
}) =>
|
||||||
|
[
|
||||||
|
if (imageButtonOptions != null)
|
||||||
|
(context, embedContext) => QuillToolbarImageButton(
|
||||||
|
controller: embedContext.controller,
|
||||||
|
options: imageButtonOptions,
|
||||||
|
// ignore: invalid_use_of_internal_member
|
||||||
|
baseOptions: embedContext.baseButtonOptions,
|
||||||
|
),
|
||||||
|
if (videoButtonOptions != null)
|
||||||
|
(context, embedContext) => QuillToolbarVideoButton(
|
||||||
|
controller: embedContext.controller,
|
||||||
|
options: videoButtonOptions,
|
||||||
|
// ignore: invalid_use_of_internal_member
|
||||||
|
baseOptions: embedContext.baseButtonOptions,
|
||||||
|
),
|
||||||
|
if (cameraButtonOptions != null)
|
||||||
|
(context, embedContext) => QuillToolbarCameraButton(
|
||||||
|
controller: embedContext.controller,
|
||||||
|
options: cameraButtonOptions,
|
||||||
|
// ignore: invalid_use_of_internal_member
|
||||||
|
baseOptions: embedContext.baseButtonOptions,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
132
lib/Screens/myTemplates/src/toolbar/camera/camera_button.dart
Normal file
132
lib/Screens/myTemplates/src/toolbar/camera/camera_button.dart
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import '../../common/default_image_insert.dart';
|
||||||
|
import '../../common/default_video_insert.dart';
|
||||||
|
import '../quill_simple_toolbar_api.dart';
|
||||||
|
import 'camera_types.dart';
|
||||||
|
import 'config/camera_config.dart';
|
||||||
|
import 'select_camera_action.dart';
|
||||||
|
|
||||||
|
// ignore: invalid_use_of_internal_member
|
||||||
|
class QuillToolbarCameraButton extends QuillToolbarBaseButtonStateless {
|
||||||
|
const QuillToolbarCameraButton({
|
||||||
|
required super.controller,
|
||||||
|
QuillToolbarCameraButtonOptions? options,
|
||||||
|
|
||||||
|
/// Shares common options between all buttons, prefer the [options]
|
||||||
|
/// over the [baseOptions].
|
||||||
|
super.baseOptions,
|
||||||
|
super.key,
|
||||||
|
}) : _options = options,
|
||||||
|
super(options: options);
|
||||||
|
|
||||||
|
final QuillToolbarCameraButtonOptions? _options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
QuillToolbarCameraButtonOptions? get options => _options;
|
||||||
|
|
||||||
|
void _sharedOnPressed(BuildContext context) {
|
||||||
|
_onPressedHandler(
|
||||||
|
context,
|
||||||
|
controller,
|
||||||
|
);
|
||||||
|
afterButtonPressed(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CameraAction?> _getCameraAction(BuildContext context) async {
|
||||||
|
final customCallback = options?.cameraConfig?.onRequestCameraActionCallback;
|
||||||
|
if (customCallback != null) {
|
||||||
|
return await customCallback(context);
|
||||||
|
}
|
||||||
|
final cameraAction = await showSelectCameraActionDialog(
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
|
||||||
|
return cameraAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onPressedHandler(
|
||||||
|
BuildContext context,
|
||||||
|
QuillController controller,
|
||||||
|
) async {
|
||||||
|
final cameraAction = await _getCameraAction(context);
|
||||||
|
|
||||||
|
if (cameraAction == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (cameraAction) {
|
||||||
|
case CameraAction.video:
|
||||||
|
final videoFile =
|
||||||
|
await ImagePicker().pickVideo(source: ImageSource.camera);
|
||||||
|
if (videoFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handleVideoInsert(
|
||||||
|
videoFile.path,
|
||||||
|
controller: controller,
|
||||||
|
onVideoInsertCallback: options?.cameraConfig?.onVideoInsertCallback,
|
||||||
|
onVideoInsertedCallback:
|
||||||
|
options?.cameraConfig?.onVideoInsertedCallback,
|
||||||
|
);
|
||||||
|
case CameraAction.image:
|
||||||
|
final imageFile =
|
||||||
|
await ImagePicker().pickImage(source: ImageSource.camera);
|
||||||
|
if (imageFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handleImageInsert(
|
||||||
|
imageFile.path,
|
||||||
|
controller: controller,
|
||||||
|
onImageInsertCallback: options?.cameraConfig?.onImageInsertCallback,
|
||||||
|
onImageInsertedCallback:
|
||||||
|
options?.cameraConfig?.onImageInsertedCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildButton(BuildContext context) {
|
||||||
|
return QuillToolbarIconButton(
|
||||||
|
icon: Icon(
|
||||||
|
iconData(context),
|
||||||
|
size: iconButtonFactor(context) * iconSize(context),
|
||||||
|
),
|
||||||
|
tooltip: tooltip(context),
|
||||||
|
isSelected: false,
|
||||||
|
onPressed: () => _sharedOnPressed(context),
|
||||||
|
iconTheme: iconTheme(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||||
|
return childBuilder?.call(
|
||||||
|
QuillToolbarCameraButtonOptions(
|
||||||
|
afterButtonPressed: afterButtonPressed(context),
|
||||||
|
iconData: iconData(context),
|
||||||
|
iconSize: iconSize(context),
|
||||||
|
iconButtonFactor: iconButtonFactor(context),
|
||||||
|
iconTheme: options?.iconTheme,
|
||||||
|
tooltip: tooltip(context),
|
||||||
|
cameraConfig: options?.cameraConfig,
|
||||||
|
),
|
||||||
|
QuillToolbarCameraButtonExtraOptions(
|
||||||
|
controller: controller,
|
||||||
|
context: context,
|
||||||
|
onPressed: () => _sharedOnPressed(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||||
|
(context) => Icons.photo_camera;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String Function(BuildContext context) get getDefaultTooltip =>
|
||||||
|
(context) => context.loc.camera;
|
||||||
|
}
|
||||||
39
lib/Screens/myTemplates/src/toolbar/camera/camera_types.dart
Normal file
39
lib/Screens/myTemplates/src/toolbar/camera/camera_types.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/widgets.dart' show BuildContext;
|
||||||
|
import 'package:meta/meta.dart' show immutable;
|
||||||
|
|
||||||
|
import '../../editor/image/image_embed_types.dart';
|
||||||
|
import '../video/config/video.dart';
|
||||||
|
|
||||||
|
enum CameraAction {
|
||||||
|
video,
|
||||||
|
image,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the user click the camera button, should we take a photo or record
|
||||||
|
/// a video using the camera
|
||||||
|
///
|
||||||
|
/// by default will show a dialog that ask the user which option he/she wants
|
||||||
|
typedef OnRequestCameraActionCallback = Future<CameraAction?> Function(
|
||||||
|
BuildContext context,
|
||||||
|
);
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class QuillToolbarCameraConfig {
|
||||||
|
const QuillToolbarCameraConfig({
|
||||||
|
this.onRequestCameraActionCallback,
|
||||||
|
this.onImageInsertCallback,
|
||||||
|
this.onImageInsertedCallback,
|
||||||
|
this.onVideoInsertedCallback,
|
||||||
|
this.onVideoInsertCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
final OnRequestCameraActionCallback? onRequestCameraActionCallback;
|
||||||
|
|
||||||
|
final OnImageInsertedCallback? onImageInsertedCallback;
|
||||||
|
|
||||||
|
final OnImageInsertCallback? onImageInsertCallback;
|
||||||
|
|
||||||
|
final OnVideoInsertedCallback? onVideoInsertedCallback;
|
||||||
|
|
||||||
|
final OnVideoInsertCallback? onVideoInsertCallback;
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
|
import '../camera_types.dart';
|
||||||
|
|
||||||
|
class QuillToolbarCameraButtonExtraOptions
|
||||||
|
extends QuillToolbarBaseButtonExtraOptions {
|
||||||
|
const QuillToolbarCameraButtonExtraOptions({
|
||||||
|
required super.controller,
|
||||||
|
required super.context,
|
||||||
|
required super.onPressed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||||
|
QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> {
|
||||||
|
const QuillToolbarCameraButtonOptions({
|
||||||
|
this.cameraConfig,
|
||||||
|
super.iconSize,
|
||||||
|
super.iconButtonFactor,
|
||||||
|
super.iconData,
|
||||||
|
super.afterButtonPressed,
|
||||||
|
super.tooltip,
|
||||||
|
super.iconTheme,
|
||||||
|
super.childBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final QuillToolbarCameraConfig? cameraConfig;
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
import 'camera_types.dart';
|
||||||
|
|
||||||
|
class SelectCameraActionDialog extends StatelessWidget {
|
||||||
|
const SelectCameraActionDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 150,
|
||||||
|
width: double.infinity,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.photo),
|
||||||
|
subtitle: Text(
|
||||||
|
context.loc.takeAPhotoUsingYourCamera,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.photo_sharp),
|
||||||
|
enabled: !isDesktopApp,
|
||||||
|
onTap: () => Navigator.of(context).pop(CameraAction.image),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.video),
|
||||||
|
subtitle: Text(
|
||||||
|
context.loc.recordAVideoUsingYourCamera,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.camera),
|
||||||
|
enabled: !isDesktopApp,
|
||||||
|
onTap: () => Navigator.of(context).pop(CameraAction.video),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CameraAction?> showSelectCameraActionDialog({
|
||||||
|
required BuildContext context,
|
||||||
|
}) async {
|
||||||
|
final imageSource = await showModalBottomSheet<CameraAction>(
|
||||||
|
showDragHandle: true,
|
||||||
|
context: context,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
builder: (context) => const SelectCameraActionDialog(),
|
||||||
|
);
|
||||||
|
return imageSource;
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:meta/meta.dart' show immutable;
|
||||||
|
|
||||||
|
import '../../../editor/image/image_embed_types.dart';
|
||||||
|
|
||||||
|
class QuillToolbarImageButtonExtraOptions
|
||||||
|
extends QuillToolbarBaseButtonExtraOptions {
|
||||||
|
const QuillToolbarImageButtonExtraOptions({
|
||||||
|
required super.controller,
|
||||||
|
required super.context,
|
||||||
|
required super.onPressed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||||
|
QuillToolbarImageButtonOptions, QuillToolbarImageButtonExtraOptions> {
|
||||||
|
const QuillToolbarImageButtonOptions({
|
||||||
|
super.iconData,
|
||||||
|
super.iconSize,
|
||||||
|
super.iconButtonFactor,
|
||||||
|
|
||||||
|
/// specifies the tooltip text for the image button.
|
||||||
|
super.tooltip,
|
||||||
|
super.afterButtonPressed,
|
||||||
|
super.childBuilder,
|
||||||
|
super.iconTheme,
|
||||||
|
this.dialogTheme,
|
||||||
|
this.linkRegExp,
|
||||||
|
this.imageButtonConfig = const QuillToolbarImageConfig(),
|
||||||
|
});
|
||||||
|
|
||||||
|
final QuillDialogTheme? dialogTheme;
|
||||||
|
|
||||||
|
/// [imageLinkRegExp] is a regular expression to identify image links.
|
||||||
|
final RegExp? linkRegExp;
|
||||||
|
|
||||||
|
final QuillToolbarImageConfig? imageButtonConfig;
|
||||||
|
}
|
||||||
135
lib/Screens/myTemplates/src/toolbar/image/image_button.dart
Normal file
135
lib/Screens/myTemplates/src/toolbar/image/image_button.dart
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import '../../common/default_image_insert.dart';
|
||||||
|
import '../../common/image_video_utils.dart';
|
||||||
|
import '../../editor/image/image_embed_types.dart';
|
||||||
|
import '../quill_simple_toolbar_api.dart';
|
||||||
|
import 'config/image_config.dart';
|
||||||
|
import 'select_image_source.dart';
|
||||||
|
|
||||||
|
// ignore: invalid_use_of_internal_member
|
||||||
|
class QuillToolbarImageButton extends QuillToolbarBaseButtonStateless {
|
||||||
|
const QuillToolbarImageButton({
|
||||||
|
required super.controller,
|
||||||
|
QuillToolbarImageButtonOptions? options,
|
||||||
|
|
||||||
|
/// Shares common options between all buttons, prefer the [options]
|
||||||
|
/// over the [baseOptions].
|
||||||
|
super.baseOptions,
|
||||||
|
super.key,
|
||||||
|
}) : _options = options,
|
||||||
|
super(options: options);
|
||||||
|
|
||||||
|
final QuillToolbarImageButtonOptions? _options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
QuillToolbarImageButtonOptions? get options => _options;
|
||||||
|
|
||||||
|
void _sharedOnPressed(BuildContext context) {
|
||||||
|
_onPressedHandler(context);
|
||||||
|
afterButtonPressed(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleImageInsert(String imageUrl) async {
|
||||||
|
await handleImageInsert(
|
||||||
|
imageUrl,
|
||||||
|
controller: controller,
|
||||||
|
onImageInsertCallback: options?.imageButtonConfig?.onImageInsertCallback,
|
||||||
|
onImageInsertedCallback:
|
||||||
|
options?.imageButtonConfig?.onImageInsertedCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onPressedHandler(BuildContext context) async {
|
||||||
|
final onRequestPickImage = options?.imageButtonConfig?.onRequestPickImage;
|
||||||
|
if (onRequestPickImage != null) {
|
||||||
|
final imageUrl = await onRequestPickImage(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
if (imageUrl != null) {
|
||||||
|
await _handleImageInsert(imageUrl);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final source = await showSelectImageSourceDialog(
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
if (source == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final imageUrl = switch (source) {
|
||||||
|
InsertImageSource.gallery =>
|
||||||
|
(await ImagePicker().pickImage(source: ImageSource.gallery))?.path,
|
||||||
|
InsertImageSource.link =>
|
||||||
|
context.mounted ? await _typeLink(context) : null,
|
||||||
|
InsertImageSource.camera =>
|
||||||
|
(await ImagePicker().pickImage(source: ImageSource.camera))?.path,
|
||||||
|
};
|
||||||
|
if (imageUrl == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (imageUrl.trim().isNotEmpty) {
|
||||||
|
await _handleImageInsert(imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _typeLink(BuildContext context) async {
|
||||||
|
final value = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => TypeLinkDialog(
|
||||||
|
dialogTheme: options?.dialogTheme,
|
||||||
|
linkRegExp: options?.linkRegExp,
|
||||||
|
linkType: LinkType.image,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildButton(BuildContext context) {
|
||||||
|
return QuillToolbarIconButton(
|
||||||
|
icon: Icon(
|
||||||
|
iconData(context),
|
||||||
|
size: iconButtonFactor(context) * iconSize(context),
|
||||||
|
),
|
||||||
|
tooltip: tooltip(context),
|
||||||
|
isSelected: false,
|
||||||
|
onPressed: () => _sharedOnPressed(context),
|
||||||
|
iconTheme: iconTheme(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||||
|
return childBuilder?.call(
|
||||||
|
QuillToolbarImageButtonOptions(
|
||||||
|
afterButtonPressed: afterButtonPressed(context),
|
||||||
|
iconData: iconData(context),
|
||||||
|
iconSize: iconSize(context),
|
||||||
|
iconButtonFactor: iconButtonFactor(context),
|
||||||
|
dialogTheme: options?.dialogTheme,
|
||||||
|
iconTheme: options?.iconTheme,
|
||||||
|
linkRegExp: options?.linkRegExp,
|
||||||
|
tooltip: tooltip(context),
|
||||||
|
imageButtonConfig: options?.imageButtonConfig,
|
||||||
|
),
|
||||||
|
QuillToolbarImageButtonExtraOptions(
|
||||||
|
context: context,
|
||||||
|
controller: controller,
|
||||||
|
onPressed: () => _sharedOnPressed(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||||
|
(context) => Icons.image;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String Function(BuildContext context) get getDefaultTooltip =>
|
||||||
|
(context) => context.loc.insertImage;
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
import '../../editor/image/image_embed_types.dart';
|
||||||
|
|
||||||
|
class SelectImageSourceDialog extends StatelessWidget {
|
||||||
|
const SelectImageSourceDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: 200),
|
||||||
|
width: double.infinity,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.gallery),
|
||||||
|
subtitle: Text(
|
||||||
|
context.loc.pickAPhotoFromYourGallery,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.photo_sharp),
|
||||||
|
onTap: () => Navigator.of(context).pop(InsertImageSource.gallery),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.camera),
|
||||||
|
subtitle: Text(
|
||||||
|
context.loc.takeAPhotoUsingYourCamera,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.camera),
|
||||||
|
enabled: !isDesktopApp,
|
||||||
|
onTap: () => Navigator.of(context).pop(InsertImageSource.camera),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.link),
|
||||||
|
subtitle: Text(
|
||||||
|
context.loc.pasteAPhotoUsingALink,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.link),
|
||||||
|
onTap: () => Navigator.of(context).pop(InsertImageSource.link),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<InsertImageSource?> showSelectImageSourceDialog({
|
||||||
|
required BuildContext context,
|
||||||
|
}) async {
|
||||||
|
final imageSource = await showModalBottomSheet<InsertImageSource>(
|
||||||
|
showDragHandle: true,
|
||||||
|
context: context,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
builder: (_) => const SelectImageSourceDialog(),
|
||||||
|
);
|
||||||
|
return imageSource;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
/// APIs that are meant to be used by the `flutter_quil_extensions` only.
|
||||||
|
///
|
||||||
|
/// Breaking changes can be introduced from `flutter_quill` in minor versions,
|
||||||
|
/// the `flutter_quill_extensions` will be updated and published at the same time.
|
||||||
|
///
|
||||||
|
/// Update both packages and use the same version for compatibility by running `flutter pub upgrade`.
|
||||||
|
@internal
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
export 'package:flutter_quill/src/toolbar/base_button/stateless_base_button.dart';
|
||||||
50
lib/Screens/myTemplates/src/toolbar/video/config/video.dart
Normal file
50
lib/Screens/myTemplates/src/toolbar/video/config/video.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/widgets.dart' show BuildContext;
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:meta/meta.dart' show immutable;
|
||||||
|
|
||||||
|
/// When request picking an video, for example when the video button toolbar
|
||||||
|
/// clicked, it should be null in case the user didn't choose any video or
|
||||||
|
/// any other reasons, and it should be the video file path as string that is
|
||||||
|
/// exists in case the user picked the video successfully
|
||||||
|
///
|
||||||
|
/// by default we already have a default implementation that show a dialog
|
||||||
|
/// request the source for picking the video, from gallery, link or camera
|
||||||
|
typedef OnRequestPickVideo = Future<String?> Function(
|
||||||
|
BuildContext context,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A callback will called when inserting a video in the editor
|
||||||
|
/// it have the logic that will insert the video block using the controller
|
||||||
|
typedef OnVideoInsertCallback = Future<void> Function(
|
||||||
|
String video,
|
||||||
|
QuillController controller,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// When a new video picked this callback will called and you might want to
|
||||||
|
/// do some logic depending on your use case
|
||||||
|
typedef OnVideoInsertedCallback = Future<void> Function(
|
||||||
|
String video,
|
||||||
|
);
|
||||||
|
|
||||||
|
enum InsertVideoSource {
|
||||||
|
gallery,
|
||||||
|
camera,
|
||||||
|
link,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configurations for dealing with videos, on insert a video
|
||||||
|
/// on request picking a video
|
||||||
|
@immutable
|
||||||
|
class QuillToolbarVideoConfig {
|
||||||
|
const QuillToolbarVideoConfig({
|
||||||
|
this.onRequestPickVideo,
|
||||||
|
this.onVideoInsertedCallback,
|
||||||
|
this.onVideoInsertCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
final OnRequestPickVideo? onRequestPickVideo;
|
||||||
|
|
||||||
|
final OnVideoInsertedCallback? onVideoInsertedCallback;
|
||||||
|
|
||||||
|
final OnVideoInsertCallback? onVideoInsertCallback;
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
|
||||||
|
import 'video.dart';
|
||||||
|
|
||||||
|
class QuillToolbarVideoButtonExtraOptions
|
||||||
|
extends QuillToolbarBaseButtonExtraOptions {
|
||||||
|
const QuillToolbarVideoButtonExtraOptions({
|
||||||
|
required super.controller,
|
||||||
|
required super.context,
|
||||||
|
required super.onPressed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||||
|
QuillToolbarVideoButtonOptions, QuillToolbarVideoButtonExtraOptions> {
|
||||||
|
const QuillToolbarVideoButtonOptions({
|
||||||
|
this.linkRegExp,
|
||||||
|
this.dialogTheme,
|
||||||
|
super.iconSize,
|
||||||
|
super.iconButtonFactor,
|
||||||
|
super.iconData,
|
||||||
|
super.afterButtonPressed,
|
||||||
|
super.tooltip,
|
||||||
|
super.iconTheme,
|
||||||
|
super.childBuilder,
|
||||||
|
this.videoConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
final RegExp? linkRegExp;
|
||||||
|
final QuillDialogTheme? dialogTheme;
|
||||||
|
final QuillToolbarVideoConfig? videoConfig;
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
import 'config/video.dart';
|
||||||
|
|
||||||
|
class SelectVideoSourceDialog extends StatelessWidget {
|
||||||
|
const SelectVideoSourceDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: 200),
|
||||||
|
width: double.infinity,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.gallery),
|
||||||
|
subtitle: Text(
|
||||||
|
context.loc.pickAVideoFromYourGallery,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.photo_sharp),
|
||||||
|
onTap: () => Navigator.of(context).pop(InsertVideoSource.gallery),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.camera),
|
||||||
|
subtitle: Text(context.loc.recordAVideoUsingYourCamera),
|
||||||
|
leading: const Icon(Icons.camera),
|
||||||
|
enabled: !isDesktopApp,
|
||||||
|
onTap: () => Navigator.of(context).pop(InsertVideoSource.camera),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.loc.link),
|
||||||
|
subtitle: Text(
|
||||||
|
context.loc.pasteAVideoUsingALink,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.link),
|
||||||
|
onTap: () => Navigator.of(context).pop(InsertVideoSource.link),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<InsertVideoSource?> showSelectVideoSourceDialog({
|
||||||
|
required BuildContext context,
|
||||||
|
}) async {
|
||||||
|
final imageSource = await showModalBottomSheet<InsertVideoSource>(
|
||||||
|
showDragHandle: true,
|
||||||
|
context: context,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
builder: (context) => const SelectVideoSourceDialog(),
|
||||||
|
);
|
||||||
|
return imageSource;
|
||||||
|
}
|
||||||
134
lib/Screens/myTemplates/src/toolbar/video/video_button.dart
Normal file
134
lib/Screens/myTemplates/src/toolbar/video/video_button.dart
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:flutter_quill/internal.dart';
|
||||||
|
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import '../../common/default_video_insert.dart';
|
||||||
|
import '../../common/image_video_utils.dart';
|
||||||
|
import '../quill_simple_toolbar_api.dart';
|
||||||
|
|
||||||
|
import 'config/video.dart';
|
||||||
|
import 'config/video_config.dart';
|
||||||
|
import 'select_video_source.dart';
|
||||||
|
|
||||||
|
// ignore: invalid_use_of_internal_member
|
||||||
|
class QuillToolbarVideoButton extends QuillToolbarBaseButtonStateless {
|
||||||
|
const QuillToolbarVideoButton({
|
||||||
|
required super.controller,
|
||||||
|
QuillToolbarVideoButtonOptions? options,
|
||||||
|
|
||||||
|
/// Shares common options between all buttons, prefer the [options]
|
||||||
|
/// over the [baseOptions].
|
||||||
|
super.baseOptions,
|
||||||
|
super.key,
|
||||||
|
}) : _options = options,
|
||||||
|
super(options: options);
|
||||||
|
|
||||||
|
final QuillToolbarVideoButtonOptions? _options;
|
||||||
|
|
||||||
|
@override
|
||||||
|
QuillToolbarVideoButtonOptions? get options => _options;
|
||||||
|
|
||||||
|
void _sharedOnPressed(BuildContext context) {
|
||||||
|
_onPressedHandler(context);
|
||||||
|
afterButtonPressed(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleVideoInsert(String videoUrl) async {
|
||||||
|
await handleVideoInsert(
|
||||||
|
videoUrl,
|
||||||
|
controller: controller,
|
||||||
|
onVideoInsertCallback: options?.videoConfig?.onVideoInsertCallback,
|
||||||
|
onVideoInsertedCallback: options?.videoConfig?.onVideoInsertedCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onPressedHandler(BuildContext context) async {
|
||||||
|
final onRequestPickVideo = options?.videoConfig?.onRequestPickVideo;
|
||||||
|
if (onRequestPickVideo != null) {
|
||||||
|
final videoUrl = await onRequestPickVideo(context);
|
||||||
|
if (videoUrl != null) {
|
||||||
|
await _handleVideoInsert(videoUrl);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final imageSource = await showSelectVideoSourceDialog(context: context);
|
||||||
|
|
||||||
|
if (imageSource == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final videoUrl = switch (imageSource) {
|
||||||
|
InsertVideoSource.gallery =>
|
||||||
|
(await ImagePicker().pickVideo(source: ImageSource.gallery))?.path,
|
||||||
|
InsertVideoSource.camera =>
|
||||||
|
(await ImagePicker().pickVideo(source: ImageSource.camera))?.path,
|
||||||
|
InsertVideoSource.link =>
|
||||||
|
context.mounted ? await _typeLink(context) : null,
|
||||||
|
};
|
||||||
|
if (videoUrl == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoUrl.trim().isNotEmpty) {
|
||||||
|
_handleVideoInsert(videoUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _typeLink(BuildContext context) async {
|
||||||
|
final value = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => TypeLinkDialog(
|
||||||
|
dialogTheme: options?.dialogTheme,
|
||||||
|
linkType: LinkType.video,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildButton(BuildContext context) {
|
||||||
|
return QuillToolbarIconButton(
|
||||||
|
icon: Icon(
|
||||||
|
iconData(context),
|
||||||
|
size: iconSize(context) * iconButtonFactor(context),
|
||||||
|
),
|
||||||
|
tooltip: tooltip(context),
|
||||||
|
isSelected: false,
|
||||||
|
onPressed: () => _sharedOnPressed(context),
|
||||||
|
iconTheme: iconTheme(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||||
|
return childBuilder?.call(
|
||||||
|
QuillToolbarVideoButtonOptions(
|
||||||
|
afterButtonPressed: afterButtonPressed(context),
|
||||||
|
iconData: iconData(context),
|
||||||
|
dialogTheme: options?.dialogTheme,
|
||||||
|
iconSize: iconSize(context),
|
||||||
|
iconButtonFactor: iconButtonFactor(context),
|
||||||
|
linkRegExp: options?.linkRegExp,
|
||||||
|
tooltip: tooltip(context),
|
||||||
|
iconTheme: options?.iconTheme,
|
||||||
|
videoConfig: options?.videoConfig,
|
||||||
|
),
|
||||||
|
QuillToolbarVideoButtonExtraOptions(
|
||||||
|
context: context,
|
||||||
|
controller: controller,
|
||||||
|
onPressed: () => _sharedOnPressed(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||||
|
(context) => Icons.movie_creation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String Function(BuildContext context) get getDefaultTooltip =>
|
||||||
|
(context) => context.loc.insertVideo;
|
||||||
|
}
|
||||||
129
lib/Screens/myTemplates/template.dart
Normal file
129
lib/Screens/myTemplates/template.dart
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io' as io show Directory, File;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_quill/quill_delta.dart';
|
||||||
|
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:responsive_builder/responsive_builder.dart';
|
||||||
|
|
||||||
|
import '../../routes/custom_appBar.dart';
|
||||||
|
import '../../routes/custom_drawer.dart';
|
||||||
|
import '../../services/apiService.dart';
|
||||||
|
|
||||||
|
/// Custom Embed Definition
|
||||||
|
class TimeStampEmbed {
|
||||||
|
static const String type = 'timeStamp';
|
||||||
|
}
|
||||||
|
|
||||||
|
class Template extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
TemplateState createState() => TemplateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TemplateState extends State<Template> {
|
||||||
|
final ApiService apiService = ApiService();
|
||||||
|
final QuillController _controller = QuillController.basic();
|
||||||
|
final FocusNode _editorFocusNode = FocusNode();
|
||||||
|
final ScrollController _editorScrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.document = Document();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _insertTimeStamp() {
|
||||||
|
final timestamp = DateTime.now().toIso8601String();
|
||||||
|
// Insert embed at the current selection
|
||||||
|
final delta = Delta()..insert({TimeStampEmbed.type: {'value': timestamp}});
|
||||||
|
_controller.compose(delta, _controller.selection, ChangeSource.local);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||||
|
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFf5f5f5),
|
||||||
|
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||||
|
drawer: CustomDrawer(isDesktop: false),
|
||||||
|
body: Padding(
|
||||||
|
padding: isDesktop
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: MediaQuery.of(context).size.width * 0.1,
|
||||||
|
)
|
||||||
|
: EdgeInsets.all(0),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
QuillSimpleToolbar(
|
||||||
|
controller: _controller,
|
||||||
|
config: QuillSimpleToolbarConfig(
|
||||||
|
embedButtons: FlutterQuillEmbeds.toolbarButtons(),
|
||||||
|
showClipboardPaste: true,
|
||||||
|
customButtons: [
|
||||||
|
QuillToolbarCustomButtonOptions(
|
||||||
|
icon: const Icon(Icons.access_time),
|
||||||
|
onPressed: _insertTimeStamp,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: QuillEditor(
|
||||||
|
focusNode: _editorFocusNode,
|
||||||
|
scrollController: _editorScrollController,
|
||||||
|
controller: _controller,
|
||||||
|
config: QuillEditorConfig(
|
||||||
|
placeholder: 'Start writing your notes...',
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
embedBuilders: [
|
||||||
|
...FlutterQuillEmbeds.editorBuilders(),
|
||||||
|
TimeStampEmbedBuilder(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Embed Builder for TimeStamp
|
||||||
|
class TimeStampEmbedBuilder extends EmbedBuilder {
|
||||||
|
@override
|
||||||
|
String get key => TimeStampEmbed.type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, EmbedContext embedContext) {
|
||||||
|
final data = embedContext.node.value.data as Map<String, dynamic>;
|
||||||
|
final String time = data['value'] ?? '';
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.access_time_rounded, size: 18),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
time,
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toPlainText(Embed node) {
|
||||||
|
final data = node.value.data as Map<String, dynamic>;
|
||||||
|
return '[${data['value'] ?? ''}]';
|
||||||
|
}
|
||||||
|
}
|
||||||
824
lib/Screens/myTemplates/templatesList.dart
Normal file
824
lib/Screens/myTemplates/templatesList.dart
Normal file
@ -0,0 +1,824 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:responsive_builder/responsive_builder.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../config/apiUrl.dart';
|
||||||
|
import '../../routes/custom_appBar.dart';
|
||||||
|
import '../../routes/custom_drawer.dart';
|
||||||
|
import '../../services/apiService.dart';
|
||||||
|
import '../../utils/auth_utils.dart';
|
||||||
|
import '../../utils/pagination.dart';
|
||||||
|
import '../../widgets/popup_userList_action.dart';
|
||||||
|
|
||||||
|
class TemplatesList extends StatefulWidget {
|
||||||
|
const TemplatesList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
TemplatesListState createState() => TemplatesListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TemplatesListState extends State<TemplatesList> {
|
||||||
|
// final GlobalKey<TemplatesListState> forexListKey =
|
||||||
|
// GlobalKey<TemplatesListState>();
|
||||||
|
|
||||||
|
final ApiService apiService = ApiService();
|
||||||
|
late Future<List<dynamic>> futureTemplates;
|
||||||
|
|
||||||
|
late Map<String, dynamic> userSingleData;
|
||||||
|
List<dynamic>? apiCountryData;
|
||||||
|
String? selectedUserId;
|
||||||
|
String? orgId;
|
||||||
|
|
||||||
|
Color? layoutColor;
|
||||||
|
Color? bodyColor;
|
||||||
|
|
||||||
|
List allTemplate = [];
|
||||||
|
List filteredTemplates = [];
|
||||||
|
TextEditingController searchController = TextEditingController();
|
||||||
|
|
||||||
|
int currentPage = 0;
|
||||||
|
int itemsPerPage = 10;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
futureTemplates = fetchGetForex();
|
||||||
|
|
||||||
|
futureTemplates.then((users) {
|
||||||
|
setState(() {
|
||||||
|
allTemplate = users;
|
||||||
|
print("AlL tEMPLATESNIT - $allTemplate");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
loadInitialData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// futurePlans = fetchPlans();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> refreshData() {
|
||||||
|
print("Calling Refresh Data");
|
||||||
|
|
||||||
|
futureTemplates = fetchGetForex();
|
||||||
|
|
||||||
|
return futureTemplates.then((users) {
|
||||||
|
setState(() {
|
||||||
|
allTemplate = users;
|
||||||
|
});
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadInitialData() async {
|
||||||
|
String? layoutString = await getLayoutColor();
|
||||||
|
String? bodyStringColor = await getBodyColor();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
layoutColor = layoutString != null
|
||||||
|
? Color(int.parse(layoutString))
|
||||||
|
: Colors.redAccent;
|
||||||
|
|
||||||
|
bodyColor = bodyStringColor != null
|
||||||
|
? Color(int.parse(bodyStringColor))
|
||||||
|
: Colors.white;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatTemplateName(String input) {
|
||||||
|
return input
|
||||||
|
.split('_') // split by underscore
|
||||||
|
.map((word) => word.isNotEmpty
|
||||||
|
? '${word[0].toUpperCase()}${word.substring(1)}'
|
||||||
|
: '')
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPlaceholderNames(String? raw) {
|
||||||
|
if (raw == null || raw.isEmpty) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
final List<dynamic> decoded = json.decode(raw);
|
||||||
|
final List<String> values = decoded
|
||||||
|
.map((e) => e['value'].toString().replaceAll('%', ''))
|
||||||
|
.toList();
|
||||||
|
return values.join(', ');
|
||||||
|
} catch (e) {
|
||||||
|
return 'Invalid placeholder';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getToken() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getString('auth_token');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> fetchGetForex() async {
|
||||||
|
orgId = await getOrgId();
|
||||||
|
// final String apiUrlData = '$apiUrl/api/getForexPerdiemList';
|
||||||
|
final String apiUrlData = '$apiUrl/api/template?org_id=${orgId}';
|
||||||
|
|
||||||
|
final String? token = await getToken();
|
||||||
|
|
||||||
|
print("Fetch Users");
|
||||||
|
print("TOEKRWE: $token");
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
throw Exception('Token not found. Please log in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse(apiUrlData),
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
|
||||||
|
print("TemplateDATA--- $data");
|
||||||
|
return data['data']; // Returning raw JSON list
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to load users');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDelete(userId) {
|
||||||
|
print("handDel - $userId");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> createTemplateData(
|
||||||
|
Map<String, dynamic> userData, String userId, String newStatus) async {
|
||||||
|
final uri = Uri.parse('$apiUrl/api/users/update/$userId');
|
||||||
|
|
||||||
|
final String? token = await getToken();
|
||||||
|
if (token == null) {
|
||||||
|
throw Exception('Token not found. Please log in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use MultipartRequest (POST only)
|
||||||
|
final request = http.MultipartRequest('POST', uri);
|
||||||
|
request.headers['Authorization'] = 'Bearer $token';
|
||||||
|
|
||||||
|
// If updating, spoof the method Laravel-style
|
||||||
|
|
||||||
|
request.fields['_method'] = 'PUT';
|
||||||
|
request.fields['user_id'] = userId;
|
||||||
|
|
||||||
|
print("STatus 2 - $newStatus");
|
||||||
|
|
||||||
|
// Add all non-null and non-empty user data fields
|
||||||
|
userData.forEach((key, value) {
|
||||||
|
if (value != null && value.toString().trim().isNotEmpty) {
|
||||||
|
request.fields[key] = value.toString();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
request.fields['is_active'] = newStatus;
|
||||||
|
|
||||||
|
print("🚀 Sending request with fields: ${request.fields}");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final streamedResponse = await request.send();
|
||||||
|
final response = await http.Response.fromStream(streamedResponse);
|
||||||
|
print("Response status: ${response.statusCode}");
|
||||||
|
print("Response body: ${response.body}");
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
print("✅ User Status submitted successfully! ");
|
||||||
|
print("📨 Response: ${response.body}");
|
||||||
|
|
||||||
|
refreshUserList();
|
||||||
|
} else {
|
||||||
|
print("❌ Submission failed. Status: ${response.statusCode}");
|
||||||
|
print("📨 Body: ${response.body}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("🔥 Error submitting user: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleToggleUserStatus(String userId, String currentStatus,
|
||||||
|
Map<String, dynamic> userData) async {
|
||||||
|
print("Toggling user status - $userId (Current: $currentStatus)");
|
||||||
|
|
||||||
|
final String apiUrlData =
|
||||||
|
'$apiUrl/api/users/update/$userId'; // API for updating user
|
||||||
|
final String? token = await getToken();
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
print("Error: Token not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle status: If active ("1"), set to inactive ("0"); otherwise, activate ("1")
|
||||||
|
String newStatus = (currentStatus == "1") ? "0" : "1";
|
||||||
|
|
||||||
|
print("STatus 1 - $newStatus");
|
||||||
|
|
||||||
|
createTemplateData(userData, userId, newStatus);
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// final response = await http.put(
|
||||||
|
// Uri.parse(apiUrlData),
|
||||||
|
// headers: {
|
||||||
|
// 'Authorization': 'Bearer $token',
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// },
|
||||||
|
// body: jsonEncode({
|
||||||
|
// "is_active": newStatus // Set new status dynamically
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// if (response.statusCode == 200) {
|
||||||
|
// print("User status updated successfully to $newStatus!");
|
||||||
|
// refreshUserList(); // Refresh users list after update
|
||||||
|
// } else {
|
||||||
|
// print("Failed to update user status. Status: ${response.statusCode}");
|
||||||
|
// print("Error: ${response.body}");
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// print("Error updating user status: $e");
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh user list after update
|
||||||
|
void refreshUserList() {
|
||||||
|
setState(() {
|
||||||
|
futureTemplates = fetchGetForex(); // Re-fetch users after status update
|
||||||
|
// Wait for futurePlans to be fetched and update allPlans
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void filterTemplates(String query) {
|
||||||
|
print("Filtering by query: $query");
|
||||||
|
final lowerQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
filteredTemplates = allTemplate.where((user) {
|
||||||
|
// Match template_name
|
||||||
|
final templateName =
|
||||||
|
(user['template_name'] ?? '').toString().toLowerCase();
|
||||||
|
final matchesTemplate = templateName.contains(lowerQuery);
|
||||||
|
|
||||||
|
// Match placeholder values
|
||||||
|
final placeholderRaw = user['placeholder'];
|
||||||
|
bool matchesPlaceholder = false;
|
||||||
|
|
||||||
|
if (placeholderRaw != null && placeholderRaw is String) {
|
||||||
|
try {
|
||||||
|
final List<dynamic> decoded = json.decode(placeholderRaw);
|
||||||
|
final List<String> placeholderValues = decoded
|
||||||
|
.map((e) =>
|
||||||
|
e['value'].toString().replaceAll('%', '').toLowerCase())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
matchesPlaceholder =
|
||||||
|
placeholderValues.any((value) => value.contains(lowerQuery));
|
||||||
|
} catch (e) {
|
||||||
|
// ignore invalid placeholder format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchesTemplate || matchesPlaceholder;
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
|
||||||
|
print("Filtered results: $filteredTemplates");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||||
|
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Color(0xFFf5f5f5),
|
||||||
|
// appBar: isDesktop ? null : const CustomAppBar(title: 'User Management'),
|
||||||
|
// drawer: isDesktop ? null : CustomDrawer(isDesktop: false),
|
||||||
|
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||||
|
drawer: CustomDrawer(isDesktop: false),
|
||||||
|
body: Padding(
|
||||||
|
padding: isDesktop
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: MediaQuery.of(context).size.width *
|
||||||
|
0.1, // 30% of screen width as horizontal padding
|
||||||
|
vertical: MediaQuery.of(context).size.height *
|
||||||
|
0, // 5% of screen height as vertical padding
|
||||||
|
)
|
||||||
|
: EdgeInsets.all(0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||||
|
// const Expanded(child: Center(child: Text("User Page Content"))),
|
||||||
|
Expanded(child: buildGroupList(isDesktop)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildGroupList(bool isDesktop) {
|
||||||
|
return Container(
|
||||||
|
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||||
|
padding: const EdgeInsets.all(1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||||
|
),
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// // color: Colors.amber,
|
||||||
|
// // color: bodyColor,
|
||||||
|
// color: Color(0xFFE1F5FE),
|
||||||
|
// border: Border.all(
|
||||||
|
// color: Colors.white,
|
||||||
|
// // color: Color(0xFFF7F7FB),
|
||||||
|
// width: 3.5)),
|
||||||
|
child: buildUserTable(isDesktop),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildUserTable(bool isDesktop) {
|
||||||
|
return Container(
|
||||||
|
// margin: isDesktop
|
||||||
|
// ? EdgeInsets.all(10.0)
|
||||||
|
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
|
||||||
|
// padding: const EdgeInsets.all(10),
|
||||||
|
height: isDesktop
|
||||||
|
? MediaQuery.of(context).size.height * 0.98
|
||||||
|
: MediaQuery.of(context).size.height,
|
||||||
|
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Divider(
|
||||||
|
// thickness: 0.2, // how "thick" the line is
|
||||||
|
// color: Colors.grey, // optional
|
||||||
|
// ),
|
||||||
|
Row(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Templates',
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: isDesktop ? 16 : 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isDesktop)
|
||||||
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.23,
|
||||||
|
),
|
||||||
|
|
||||||
|
if (isDesktop)
|
||||||
|
Container(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.2,
|
||||||
|
height: 40,
|
||||||
|
child: TextField(
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: filterTemplates,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search ...",
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: Color(0xFF9E9DBD),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.grey.shade200, width: 0.5),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.grey.shade300, width: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// SizedBox(width: 16),
|
||||||
|
Spacer(),
|
||||||
|
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF114D8B),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
disabledBackgroundColor: Color(0xFF114D8B),
|
||||||
|
disabledForegroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
side:
|
||||||
|
BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 20, vertical: 12),
|
||||||
|
),
|
||||||
|
onPressed: () async {},
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize:
|
||||||
|
MainAxisSize.min, // Ensures content fits nicely
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Add Templates",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: isDesktop ? 13 : 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8), // spacing between icon and text
|
||||||
|
Icon(
|
||||||
|
Icons.add_circle_outline_rounded,
|
||||||
|
size: 15,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (!isDesktop)
|
||||||
|
SizedBox(
|
||||||
|
height: 5,
|
||||||
|
),
|
||||||
|
isDesktop
|
||||||
|
? SizedBox.shrink()
|
||||||
|
: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
height: 35,
|
||||||
|
child: TextField(
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: filterTemplates,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search ...",
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
fontSize: 12, color: Color(0xFF9E9DBD)),
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: Color(0xFF9E9DBD),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.grey.shade200,
|
||||||
|
width: 0.5),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.grey.shade300, width: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
FutureBuilder<List<dynamic>>(
|
||||||
|
future: futureTemplates,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
} else if (snapshot.hasError ||
|
||||||
|
!snapshot.hasData ||
|
||||||
|
snapshot.data!.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// const Icon(Icons.error_outline,
|
||||||
|
// color: Colors.redAccent, size: 60),
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
// Text(
|
||||||
|
// "Oops!",
|
||||||
|
// style: GoogleFonts.poppins(
|
||||||
|
// fontSize: 20,
|
||||||
|
// fontWeight: FontWeight.bold,
|
||||||
|
// color: Colors.redAccent),
|
||||||
|
// ),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"No Templates Available ",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
"Please Create Templates",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 16, color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<dynamic> templates = filteredTemplates.isNotEmpty
|
||||||
|
? filteredTemplates
|
||||||
|
: allTemplate;
|
||||||
|
|
||||||
|
// users.sort((a, b) {
|
||||||
|
// DateTime dateA = DateTime.parse(a['created_on']);
|
||||||
|
// DateTime dateB = DateTime.parse(b['created_on']);
|
||||||
|
//
|
||||||
|
// return dateB
|
||||||
|
// .compareTo(dateA); // Descending: newest first
|
||||||
|
// });
|
||||||
|
|
||||||
|
templates.sort((a, b) {
|
||||||
|
try {
|
||||||
|
DateTime dateA =
|
||||||
|
DateTime.parse(a['created_at'] ?? '2000-01-01');
|
||||||
|
DateTime dateB =
|
||||||
|
DateTime.parse(b['created_at'] ?? '2000-01-01');
|
||||||
|
return dateB.compareTo(dateA);
|
||||||
|
} catch (e) {
|
||||||
|
return 0; // If parsing fails, consider them equal
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
List paginatedTemplates = templates
|
||||||
|
.skip(currentPage * itemsPerPage)
|
||||||
|
.take(itemsPerPage)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Widget table = LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
double minWidth =
|
||||||
|
isDesktop ? constraints.maxWidth : 1300;
|
||||||
|
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: minWidth),
|
||||||
|
child: DataTable(
|
||||||
|
dividerThickness: 0.5,
|
||||||
|
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||||
|
border: TableBorder(
|
||||||
|
horizontalInside: BorderSide(
|
||||||
|
width: 0.5, color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
columns: [
|
||||||
|
DataColumn(
|
||||||
|
label: Text(
|
||||||
|
'Template Name',
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
)),
|
||||||
|
DataColumn(
|
||||||
|
label: Text(
|
||||||
|
'Attributes',
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
)),
|
||||||
|
DataColumn(
|
||||||
|
label: Text(
|
||||||
|
'Actions',
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
rows: paginatedTemplates.map((forex) {
|
||||||
|
String forexId = forex['template_id']
|
||||||
|
.toString(); // Get user ID
|
||||||
|
bool isSelected = selectedUserId == forexId;
|
||||||
|
|
||||||
|
return DataRow(cells: [
|
||||||
|
DataCell(Text(
|
||||||
|
// "${forex['template_name'] ?? ''}",
|
||||||
|
formatTemplateName(
|
||||||
|
forex['template_name'] ?? ''),
|
||||||
|
// Text("{forex['template_name'] ?? ''}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: "Inter",
|
||||||
|
))),
|
||||||
|
DataCell(Text(
|
||||||
|
getPlaceholderNames(forex['placeholder']),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: "Inter",
|
||||||
|
))),
|
||||||
|
DataCell(
|
||||||
|
// UserActionsMenu(
|
||||||
|
// user: forex,
|
||||||
|
// getUserDetails: (id) =>
|
||||||
|
// apiService.getSingleUser(id),
|
||||||
|
// ),
|
||||||
|
GestureDetector(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/IconsImg/edit.png',
|
||||||
|
width: 20,
|
||||||
|
height: 15),
|
||||||
|
onTap: () async {
|
||||||
|
// final userId = getUserId(user['user_id']);
|
||||||
|
// final usersData = await getUserDetails(userId);
|
||||||
|
//
|
||||||
|
final forexId = int.tryParse(
|
||||||
|
forex['forex_perdiem_id']
|
||||||
|
.toString());
|
||||||
|
|
||||||
|
if (forexId != null) {
|
||||||
|
print("ForexId -- $forexId");
|
||||||
|
final data = await apiService
|
||||||
|
.getForexDetailsFind(forexId);
|
||||||
|
print("ForexId -- $data");
|
||||||
|
} else {
|
||||||
|
print("Invalid Forex ID");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget buildMobileCardView(List<dynamic> paginatedUser) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: paginatedUser.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final forex = paginatedUser[index];
|
||||||
|
return Card(
|
||||||
|
color: Colors.white,
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 6),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 3,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Status and Employee Code
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
// forex['template_name'] ?? 'N/A',
|
||||||
|
formatTemplateName(
|
||||||
|
forex['template_name'] ?? ''),
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.black87,
|
||||||
|
fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
UserActionsMenu(
|
||||||
|
user: forex,
|
||||||
|
getUserDetails: (id) =>
|
||||||
|
apiService.getSingleUser(id),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 2),
|
||||||
|
// Trip Id and Trip Name
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
getPlaceholderNames(
|
||||||
|
forex['placeholder']),
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black87,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
// Actions
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: isDesktop
|
||||||
|
? (searchController.text.isNotEmpty &&
|
||||||
|
filteredTemplates.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
"No matches found",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: table,
|
||||||
|
))
|
||||||
|
: (searchController.text.isNotEmpty &&
|
||||||
|
filteredTemplates.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
"No matches found",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: buildMobileCardView(
|
||||||
|
paginatedTemplates)),
|
||||||
|
),
|
||||||
|
// Expanded(
|
||||||
|
// child: isDesktop
|
||||||
|
// ? SingleChildScrollView(
|
||||||
|
// scrollDirection: Axis.vertical,
|
||||||
|
// child: table, // <-- your existing table
|
||||||
|
// )
|
||||||
|
// : buildMobileCardView(paginatedUser),
|
||||||
|
// ),
|
||||||
|
PaginationControls(
|
||||||
|
currentPage: currentPage,
|
||||||
|
itemsPerPage: itemsPerPage,
|
||||||
|
totalItems: templates.length,
|
||||||
|
activeColor: layoutColor, // your theme color
|
||||||
|
onPageChanged: (page) {
|
||||||
|
setState(() {
|
||||||
|
currentPage = page;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onItemsPerPageChanged: (items) {
|
||||||
|
setState(() {
|
||||||
|
itemsPerPage = items;
|
||||||
|
currentPage = 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -94,6 +94,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
String? selectedFirstApprover;
|
String? selectedFirstApprover;
|
||||||
String? selectedSecondApprover;
|
String? selectedSecondApprover;
|
||||||
String? selectedThirdApprover;
|
String? selectedThirdApprover;
|
||||||
|
String? selectedSubstituteApprover;
|
||||||
|
|
||||||
String? selectedFileNames;
|
String? selectedFileNames;
|
||||||
Uint8List? passportDocumentBytes;
|
Uint8List? passportDocumentBytes;
|
||||||
@ -125,6 +126,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
"secondApproval",
|
"secondApproval",
|
||||||
"thirdApproval",
|
"thirdApproval",
|
||||||
"employeeCode",
|
"employeeCode",
|
||||||
|
"delegationStartDate",
|
||||||
|
"delegationEndDate",
|
||||||
"dateOfIssue",
|
"dateOfIssue",
|
||||||
"dateOfExpiry",
|
"dateOfExpiry",
|
||||||
"changePassword"
|
"changePassword"
|
||||||
@ -151,7 +154,8 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
"postal_code": controllers["postalCode"]?.text,
|
"postal_code": controllers["postalCode"]?.text,
|
||||||
"country_code": selectedCountry,
|
"country_code": selectedCountry,
|
||||||
"employee_code": controllers["employeeCode"]?.text,
|
"employee_code": controllers["employeeCode"]?.text,
|
||||||
|
"delegation_start_date": controllers["delegationStartDate"]?.text,
|
||||||
|
"delegation_end_date": controllers["delegationEndDate"]?.text,
|
||||||
"user_type": selectedUserType,
|
"user_type": selectedUserType,
|
||||||
"role_id": selectedRole,
|
"role_id": selectedRole,
|
||||||
"department_id": selectedDepartment,
|
"department_id": selectedDepartment,
|
||||||
@ -161,6 +165,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
"first_approver": selectedFirstApprover,
|
"first_approver": selectedFirstApprover,
|
||||||
"second_approver": selectedSecondApprover,
|
"second_approver": selectedSecondApprover,
|
||||||
"third_approver": selectedThirdApprover,
|
"third_approver": selectedThirdApprover,
|
||||||
|
"delegated_to_user_id": selectedSubstituteApprover,
|
||||||
"passport_number": controllers["passportNumber"]?.text,
|
"passport_number": controllers["passportNumber"]?.text,
|
||||||
"place_of_issue": controllers["placeOfIssue"]?.text,
|
"place_of_issue": controllers["placeOfIssue"]?.text,
|
||||||
"passport_document": passportFile,
|
"passport_document": passportFile,
|
||||||
@ -213,6 +218,12 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
controllers["employeeCode"]?.text =
|
controllers["employeeCode"]?.text =
|
||||||
apiselectedUser?["employee_code"] ?? "";
|
apiselectedUser?["employee_code"] ?? "";
|
||||||
|
|
||||||
|
controllers["delegationStartDate"]?.text =
|
||||||
|
apiselectedUser?["delegation_start_date"] ?? "";
|
||||||
|
|
||||||
|
controllers["delegationEndDate"]?.text =
|
||||||
|
apiselectedUser?["delegation_end_date"] ?? "";
|
||||||
|
|
||||||
controllers["passportNumber"]?.text =
|
controllers["passportNumber"]?.text =
|
||||||
apiselectedUser?["passport_number"] ?? "";
|
apiselectedUser?["passport_number"] ?? "";
|
||||||
|
|
||||||
@ -271,6 +282,15 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
selectedThirdApprover =
|
selectedThirdApprover =
|
||||||
apiselectedUser?["third_approver"]?.toString() ?? "";
|
apiselectedUser?["third_approver"]?.toString() ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiselectedUser?["delegated_to_user_id"] != null) {
|
||||||
|
print(
|
||||||
|
"UPDADele- ${apiselectedUser?["delegated_to_user_id"]?.toString()}");
|
||||||
|
selectedSubstituteApprover =
|
||||||
|
apiselectedUser?["delegated_to_user_id"]?.toString() ?? "";
|
||||||
|
|
||||||
|
print("UPDADele1- $selectedSubstituteApprover");
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
final raw = apiselectedUser!["agent_supported_service_ids"];
|
final raw = apiselectedUser!["agent_supported_service_ids"];
|
||||||
|
|
||||||
@ -315,6 +335,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
prepareForNewEntry();
|
||||||
selectedTab = "personal";
|
selectedTab = "personal";
|
||||||
|
|
||||||
// WidgetsFlutterBinding.ensureInitialized();
|
// WidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -510,6 +531,12 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void prepareForNewEntry() {
|
||||||
|
for (var controller in controllers.values) {
|
||||||
|
controller.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void handleNext() async {
|
void handleNext() async {
|
||||||
print("USR Detail Next");
|
print("USR Detail Next");
|
||||||
printFormData();
|
printFormData();
|
||||||
@ -595,7 +622,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
"last_name",
|
"last_name",
|
||||||
"email",
|
"email",
|
||||||
"mobile_no",
|
"mobile_no",
|
||||||
"employeeCode"
|
// "employeeCode"
|
||||||
];
|
];
|
||||||
|
|
||||||
if (apiselectedUser == null) {
|
if (apiselectedUser == null) {
|
||||||
@ -750,7 +777,9 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
print("Response body: ${response.body}");
|
print("Response body: ${response.body}");
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
dispose();
|
||||||
print("✅ User submitted successfully!");
|
print("✅ User submitted successfully!");
|
||||||
|
|
||||||
print("📨 Response: ${response.body}");
|
print("📨 Response: ${response.body}");
|
||||||
context.go('/listUser');
|
context.go('/listUser');
|
||||||
} else {
|
} else {
|
||||||
@ -973,6 +1002,7 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
selectedFirstApprover: selectedFirstApprover,
|
selectedFirstApprover: selectedFirstApprover,
|
||||||
selectedSecondApprover: selectedSecondApprover,
|
selectedSecondApprover: selectedSecondApprover,
|
||||||
selectedThirdApprover: selectedThirdApprover,
|
selectedThirdApprover: selectedThirdApprover,
|
||||||
|
selectedSubstituteApprover: selectedSubstituteApprover,
|
||||||
onLevelChanged: (gender) {
|
onLevelChanged: (gender) {
|
||||||
setState(() {
|
setState(() {
|
||||||
selectedLevel = gender;
|
selectedLevel = gender;
|
||||||
@ -998,6 +1028,11 @@ class _CreateUserFormDetialsState extends State<CreateUserFormDetials> {
|
|||||||
selectedThirdApprover = role;
|
selectedThirdApprover = role;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onFirstSubsApproverChanged: (role) {
|
||||||
|
setState(() {
|
||||||
|
selectedSubstituteApprover = role;
|
||||||
|
});
|
||||||
|
},
|
||||||
);
|
);
|
||||||
case "travel":
|
case "travel":
|
||||||
return TravellerDetails(
|
return TravellerDetails(
|
||||||
|
|||||||
@ -21,12 +21,14 @@ class OfficeDetails extends StatefulWidget {
|
|||||||
final ValueChanged<String?>? onFirstApproverChanged;
|
final ValueChanged<String?>? onFirstApproverChanged;
|
||||||
final ValueChanged<String?>? onSecondApproverChanged;
|
final ValueChanged<String?>? onSecondApproverChanged;
|
||||||
final ValueChanged<String?>? onThirdApproverChanged;
|
final ValueChanged<String?>? onThirdApproverChanged;
|
||||||
|
final ValueChanged<String?>? onFirstSubsApproverChanged;
|
||||||
|
|
||||||
final String? selectedLevel;
|
final String? selectedLevel;
|
||||||
final String? selectedDepartment;
|
final String? selectedDepartment;
|
||||||
final String? selectedFirstApprover;
|
final String? selectedFirstApprover;
|
||||||
final String? selectedSecondApprover;
|
final String? selectedSecondApprover;
|
||||||
final String? selectedThirdApprover;
|
final String? selectedThirdApprover;
|
||||||
|
final String? selectedSubstituteApprover;
|
||||||
|
|
||||||
const OfficeDetails({
|
const OfficeDetails({
|
||||||
Key? key,
|
Key? key,
|
||||||
@ -44,6 +46,8 @@ class OfficeDetails extends StatefulWidget {
|
|||||||
this.onFirstApproverChanged,
|
this.onFirstApproverChanged,
|
||||||
this.onSecondApproverChanged,
|
this.onSecondApproverChanged,
|
||||||
this.onThirdApproverChanged,
|
this.onThirdApproverChanged,
|
||||||
|
this.selectedSubstituteApprover,
|
||||||
|
this.onFirstSubsApproverChanged,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -57,6 +61,8 @@ class _OfficeDetailsState extends State<OfficeDetails> {
|
|||||||
late List<String> countryCodes; // List of country codes
|
late List<String> countryCodes; // List of country codes
|
||||||
late List<dynamic>? apiCountryData;
|
late List<dynamic>? apiCountryData;
|
||||||
|
|
||||||
|
bool isResetTrue = false;
|
||||||
|
|
||||||
late List<dynamic>? apiCostData;
|
late List<dynamic>? apiCostData;
|
||||||
late List<dynamic>? apiRoleData;
|
late List<dynamic>? apiRoleData;
|
||||||
late List<dynamic>? apiUserData;
|
late List<dynamic>? apiUserData;
|
||||||
@ -84,6 +90,7 @@ class _OfficeDetailsState extends State<OfficeDetails> {
|
|||||||
String? selectedFirstApprover;
|
String? selectedFirstApprover;
|
||||||
String? selectedSecondApprover;
|
String? selectedSecondApprover;
|
||||||
String? selectedThirdApprover;
|
String? selectedThirdApprover;
|
||||||
|
String? selectedSubstituteApprover;
|
||||||
|
|
||||||
String? selectedFileNames;
|
String? selectedFileNames;
|
||||||
Uint8List? passportDocumentBytes;
|
Uint8List? passportDocumentBytes;
|
||||||
@ -139,6 +146,7 @@ class _OfficeDetailsState extends State<OfficeDetails> {
|
|||||||
selectedFirstApprover = widget.selectedFirstApprover;
|
selectedFirstApprover = widget.selectedFirstApprover;
|
||||||
selectedSecondApprover = widget.selectedSecondApprover;
|
selectedSecondApprover = widget.selectedSecondApprover;
|
||||||
selectedThirdApprover = widget.selectedThirdApprover;
|
selectedThirdApprover = widget.selectedThirdApprover;
|
||||||
|
selectedSubstituteApprover = widget.selectedSubstituteApprover;
|
||||||
|
|
||||||
fetchDepartment();
|
fetchDepartment();
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
@ -151,6 +159,27 @@ class _OfficeDetailsState extends State<OfficeDetails> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// void handleReset() {
|
||||||
|
// setState(() {
|
||||||
|
// selectedSubstituteApprover = "";
|
||||||
|
// widget.controllers["delegationEndDate"]?.text = "";
|
||||||
|
// widget.controllers["delegationStartDate"]?.text = "";
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
void handleReset() {
|
||||||
|
setState(() {
|
||||||
|
isResetTrue = true;
|
||||||
|
// selectedSubstituteApprover = null;
|
||||||
|
// widget.onFirstSubsApproverChanged?.call(null);
|
||||||
|
// widget.controllers["delegationStartDate"]?.clear();
|
||||||
|
// widget.controllers["delegationEndDate"]?.clear();
|
||||||
|
//
|
||||||
|
// print("Start Date: ${widget.controllers["delegationStartDate"]?.text}");
|
||||||
|
// print("End Date: ${widget.controllers["delegationEndDate"]?.text}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> fetchUsers() async {
|
Future<void> fetchUsers() async {
|
||||||
try {
|
try {
|
||||||
List<dynamic> users = await apiService.fetchUsers();
|
List<dynamic> users = await apiService.fetchUsers();
|
||||||
@ -224,6 +253,7 @@ class _OfficeDetailsState extends State<OfficeDetails> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 10,
|
height: 10,
|
||||||
@ -234,6 +264,30 @@ class _OfficeDetailsState extends State<OfficeDetails> {
|
|||||||
height: 10,
|
height: 10,
|
||||||
),
|
),
|
||||||
_buildSecondRow(widget.isDesktop),
|
_buildSecondRow(widget.isDesktop),
|
||||||
|
if (widget.isDesktop)
|
||||||
|
SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
thickness: 0.2,
|
||||||
|
color: Colors.blueGrey.shade100,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Delegation",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
_buildThirdRow(widget.isDesktop),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -293,6 +347,38 @@ class _OfficeDetailsState extends State<OfficeDetails> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildThirdRow(bool isDesktop) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.white,
|
||||||
|
child: widget.isDesktop
|
||||||
|
? Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildApproverSubstitute1(isDesktop),
|
||||||
|
Spacer(),
|
||||||
|
buildDelegationStartDateField(isDesktop),
|
||||||
|
Spacer(), // Space after Last Name
|
||||||
|
buildDelegationEndDateField(isDesktop),
|
||||||
|
SizedBox(
|
||||||
|
width: 15,
|
||||||
|
),
|
||||||
|
buildReset(isDesktop)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
buildApproverSubstitute1(isDesktop),
|
||||||
|
SizedBox(height: 8), // Vertical space
|
||||||
|
buildDelegationStartDateField(isDesktop),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
buildDelegationEndDateField(isDesktop),
|
||||||
|
buildReset(isDesktop)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildEmpCodeField() {
|
Widget buildEmpCodeField() {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -848,4 +934,344 @@ class _OfficeDetailsState extends State<OfficeDetails> {
|
|||||||
// ),
|
// ),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildApproverSubstitute1(bool isDesktop) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.white,
|
||||||
|
// child: Expanded(
|
||||||
|
// Allow first column to take available space
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Delegate To",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: Colors.black),
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
CustomTextFieldUserWrapper(
|
||||||
|
isFocused: false,
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: apiUserData == null
|
||||||
|
? Center(
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 0.5,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: DropdownSearch<String>(
|
||||||
|
// selectedItem: userMap[selectedSubstituteApprover],
|
||||||
|
selectedItem: selectedSubstituteApprover != null
|
||||||
|
? userMap[selectedSubstituteApprover]
|
||||||
|
: null,
|
||||||
|
enabled: !widget.isViewMode,
|
||||||
|
popupProps: PopupProps.menu(
|
||||||
|
showSearchBox: true,
|
||||||
|
fit: FlexFit.loose, // Allows flexible height
|
||||||
|
constraints: BoxConstraints(maxHeight: 250),
|
||||||
|
searchFieldProps: TextFieldProps(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search User...",
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: apiUserData!.map((user) {
|
||||||
|
return "${user['first_name']} ${user['last_name']}";
|
||||||
|
}).toList(),
|
||||||
|
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||||
|
dropdownSearchDecoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dropdownBuilder: (context, selectedItem) => Align(
|
||||||
|
// Center-align selected item
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
selectedItem ?? "Select",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// onChanged: (String? newValue) {
|
||||||
|
// setState(() {
|
||||||
|
// selectedFirstApprover = userMap.entries
|
||||||
|
// .firstWhere(
|
||||||
|
// (entry) => entry.value == newValue)
|
||||||
|
// .key;
|
||||||
|
//
|
||||||
|
// // if (selectedCountry!.isNotEmpty) {
|
||||||
|
// // errorMessages.remove("country_code");
|
||||||
|
// // }
|
||||||
|
// });
|
||||||
|
// widget.onFirstApproverChanged?.call(newValue);
|
||||||
|
// },
|
||||||
|
|
||||||
|
onChanged: (String? newValue) {
|
||||||
|
if (newValue == null) return;
|
||||||
|
|
||||||
|
final approverId = userMap.entries
|
||||||
|
.firstWhere(
|
||||||
|
(entry) => entry.value == newValue)
|
||||||
|
.key;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
selectedSubstituteApprover = approverId;
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.onFirstSubsApproverChanged?.call(
|
||||||
|
approverId); // ✅ not newValue, but approverId
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildDelegationStartDateField(bool isDesktop) {
|
||||||
|
DateTime? _selectedCheckOutDate;
|
||||||
|
DateTime? _selectedEndDate;
|
||||||
|
|
||||||
|
Future<void> _selectCheckOutDate(BuildContext context) async {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
DateTime today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
|
// Parse date from notifier if available, else use today
|
||||||
|
DateTime initialDate;
|
||||||
|
|
||||||
|
initialDate = today;
|
||||||
|
|
||||||
|
// // Use previously selected date if valid
|
||||||
|
// if (_selectedCheckOutDate != null &&
|
||||||
|
// _selectedCheckOutDate!.isAfter(today)) {
|
||||||
|
// initialDate = _selectedCheckOutDate!;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// final pickedDate = await showDatePicker(
|
||||||
|
// context: context,
|
||||||
|
// initialDate: initialDate,
|
||||||
|
// firstDate: initialDate,
|
||||||
|
// lastDate: DateTime(2100),
|
||||||
|
// );
|
||||||
|
|
||||||
|
DateTime? pickedDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _selectedCheckOutDate != null &&
|
||||||
|
_selectedCheckOutDate!.isAfter(today)
|
||||||
|
? _selectedCheckOutDate!
|
||||||
|
: today,
|
||||||
|
firstDate: today,
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pickedDate != null && pickedDate != _selectedCheckOutDate) {
|
||||||
|
setState(() {
|
||||||
|
_selectedCheckOutDate = pickedDate;
|
||||||
|
widget.controllers["delegationStartDate"]?.text =
|
||||||
|
DateFormat('dd-MM-yyyy').format(pickedDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Start Date",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF575A74)),
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
CustomTextFieldUserWrapper(
|
||||||
|
isFocused: false,
|
||||||
|
isDesktop: widget.isDesktop,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: GestureDetector(
|
||||||
|
// onTap: () async{
|
||||||
|
// _selectCheckOutDate(context);
|
||||||
|
//
|
||||||
|
// },
|
||||||
|
onTap: () async {
|
||||||
|
await _selectCheckOutDate(context);
|
||||||
|
},
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: TextField(
|
||||||
|
controller: widget.controllers["delegationStartDate"],
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Select Date",
|
||||||
|
labelStyle:
|
||||||
|
const TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
suffixIcon: const Icon(Icons.calendar_today,
|
||||||
|
size: 16, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// if (errorMessages["start_date"] != null) ...[
|
||||||
|
// SizedBox(height: 5), // Space before error message
|
||||||
|
// Text(
|
||||||
|
// "Select Start Date",
|
||||||
|
// style: TextStyle(color: Colors.red, fontSize: 12),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildDelegationEndDateField(bool isDesktop) {
|
||||||
|
DateTime? _selectedEndDate;
|
||||||
|
Future<void> _selectForexEndDate(BuildContext context) async {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
DateTime today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
|
// Parse date from notifier if available, else use today
|
||||||
|
DateTime initialDate;
|
||||||
|
|
||||||
|
initialDate = today;
|
||||||
|
|
||||||
|
//
|
||||||
|
// final pickedDate = await showDatePicker(
|
||||||
|
// context: context,
|
||||||
|
// initialDate: initialDate,
|
||||||
|
// firstDate: initialDate,
|
||||||
|
// lastDate: DateTime(2100),
|
||||||
|
// );
|
||||||
|
|
||||||
|
DateTime? pickedDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate:
|
||||||
|
_selectedEndDate != null && _selectedEndDate!.isAfter(today)
|
||||||
|
? _selectedEndDate!
|
||||||
|
: today,
|
||||||
|
firstDate: today,
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pickedDate != null && pickedDate != _selectedEndDate) {
|
||||||
|
setState(() {
|
||||||
|
_selectedEndDate = pickedDate;
|
||||||
|
widget.controllers["delegationEndDate"]?.text =
|
||||||
|
DateFormat('dd-MM-yyyy').format(pickedDate);
|
||||||
|
// textControllers["_forexEndDate"]?.text =
|
||||||
|
// DateFormat('dd-MM-yyyy').format(initialDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"End Date",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12, fontWeight: FontWeight.w400, color: Colors.black),
|
||||||
|
),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
CustomTextFieldUserWrapper(
|
||||||
|
width: isDesktop ? MediaQuery.of(context).size.width * 0.18 : null,
|
||||||
|
isFocused: false,
|
||||||
|
isDesktop: widget.isDesktop,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 40,
|
||||||
|
child: GestureDetector(
|
||||||
|
// onTap: () async{
|
||||||
|
// _selectCheckOutDate(context);
|
||||||
|
//
|
||||||
|
// },
|
||||||
|
onTap: () async {
|
||||||
|
await _selectForexEndDate(context);
|
||||||
|
},
|
||||||
|
child: AbsorbPointer(
|
||||||
|
child: TextField(
|
||||||
|
controller: widget.controllers["delegationEndDate"],
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Select Date",
|
||||||
|
labelStyle:
|
||||||
|
const TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
suffixIcon: const Icon(Icons.calendar_today,
|
||||||
|
size: 16, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// if (widget.errorMessages["employeeCode"] != null) ...[
|
||||||
|
// SizedBox(height: 5), // Space before error message
|
||||||
|
// Text(
|
||||||
|
// widget.errorMessages["employeeCode"]!,
|
||||||
|
// style: TextStyle(color: Colors.red, fontSize: 12),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildReset(bool isDesktop) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"",
|
||||||
|
style: GoogleFonts.poppins(
|
||||||
|
fontSize: 12, fontWeight: FontWeight.w400, color: Colors.black),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF114D8B),
|
||||||
|
|
||||||
|
foregroundColor: Colors.white, // Keep original color
|
||||||
|
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
handleReset();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Reset",
|
||||||
|
style: GoogleFonts.poppins(fontSize: 11),
|
||||||
|
))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -80,6 +80,7 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
|
|
||||||
Map<String, String> countryMap = {};
|
Map<String, String> countryMap = {};
|
||||||
late List<dynamic>? apiCountryData;
|
late List<dynamic>? apiCountryData;
|
||||||
|
late List<dynamic>? apiHotelsData;
|
||||||
late List<dynamic>? apiAirlineCountryData;
|
late List<dynamic>? apiAirlineCountryData;
|
||||||
Map<String, dynamic>? apiData;
|
Map<String, dynamic>? apiData;
|
||||||
final Map<String, TextEditingController> controllers = {};
|
final Map<String, TextEditingController> controllers = {};
|
||||||
@ -124,12 +125,14 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
// userId = getUserId();
|
// userId = getUserId();
|
||||||
|
|
||||||
apiCountryData = null;
|
apiCountryData = null;
|
||||||
|
apiHotelsData = [];
|
||||||
apiAirlineCountryData = [];
|
apiAirlineCountryData = [];
|
||||||
apiData = null;
|
apiData = null;
|
||||||
for (var field in dataHeader) {
|
for (var field in dataHeader) {
|
||||||
controllers[field] = TextEditingController();
|
controllers[field] = TextEditingController();
|
||||||
}
|
}
|
||||||
fetchCountries();
|
fetchCountries();
|
||||||
|
fetchHotels();
|
||||||
loadCountryList();
|
loadCountryList();
|
||||||
fetchApiData();
|
fetchApiData();
|
||||||
// Delay adding the row until after the first frame
|
// Delay adding the row until after the first frame
|
||||||
@ -155,7 +158,8 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
hotelLoyaltyEntries.add({
|
hotelLoyaltyEntries.add({
|
||||||
"local_id": localId,
|
"local_id": localId,
|
||||||
"id": null, // to be set when backend responds
|
"id": null, // to be set when backend responds
|
||||||
"controller_hotel": TextEditingController(),
|
"hotel_name": null,
|
||||||
|
// "controller_hotel": TextEditingController(),
|
||||||
"controller_membership": TextEditingController(),
|
"controller_membership": TextEditingController(),
|
||||||
"is_active": "1"
|
"is_active": "1"
|
||||||
});
|
});
|
||||||
@ -219,12 +223,12 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
for (var entry in hotelLoyaltyEntries) {
|
for (var entry in hotelLoyaltyEntries) {
|
||||||
entry["controller_hotel"].dispose();
|
// entry["controller_hotel"].dispose();
|
||||||
entry["controller_membership"].dispose();
|
entry["controller_membership"].dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var entry in frequentFlierEntries) {
|
for (var entry in frequentFlierEntries) {
|
||||||
entry["controller_airline"].dispose();
|
// entry["controller_airline"].dispose();
|
||||||
entry["controller_flier_number"].dispose();
|
entry["controller_flier_number"].dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +239,12 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void disposeController(dynamic controller) {
|
||||||
|
if (controller is TextEditingController) {
|
||||||
|
controller.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> get travel_Detials {
|
Map<String, dynamic> get travel_Detials {
|
||||||
Map<String, dynamic> data = {
|
Map<String, dynamic> data = {
|
||||||
// "given_Name": controllers["Fname"]?.text,
|
// "given_Name": controllers["Fname"]?.text,
|
||||||
@ -264,12 +274,41 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List<Map<String, dynamic>> get hotelMembershipList {
|
||||||
|
// final mappedList = hotelLoyaltyEntries.map((entry) {
|
||||||
|
// return {
|
||||||
|
// "id": entry["id"],
|
||||||
|
// "hotel_name": entry["controller_hotel"].text ?? '',
|
||||||
|
// "membership_number": entry["controller_membership"].text ?? '',
|
||||||
|
// "created_by": null,
|
||||||
|
// "updated_by": null,
|
||||||
|
// "is_active": entry["is_active"] ?? "1",
|
||||||
|
// };
|
||||||
|
// }).toList();
|
||||||
|
//
|
||||||
|
// final allEntriesEmpty = mappedList.every((entry) =>
|
||||||
|
// (entry["hotel_name"] as String).trim().isEmpty &&
|
||||||
|
// (entry["membership_number"] as String).trim().isEmpty);
|
||||||
|
//
|
||||||
|
// return allEntriesEmpty ? [] : mappedList;
|
||||||
|
// }
|
||||||
|
|
||||||
List<Map<String, dynamic>> get hotelMembershipList {
|
List<Map<String, dynamic>> get hotelMembershipList {
|
||||||
final mappedList = hotelLoyaltyEntries.map((entry) {
|
final mappedList = hotelLoyaltyEntries.map((entry) {
|
||||||
|
final membership_number = entry["controller_membership"];
|
||||||
|
// final hotelName = entry["controller_hotel"];
|
||||||
|
|
||||||
|
print("membership_number- ${membership_number}");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": entry["id"],
|
"id": entry["id"],
|
||||||
"hotel_name": entry["controller_hotel"].text ?? '',
|
"hotel_id": entry["hotel_id"] ?? "",
|
||||||
"membership_number": entry["controller_membership"].text ?? '',
|
// "hotel_name": entry["hotel_id"] ?? "",
|
||||||
|
"hotel_name": entry["hotel_name"] ?? "",
|
||||||
|
// "hotel_name": hotelName is TextEditingController ? hotelName.text : "",
|
||||||
|
"membership_number": membership_number is TextEditingController
|
||||||
|
? membership_number.text
|
||||||
|
: "",
|
||||||
"created_by": null,
|
"created_by": null,
|
||||||
"updated_by": null,
|
"updated_by": null,
|
||||||
"is_active": entry["is_active"] ?? "1",
|
"is_active": entry["is_active"] ?? "1",
|
||||||
@ -277,7 +316,7 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final allEntriesEmpty = mappedList.every((entry) =>
|
final allEntriesEmpty = mappedList.every((entry) =>
|
||||||
(entry["hotel_name"] as String).trim().isEmpty &&
|
(entry["hotel_id"] as String).trim().isEmpty &&
|
||||||
(entry["membership_number"] as String).trim().isEmpty);
|
(entry["membership_number"] as String).trim().isEmpty);
|
||||||
|
|
||||||
return allEntriesEmpty ? [] : mappedList;
|
return allEntriesEmpty ? [] : mappedList;
|
||||||
@ -430,10 +469,23 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
//----------------------------------------------------
|
//----------------------------------------------------
|
||||||
|
|
||||||
// 🚨 Hotel Membership Logic (pre-fill for edit)
|
// 🚨 Hotel Membership Logic (pre-fill for edit)
|
||||||
|
// for (var entry in hotelLoyaltyEntries) {
|
||||||
|
// // entry["controller_hotel"].dispose();
|
||||||
|
// entry["controller_membership"].dispose();
|
||||||
|
// }
|
||||||
|
// hotelLoyaltyEntries.clear();
|
||||||
|
|
||||||
for (var entry in hotelLoyaltyEntries) {
|
for (var entry in hotelLoyaltyEntries) {
|
||||||
entry["controller_hotel"].dispose();
|
disposeController(entry["controller_membership"]);
|
||||||
entry["controller_membership"].dispose();
|
// (entry["controller_membership"] as TextEditingController?)?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mounted) return; // ✅ avoids context errors
|
||||||
|
print(MediaQuery.of(context).size);
|
||||||
|
//
|
||||||
|
// for (var entry in hotelLoyaltyEntries) {
|
||||||
|
// (entry["controller_membership"] as TextEditingController?)?.dispose();
|
||||||
|
// }
|
||||||
hotelLoyaltyEntries.clear();
|
hotelLoyaltyEntries.clear();
|
||||||
|
|
||||||
final hotelMembershipList = widget.travelDetails?['hotel_membership'];
|
final hotelMembershipList = widget.travelDetails?['hotel_membership'];
|
||||||
@ -442,11 +494,16 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
if (item["is_active"]?.toString() == "0") continue;
|
if (item["is_active"]?.toString() == "0") continue;
|
||||||
|
|
||||||
final localId = _rowhotelCounter++;
|
final localId = _rowhotelCounter++;
|
||||||
|
final hotelDataId = item["hotel_id"];
|
||||||
hotelLoyaltyEntries.add({
|
hotelLoyaltyEntries.add({
|
||||||
"local_id": localId,
|
"local_id": localId,
|
||||||
"id": item["id"],
|
"id": item["id"],
|
||||||
"controller_hotel":
|
// "controller_hotel":
|
||||||
TextEditingController(text: item["hotel_name"] ?? ""),
|
// TextEditingController(text: item["hotel_name"] ?? ""),
|
||||||
|
"hotel_name": item["hotel_name"] ?? "",
|
||||||
|
"hotel_id": hotelDataId ?? "",
|
||||||
|
// "controller_membership":
|
||||||
|
// TextEditingController(text: item["membership_number"] ?? ""),
|
||||||
"controller_membership":
|
"controller_membership":
|
||||||
TextEditingController(text: item["membership_number"] ?? ""),
|
TextEditingController(text: item["membership_number"] ?? ""),
|
||||||
});
|
});
|
||||||
@ -490,12 +547,19 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
// 🚨 Frequent Flier Logic (pre-fill for edit)
|
// 🚨 Frequent Flier Logic (pre-fill for edit)
|
||||||
|
// for (var entry in frequentFlierEntries) {
|
||||||
|
// // entry["controller_airline"].dispose();
|
||||||
|
// // entry["controller_flier_number"].dispose();
|
||||||
|
// (entry["controller_flier_number"] as TextEditingController?)?.dispose();
|
||||||
|
// }
|
||||||
for (var entry in frequentFlierEntries) {
|
for (var entry in frequentFlierEntries) {
|
||||||
// entry["controller_airline"].dispose();
|
entry["controller_flier_number"]?.dispose();
|
||||||
entry["controller_flier_number"].dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
frequentFlierEntries.clear();
|
frequentFlierEntries.clear();
|
||||||
|
|
||||||
|
print(MediaQuery.of(context).size);
|
||||||
|
|
||||||
final frequentFlierList =
|
final frequentFlierList =
|
||||||
widget.travelDetails?['frequent_flier_information'];
|
widget.travelDetails?['frequent_flier_information'];
|
||||||
|
|
||||||
@ -537,6 +601,17 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchHotels() async {
|
||||||
|
try {
|
||||||
|
List<dynamic> countries = await apiService.fetchHotelsList();
|
||||||
|
setState(() {
|
||||||
|
apiHotelsData = countries;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching country list: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadCountryList() async {
|
Future<void> loadCountryList() async {
|
||||||
final newTripType = "2";
|
final newTripType = "2";
|
||||||
final result = await apiService.fetchFlightsCountryList(newTripType);
|
final result = await apiService.fetchFlightsCountryList(newTripType);
|
||||||
@ -2326,9 +2401,22 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
|
|
||||||
popupProps: PopupProps.menu(
|
popupProps: PopupProps.menu(
|
||||||
showSearchBox: true, // Enables search functionality
|
showSearchBox: true, // Enables search functionality
|
||||||
|
menuProps: const MenuProps(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(maxHeight: 250),
|
||||||
|
itemBuilder: (context, item, isSelected) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8.0, vertical: 6.0),
|
||||||
|
child: Text(
|
||||||
|
item,
|
||||||
|
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
searchFieldProps: TextFieldProps(
|
searchFieldProps: TextFieldProps(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Search Country...",
|
hintText: "Search Country...",
|
||||||
|
hintStyle: GoogleFonts.poppins(fontSize: 11.5),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -2528,6 +2616,26 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildHotel(Map<String, dynamic> entry) {
|
Widget buildHotel(Map<String, dynamic> entry) {
|
||||||
|
late Map<String, String> countryMap; // Mapping country_code -> country_name
|
||||||
|
late List<String> countryCodes; // List of country codes
|
||||||
|
|
||||||
|
bool hasAirlineCountryData =
|
||||||
|
apiHotelsData == null || apiHotelsData!.isEmpty;
|
||||||
|
|
||||||
|
countryList = apiHotelsData!;
|
||||||
|
print("TestcountryList2 - $countryList");
|
||||||
|
|
||||||
|
countryMap = {
|
||||||
|
for (var country in countryList)
|
||||||
|
country['hotel_id'] as String: '${country['hotel_name']}'
|
||||||
|
};
|
||||||
|
|
||||||
|
countryCodes = countryMap.keys.toList();
|
||||||
|
String? selectedCode = entry['hotel_id'];
|
||||||
|
// String? selectedCode = entry['hotel_id'];
|
||||||
|
String? selectedText =
|
||||||
|
selectedCode != null ? countryMap[selectedCode] : null;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -2538,28 +2646,81 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 5),
|
SizedBox(height: 5),
|
||||||
CustomTextFieldUserTravellerWrapper(
|
CustomTextFieldUserTravellerWrapper(
|
||||||
width: widget.isDesktop
|
// width: widget.isDesktop
|
||||||
? MediaQuery.of(context).size.width * 0.15
|
// ? MediaQuery.of(context).size.width * 0.15
|
||||||
: null,
|
// : null,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
isDesktop: widget.isDesktop,
|
isDesktop: widget.isDesktop,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: TextField(
|
child: hasAirlineCountryData
|
||||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
? CircularProgressIndicator()
|
||||||
controller: entry["controller_hotel"],
|
: DropdownSearch<String>(
|
||||||
enabled: !widget.isViewMode,
|
selectedItem: (entry["hotel_id"] != null &&
|
||||||
onChanged: (value) {
|
countryMap.containsKey(entry["hotel_id"]))
|
||||||
_clearError("first_name");
|
? countryMap[entry["hotel_id"]]
|
||||||
},
|
: null,
|
||||||
decoration: InputDecoration(
|
popupProps: PopupProps.menu(
|
||||||
labelText: "Hotel",
|
showSearchBox: true, // Enables search functionality
|
||||||
labelStyle:
|
|
||||||
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
|
menuProps: const MenuProps(
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
backgroundColor: Colors.white,
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
|
||||||
),
|
),
|
||||||
|
constraints: BoxConstraints(maxHeight: 250),
|
||||||
|
itemBuilder: (context, item, isSelected) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8.0, vertical: 6.0),
|
||||||
|
child: Text(
|
||||||
|
item,
|
||||||
|
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
searchFieldProps: TextFieldProps(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search Hotels...",
|
||||||
|
hintStyle: GoogleFonts.poppins(fontSize: 11.5),
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: countryMap.values.toList(),
|
||||||
|
dropdownDecoratorProps: DropDownDecoratorProps(
|
||||||
|
dropdownSearchDecoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dropdownBuilder: (context, selectedItem) => Align(
|
||||||
|
// Center-align selected item
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
selectedItem ?? "Select Hotels",
|
||||||
|
style: GoogleFonts.poppins(fontSize: 11),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (String? newValue) {
|
||||||
|
setState(() {
|
||||||
|
// Find the country_code based on selected country_name
|
||||||
|
// selectedCountry = countryMap.entries
|
||||||
|
// .firstWhere((entry) => entry.value == newValue)
|
||||||
|
// .key;
|
||||||
|
|
||||||
|
final selectedCode = countryMap.entries
|
||||||
|
.firstWhere((entry) => entry.value == newValue)
|
||||||
|
.key;
|
||||||
|
|
||||||
|
print("SelectedHotelId - $selectedCode");
|
||||||
|
entry['hotel_id'] = selectedCode;
|
||||||
|
// entry["controller_hotel"] = newValue;
|
||||||
|
entry['hotel_name'] = newValue;
|
||||||
|
// final selectedCode = countryMap.entries
|
||||||
|
// .firstWhere((entry) => entry.value == newValue)
|
||||||
|
// .key;
|
||||||
|
// entry['airline'] = selectedCode;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -2740,9 +2901,22 @@ class TravellerDetailsState extends State<TravellerDetails> {
|
|||||||
selectedItem: countryMap[selectedCountry],
|
selectedItem: countryMap[selectedCountry],
|
||||||
popupProps: PopupProps.menu(
|
popupProps: PopupProps.menu(
|
||||||
showSearchBox: true, // Enables search functionality
|
showSearchBox: true, // Enables search functionality
|
||||||
|
menuProps: const MenuProps(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
// constraints: BoxConstraints(maxHeight: 250),
|
||||||
|
itemBuilder: (context, item, isSelected) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8.0, vertical: 6.0),
|
||||||
|
child: Text(
|
||||||
|
item,
|
||||||
|
style: GoogleFonts.poppins(fontSize: 11.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
searchFieldProps: TextFieldProps(
|
searchFieldProps: TextFieldProps(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Search Country...",
|
hintText: "Search Country...",
|
||||||
|
hintStyle: GoogleFonts.poppins(fontSize: 11.5),
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
12
lib/app.dart
12
lib/app.dart
@ -16,7 +16,10 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart';
|
||||||
import 'package:frontend/routes/custom_router.dart';
|
import 'package:frontend/routes/custom_router.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||||
|
|
||||||
class MyApp extends StatefulWidget {
|
class MyApp extends StatefulWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
@ -38,6 +41,15 @@ class _MyAppState extends State<MyApp> {
|
|||||||
title: 'TRIP MANAGEMENT',
|
title: 'TRIP MANAGEMENT',
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
FlutterQuillLocalizations.delegate, // ✅ Needed for flutter_quill
|
||||||
|
],
|
||||||
|
supportedLocales: const [
|
||||||
|
Locale('en'), // ✅ Add more if needed
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -320,7 +320,6 @@ class _CustomAppBarState extends State<CustomAppBar> {
|
|||||||
// () => context.go('/ApprovalList'),
|
// () => context.go('/ApprovalList'),
|
||||||
isSelected: selectedTab == TabSelection.myApprovals,
|
isSelected: selectedTab == TabSelection.myApprovals,
|
||||||
icon: Icons.verified_outlined,
|
icon: Icons.verified_outlined,
|
||||||
// icon: Icons.assessment_outlined,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// buildNavItem("My Approvals", _myApprovalsColor, () {
|
// buildNavItem("My Approvals", _myApprovalsColor, () {
|
||||||
@ -371,15 +370,19 @@ class _CustomAppBarState extends State<CustomAppBar> {
|
|||||||
break;
|
break;
|
||||||
case '/PolicyList':
|
case '/PolicyList':
|
||||||
context.go('/PolicyList');
|
context.go('/PolicyList');
|
||||||
case '/getForexDetails':
|
case '/getPerdiem':
|
||||||
context.go('/getForexDetails');
|
context.go('/getPerdiem');
|
||||||
|
case '/templateList':
|
||||||
|
context.go('/templateList');
|
||||||
|
case '/template':
|
||||||
|
context.go('/template');
|
||||||
case '/CreateUserDetails':
|
case '/CreateUserDetails':
|
||||||
context.go(
|
context.go(
|
||||||
"/CreateUserDetails",
|
"/CreateUserDetails",
|
||||||
extra: {
|
extra: {
|
||||||
"selectedUser": profileUserDetails,
|
"selectedUser": profileUserDetails,
|
||||||
"isEditProfile": true,
|
"isEditProfile": true,
|
||||||
"isViewMode": true,
|
"isViewMode": false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
case '/logout':
|
case '/logout':
|
||||||
@ -509,7 +512,13 @@ final List<Map<String, dynamic>> menuItems = [
|
|||||||
},
|
},
|
||||||
{'value': '/group', 'icon': Icons.group, 'label': 'Group'},
|
{'value': '/group', 'icon': Icons.group, 'label': 'Group'},
|
||||||
{'value': '/PolicyList', 'icon': Icons.policy, 'label': 'Policy'},
|
{'value': '/PolicyList', 'icon': Icons.policy, 'label': 'Policy'},
|
||||||
{'value': '/getForexDetails', 'icon': Icons.policy, 'label': 'Forex'},
|
{'value': '/getPerdiem', 'icon': Icons.ac_unit_sharp, 'label': 'Forex'},
|
||||||
|
{
|
||||||
|
'value': '/templateList',
|
||||||
|
'icon': Icons.ac_unit_sharp,
|
||||||
|
'label': 'Template List'
|
||||||
|
},
|
||||||
|
{'value': '/template', 'icon': Icons.ac_unit_sharp, 'label': 'Template'},
|
||||||
{
|
{
|
||||||
'value': '/CreateUserDetails',
|
'value': '/CreateUserDetails',
|
||||||
'icon': Icons.account_circle,
|
'icon': Icons.account_circle,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:frontend/Screens/authentication/login/login_page.dart';
|
|||||||
import 'package:frontend/Screens/authentication/loginPage1.dart';
|
import 'package:frontend/Screens/authentication/loginPage1.dart';
|
||||||
import 'package:frontend/Screens/dashboard/home_page.dart';
|
import 'package:frontend/Screens/dashboard/home_page.dart';
|
||||||
import 'package:frontend/Screens/forex/forex_list.dart';
|
import 'package:frontend/Screens/forex/forex_list.dart';
|
||||||
|
import 'package:frontend/Screens/myTemplates/templatesList.dart';
|
||||||
import 'package:frontend/Screens/organization/orgSetup.dart';
|
import 'package:frontend/Screens/organization/orgSetup.dart';
|
||||||
import 'package:frontend/Screens/organization/org_List.dart';
|
import 'package:frontend/Screens/organization/org_List.dart';
|
||||||
import 'package:frontend/Screens/plans/create_plans.dart';
|
import 'package:frontend/Screens/plans/create_plans.dart';
|
||||||
@ -20,6 +21,7 @@ import '../Screens/allTrips/travel_agent_list.dart';
|
|||||||
import '../Screens/approvals/approval_list.dart';
|
import '../Screens/approvals/approval_list.dart';
|
||||||
import '../Screens/group/group.dart';
|
import '../Screens/group/group.dart';
|
||||||
import '../Screens/group/groupList.dart';
|
import '../Screens/group/groupList.dart';
|
||||||
|
import '../Screens/myTemplates/template.dart';
|
||||||
import '../Screens/userManagement/create_user/create_user.dart';
|
import '../Screens/userManagement/create_user/create_user.dart';
|
||||||
|
|
||||||
final GoRouter router = GoRouter(
|
final GoRouter router = GoRouter(
|
||||||
@ -97,9 +99,17 @@ final GoRouter router = GoRouter(
|
|||||||
builder: (context, state) => GroupList(),
|
builder: (context, state) => GroupList(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/getForexDetails',
|
path: '/getPerdiem',
|
||||||
builder: (context, state) => ForexDataList(),
|
builder: (context, state) => ForexDataList(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/templateList',
|
||||||
|
builder: (context, state) => TemplatesList(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/template',
|
||||||
|
builder: (context, state) => Template(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/approvallist',
|
path: '/approvallist',
|
||||||
builder: (context, state) => ApprovalList(),
|
builder: (context, state) => ApprovalList(),
|
||||||
|
|||||||
@ -44,6 +44,41 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<dynamic>> fetchHotelsList() async {
|
||||||
|
final String apiUrldata = '$apiUrl/api/getHotels';
|
||||||
|
final token = await getToken();
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
throw Exception('Token not found. Please log in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse(apiUrldata),
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
try {
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
print("Hotelss - $data");
|
||||||
|
|
||||||
|
if (!data.containsKey('data') || data['data'] is! List) {
|
||||||
|
throw Exception(
|
||||||
|
"Invalid response format: 'data' field is missing or not a List");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data['data'];
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Error parsing response: $e');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to load country list');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<dynamic>> fetchUsers() async {
|
Future<List<dynamic>> fetchUsers() async {
|
||||||
String? ordId = await getOrgId();
|
String? ordId = await getOrgId();
|
||||||
final String apiUrlData = '$apiUrl/api/users?org_id=$ordId';
|
final String apiUrlData = '$apiUrl/api/users?org_id=$ordId';
|
||||||
@ -733,6 +768,52 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getForexDetailsFind(int userId) async {
|
||||||
|
print('Single Forez 1 - $userId');
|
||||||
|
|
||||||
|
// final String apiUrldata = '$apiUrl/api/users/find/$userId';
|
||||||
|
final String apiUrldata =
|
||||||
|
'$apiUrl/api/findForexPerdiem?forex_perdiem_id=$userId';
|
||||||
|
|
||||||
|
final token = await getToken();
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
throw Exception('Token not found. Please log in.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse(apiUrldata),
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
try {
|
||||||
|
final data = json.decode(response.body);
|
||||||
|
print("forexDat - $data");
|
||||||
|
|
||||||
|
if (!data.containsKey('data') || data['data'] is! List) {
|
||||||
|
throw Exception(
|
||||||
|
"Invalid response format: 'data' field is missing or not a List");
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<dynamic> forexList = data['data'];
|
||||||
|
|
||||||
|
if (forexList.isEmpty) {
|
||||||
|
throw Exception('No forex data found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return forexList.first as Map<String, dynamic>;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Error parsing response: $e');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to load plans');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
Future<Map<String, dynamic>> getDepartmentDetailsFind(int id) async {
|
Future<Map<String, dynamic>> getDepartmentDetailsFind(int id) async {
|
||||||
final String apiUrldata = '$apiUrl/api/findDepartment?department_id=$id';
|
final String apiUrldata = '$apiUrl/api/findDepartment?department_id=$id';
|
||||||
|
|||||||
@ -7,9 +7,17 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
|
#include <flutter_localization/flutter_localization_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) file_selector_linux_registrar =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) flutter_localization_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin");
|
||||||
|
flutter_localization_plugin_register_with_registrar(flutter_localization_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
|
flutter_localization
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@ -7,12 +7,20 @@ import Foundation
|
|||||||
|
|
||||||
import file_picker
|
import file_picker
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
|
import flutter_localization
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import quill_native_bridge_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import url_launcher_macos
|
||||||
|
import video_player_avfoundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
|
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
309
pubspec.lock
309
pubspec.lock
@ -9,6 +9,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.4"
|
version: "4.0.4"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -97,6 +105,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
|
dart_quill_delta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_quill_delta
|
||||||
|
sha256: bddb0b2948bd5b5a328f1651764486d162c59a8ccffd4c63e8b2c5e44be1dac4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.8.3"
|
||||||
|
diff_match_patch:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: diff_match_patch
|
||||||
|
sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.1"
|
||||||
dropdown_search:
|
dropdown_search:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -182,6 +206,54 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_colorpicker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_colorpicker
|
||||||
|
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
|
flutter_keyboard_visibility_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_linux
|
||||||
|
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
flutter_keyboard_visibility_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_macos
|
||||||
|
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
flutter_keyboard_visibility_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_platform_interface
|
||||||
|
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
flutter_keyboard_visibility_temp_fork:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_temp_fork
|
||||||
|
sha256: e3d02900640fbc1129245540db16944a0898b8be81694f4bf04b6c985bed9048
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.5"
|
||||||
|
flutter_keyboard_visibility_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_windows
|
||||||
|
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -190,6 +262,19 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
|
flutter_localization:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_localization
|
||||||
|
sha256: "987faf0a6c13a267202b28d3ed680647e234245ead1a1c1f95f87e86c6f12490"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.2"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -198,6 +283,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.27"
|
version: "2.0.27"
|
||||||
|
flutter_quill:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_quill
|
||||||
|
sha256: "7e60963632bbc8615627f0bae8e178515f69ecb378ad49fa68c43c2aabf33e21"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.4.1"
|
||||||
|
flutter_quill_delta_from_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_quill_delta_from_html
|
||||||
|
sha256: "4597bd0853a704696837aa6b05cffd851f587b176204c234edddfed1c1862a09"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.2"
|
||||||
|
flutter_quill_extensions:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_quill_extensions
|
||||||
|
sha256: "099dbaa962d14ac562eb028fd24d37670338352863044b7751fe642a2d2de938"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -324,10 +433,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.19.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -376,6 +485,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.1"
|
version: "3.3.1"
|
||||||
|
markdown:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: markdown
|
||||||
|
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -472,6 +589,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
photo_view:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: photo_view
|
||||||
|
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -504,6 +629,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.2"
|
version: "6.1.2"
|
||||||
|
quill_native_bridge:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge
|
||||||
|
sha256: "00752aca7d67cbd3254709a47558be78427750cb81aa42cfbed354d4a079bcfa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.1"
|
||||||
|
quill_native_bridge_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_android
|
||||||
|
sha256: b75c7e6ede362a7007f545118e756b1f19053994144ec9eda932ce5e54a57569
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1+2"
|
||||||
|
quill_native_bridge_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_ios
|
||||||
|
sha256: d23de3cd7724d482fe2b514617f8eedc8f296e120fb297368917ac3b59d8099f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
|
quill_native_bridge_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_linux
|
||||||
|
sha256: "5fcc60cab2ab9079e0746941f05c5ca5fec85cc050b738c8c8b9da7c09da17eb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
|
quill_native_bridge_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_macos
|
||||||
|
sha256: "1c0631bd1e2eee765a8b06017c5286a4e829778f4585736e048eb67c97af8a77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
|
quill_native_bridge_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_platform_interface
|
||||||
|
sha256: "8264a2bdb8a294c31377a27b46c0f8717fa9f968cf113f7dc52d332ed9c84526"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2+1"
|
||||||
|
quill_native_bridge_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_web
|
||||||
|
sha256: "7c723f6824b0250d7f33e8b6c23f2f8eb0103fe48ee7ebf47ab6786b64d5c05d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
|
quill_native_bridge_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quill_native_bridge_windows
|
||||||
|
sha256: "60e50d74238f22ceb43113d9a42b6627451dab9fc27f527b979a32051cf1da45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
|
quiver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: quiver
|
||||||
|
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
responsive_builder:
|
responsive_builder:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -516,10 +713,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.2"
|
version: "2.5.3"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -653,6 +850,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.2.2"
|
||||||
|
url_launcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.1"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.16"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.3"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.4"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -661,6 +922,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
video_player:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player
|
||||||
|
sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.5"
|
||||||
|
video_player_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_android
|
||||||
|
sha256: "28dcc4122079f40f93a0965b3679aff1a5f4251cf79611bd8011f937eb6b69de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.8.4"
|
||||||
|
video_player_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_avfoundation
|
||||||
|
sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.1"
|
||||||
|
video_player_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_platform_interface
|
||||||
|
sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.0"
|
||||||
|
video_player_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_web
|
||||||
|
sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.5"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -39,7 +39,7 @@ dependencies:
|
|||||||
responsive_builder: ^0.7.1
|
responsive_builder: ^0.7.1
|
||||||
shared_preferences: ^2.5.2
|
shared_preferences: ^2.5.2
|
||||||
easy_stepper: ^0.8.5+1
|
easy_stepper: ^0.8.5+1
|
||||||
intl: ^0.20.2
|
intl: ^0.19.0
|
||||||
dropdown_search: ^5.0.6
|
dropdown_search: ^5.0.6
|
||||||
file_picker: ^10.0.0
|
file_picker: ^10.0.0
|
||||||
bcrypt: ^1.1.3
|
bcrypt: ^1.1.3
|
||||||
@ -50,6 +50,9 @@ dependencies:
|
|||||||
super_tooltip: ^2.0.9
|
super_tooltip: ^2.0.9
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
fluttertoast: ^8.2.12
|
fluttertoast: ^8.2.12
|
||||||
|
flutter_quill: ^11.4.1
|
||||||
|
flutter_quill_extensions: ^11.0.0
|
||||||
|
flutter_localization: ^0.3.2
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
@ -7,8 +7,14 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <flutter_localization/flutter_localization_plugin_c_api.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
FlutterLocalizationPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
flutter_localization
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user