Mail Template Basic Format
This commit is contained in:
parent
954fc5873e
commit
5ddd028fa8
@ -5,15 +5,7 @@
|
||||
android:label="frontend"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" >
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@ -36,6 +28,15 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" >
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@ -4,4 +4,5 @@
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
</manifest>
|
||||
|
||||
562
lib/Screens/myTemplates/Tempale
Normal file
562
lib/Screens/myTemplates/Tempale
Normal file
@ -0,0 +1,562 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:flutter/cupertino.dart' as dom;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as dom hide Element;
|
||||
|
||||
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/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.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 '../../widgets/custom_user_travel.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
|
||||
const Template({super.key, required this.templateData});
|
||||
|
||||
static Template fromState(GoRouterState state) {
|
||||
return Template(templateData: state.extra as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
@override
|
||||
TemplateState createState() => TemplateState();
|
||||
}
|
||||
|
||||
class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
String? orgId;
|
||||
String? userId;
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
List<String> placeholders = [];
|
||||
|
||||
late QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
late int templateId = 0;
|
||||
late String templateName = "";
|
||||
late List<Map<String, dynamic>> placeholderList = [];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
"org_id": orgId,
|
||||
|
||||
// "template_id": templateId,
|
||||
// "template_name": controllers["templateName"]?.text,
|
||||
"template_id": templateId,
|
||||
"template_name": templateName,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": jsonEncode(_controller.document.toDelta().toJson()),
|
||||
|
||||
// "body_html": _controller,
|
||||
// "body_html": convertQuillDocToHtml(_controller.document),
|
||||
// ✅ convert delta to HTML
|
||||
"placeholder": jsonEncode(placeholderList),
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
// }
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
updateData();
|
||||
loadinitializeData();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
// void dispose() {
|
||||
// // controllers.dispose();
|
||||
// // _editorScrollController.dispose();
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
void loadinitializeData() async {
|
||||
orgId = await getOrgId();
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
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 convertQuillDocToHtml(quill.Document doc) {
|
||||
// final buffer = StringBuffer();
|
||||
//
|
||||
// print("convertQuillDocToHtml");
|
||||
// for (final op in doc.toDelta().toList()) {
|
||||
// final insert = op.data;
|
||||
// final attrs = op.attributes ?? {};
|
||||
//
|
||||
// if (insert is String) {
|
||||
// var content = insert;
|
||||
//
|
||||
// // Handle formatting (bold, italic, etc.)
|
||||
// if (attrs.containsKey('bold')) {
|
||||
// content = '<strong>$content</strong>';
|
||||
// }
|
||||
// if (attrs.containsKey('italic')) {
|
||||
// content = '<em>$content</em>';
|
||||
// }
|
||||
//
|
||||
// // Wrap each paragraph with <p>
|
||||
// if (content.trim().isNotEmpty) {
|
||||
// buffer.write('<p>${content.trim()}</p>');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String convertQuillDocToHtml2(quill.Document doc) {
|
||||
// final buffer = StringBuffer();
|
||||
// final lines = <String>[];
|
||||
// final delta = doc.toDelta();
|
||||
//
|
||||
// String applyStyles(String text, Map<String, dynamic>? attrs) {
|
||||
// if (attrs == null) return text;
|
||||
// if (attrs.containsKey('bold')) {
|
||||
// text = '<strong>$text</strong>';
|
||||
// }
|
||||
// if (attrs.containsKey('italic')) {
|
||||
// text = '<em>$text</em>';
|
||||
// }
|
||||
// return text;
|
||||
// }
|
||||
//
|
||||
// for (final op in delta.toList()) {
|
||||
// final insert = op.data;
|
||||
// final attrs = op.attributes;
|
||||
//
|
||||
// if (insert is String) {
|
||||
// final parts = insert.split('\n');
|
||||
// for (int i = 0; i < parts.length; i++) {
|
||||
// final part = applyStyles(parts[i], attrs);
|
||||
// lines.add(part);
|
||||
//
|
||||
// if (i < parts.length - 1) {
|
||||
// // End of line: wrap accumulated content into <p>
|
||||
// final joined = lines.join('');
|
||||
// if (joined.trim().isNotEmpty) {
|
||||
// buffer.writeln('<p>${joined.trim()}</p>');
|
||||
// }
|
||||
// lines.clear();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Add remaining lines
|
||||
// final joined = lines.join('');
|
||||
// if (joined.trim().isNotEmpty) {
|
||||
// buffer.writeln('<p>${joined.trim()}</p>');
|
||||
// }
|
||||
//
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String extractPlainTextFromHtml(String html) {
|
||||
// final regex = RegExp(r'<p>(.*?)<\/p>', multiLine: true, dotAll: true);
|
||||
// final matches = regex.allMatches(html);
|
||||
//
|
||||
// final buffer = StringBuffer();
|
||||
// for (final match in matches) {
|
||||
// final text = match.group(1)?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
|
||||
// buffer.writeln(text.trim());
|
||||
// }
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String decodeHtmlEntities(String text) {
|
||||
// return text
|
||||
// .replaceAll(' ', ' ')
|
||||
// .replaceAll('&', '&')
|
||||
// .replaceAll('<', '<')
|
||||
// .replaceAll('>', '>')
|
||||
// .replaceAll('"', '"')
|
||||
// .replaceAll(''', "'"); // add more as needed
|
||||
// }
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}",
|
||||
);
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["templateName"]?.text =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
final bodyHtml =
|
||||
widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
print("bodyHtml - $bodyHtml");
|
||||
|
||||
// /*inal converter = DeltaFromHTML();
|
||||
// final delta = converter.convert(bodyHtml); // Convert HTML → Delta
|
||||
// final quillDoc = quill.Document.fromDelta(delta);
|
||||
// */
|
||||
|
||||
// final plainText = extractPlainTextFromHtml(bodyHtml);
|
||||
// final decodedText = decodeHtmlEntities(plainText);
|
||||
//
|
||||
// final quillDoc = quill.Document()..insert(0, decodedText);
|
||||
// // final quillDoc = convertBasicHtmlToQuill(bodyHtml);
|
||||
// // final quillDoc = quill.Document()..insert(0, plainText);
|
||||
// // final quillDoc = quill.Document.fromDelta(delta);
|
||||
// _controller = quill.QuillController(
|
||||
// document: quillDoc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
|
||||
final deltaJsonString =
|
||||
widget.templateData?["templateData"]?["body_delta"];
|
||||
if (deltaJsonString != null) {
|
||||
final deltaJson = jsonDecode(deltaJsonString);
|
||||
final quillDoc = quill.Document.fromJson(deltaJson);
|
||||
|
||||
_controller = quill.QuillController(
|
||||
document: quillDoc,
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
}
|
||||
|
||||
templateName =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
print("Fetched template_name: $templateName");
|
||||
|
||||
final rawPlaceholder =
|
||||
widget.templateData?["templateData"]?["placeholder"];
|
||||
|
||||
if (rawPlaceholder is String) {
|
||||
// If it's a JSON string, decode it first
|
||||
placeholderList = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(rawPlaceholder),
|
||||
);
|
||||
} else if (rawPlaceholder is List) {
|
||||
// If it's already a list (ideal case)
|
||||
placeholderList = List<Map<String, dynamic>>.from(rawPlaceholder);
|
||||
}
|
||||
|
||||
print("Extracted placeholders: $placeholders");
|
||||
|
||||
print("Fetched placeholders: $placeholderList");
|
||||
|
||||
templateId =
|
||||
int.tryParse(
|
||||
widget.templateData?["templateData"]?["template_id"]
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
print("Fetched template_id: $templateId");
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
// widget.group!["international_policy_id"].toString();
|
||||
// }
|
||||
});
|
||||
} else {
|
||||
print("API Selected User Has Data - No data available yet");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
Map<String, dynamic> data = TemplateData;
|
||||
setState(() {
|
||||
// updateTemplateData(data);
|
||||
// This triggers UI rebuild with error messages
|
||||
// if (validateData()) {
|
||||
// postGroupData();
|
||||
// }
|
||||
});
|
||||
|
||||
final TemplateData1 = TemplateData;
|
||||
print("TemplateData - $TemplateData1");
|
||||
}
|
||||
|
||||
Future<void> updateTemplateData(Map<String, dynamic> policyData) async {
|
||||
final String apiUrldata = '$apiUrl/api/template/update/${templateId}';
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrldata),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
context.go('/templateList');
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@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, // 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),
|
||||
Expanded(
|
||||
child: buildUserTable(
|
||||
isDesktop,
|
||||
context,
|
||||
bodyColor,
|
||||
layoutColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUserTable(
|
||||
bool isDesktop,
|
||||
context,
|
||||
Color? bodyColor,
|
||||
Color layoutColor,
|
||||
) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Editor"),
|
||||
SizedBox(height: 10),
|
||||
buildTempalteSubject(isDesktop),
|
||||
|
||||
SizedBox(height: 10),
|
||||
buildTempalteBody(isDesktop),
|
||||
Spacer(),
|
||||
buildActions(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteSubject(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||||
controller: controllers["subject"],
|
||||
onChanged: (value) {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "enter the subject",
|
||||
labelStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteBody(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Content",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(child: QuillSimpleToolbar(controller: _controller)),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(bool isDesktop) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/templateList');
|
||||
// You can get text from commentController.text
|
||||
Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
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';
|
||||
const kScreenshot4 =
|
||||
'assets/images/screenshot_4.png'; // TODO Implement this library.
|
||||
|
||||
@ -16,14 +16,8 @@ class CustomToolbar extends StatelessWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
children: [
|
||||
QuillToolbarHistoryButton(
|
||||
isUndo: true,
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarHistoryButton(
|
||||
isUndo: false,
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarHistoryButton(isUndo: true, controller: controller),
|
||||
QuillToolbarHistoryButton(isUndo: false, controller: controller),
|
||||
QuillToolbarToggleStyleButton(
|
||||
options: const QuillToolbarToggleStyleButtonOptions(),
|
||||
controller: controller,
|
||||
@ -38,40 +32,22 @@ class CustomToolbar extends StatelessWidget {
|
||||
controller: controller,
|
||||
attribute: Attribute.underline,
|
||||
),
|
||||
QuillToolbarClearFormatButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarClearFormatButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarImageButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarCameraButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarVideoButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarImageButton(controller: controller),
|
||||
QuillToolbarCameraButton(controller: controller),
|
||||
QuillToolbarVideoButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarColorButton(
|
||||
controller: controller,
|
||||
isBackground: false,
|
||||
),
|
||||
QuillToolbarColorButton(
|
||||
controller: controller,
|
||||
isBackground: true,
|
||||
),
|
||||
QuillToolbarColorButton(controller: controller, isBackground: false),
|
||||
QuillToolbarColorButton(controller: controller, isBackground: true),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarSelectHeaderStyleDropdownButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarSelectHeaderStyleDropdownButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarSelectLineHeightStyleDropdownButton(
|
||||
controller: controller,
|
||||
),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarToggleCheckListButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarToggleCheckListButton(controller: controller),
|
||||
QuillToolbarToggleStyleButton(
|
||||
controller: controller,
|
||||
attribute: Attribute.ol,
|
||||
@ -88,18 +64,12 @@ class CustomToolbar extends StatelessWidget {
|
||||
controller: controller,
|
||||
attribute: Attribute.blockQuote,
|
||||
),
|
||||
QuillToolbarIndentButton(
|
||||
controller: controller,
|
||||
isIncrease: true,
|
||||
),
|
||||
QuillToolbarIndentButton(
|
||||
controller: controller,
|
||||
isIncrease: false,
|
||||
),
|
||||
QuillToolbarIndentButton(controller: controller, isIncrease: true),
|
||||
QuillToolbarIndentButton(controller: controller, isIncrease: false),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarLinkStyleButton(controller: controller),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
81
lib/Screens/myTemplates/dialog_placeholders.dart
Normal file
81
lib/Screens/myTemplates/dialog_placeholders.dart
Normal file
@ -0,0 +1,81 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend/utils/auth_utils.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class PlaceholdersModal extends StatefulWidget {
|
||||
final List<Map<String, dynamic>> placeholders;
|
||||
const PlaceholdersModal({Key? key, required this.placeholders})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_PlaceholdersModalState createState() => _PlaceholdersModalState();
|
||||
}
|
||||
|
||||
class _PlaceholdersModalState extends State<PlaceholdersModal> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String templLabel(String placeholder) {
|
||||
var label = placeholder.replaceAll('%', '').replaceAll('_', ' ');
|
||||
return label
|
||||
.split(' ')
|
||||
.map(
|
||||
(word) =>
|
||||
word.isNotEmpty
|
||||
? word[0].toUpperCase() + word.substring(1)
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text("Available Placeholders"),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
// Set max height so ListView knows constraints
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.placeholders.length,
|
||||
itemBuilder: (context, index) {
|
||||
final value = widget.placeholders[index]['value'] ?? '';
|
||||
return ListTile(
|
||||
hoverColor: Colors.white,
|
||||
focusColor: Colors.white,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
templLabel(value),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SelectableText(
|
||||
value,
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
// onTap: () {
|
||||
// Navigator.of(context).pop(value);
|
||||
// },
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text("Close"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,36 +1 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
// 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 '';
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export 'dart:ui' if (dart.library.js_interop) 'dart:ui_web';
|
||||
@ -1,84 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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,
|
||||
);
|
||||
@ -1,30 +0,0 @@
|
||||
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();
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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 +0,0 @@
|
||||
export './web_stub.dart' if (dart.library.js_interop) './web_real.dart';
|
||||
@ -1,46 +0,0 @@
|
||||
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;
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
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.');
|
||||
@ -1,165 +1 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,11 +1 @@
|
||||
import 'package:flutter/widgets.dart' show BoxConstraints;
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
@immutable
|
||||
class QuillEditorWebImageEmbedConfig {
|
||||
const QuillEditorWebImageEmbedConfig({
|
||||
this.constraints,
|
||||
});
|
||||
|
||||
final BoxConstraints? constraints;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,77 +1 @@
|
||||
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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,67 +1 @@
|
||||
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;
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,246 +0,0 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,254 +0,0 @@
|
||||
@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');
|
||||
}
|
||||
}
|
||||
@ -1,64 +1 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,186 +0,0 @@
|
||||
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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,46 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,6 +1 @@
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
@immutable
|
||||
class QuillEditorWebVideoEmbedConfig {
|
||||
const QuillEditorWebVideoEmbedConfig();
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,55 +1 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,55 +1 @@
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,106 +1 @@
|
||||
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,
|
||||
),
|
||||
];
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,132 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,39 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,28 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,39 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,135 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
/// 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';
|
||||
@ -1,50 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,32 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,134 +1 @@
|
||||
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;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,244 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
|
||||
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/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:frontend/Screens/myTemplates/quill_delta_sample.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.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';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
|
||||
const Template({super.key, required this.templateData});
|
||||
|
||||
static Template fromState(GoRouterState state) {
|
||||
return Template(templateData: state.extra as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
@override
|
||||
TemplateState createState() => TemplateState();
|
||||
}
|
||||
|
||||
class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
// "org_id": orgId;
|
||||
"template_name": controllers["templateName"]?.text,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": controllers["bodyData"]?.text,
|
||||
"placeholder": [],
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
// }
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
updateData();
|
||||
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
// void dispose() {
|
||||
// // controllers.dispose();
|
||||
// // _editorScrollController.dispose();
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}");
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
// widget.group!["international_policy_id"].toString();
|
||||
// }
|
||||
});
|
||||
} else {
|
||||
print("API Selected User Has Data - No data available yet");
|
||||
}
|
||||
}
|
||||
|
||||
@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, // 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),
|
||||
Expanded(
|
||||
child: buildUserTable(
|
||||
isDesktop, context, bodyColor, layoutColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildUserTable(
|
||||
bool isDesktop, context, Color? bodyColor, Color layoutColor) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Editor"),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
buildTempalteSubject(isDesktop),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.output),
|
||||
tooltip: 'Print Delta JSON to log',
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'The JSON Delta has been printed to the console.')));
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
buildTempalteBody(isDesktop)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteSubject(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12, fontWeight: FontWeight.w400, color: Colors.black),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||||
controller: controllers["subject"],
|
||||
onChanged: (value) {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "enter the subject",
|
||||
labelStyle:
|
||||
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteBody(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Content",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,19 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:delta_to_html/delta_to_html.dart';
|
||||
import 'package:flutter/cupertino.dart' as dom;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart' as quill;
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as dom hide Element, Text;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -11,18 +21,20 @@ import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:frontend/Screens/myTemplates/quill_delta_sample.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.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 '../../widgets/custom_user_travel.dart';
|
||||
import 'dialog_placeholders.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
@ -41,21 +53,44 @@ class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
String? orgId;
|
||||
String? userId;
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
List<String> placeholders = [];
|
||||
|
||||
late QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
late int templateId = 0;
|
||||
late String templateName = "";
|
||||
late List<Map<String, dynamic>> placeholderList = [];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
// "org_id": orgId;
|
||||
"template_name": controllers["templateName"]?.text,
|
||||
"org_id": orgId,
|
||||
|
||||
// "template_id": templateId,
|
||||
// "template_name": controllers["templateName"]?.text,
|
||||
"template_id": templateId,
|
||||
"template_name": templateName,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": controllers["bodyData"]?.text,
|
||||
"placeholder": [],
|
||||
"body_html": DeltaToHTML.encodeJson(
|
||||
_controller.document.toDelta().toJson(),
|
||||
),
|
||||
// "body_html": jsonEncode(_controller.document.toDelta().toJson()),
|
||||
|
||||
// "body_html": _controller,
|
||||
// "body_html": convertQuillDocToHtml(_controller.document),
|
||||
// ✅ convert delta to HTML
|
||||
"placeholder": jsonEncode(placeholderList),
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
print('start 123');
|
||||
print(jsonEncode(_controller.document.toDelta().toJson()));
|
||||
// print(jsonEncode(_controller.document));
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
@ -73,7 +108,7 @@ class TemplateState extends State<Template> {
|
||||
}
|
||||
|
||||
updateData();
|
||||
|
||||
loadinitializeData();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@ -84,32 +119,255 @@ class TemplateState extends State<Template> {
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
void loadinitializeData() async {
|
||||
orgId = await getOrgId();
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
String convertQuillDocToHtml(quill.Document doc) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
print("convertQuillDocToHtml");
|
||||
for (final op in doc.toDelta().toList()) {
|
||||
final insert = op.data;
|
||||
final attrs = op.attributes ?? {};
|
||||
|
||||
if (insert is String) {
|
||||
var content = insert;
|
||||
|
||||
// Handle formatting (bold, italic, etc.)
|
||||
if (attrs.containsKey('bold')) {
|
||||
content = '<strong>$content</strong>';
|
||||
}
|
||||
if (attrs.containsKey('italic')) {
|
||||
content = '<em>$content</em>';
|
||||
}
|
||||
|
||||
// Wrap each paragraph with <p>
|
||||
if (content.trim().isNotEmpty) {
|
||||
buffer.write('<p>${content.trim()}</p>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String convertQuillDocToHtml2(quill.Document doc) {
|
||||
final buffer = StringBuffer();
|
||||
final lines = <String>[];
|
||||
final delta = doc.toDelta();
|
||||
|
||||
String applyStyles(String text, Map<String, dynamic>? attrs) {
|
||||
if (attrs == null) return text;
|
||||
if (attrs.containsKey('bold')) {
|
||||
text = '<strong>$text</strong>';
|
||||
}
|
||||
if (attrs.containsKey('italic')) {
|
||||
text = '<em>$text</em>';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
for (final op in delta.toList()) {
|
||||
final insert = op.data;
|
||||
final attrs = op.attributes;
|
||||
|
||||
if (insert is String) {
|
||||
final parts = insert.split('\n');
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
final part = applyStyles(parts[i], attrs);
|
||||
lines.add(part);
|
||||
|
||||
if (i < parts.length - 1) {
|
||||
// End of line: wrap accumulated content into <p>
|
||||
final joined = lines.join('');
|
||||
if (joined.trim().isNotEmpty) {
|
||||
buffer.writeln('<p>${joined.trim()}</p>');
|
||||
}
|
||||
lines.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining lines
|
||||
final joined = lines.join('');
|
||||
if (joined.trim().isNotEmpty) {
|
||||
buffer.writeln('<p>${joined.trim()}</p>');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String extractPlainTextFromHtml(String html) {
|
||||
final regex = RegExp(r'<p>(.*?)<\/p>', multiLine: true, dotAll: true);
|
||||
final matches = regex.allMatches(html);
|
||||
|
||||
final buffer = StringBuffer();
|
||||
for (final match in matches) {
|
||||
final text = match.group(1)?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
|
||||
buffer.writeln(text.trim());
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String decodeHtmlEntities(String text) {
|
||||
return text
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'"); // add more as needed
|
||||
}
|
||||
|
||||
// quill.Document convertSimpleHtmlToQuill(String htmlString) {
|
||||
// final delta = quill.Delta();
|
||||
// final doc = html_parser.parse(htmlString);
|
||||
// final body = doc.body;
|
||||
//
|
||||
// void walk(Node node) {
|
||||
// if (node is Text) {
|
||||
// delta.insert(node.text);
|
||||
// } else if (node is Element) {
|
||||
// switch (node.localName) {
|
||||
// case 'p':
|
||||
// node.nodes.forEach(walk);
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'br':
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'strong':
|
||||
// case 'b':
|
||||
// delta.insert(node.text, {'bold': true});
|
||||
// break;
|
||||
// case 'em':
|
||||
// case 'i':
|
||||
// delta.insert(node.text, {'italic': true});
|
||||
// break;
|
||||
// case 'a':
|
||||
// delta.insert(node.text, {'link': node.attributes['href']});
|
||||
// break;
|
||||
// default:
|
||||
// node.nodes.forEach(walk);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (body != null) {
|
||||
// walk(body);
|
||||
// }
|
||||
//
|
||||
// return quill.Document.fromDelta(delta..insert('\n'));
|
||||
// }
|
||||
|
||||
String formatTemplateName(String input) {
|
||||
return input
|
||||
.split('_') // split by underscore
|
||||
.map(
|
||||
(word) =>
|
||||
word.isNotEmpty
|
||||
? '${word[0].toUpperCase()}${word.substring(1)}'
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}");
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}",
|
||||
);
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["templateName"]?.text =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
final bodyHtml =
|
||||
widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
print("bodyHtml - $bodyHtml");
|
||||
|
||||
String html = widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
final htmlToDelta = HtmlToDelta();
|
||||
|
||||
// Convert the HTML string to Quill Delta format
|
||||
// This is where the magic happens, but also where complex HTML might be simplified
|
||||
final quill.Delta initialDelta = htmlToDelta.convert(html);
|
||||
|
||||
// Create a Quill Document from the Delta
|
||||
final quill.Document quillDoc = quill.Document.fromDelta(initialDelta);
|
||||
|
||||
// Initialize the QuillController with the converted document
|
||||
_controller = quill.QuillController(
|
||||
document: quillDoc,
|
||||
selection: const TextSelection.collapsed(
|
||||
offset: 0,
|
||||
), // Cursor at the start
|
||||
);
|
||||
|
||||
// final plainText = extractPlainTextFromHtml(bodyHtml);
|
||||
// final decodedText = decodeHtmlEntities(plainText);
|
||||
// final quillDoc = quill.Document()..insert(0, decodedText);
|
||||
// _controller = quill.QuillController(
|
||||
// document: quillDoc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
|
||||
templateName =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
print("Fetched template_name: $templateName");
|
||||
|
||||
final rawPlaceholder =
|
||||
widget.templateData?["templateData"]?["placeholder"];
|
||||
|
||||
if (rawPlaceholder is String) {
|
||||
// If it's a JSON string, decode it first
|
||||
placeholderList = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(rawPlaceholder),
|
||||
);
|
||||
} else if (rawPlaceholder is List) {
|
||||
// If it's already a list (ideal case)
|
||||
placeholderList = List<Map<String, dynamic>>.from(rawPlaceholder);
|
||||
}
|
||||
|
||||
print("Extracted placeholders: $placeholders");
|
||||
|
||||
print("Fetched placeholders: $placeholderList");
|
||||
|
||||
templateId =
|
||||
int.tryParse(
|
||||
widget.templateData?["templateData"]?["template_id"]
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
print("Fetched template_id: $templateId");
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
@ -121,42 +379,123 @@ class TemplateState extends State<Template> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
Future<void> handleSubmit() async {
|
||||
Map<String, dynamic> data = TemplateData;
|
||||
|
||||
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, // 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),
|
||||
Expanded(
|
||||
child: buildUserTable(
|
||||
isDesktop, context, bodyColor, layoutColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
print('TemplateData - $data');
|
||||
// final String deltaJsonString = TemplateData?["body_html"] ?? "[]";
|
||||
//
|
||||
// print('deltaJsonString $deltaJsonString');
|
||||
//
|
||||
// // Decode the JSON string into a list
|
||||
// final List<dynamic> deltaJson = jsonDecode(deltaJsonString);
|
||||
//
|
||||
// print(DeltaToHTML.encodeJson(deltaJson));
|
||||
//
|
||||
// // Then convert it to a Quill document
|
||||
// final doc = Document.fromJson(List<Map<String, dynamic>>.from(deltaJson));
|
||||
//
|
||||
// // Set it to the controller
|
||||
// _controller = QuillController(
|
||||
// document: doc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
// print('doc JSON: ${jsonEncode(doc.toDelta().toJson())}');
|
||||
// print('doc plain text: ${doc.toPlainText()}');
|
||||
// print('doc $doc');
|
||||
|
||||
setState(() {
|
||||
updateTemplateData(data);
|
||||
// This triggers UI rebuild with error messages
|
||||
// if (validateData()) {
|
||||
// postGroupData();
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateTemplateData(policyData) async {
|
||||
final String apiUrldata = '$apiUrl/api/template/update/${templateId}';
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrldata),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
context.go('/templateList');
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@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, // 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),
|
||||
Expanded(
|
||||
child: buildUserTable(
|
||||
isDesktop,
|
||||
context,
|
||||
bodyColor,
|
||||
layoutColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUserTable(
|
||||
bool isDesktop, context, Color? bodyColor, Color layoutColor) {
|
||||
bool isDesktop,
|
||||
context,
|
||||
Color? bodyColor,
|
||||
Color layoutColor,
|
||||
) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 5.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
@ -165,24 +504,22 @@ class TemplateState extends State<Template> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Editor"),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
Text(
|
||||
formatTemplateName(templateName),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 10),
|
||||
buildTempalteSubject(isDesktop),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.output),
|
||||
tooltip: 'Print Delta JSON to log',
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'The JSON Delta has been printed to the console.')));
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
buildTempalteBody(isDesktop)
|
||||
|
||||
SizedBox(height: 10),
|
||||
buildTempalteBody(isDesktop),
|
||||
Spacer(),
|
||||
buildActions(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -196,11 +533,16 @@ class TemplateState extends State<Template> {
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12, fontWeight: FontWeight.w400, color: Colors.black),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
color: Colors.white,
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.8 : null,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
@ -211,9 +553,11 @@ class TemplateState extends State<Template> {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "enter the subject",
|
||||
labelStyle:
|
||||
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
|
||||
labelText: "Enter the subject",
|
||||
labelStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
@ -233,11 +577,138 @@ class TemplateState extends State<Template> {
|
||||
"Content",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
color: Color(0xFFFFFEF0),
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.8 : null,
|
||||
child: IconTheme(
|
||||
data: IconThemeData(size: 18), // Set icon size here
|
||||
child: QuillSimpleToolbar(controller: _controller),
|
||||
),
|
||||
),
|
||||
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final selected = await showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) =>
|
||||
PlaceholdersModal(placeholders: placeholderList),
|
||||
);
|
||||
|
||||
if (selected != null) {
|
||||
print("User selected placeholder: $selected");
|
||||
// You can now insert into a controller or editor
|
||||
// Ensure the editor is focused
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
|
||||
final selection = _controller.selection;
|
||||
final position = selection.baseOffset;
|
||||
|
||||
// if (position >= 0) {
|
||||
// final intPosition = position.toInt();
|
||||
//
|
||||
// _controller.document.insert(intPosition, selected);
|
||||
//
|
||||
// _controller.updateSelection(
|
||||
// TextSelection.collapsed(
|
||||
// offset: intPosition + selected.length,
|
||||
// ),
|
||||
// ChangeSource.local,
|
||||
// );
|
||||
// }
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.grey,
|
||||
// padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
'Placeholders',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Color(0xFFD6D5E6), width: 0.5),
|
||||
),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(bool isDesktop) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/templateList');
|
||||
// You can get text from commentController.text
|
||||
Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: layoutColor,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: layoutColor, width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: layoutColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: layoutColor,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,30 +3,31 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({Key? key, required String title}) : super(key: key);
|
||||
const MyHomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
MyHomePageState createState() => MyHomePageState();
|
||||
}
|
||||
|
||||
class MyHomePageState extends State<MyHomePage> {
|
||||
QuillController _controller = QuillController.basic();
|
||||
final QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("title")),
|
||||
body: Column(
|
||||
children: [
|
||||
Text("data"),
|
||||
QuillSimpleToolbar(
|
||||
controller: _controller,
|
||||
config: const QuillSimpleToolbarConfig(),
|
||||
),
|
||||
QuillSimpleToolbar(controller: _controller),
|
||||
Expanded(
|
||||
child: QuillEditor.basic(
|
||||
controller: _controller,
|
||||
config: const QuillEditorConfig(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -40,13 +40,15 @@ class _PolicyListState extends State<PolicyList> {
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
@ -144,31 +146,37 @@ class _PolicyListState extends State<PolicyList> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
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),
|
||||
Expanded(child: buildGroupList(isDesktop))
|
||||
],
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
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),
|
||||
Expanded(child: buildGroupList(isDesktop)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
@ -196,9 +204,10 @@ class _PolicyListState extends State<PolicyList> {
|
||||
// ? 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,
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
// decoration: BoxDecoration(
|
||||
// border: isDesktop
|
||||
// ? Border.all(
|
||||
@ -249,23 +258,18 @@ class _PolicyListState extends State<PolicyList> {
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Text('New Policy',
|
||||
style: GoogleFonts.poppins(fontSize: 12)),
|
||||
SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Icon(
|
||||
Icons.add_circle_outline_rounded,
|
||||
color: Colors.white,
|
||||
Text(
|
||||
'New Policy',
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Icon(Icons.add_circle_outline_rounded, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -277,16 +281,12 @@ class _PolicyListState extends State<PolicyList> {
|
||||
// color: Colors.red.shade100,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
children: [
|
||||
buildGroupListView(isDesktop),
|
||||
],
|
||||
),
|
||||
child: Column(children: [buildGroupListView(isDesktop)]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -300,7 +300,7 @@ class _PolicyListState extends State<PolicyList> {
|
||||
|
||||
Widget buildGroupListView(bool isDesktop) {
|
||||
if (apiAllGroups == null || apiAllGroups!.isEmpty) {
|
||||
return Center(child: Text("No groups found."));
|
||||
return Center(child: Text("No Policy Found."));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
@ -323,15 +323,24 @@ class _PolicyListState extends State<PolicyList> {
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text("Policy Name",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
child: Text(
|
||||
"Policy Name",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text("Policy Type",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400))),
|
||||
flex: 1,
|
||||
child: Text(
|
||||
"Policy Type",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(flex: 1, child: Text("${policy['created_by']}")),
|
||||
],
|
||||
),
|
||||
@ -340,18 +349,26 @@ class _PolicyListState extends State<PolicyList> {
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text("${policy['name']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
child: Text(
|
||||
"${policy['name']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
policy['domestic'] == "1"
|
||||
? "Domestic"
|
||||
: "International",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600))),
|
||||
flex: 1,
|
||||
child: Text(
|
||||
policy['domestic'] == "1"
|
||||
? "Domestic"
|
||||
: "International",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(flex: 1, child: Text("${policy['created_by']}")),
|
||||
],
|
||||
),
|
||||
@ -361,23 +378,25 @@ class _PolicyListState extends State<PolicyList> {
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final rawId = policy['policy_id'];
|
||||
final intPolicyId = rawId is int
|
||||
? rawId
|
||||
: int.tryParse(rawId.toString()) ?? 0;
|
||||
final intPolicyId =
|
||||
rawId is int
|
||||
? rawId
|
||||
: int.tryParse(rawId.toString()) ?? 0;
|
||||
|
||||
Map<String, dynamic> policyData =
|
||||
await apiService.getSinglePolicy(intPolicyId);
|
||||
Map<String, dynamic> policyData = await apiService
|
||||
.getSinglePolicy(intPolicyId);
|
||||
|
||||
print("PolicyDATa: $policyData");
|
||||
|
||||
context.go("/Policy", extra: policyData);
|
||||
},
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
),
|
||||
SizedBox(
|
||||
width: 5,
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final idStr = policy['policy_id'];
|
||||
@ -391,8 +410,11 @@ class _PolicyListState extends State<PolicyList> {
|
||||
|
||||
deletePolicy(policy, id, status);
|
||||
},
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
243
lib/app.dart
243
lib/app.dart
@ -1,33 +1,19 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:frontend/routes/custom_router.dart';
|
||||
//
|
||||
// class MyApp extends StatelessWidget {
|
||||
// const MyApp({super.key});
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return MaterialApp.router(
|
||||
// title: 'TRIP MANAGEMENT',
|
||||
// routerConfig: router,
|
||||
// debugShowCheckedModeBanner: false,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:frontend/routes/custom_router.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
// import 'package:flutter/rendering.dart';
|
||||
import 'dart:html' as html;
|
||||
import 'package:frontend/config/apiUrl.dart'; // 1 newly added
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart' as http; // 2 newly added
|
||||
import 'dart:convert'; // 3 newly added
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:frontend/routes/custom_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'Screens/myTemplates/templateTest.dart';
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
@ -43,7 +29,7 @@ class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SemanticsBinding.instance.ensureSemantics(); // ✅ Safe here
|
||||
// SemanticsBinding.instance.ensureSemantics(); // ✅ Safe here
|
||||
if (kIsWeb) {
|
||||
final uri = Uri.parse(html.window.location.href);
|
||||
if (uri.path == '/authredirection' &&
|
||||
@ -68,8 +54,10 @@ class _MyAppState extends State<MyApp> {
|
||||
|
||||
final url = '$apiUrl/auth/verifyMSAuthUser?code=$authCode';
|
||||
try {
|
||||
final response = await http
|
||||
.get(Uri.parse(url), headers: {'Content-Type': 'application/json'});
|
||||
final response = await http.get(
|
||||
Uri.parse(url),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final MS_Token = json.decode(response.body)['token'];
|
||||
@ -107,15 +95,18 @@ class _MyAppState extends State<MyApp> {
|
||||
final parts = token.split('.');
|
||||
if (parts.length != 3) throw Exception('Invalid token format');
|
||||
|
||||
final payload = json
|
||||
.decode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
|
||||
final payload = json.decode(
|
||||
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
|
||||
);
|
||||
|
||||
final userData = payload['data'];
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('auth_token', token);
|
||||
await prefs.setString(
|
||||
'user_data', jsonEncode(userData)); // Store full user data
|
||||
'user_data',
|
||||
jsonEncode(userData),
|
||||
); // Store full user data
|
||||
|
||||
if (userData != null) {
|
||||
final pref = await SharedPreferences.getInstance();
|
||||
@ -135,27 +126,193 @@ class _MyAppState extends State<MyApp> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// if (_isAuthRedirect) {
|
||||
// print("Rendering MicrosoftPage with code: $_authCode");
|
||||
// return MaterialApp(
|
||||
// home: MicrosoftPage(code: _authCode),
|
||||
// debugShowCheckedModeBanner: false,
|
||||
// );
|
||||
// }
|
||||
|
||||
return MaterialApp.router(
|
||||
title: 'TRIP MANAGEMENT',
|
||||
routerConfig: router,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
routerConfig: router, // <- your configured GoRouter or other RouterConfig
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
FlutterQuillLocalizations.delegate, // ✅ Needed for flutter_quill
|
||||
],
|
||||
supportedLocales: const [
|
||||
Locale('en'), // ✅ Add more if needed
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
FlutterQuillLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: const [Locale('en'), Locale('es')],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/rendering.dart';
|
||||
// import 'package:flutter_quill/flutter_quill.dart';
|
||||
// import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
//
|
||||
// import 'package:frontend/routes/custom_router.dart';
|
||||
// import 'dart:html' as html;
|
||||
// import 'package:frontend/config/apiUrl.dart'; // 1 newly added
|
||||
// import 'package:go_router/go_router.dart';
|
||||
// import 'package:http/http.dart' as http; // 2 newly added
|
||||
// import 'dart:convert'; // 3 newly added
|
||||
//
|
||||
// import 'package:shared_preferences/shared_preferences.dart';
|
||||
//
|
||||
// import 'Screens/myTemplates/templateTest.dart';
|
||||
//
|
||||
// class MyApp extends StatefulWidget {
|
||||
// const MyApp({super.key});
|
||||
//
|
||||
// @override
|
||||
// State<MyApp> createState() => _MyAppState();
|
||||
// }
|
||||
//
|
||||
// class _MyAppState extends State<MyApp> {
|
||||
// String? _authCode;
|
||||
// String? userRole;
|
||||
// bool _isAuthRedirect = false;
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// SemanticsBinding.instance.ensureSemantics(); // ✅ Safe here
|
||||
// if (kIsWeb) {
|
||||
// final uri = Uri.parse(html.window.location.href);
|
||||
// if (uri.path == '/authredirection' &&
|
||||
// uri.queryParameters['code'] != null) {
|
||||
// _authCode = uri.queryParameters['code'];
|
||||
// // _isAuthRedirect = true;
|
||||
// html.window.console.log("Auth code detected: $_authCode");
|
||||
// // print(">>> _isAuthRedirect: $_isAuthRedirect");
|
||||
// print(">>> Auth code found at startup: $_authCode");
|
||||
//
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// handleTokenUsingMS(_authCode);
|
||||
// });
|
||||
// } else {
|
||||
// html.window.console.log("No auth code found or wrong path.");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Future<void> handleTokenUsingMS(authCode) async {
|
||||
// if (authCode == null) return;
|
||||
//
|
||||
// final url = '$apiUrl/auth/verifyMSAuthUser?code=$authCode';
|
||||
// try {
|
||||
// final response = await http.get(
|
||||
// Uri.parse(url),
|
||||
// headers: {'Content-Type': 'application/json'},
|
||||
// );
|
||||
//
|
||||
// if (response.statusCode == 200) {
|
||||
// final MS_Token = json.decode(response.body)['token'];
|
||||
// print("MS_Token - $MS_Token");
|
||||
//
|
||||
// if (MS_Token != '') {
|
||||
// print('Microsoft - Token Available');
|
||||
// await storeUserDetails(MS_Token);
|
||||
//
|
||||
// print("userRole - $userRole");
|
||||
//
|
||||
// if (userRole == "Travel Agent") {
|
||||
// router.go('/listTravelAgentPlan');
|
||||
// } else if (userRole == "Org Admin" || userRole == "Travel Admin") {
|
||||
// router.go('/listAllPlan');
|
||||
// } else {
|
||||
// router.go('/listPlan');
|
||||
// }
|
||||
// } else {
|
||||
// print('Microsoft - Token Not Available');
|
||||
// throw Exception('Token not Founded');
|
||||
// }
|
||||
// } else {
|
||||
// final errorMessage = json.decode(response.body)['message'];
|
||||
// print(errorMessage);
|
||||
// throw Exception(errorMessage);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print("Error: $e");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Future<void> storeUserDetails(String token) async {
|
||||
// try {
|
||||
// final parts = token.split('.');
|
||||
// if (parts.length != 3) throw Exception('Invalid token format');
|
||||
//
|
||||
// final payload = json.decode(
|
||||
// utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
|
||||
// );
|
||||
//
|
||||
// final userData = payload['data'];
|
||||
//
|
||||
// final prefs = await SharedPreferences.getInstance();
|
||||
// await prefs.setString('auth_token', token);
|
||||
// await prefs.setString(
|
||||
// 'user_data',
|
||||
// jsonEncode(userData),
|
||||
// ); // Store full user data
|
||||
//
|
||||
// if (userData != null) {
|
||||
// final pref = await SharedPreferences.getInstance();
|
||||
// await pref.setString('auth_token', token);
|
||||
// await pref.setString('user_data', jsonEncode(userData));
|
||||
//
|
||||
// userRole = userData['role'];
|
||||
//
|
||||
// print("userData - $userData");
|
||||
// print("userData11 - ${userData['role']}");
|
||||
// print("userData12 - $userRole");
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print('Error decoding token: $e');
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// // if (_isAuthRedirect) {
|
||||
// // print("Rendering MicrosoftPage with code: $_authCode");
|
||||
// // return MaterialApp(
|
||||
// // home: MicrosoftPage(code: _authCode),
|
||||
// // debugShowCheckedModeBanner: false,
|
||||
// // );
|
||||
// // }
|
||||
// // return MaterialApp(
|
||||
// // title: 'Quill Web Test',
|
||||
// // localizationsDelegates: const [
|
||||
// // GlobalMaterialLocalizations.delegate,
|
||||
// // GlobalCupertinoLocalizations.delegate,
|
||||
// // GlobalWidgetsLocalizations.delegate,
|
||||
// // FlutterQuillLocalizations.delegate,
|
||||
// // ],
|
||||
// // supportedLocales: const [Locale('en')],
|
||||
// // home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
// // );
|
||||
// return MaterialApp.router(
|
||||
// title: 'TRIP MANAGEMENT',
|
||||
// routerConfig: router,
|
||||
// debugShowCheckedModeBanner: false,
|
||||
// localizationsDelegates: const [
|
||||
// GlobalMaterialLocalizations.delegate,
|
||||
// GlobalCupertinoLocalizations.delegate,
|
||||
// GlobalWidgetsLocalizations.delegate,
|
||||
// FlutterQuillLocalizations.delegate,
|
||||
// ],
|
||||
// supportedLocales: const [
|
||||
// Locale('en'),
|
||||
// Locale('es'), // Add more as needed
|
||||
// ],
|
||||
// localeResolutionCallback: (locale, supportedLocales) {
|
||||
// for (var supportedLocale in supportedLocales) {
|
||||
// if (supportedLocale.languageCode == locale?.languageCode) {
|
||||
// return supportedLocale;
|
||||
// }
|
||||
// }
|
||||
// return supportedLocales.first;
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
//api url
|
||||
// const String apiUrl = 'http://apitest.tripapprovaltool.com';
|
||||
const String apiUrl = 'https://uat.tripapprovaltool.com';
|
||||
const String apiUrl = 'http://apitest.tripapprovaltool.com';
|
||||
// const String apiUrl = 'https://uat.tripapprovaltool.com';
|
||||
|
||||
@ -23,6 +23,7 @@ import '../Screens/approvals/approval_list.dart';
|
||||
import '../Screens/group/group.dart';
|
||||
import '../Screens/group/groupList.dart';
|
||||
import '../Screens/myTemplates/template.dart';
|
||||
import '../Screens/myTemplates/templateTest.dart';
|
||||
import '../Screens/userManagement/create_user/create_user.dart';
|
||||
import '../Screens/department/department_list.dart';
|
||||
import '../Screens/costCenter/costCenter_list.dart';
|
||||
@ -31,10 +32,7 @@ import '../Screens/hotels/hotels_list.dart';
|
||||
|
||||
final GoRouter router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => LoginPage(),
|
||||
),
|
||||
GoRoute(path: '/', builder: (context, state) => LoginPage()),
|
||||
// GoRoute(
|
||||
// path: '/authredirection',
|
||||
// builder: (context, state) {
|
||||
@ -42,38 +40,17 @@ final GoRouter router = GoRouter(
|
||||
// return MicrosoftPage(code: code);
|
||||
// },
|
||||
// ),
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
builder: (context, state) => HomePage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/listAllPlan',
|
||||
builder: (context, state) => ListAllPlans(),
|
||||
),
|
||||
GoRoute(path: '/home', builder: (context, state) => HomePage()),
|
||||
GoRoute(path: '/listAllPlan', builder: (context, state) => ListAllPlans()),
|
||||
GoRoute(
|
||||
path: '/listTravelAgentPlan',
|
||||
builder: (context, state) => TravelAgentListPlans(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/listPlan',
|
||||
builder: (context, state) => ListPlans(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/createPlan',
|
||||
builder: (context, state) => CreatePlan(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/allTrips/trips',
|
||||
builder: (context, state) => CreatePlan(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/approver/plans',
|
||||
builder: (context, state) => CreatePlan(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/listUser',
|
||||
builder: (context, state) => UserListScreen(),
|
||||
),
|
||||
GoRoute(path: '/listPlan', builder: (context, state) => ListPlans()),
|
||||
GoRoute(path: '/createPlan', builder: (context, state) => CreatePlan()),
|
||||
GoRoute(path: '/allTrips/trips', builder: (context, state) => CreatePlan()),
|
||||
GoRoute(path: '/approver/plans', builder: (context, state) => CreatePlan()),
|
||||
GoRoute(path: '/listUser', builder: (context, state) => UserListScreen()),
|
||||
GoRoute(
|
||||
path: '/CreateUserDetails',
|
||||
builder: (context, state) => CreateUserFormDetials(),
|
||||
@ -98,14 +75,10 @@ final GoRouter router = GoRouter(
|
||||
GoRoute(
|
||||
path: '/Policy',
|
||||
// builder: (context, state) => Policy(),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
child: Policy.fromState(state),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/PolicyList',
|
||||
builder: (context, state) => PolicyList(),
|
||||
pageBuilder:
|
||||
(context, state) => MaterialPage(child: Policy.fromState(state)),
|
||||
),
|
||||
GoRoute(path: '/PolicyList', builder: (context, state) => PolicyList()),
|
||||
GoRoute(
|
||||
path: '/OrganizationSetup',
|
||||
builder: (context, state) => OrgSetUp(),
|
||||
@ -114,50 +87,34 @@ final GoRouter router = GoRouter(
|
||||
path: '/OrganizationSettings',
|
||||
builder: (context, state) => OrganizationSetting(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/group',
|
||||
builder: (context, state) => GroupList(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/getPerdiem',
|
||||
builder: (context, state) => ForexDataList(),
|
||||
),
|
||||
GoRoute(path: '/group', builder: (context, state) => GroupList()),
|
||||
GoRoute(path: '/getPerdiem', builder: (context, state) => ForexDataList()),
|
||||
GoRoute(
|
||||
path: '/templateList',
|
||||
builder: (context, state) => TemplatesList(),
|
||||
),
|
||||
// GoRoute(
|
||||
// path: '/template',
|
||||
// builder: (context, state) => MyHomePage(),
|
||||
// ),
|
||||
GoRoute(
|
||||
path: '/template',
|
||||
// builder: (context, state) => Template(),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
child: Template.fromState(state),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/approvallist',
|
||||
builder: (context, state) => ApprovalList(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/department',
|
||||
builder: (context, state) => DepartmentList(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/costcenter',
|
||||
builder: (context, state) => CostCenterList(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/hotels',
|
||||
builder: (context, state) => HotelsDataList(),
|
||||
pageBuilder:
|
||||
(context, state) => MaterialPage(child: Template.fromState(state)),
|
||||
),
|
||||
GoRoute(path: '/approvallist', builder: (context, state) => ApprovalList()),
|
||||
GoRoute(path: '/department', builder: (context, state) => DepartmentList()),
|
||||
GoRoute(path: '/costcenter', builder: (context, state) => CostCenterList()),
|
||||
GoRoute(path: '/hotels', builder: (context, state) => HotelsDataList()),
|
||||
GoRoute(
|
||||
path: '/statusdashboard',
|
||||
builder: (context, state) => StatusDashboard(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/CreateGroup',
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
child: Group.fromState(state),
|
||||
),
|
||||
pageBuilder:
|
||||
(context, state) => MaterialPage(child: Group.fromState(state)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
76
pubspec.lock
76
pubspec.lock
@ -21,10 +21,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.13.0"
|
||||
bcrypt:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -113,6 +113,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.8.3"
|
||||
delta_to_html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: delta_to_html
|
||||
sha256: f366e50e5764a98f6be89da4881b159288ba60c2b8e1a77bd29282879ebc6145
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
diff_match_patch:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -141,10 +149,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -214,6 +222,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter_html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_html
|
||||
sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_keyboard_visibility_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -271,7 +287,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
flutter_localizations:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
@ -292,7 +308,7 @@ packages:
|
||||
source: hosted
|
||||
version: "11.4.1"
|
||||
flutter_quill_delta_from_html:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_quill_delta_from_html
|
||||
sha256: "4597bd0853a704696837aa6b05cffd851f587b176204c234edddfed1c1862a09"
|
||||
@ -342,13 +358,29 @@ packages:
|
||||
source: hosted
|
||||
version: "6.2.1"
|
||||
html:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
html2md:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html2md
|
||||
sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
html_unescape:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html_unescape
|
||||
sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -433,18 +465,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "10.0.9"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -469,6 +501,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
list_counter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: list_counter
|
||||
sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -974,10 +1014,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
version: "15.0.0"
|
||||
vsc_quill_delta_to_html:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vsc_quill_delta_to_html
|
||||
sha256: "9aca60d53ed1b700e922dabff8cd8b3490daecbf99c258f19eb45820b794fa45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
web:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1003,5 +1051,5 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
dart: ">=3.7.2 <4.0.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
23
pubspec.yaml
23
pubspec.yaml
@ -19,8 +19,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.1
|
||||
|
||||
# sdk: ^3.6.1
|
||||
sdk: ^3.7.2
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
@ -30,16 +30,21 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
flutter_quill: ^11.4.1
|
||||
flutter_quill_extensions: ^11.0.0
|
||||
flutter_localization: ^0.3.2
|
||||
cupertino_icons: ^1.0.8
|
||||
go_router: ^14.8.1
|
||||
http: ^1.3.0
|
||||
responsive_builder: ^0.7.1
|
||||
shared_preferences: ^2.5.2
|
||||
easy_stepper: ^0.8.5+1
|
||||
intl: ^0.19.0
|
||||
intl: ^0.20.2
|
||||
dropdown_search: ^5.0.6
|
||||
file_picker: ^10.0.0
|
||||
bcrypt: ^1.1.3
|
||||
@ -50,10 +55,15 @@ dependencies:
|
||||
super_tooltip: ^2.0.9
|
||||
google_fonts: ^6.2.1
|
||||
fluttertoast: ^8.2.12
|
||||
flutter_quill: ^11.4.1
|
||||
flutter_quill_extensions: ^11.0.0
|
||||
flutter_localization: ^0.3.2
|
||||
reorderables: ^0.6.0
|
||||
vsc_quill_delta_to_html: ^1.0.5
|
||||
delta_to_html: ^0.2.2
|
||||
html: ^0.15.6
|
||||
flutter_html: ^3.0.0
|
||||
html2md: ^1.3.2
|
||||
flutter_quill_delta_from_html: ^1.5.2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -62,6 +72,7 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user