Merge remote-tracking branch 'origin/main'

# Conflicts:
#	lib/Screens/policy/policy_list.dart
#	pubspec.lock
This commit is contained in:
venba-Inspriron-3558 2025-06-02 19:24:07 +05:30
commit 905759ef90
65 changed files with 4240 additions and 6500 deletions

View File

@ -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

View File

@ -4,4 +4,5 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View 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('&nbsp;', ' ')
// .replaceAll('&amp;', '&')
// .replaceAll('&lt;', '<')
// .replaceAll('&gt;', '>')
// .replaceAll('&quot;', '"')
// .replaceAll('&#39;', "'"); // 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),
),
),
),
],
);
}
}

View File

@ -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.

View File

@ -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),
],
),
);
}
}
}

View File

@ -0,0 +1,88 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill_internal.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",
style: GoogleFonts.poppins(fontSize: 15, fontWeight: FontWeight.w500),
),
content: Container(
width:
isDesktop
? MediaQuery.of(context).size.width * 0.3
: 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", style: GoogleFonts.poppins(fontSize: 12)),
),
],
);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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 '';
}
}

View File

@ -1 +0,0 @@
export 'dart:ui' if (dart.library.js_interop) 'dart:ui_web';

View File

@ -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;
}

View File

@ -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,
);
}
}

View File

@ -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);
}

View File

@ -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,
);

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -1 +0,0 @@
export './web_stub.dart' if (dart.library.js_interop) './web_real.dart';

View File

@ -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;
},
);
}

View File

@ -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.');

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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,
),
),
),
),
],
),
);
}
}

View File

@ -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');
}
}

View File

@ -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.

View File

@ -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,
),
)
],
),
),
),
],
),
),
);
}
}

View File

@ -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;
});
}
}

View File

@ -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.

View File

@ -1,6 +1 @@
import 'package:meta/meta.dart' show immutable;
@immutable
class QuillEditorWebVideoEmbedConfig {
const QuillEditorWebVideoEmbedConfig();
}
// TODO Implement this library.

View File

@ -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.

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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;
}

View File

@ -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.

View File

@ -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.

View File

@ -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;
}

View File

@ -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';

View File

@ -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.

View File

@ -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.

View File

@ -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;
}

View File

@ -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.

View File

@ -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),
],
);
}
}

View File

@ -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('&nbsp;', ' ')
.replaceAll('&amp;', '&')
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&#39;', "'"); // 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),
),
),
),
],
);
}

View File

@ -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,
),
),
),
],

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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 '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});
@ -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;
// },
// );
// }
// }

View File

@ -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';

View File

@ -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';
@ -32,10 +33,7 @@ import '../Screens/traveller/travellerList.dart';
final GoRouter router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => LoginPage(),
),
GoRoute(path: '/', builder: (context, state) => LoginPage()),
// GoRoute(
// path: '/authredirection',
// builder: (context, state) {
@ -43,38 +41,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(),
@ -99,14 +76,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(),
@ -115,41 +88,26 @@ 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(),
@ -160,9 +118,8 @@ final GoRouter router = GoRouter(
),
GoRoute(
path: '/CreateGroup',
pageBuilder: (context, state) => MaterialPage(
child: Group.fromState(state),
),
pageBuilder:
(context, state) => MaterialPage(child: Group.fromState(state)),
),
],
);

View File

@ -17,35 +17,41 @@ class OrganizationSetting extends StatefulWidget {
class OrganizationSettingState extends State<OrganizationSetting> {
@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),
// backgroundColor: Color(0xFFFCFCFC),
return Scaffold(
backgroundColor: Color(0xFFf5f5f5),
// backgroundColor: Color(0xFFFCFCFC),
// appBar: isDesktop ? null : const CustomAppBar(title: 'Home'),
// drawer: isDesktop ? null : CustomDrawer(isDesktop: false),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding: isDesktop
? EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical: MediaQuery.of(context).size.height *
0, // 5% of screen height as vertical padding
)
: EdgeInsets.all(0),
child: Row(
children: [
// if (isDesktop) CustomDrawer(isDesktop: true),
Expanded(child: buildGroupListLayout(isDesktop))
],
// appBar: isDesktop ? null : const CustomAppBar(title: 'Home'),
// drawer: isDesktop ? null : CustomDrawer(isDesktop: false),
appBar: CustomAppBar(isDesktop: isDesktop),
drawer: CustomDrawer(isDesktop: false),
body: Padding(
padding:
isDesktop
? EdgeInsets.symmetric(
horizontal:
MediaQuery.of(context).size.width *
0.1, // 30% of screen width as horizontal padding
vertical:
MediaQuery.of(context).size.height *
0, // 5% of screen height as vertical padding
)
: EdgeInsets.all(0),
child: Row(
children: [
// if (isDesktop) CustomDrawer(isDesktop: true),
Expanded(child: buildGroupListLayout(isDesktop)),
],
),
),
),
);
});
);
},
);
}
Widget buildGroupListLayout(bool isDesktop) {
@ -56,57 +62,63 @@ class OrganizationSettingState extends State<OrganizationSetting> {
'value': '/OrganizationSetup',
'icon': Icons.business,
'label': 'Organization Settings',
'description': 'Edit Logo and Email Configurations'
'description': 'Edit Logo and Email Configurations',
},
{
'value': '/group',
'icon': Icons.groups,
'label': 'Groups',
'description': 'Create and Edit Groups'
'description': 'Create and Edit Groups',
},
{
'value': '/PolicyList',
'icon': Icons.policy,
'label': 'Travel Policies',
'description': 'Create and Edit Travel Policy'
},
{
'value': '/department',
'icon': Icons.group_add_outlined,
'label': 'Departments',
'description': 'Create and Edit Department'
'description': 'Create and Edit Travel Policy',
},
{
'value': '/getPerdiem',
'icon': Icons.attach_money,
'label': 'Perdiem Amount',
'description': 'Create and Edit Perdiem Amount'
'description': 'Create and Edit Perdiem Amount',
},
{
'value': '/department',
'icon': Icons.group_add_outlined,
'label': 'Forex Template',
'description': 'Create and Edit Template',
},
{
'value': '/templateList',
'icon': Icons.email_outlined,
'label': 'Email Templates',
'description': ' Edit Email Template'
'description': ' Edit Email Template',
},
{
'value': '/department',
'icon': Icons.group_add_outlined,
'label': 'Departments',
'description': 'Create and Edit Department',
},
{
'value': '/costcenter',
'icon': Icons.account_balance_wallet,
'label': 'Cost Center',
'description': 'Create and Edit Cost Center'
'description': 'Create and Edit Cost Center',
},
{
'value': '/hotels',
'icon': Icons.add_business,
'label': 'Hotels',
'description': 'Create and Edit Hotels'
'description': 'Create and Edit Hotels',
},
{
'value': '/traveller',
'icon': Icons.travel_explore,
'label': 'Traveller',
'description': 'Create and Edit Traveller'
'description': 'Create and Edit Traveller',
},
];
// List<Widget> rows = [];
@ -225,55 +237,61 @@ class OrganizationSettingState extends State<OrganizationSetting> {
child: Wrap(
spacing: 16,
runSpacing: 16,
children: menuItems.map((item) {
return SizedBox(
width: isDesktop ? 325 : double.infinity,
child: Card(
color: Colors.white,
child: InkWell(
onTap: () {
final route = item['value'] as String;
context.go(route);
},
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
Icon(item['icon'], size: 40, color: Color(0xFF114D8B)),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item['label'],
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4),
Text(
item['description'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
),
],
children:
menuItems.map((item) {
return SizedBox(
width: isDesktop ? 325 : double.infinity,
child: Card(
color: Colors.white,
child: InkWell(
onTap: () {
final route = item['value'] as String;
context.go(route);
},
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
Icon(
item['icon'],
size: 40,
color: Color(0xFF114D8B),
),
),
],
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
item['label'],
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4),
Text(
item['description'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
),
],
),
),
],
),
),
),
),
),
);
}).toList(),
);
}).toList(),
),
),
),
@ -281,6 +299,5 @@ class OrganizationSettingState extends State<OrganizationSetting> {
],
),
);
}
}

View File

@ -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:
@ -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:
@ -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:
@ -978,6 +1018,14 @@ packages:
url: "https://pub.dev"
source: hosted
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"

View File

@ -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,9 +30,14 @@ 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
@ -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

View File

@ -35,6 +35,7 @@
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>