David PHAM-VAN

Improve PdfPreview

@@ -14,6 +14,7 @@ @@ -14,6 +14,7 @@
14 - Update android projects (mavenCentral, compileSdkVersion 30, gradle:4.1.0) 14 - Update android projects (mavenCentral, compileSdkVersion 30, gradle:4.1.0)
15 - Use syscall(SYS_memfd_create) instead of glibc function memfd_create [Obezyan] 15 - Use syscall(SYS_memfd_create) instead of glibc function memfd_create [Obezyan]
16 - Fix directPrint issue with iOS 15 16 - Fix directPrint issue with iOS 15
  17 +- Improve PdfPreview actions
17 18
18 ## 5.6.6 19 ## 5.6.6
19 20
@@ -22,8 +22,8 @@ export 'src/asset_utils.dart'; @@ -22,8 +22,8 @@ export 'src/asset_utils.dart';
22 export 'src/cache.dart'; 22 export 'src/cache.dart';
23 export 'src/callback.dart'; 23 export 'src/callback.dart';
24 export 'src/fonts/gfonts.dart'; 24 export 'src/fonts/gfonts.dart';
  25 +export 'src/preview/actions.dart';
25 export 'src/preview/pdf_preview.dart'; 26 export 'src/preview/pdf_preview.dart';
26 -export 'src/preview/pdf_preview_action.dart';  
27 export 'src/printer.dart'; 27 export 'src/printer.dart';
28 export 'src/printing.dart'; 28 export 'src/printing.dart';
29 export 'src/printing_info.dart'; 29 export 'src/printing_info.dart';
  1 +/*
  2 + * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import 'dart:math' as math;
  18 +
  19 +import 'package:flutter/foundation.dart';
  20 +import 'package:flutter/material.dart';
  21 +import 'package:pdf/pdf.dart';
  22 +
  23 +import '../../printing.dart';
  24 +import 'controller.dart';
  25 +
  26 +/// Base Action callback
  27 +typedef OnPdfPreviewActionPressed = void Function(
  28 + BuildContext context,
  29 + LayoutCallback build,
  30 + PdfPageFormat pageFormat,
  31 +);
  32 +
  33 +mixin PdfPreviewActionBounds {
  34 + final childKey = GlobalKey();
  35 +
  36 + /// Calculate the widget bounds for iPad popup position
  37 + Rect get bounds {
  38 + final referenceBox =
  39 + childKey.currentContext!.findRenderObject() as RenderBox;
  40 + final topLeft =
  41 + referenceBox.localToGlobal(referenceBox.paintBounds.topLeft);
  42 + final bottomRight =
  43 + referenceBox.localToGlobal(referenceBox.paintBounds.bottomRight);
  44 + return Rect.fromPoints(topLeft, bottomRight);
  45 + }
  46 +}
  47 +
  48 +/// Action to add the the [PdfPreview] widget
  49 +class PdfPreviewAction extends StatelessWidget {
  50 + /// Represents an icon to add to [PdfPreview]
  51 + const PdfPreviewAction({
  52 + Key? key,
  53 + required this.icon,
  54 + required this.onPressed,
  55 + }) : super(key: key);
  56 +
  57 + /// The icon to display
  58 + final Widget icon;
  59 +
  60 + /// The callback called when the user tap on the icon
  61 + final OnPdfPreviewActionPressed? onPressed;
  62 +
  63 + @override
  64 + Widget build(BuildContext context) {
  65 + return IconButton(
  66 + icon: icon,
  67 + onPressed: onPressed == null ? null : () => pressed(context),
  68 + );
  69 + }
  70 +
  71 + Future<void> pressed(BuildContext context) async {
  72 + final data = PdfPreviewController.of(context);
  73 + onPressed!(context, data.buildDocument, data.pageFormat);
  74 + }
  75 +}
  76 +
  77 +class PdfPrintAction extends StatelessWidget {
  78 + const PdfPrintAction({
  79 + Key? key,
  80 + Widget? icon,
  81 + String? jobName,
  82 + this.onPrinted,
  83 + this.onPrintError,
  84 + this.dynamicLayout = true,
  85 + this.usePrinterSettings = false,
  86 + }) : icon = icon ?? const Icon(Icons.print),
  87 + jobName = jobName ?? 'Document',
  88 + super(key: key);
  89 +
  90 + final Widget icon;
  91 +
  92 + final String jobName;
  93 +
  94 + /// Request page re-layout to match the printer paper and margins.
  95 + /// Mitigate an issue with iOS and macOS print dialog that prevent any
  96 + /// channel message while opened.
  97 + final bool dynamicLayout;
  98 +
  99 + /// Set [usePrinterSettings] to true to use the configuration defined by
  100 + /// the printer. May not work for all the printers and can depend on the
  101 + /// drivers. (Supported platforms: Windows)
  102 + final bool usePrinterSettings;
  103 +
  104 + /// Called if the user prints the pdf document
  105 + final VoidCallback? onPrinted;
  106 +
  107 + /// Called if an error creating the Pdf occured
  108 + final void Function(dynamic error)? onPrintError;
  109 +
  110 + @override
  111 + Widget build(BuildContext context) {
  112 + return PdfPreviewAction(
  113 + icon: icon,
  114 + onPressed: _print,
  115 + );
  116 + }
  117 +
  118 + Future<void> _print(
  119 + BuildContext context,
  120 + LayoutCallback build,
  121 + PdfPageFormat pageFormat,
  122 + ) async {
  123 + final data = PdfPreviewController.of(context);
  124 +
  125 + try {
  126 + final result = await Printing.layoutPdf(
  127 + onLayout: build,
  128 + name: jobName,
  129 + format: data.actualPageFormat,
  130 + dynamicLayout: dynamicLayout,
  131 + usePrinterSettings: usePrinterSettings,
  132 + );
  133 +
  134 + if (result) {
  135 + onPrinted?.call();
  136 + }
  137 + } catch (exception, stack) {
  138 + InformationCollector? collector;
  139 +
  140 + assert(() {
  141 + collector = () sync* {
  142 + yield StringProperty('PageFormat', data.actualPageFormat.toString());
  143 + };
  144 + return true;
  145 + }());
  146 +
  147 + FlutterError.reportError(FlutterErrorDetails(
  148 + exception: exception,
  149 + stack: stack,
  150 + library: 'printing',
  151 + context: ErrorDescription('while printing a PDF'),
  152 + informationCollector: collector,
  153 + ));
  154 +
  155 + onPrintError?.call(exception);
  156 + }
  157 + }
  158 +}
  159 +
  160 +class PdfShareAction extends StatelessWidget with PdfPreviewActionBounds {
  161 + PdfShareAction({
  162 + Key? key,
  163 + Widget? icon,
  164 + String? filename,
  165 + this.subject,
  166 + this.body,
  167 + this.emails,
  168 + this.onShared,
  169 + this.onShareError,
  170 + }) : icon = icon ?? const Icon(Icons.share),
  171 + filename = filename ?? 'document.pdf',
  172 + super(key: key);
  173 +
  174 + final Widget icon;
  175 +
  176 + final String filename;
  177 +
  178 + /// email subject when email application is selected from the share dialog
  179 + final String? subject;
  180 +
  181 + /// extra text to share with Pdf document
  182 + final String? body;
  183 +
  184 + /// list of email addresses which will be filled automatically if the email application
  185 + /// is selected from the share dialog.
  186 + /// This will work only for Android platform.
  187 + final List<String>? emails;
  188 +
  189 + /// Called if the user prints the pdf document
  190 + final VoidCallback? onShared;
  191 +
  192 + /// Called if an error creating the Pdf occured
  193 + final void Function(dynamic error)? onShareError;
  194 +
  195 + @override
  196 + Widget build(BuildContext context) {
  197 + return PdfPreviewAction(
  198 + key: childKey,
  199 + icon: icon,
  200 + onPressed: _share,
  201 + );
  202 + }
  203 +
  204 + Future<void> _share(
  205 + BuildContext context,
  206 + LayoutCallback build,
  207 + PdfPageFormat pageFormat,
  208 + ) async {
  209 + final bytes = await build(pageFormat);
  210 +
  211 + final result = await Printing.sharePdf(
  212 + bytes: bytes,
  213 + bounds: bounds,
  214 + filename: filename,
  215 + body: body,
  216 + subject: subject,
  217 + emails: emails,
  218 + );
  219 +
  220 + if (result) {
  221 + onShared?.call();
  222 + }
  223 + }
  224 +}
  225 +
  226 +class PdfPageFormatAction extends StatelessWidget {
  227 + const PdfPageFormatAction({
  228 + Key? key,
  229 + required this.pageFormats,
  230 + }) : super(key: key);
  231 +
  232 + /// List of page formats the user can choose
  233 + final Map<String, PdfPageFormat> pageFormats;
  234 +
  235 + @override
  236 + Widget build(BuildContext context) {
  237 + final theme = Theme.of(context);
  238 + final iconColor = theme.primaryIconTheme.color ?? Colors.white;
  239 + final data = PdfPreviewController.listen(context);
  240 + final keys = pageFormats.keys.toList();
  241 +
  242 + return DropdownButton<PdfPageFormat>(
  243 + dropdownColor: theme.primaryColor,
  244 + icon: Icon(
  245 + Icons.arrow_drop_down,
  246 + color: iconColor,
  247 + ),
  248 + value: data.pageFormat,
  249 + items: List<DropdownMenuItem<PdfPageFormat>>.generate(
  250 + pageFormats.length,
  251 + (int index) {
  252 + final key = keys[index];
  253 + final val = pageFormats[key];
  254 + return DropdownMenuItem<PdfPageFormat>(
  255 + value: val,
  256 + child: Text(key, style: TextStyle(color: iconColor)),
  257 + );
  258 + },
  259 + ),
  260 + onChanged: (PdfPageFormat? pageFormat) {
  261 + if (pageFormat != null) {
  262 + data.pageFormat = pageFormat;
  263 + }
  264 + },
  265 + );
  266 + }
  267 +}
  268 +
  269 +class PdfPageOrientationAction extends StatelessWidget {
  270 + const PdfPageOrientationAction({
  271 + Key? key,
  272 + }) : super(key: key);
  273 +
  274 + @override
  275 + Widget build(BuildContext context) {
  276 + final theme = Theme.of(context);
  277 + final iconColor = theme.primaryIconTheme.color ?? Colors.white;
  278 + final data = PdfPreviewController.listen(context);
  279 + final horizontal = data.horizontal;
  280 +
  281 + final disabledColor = iconColor.withAlpha(120);
  282 + return ToggleButtons(
  283 + renderBorder: false,
  284 + borderColor: disabledColor,
  285 + color: disabledColor,
  286 + selectedBorderColor: iconColor,
  287 + selectedColor: iconColor,
  288 + onPressed: (int index) {
  289 + data.horizontal = index == 1;
  290 + },
  291 + isSelected: <bool>[horizontal == false, horizontal == true],
  292 + children: <Widget>[
  293 + Transform.rotate(
  294 + angle: -math.pi / 2,
  295 + child: const Icon(
  296 + Icons.note_outlined,
  297 + ),
  298 + ),
  299 + const Icon(Icons.note_outlined),
  300 + ],
  301 + );
  302 + }
  303 +}
  1 +/*
  2 + * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import 'package:flutter/material.dart';
  18 +import 'package:pdf/pdf.dart';
  19 +
  20 +import '../callback.dart';
  21 +
  22 +mixin PdfPreviewData implements ChangeNotifier {
  23 + // PdfPreviewData(this.build);
  24 +
  25 + LayoutCallback get buildDocument;
  26 +
  27 + PdfPageFormat? _pageFormat;
  28 +
  29 + PdfPageFormat get initialPageFormat;
  30 +
  31 + PdfPageFormat get pageFormat => _pageFormat ?? initialPageFormat;
  32 +
  33 + set pageFormat(PdfPageFormat value) {
  34 + if (_pageFormat == value) {
  35 + return;
  36 + }
  37 + _pageFormat = value;
  38 + notifyListeners();
  39 + }
  40 +
  41 + bool? _horizontal;
  42 +
  43 + /// Is the print horizontal
  44 + bool get horizontal => _horizontal ?? pageFormat.width > pageFormat.height;
  45 +
  46 + set horizontal(bool value) {
  47 + if (_horizontal == value) {
  48 + return;
  49 + }
  50 + _horizontal = value;
  51 + notifyListeners();
  52 + }
  53 +
  54 + /// Computed page format
  55 + PdfPageFormat get computedPageFormat =>
  56 + horizontal ? pageFormat.landscape : pageFormat.portrait;
  57 +
  58 + String get localPageFormat {
  59 + final locale = WidgetsBinding.instance!.window.locale;
  60 + // ignore: unnecessary_cast
  61 + final cc = (locale as Locale?)?.countryCode ?? 'US';
  62 +
  63 + if (cc == 'US' || cc == 'CA' || cc == 'MX') {
  64 + return 'Letter';
  65 + }
  66 + return 'A4';
  67 + }
  68 +
  69 + PdfPageFormat get actualPageFormat => pageFormat;
  70 +}
  71 +
  72 +class PdfPreviewController extends InheritedNotifier {
  73 + const PdfPreviewController({
  74 + Key? key,
  75 + required this.data,
  76 + required Widget child,
  77 + }) : super(key: key, child: child, notifier: data);
  78 +
  79 + final PdfPreviewData data;
  80 +
  81 + static PdfPreviewData of(BuildContext context) {
  82 + final result =
  83 + context.findAncestorWidgetOfExactType<PdfPreviewController>();
  84 + assert(result != null, 'No PdfPreview found in context');
  85 + return result!.data;
  86 + }
  87 +
  88 + static PdfPreviewData listen(BuildContext context) {
  89 + final result =
  90 + context.dependOnInheritedWidgetOfExactType<PdfPreviewController>();
  91 + assert(result != null, 'No PdfPreview found in context');
  92 + return result!.data;
  93 + }
  94 +
  95 + @override
  96 + bool updateShouldNotify(covariant InheritedWidget oldWidget) {
  97 + return false;
  98 + }
  99 +}
  1 +/*
  2 + * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import 'dart:async';
  18 +
  19 +import 'package:flutter/material.dart';
  20 +import 'package:pdf/pdf.dart';
  21 +
  22 +import '../callback.dart';
  23 +import '../printing.dart';
  24 +import '../printing_info.dart';
  25 +import 'raster.dart';
  26 +
  27 +/// Flutter widget that uses the rasterized pdf pages to display a document.
  28 +class PdfPreviewCustom extends StatefulWidget {
  29 + /// Show a pdf document built on demand
  30 + const PdfPreviewCustom({
  31 + Key? key,
  32 + this.pageFormat = PdfPageFormat.a4,
  33 + required this.build,
  34 + this.maxPageWidth,
  35 + this.onError,
  36 + this.scrollViewDecoration,
  37 + this.pdfPreviewPageDecoration,
  38 + this.pages,
  39 + this.previewPageMargin,
  40 + this.padding,
  41 + this.shouldRepaint = false,
  42 + this.loadingWidget,
  43 + }) : super(key: key);
  44 +
  45 + /// Pdf paper page format
  46 + final PdfPageFormat pageFormat;
  47 +
  48 + /// Called when a pdf document is needed
  49 + final LayoutCallback build;
  50 +
  51 + /// Maximum width of the pdf document on screen
  52 + final double? maxPageWidth;
  53 +
  54 + /// Widget to display if the PDF document cannot be displayed
  55 + final Widget Function(BuildContext context, Object error)? onError;
  56 +
  57 + /// Decoration of scrollView
  58 + final Decoration? scrollViewDecoration;
  59 +
  60 + /// Decoration of PdfPreviewPage
  61 + final Decoration? pdfPreviewPageDecoration;
  62 +
  63 + /// Pages to display. Default will display all the pages.
  64 + final List<int>? pages;
  65 +
  66 + /// margin for the document preview page
  67 + ///
  68 + /// defaults to [EdgeInsets.only(left: 20, top: 8, right: 20, bottom: 12)],
  69 + final EdgeInsets? previewPageMargin;
  70 +
  71 + /// padding for the pdf_preview widget
  72 + final EdgeInsets? padding;
  73 +
  74 + /// Force repainting the PDF document
  75 + final bool shouldRepaint;
  76 +
  77 + /// Custom loading widget to use that is shown while PDF is being generated.
  78 + /// If null, a [CircularProgressIndicator] is used instead.
  79 + final Widget? loadingWidget;
  80 +
  81 + @override
  82 + PdfPreviewCustomState createState() => PdfPreviewCustomState();
  83 +}
  84 +
  85 +class PdfPreviewCustomState extends State<PdfPreviewCustom>
  86 + with PdfPreviewRaster {
  87 + final listView = GlobalKey();
  88 +
  89 + bool infoLoaded = false;
  90 +
  91 + int? preview;
  92 +
  93 + double? updatePosition;
  94 +
  95 + final scrollController = ScrollController(
  96 + keepScrollOffset: true,
  97 + );
  98 +
  99 + final transformationController = TransformationController();
  100 +
  101 + Timer? previewUpdate;
  102 +
  103 + static const _errorMessage = 'Unable to display the document';
  104 +
  105 + @override
  106 + void dispose() {
  107 + previewUpdate?.cancel();
  108 + super.dispose();
  109 + }
  110 +
  111 + @override
  112 + void reassemble() {
  113 + raster();
  114 + super.reassemble();
  115 + }
  116 +
  117 + @override
  118 + void didUpdateWidget(covariant PdfPreviewCustom oldWidget) {
  119 + if (oldWidget.build != widget.build ||
  120 + widget.shouldRepaint ||
  121 + widget.pageFormat != oldWidget.pageFormat) {
  122 + preview = null;
  123 + updatePosition = null;
  124 + raster();
  125 + }
  126 + super.didUpdateWidget(oldWidget);
  127 + }
  128 +
  129 + @override
  130 + void didChangeDependencies() {
  131 + if (!infoLoaded) {
  132 + infoLoaded = true;
  133 + Printing.info().then((PrintingInfo _info) {
  134 + setState(() {
  135 + info = _info;
  136 + raster();
  137 + });
  138 + });
  139 + }
  140 +
  141 + raster();
  142 + super.didChangeDependencies();
  143 + }
  144 +
  145 + Widget _showError(Object error) {
  146 + if (widget.onError != null) {
  147 + return widget.onError!(context, error);
  148 + }
  149 +
  150 + return ErrorWidget(error);
  151 + }
  152 +
  153 + Widget _createPreview() {
  154 + if (error != null) {
  155 + return _showError(error!);
  156 + }
  157 +
  158 + final _info = info;
  159 + if (_info != null && !_info.canRaster) {
  160 + return _showError(_errorMessage);
  161 + }
  162 +
  163 + if (pages.isEmpty) {
  164 + return widget.loadingWidget ??
  165 + const Center(
  166 + child: CircularProgressIndicator(),
  167 + );
  168 + }
  169 +
  170 + return ListView.builder(
  171 + controller: scrollController,
  172 + padding: widget.padding,
  173 + itemCount: pages.length,
  174 + itemBuilder: (BuildContext context, int index) => GestureDetector(
  175 + onDoubleTap: () {
  176 + setState(() {
  177 + updatePosition = scrollController.position.pixels;
  178 + preview = index;
  179 + transformationController.value.setIdentity();
  180 + });
  181 + },
  182 + child: pages[index],
  183 + ),
  184 + );
  185 + }
  186 +
  187 + Widget _zoomPreview() {
  188 + return GestureDetector(
  189 + onDoubleTap: () {
  190 + setState(() {
  191 + preview = null;
  192 + });
  193 + },
  194 + child: InteractiveViewer(
  195 + transformationController: transformationController,
  196 + maxScale: 5,
  197 + child: Center(child: pages[preview!]),
  198 + ),
  199 + );
  200 + }
  201 +
  202 + @override
  203 + Widget build(BuildContext context) {
  204 + Widget page;
  205 +
  206 + if (preview != null) {
  207 + page = _zoomPreview();
  208 + } else {
  209 + page = Container(
  210 + constraints: widget.maxPageWidth != null
  211 + ? BoxConstraints(maxWidth: widget.maxPageWidth!)
  212 + : null,
  213 + child: _createPreview(),
  214 + );
  215 +
  216 + if (updatePosition != null) {
  217 + Timer.run(() {
  218 + scrollController.jumpTo(updatePosition!);
  219 + updatePosition = null;
  220 + });
  221 + }
  222 + }
  223 +
  224 + return Container(
  225 + decoration: widget.scrollViewDecoration ??
  226 + BoxDecoration(
  227 + gradient: LinearGradient(
  228 + colors: <Color>[Colors.grey.shade400, Colors.grey.shade200],
  229 + begin: Alignment.topCenter,
  230 + end: Alignment.bottomCenter,
  231 + ),
  232 + ),
  233 + width: double.infinity,
  234 + alignment: Alignment.center,
  235 + child: page,
  236 + );
  237 + }
  238 +}
@@ -14,10 +14,6 @@ @@ -14,10 +14,6 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
17 -import 'dart:async';  
18 -import 'dart:math';  
19 -  
20 -import 'package:flutter/foundation.dart';  
21 import 'package:flutter/material.dart'; 17 import 'package:flutter/material.dart';
22 import 'package:pdf/pdf.dart'; 18 import 'package:pdf/pdf.dart';
23 import 'package:pdf/widgets.dart' as pw; 19 import 'package:pdf/widgets.dart' as pw;
@@ -25,8 +21,11 @@ import 'package:pdf/widgets.dart' as pw; @@ -25,8 +21,11 @@ import 'package:pdf/widgets.dart' as pw;
25 import '../callback.dart'; 21 import '../callback.dart';
26 import '../printing.dart'; 22 import '../printing.dart';
27 import '../printing_info.dart'; 23 import '../printing_info.dart';
28 -import 'pdf_preview_action.dart';  
29 -import 'pdf_preview_raster.dart'; 24 +import 'actions.dart';
  25 +import 'controller.dart';
  26 +import 'custom.dart';
  27 +
  28 +export 'custom.dart';
30 29
31 /// Flutter widget that uses the rasterized pdf pages to display a document. 30 /// Flutter widget that uses the rasterized pdf pages to display a document.
32 class PdfPreview extends StatefulWidget { 31 class PdfPreview extends StatefulWidget {
@@ -115,7 +114,7 @@ class PdfPreview extends StatefulWidget { @@ -115,7 +114,7 @@ class PdfPreview extends StatefulWidget {
115 /// Decoration of scrollView 114 /// Decoration of scrollView
116 final Decoration? scrollViewDecoration; 115 final Decoration? scrollViewDecoration;
117 116
118 - /// Decoration of _PdfPreviewPage 117 + /// Decoration of PdfPreviewPage
119 final Decoration? pdfPreviewPageDecoration; 118 final Decoration? pdfPreviewPageDecoration;
120 119
121 /// Name of the PDF when sharing. It must include the extension. 120 /// Name of the PDF when sharing. It must include the extension.
@@ -159,89 +158,64 @@ class PdfPreview extends StatefulWidget { @@ -159,89 +158,64 @@ class PdfPreview extends StatefulWidget {
159 _PdfPreviewState createState() => _PdfPreviewState(); 158 _PdfPreviewState createState() => _PdfPreviewState();
160 } 159 }
161 160
162 -class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster {  
163 - final GlobalKey<State<StatefulWidget>> shareWidget = GlobalKey();  
164 - final GlobalKey<State<StatefulWidget>> listView = GlobalKey(); 161 +class _PdfPreviewState extends State<PdfPreview>
  162 + with PdfPreviewData, ChangeNotifier {
  163 + final previewWidget = GlobalKey<PdfPreviewCustomState>();
165 164
166 - PdfPageFormat? _pageFormat; 165 + /// Printing subsystem information
  166 + PrintingInfo? info;
  167 + var infoLoaded = false;
167 168
168 - String get localPageFormat {  
169 - final locale = WidgetsBinding.instance!.window.locale;  
170 - // ignore: unnecessary_cast  
171 - final cc = (locale as Locale?)?.countryCode ?? 'US'; 169 + @override
  170 + LayoutCallback get buildDocument => widget.build;
172 171
173 - if (cc == 'US' || cc == 'CA' || cc == 'MX') {  
174 - return 'Letter';  
175 - }  
176 - return 'A4';  
177 - } 172 + @override
  173 + PdfPageFormat get initialPageFormat =>
  174 + widget.initialPageFormat ??
  175 + (widget.pageFormats.isNotEmpty
  176 + ? (widget.pageFormats[localPageFormat] ??
  177 + widget.pageFormats.values.first)
  178 + : (PdfPreview._defaultPageFormats[localPageFormat]) ??
  179 + PdfPreview._defaultPageFormats.values.first);
178 180
179 @override 181 @override
180 PdfPageFormat get pageFormat { 182 PdfPageFormat get pageFormat {
181 - _pageFormat ??= widget.initialPageFormat == null  
182 - ? widget.pageFormats[localPageFormat]  
183 - : _pageFormat = widget.initialPageFormat!; 183 + var _pageFormat = super.pageFormat;
184 184
185 if (!widget.pageFormats.containsValue(_pageFormat)) { 185 if (!widget.pageFormats.containsValue(_pageFormat)) {
186 - _pageFormat = widget.initialPageFormat ??  
187 - (widget.pageFormats.isNotEmpty  
188 - ? widget.pageFormats.values.first  
189 - : PdfPreview._defaultPageFormats[localPageFormat]); 186 + _pageFormat = initialPageFormat;
  187 + pageFormat = _pageFormat;
190 } 188 }
191 189
192 - return _pageFormat!; 190 + return _pageFormat;
193 } 191 }
194 192
195 - bool infoLoaded = false;  
196 -  
197 - int? preview;  
198 -  
199 - double? updatePosition;  
200 -  
201 - final scrollController = ScrollController(  
202 - keepScrollOffset: true,  
203 - );  
204 -  
205 - final transformationController = TransformationController();  
206 -  
207 - Timer? previewUpdate;  
208 -  
209 - static const _errorMessage = 'Unable to display the document';  
210 -  
211 @override 193 @override
212 - void initState() {  
213 - if (widget.initialPageFormat == null) {  
214 - final locale = WidgetsBinding.instance!.window.locale;  
215 - // ignore: unnecessary_cast  
216 - final cc = (locale as Locale?)?.countryCode ?? 'US';  
217 -  
218 - if (cc == 'US' || cc == 'CA' || cc == 'MX') {  
219 - _pageFormat = PdfPageFormat.letter;  
220 - } else {  
221 - _pageFormat = PdfPageFormat.a4;  
222 - }  
223 - } else {  
224 - _pageFormat = widget.initialPageFormat!;  
225 - } 194 + PdfPageFormat get actualPageFormat {
  195 + var format = pageFormat;
  196 + final pages = previewWidget.currentState?.pages ?? const [];
  197 + final dpi = previewWidget.currentState?.dpi ?? 72;
226 198
227 - final _pageFormats = widget.pageFormats;  
228 - if (!_pageFormats.containsValue(pageFormat)) {  
229 - _pageFormat = _pageFormats.values.first; 199 + if (!widget.canChangePageFormat && pages.isNotEmpty) {
  200 + format = PdfPageFormat(
  201 + pages.first.page!.width * 72 / dpi,
  202 + pages.first.page!.height * 72 / dpi,
  203 + marginAll: 5 * PdfPageFormat.mm,
  204 + );
230 } 205 }
231 206
232 - super.initState(); 207 + return format;
233 } 208 }
234 209
235 @override 210 @override
236 - void dispose() {  
237 - previewUpdate?.cancel();  
238 - super.dispose(); 211 + void initState() {
  212 + addListener(() {
  213 + if (mounted) {
  214 + setState(() {});
239 } 215 }
  216 + });
240 217
241 - @override  
242 - void reassemble() {  
243 - raster();  
244 - super.reassemble(); 218 + super.initState();
245 } 219 }
246 220
247 @override 221 @override
@@ -249,10 +223,7 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster { @@ -249,10 +223,7 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster {
249 if (oldWidget.build != widget.build || 223 if (oldWidget.build != widget.build ||
250 widget.shouldRepaint || 224 widget.shouldRepaint ||
251 widget.pageFormats != oldWidget.pageFormats) { 225 widget.pageFormats != oldWidget.pageFormats) {
252 - preview = null;  
253 - updatePosition = null;  
254 -  
255 - raster(); 226 + setState(() {});
256 } 227 }
257 super.didUpdateWidget(oldWidget); 228 super.didUpdateWidget(oldWidget);
258 } 229 }
@@ -264,208 +235,51 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster { @@ -264,208 +235,51 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster {
264 Printing.info().then((PrintingInfo _info) { 235 Printing.info().then((PrintingInfo _info) {
265 setState(() { 236 setState(() {
266 info = _info; 237 info = _info;
267 - raster();  
268 }); 238 });
269 }); 239 });
270 } 240 }
271 241
272 - raster();  
273 super.didChangeDependencies(); 242 super.didChangeDependencies();
274 } 243 }
275 244
276 - Widget _showError(Object error) {  
277 - if (widget.onError != null) {  
278 - return widget.onError!(context, error);  
279 - }  
280 -  
281 - return ErrorWidget(error);  
282 - }  
283 -  
284 - Widget _createPreview() {  
285 - if (error != null) {  
286 - return _showError(error!);  
287 - }  
288 -  
289 - final _info = info;  
290 - if (_info != null && !_info.canRaster) {  
291 - return _showError(_errorMessage);  
292 - }  
293 -  
294 - if (pages.isEmpty) {  
295 - return widget.loadingWidget ??  
296 - const Center(  
297 - child: CircularProgressIndicator(),  
298 - );  
299 - }  
300 -  
301 - return ListView.builder(  
302 - controller: scrollController,  
303 - padding: widget.padding,  
304 - itemCount: pages.length,  
305 - itemBuilder: (BuildContext context, int index) => GestureDetector(  
306 - onDoubleTap: () {  
307 - setState(() {  
308 - updatePosition = scrollController.position.pixels;  
309 - preview = index;  
310 - transformationController.value.setIdentity();  
311 - });  
312 - },  
313 - child: pages[index],  
314 - ),  
315 - );  
316 - }  
317 -  
318 - Widget _zoomPreview() {  
319 - return GestureDetector(  
320 - onDoubleTap: () {  
321 - setState(() {  
322 - preview = null;  
323 - });  
324 - },  
325 - child: InteractiveViewer(  
326 - transformationController: transformationController,  
327 - maxScale: 5,  
328 - child: Center(child: pages[preview!]),  
329 - ),  
330 - );  
331 - }  
332 -  
333 @override 245 @override
334 Widget build(BuildContext context) { 246 Widget build(BuildContext context) {
335 final theme = Theme.of(context); 247 final theme = Theme.of(context);
336 final iconColor = theme.primaryIconTheme.color ?? Colors.white; 248 final iconColor = theme.primaryIconTheme.color ?? Colors.white;
337 249
338 - Widget page;  
339 -  
340 - if (preview != null) {  
341 - page = _zoomPreview();  
342 - } else {  
343 - page = Container(  
344 - constraints: widget.maxPageWidth != null  
345 - ? BoxConstraints(maxWidth: widget.maxPageWidth!)  
346 - : null,  
347 - child: _createPreview(),  
348 - );  
349 -  
350 - if (updatePosition != null) {  
351 - Timer.run(() {  
352 - scrollController.jumpTo(updatePosition!);  
353 - updatePosition = null;  
354 - });  
355 - }  
356 - }  
357 -  
358 - final Widget scrollView = Container(  
359 - decoration: widget.scrollViewDecoration ??  
360 - BoxDecoration(  
361 - gradient: LinearGradient(  
362 - colors: <Color>[Colors.grey.shade400, Colors.grey.shade200],  
363 - begin: Alignment.topCenter,  
364 - end: Alignment.bottomCenter,  
365 - ),  
366 - ),  
367 - width: double.infinity,  
368 - alignment: Alignment.center,  
369 - child: page,  
370 - );  
371 -  
372 final actions = <Widget>[]; 250 final actions = <Widget>[];
373 251
374 if (widget.allowPrinting && info?.canPrint == true) { 252 if (widget.allowPrinting && info?.canPrint == true) {
375 - actions.add(  
376 - IconButton(  
377 - icon: const Icon(Icons.print),  
378 - onPressed: _print,  
379 - ),  
380 - ); 253 + actions.add(PdfPrintAction(
  254 + jobName: widget.pdfFileName,
  255 + dynamicLayout: widget.dynamicLayout,
  256 + onPrinted:
  257 + widget.onPrinted == null ? null : () => widget.onPrinted!(context),
  258 + onPrintError: widget.onPrintError == null
  259 + ? null
  260 + : (dynamic error) => widget.onPrintError!(context, error),
  261 + ));
381 } 262 }
382 263
383 if (widget.allowSharing && info?.canShare == true) { 264 if (widget.allowSharing && info?.canShare == true) {
384 - actions.add(  
385 - IconButton(  
386 - key: shareWidget,  
387 - icon: const Icon(Icons.share),  
388 - onPressed: _share,  
389 - ),  
390 - ); 265 + actions.add(PdfShareAction(
  266 + filename: widget.pdfFileName,
  267 + onShared:
  268 + widget.onPrinted == null ? null : () => widget.onPrinted!(context),
  269 + ));
391 } 270 }
392 271
393 if (widget.canChangePageFormat) { 272 if (widget.canChangePageFormat) {
394 - final keys = widget.pageFormats.keys.toList();  
395 - actions.add(  
396 - DropdownButton<PdfPageFormat>(  
397 - dropdownColor: theme.primaryColor,  
398 - icon: Icon(  
399 - Icons.arrow_drop_down,  
400 - color: iconColor,  
401 - ),  
402 - value: pageFormat,  
403 - items: List<DropdownMenuItem<PdfPageFormat>>.generate(  
404 - widget.pageFormats.length,  
405 - (int index) {  
406 - final key = keys[index];  
407 - final val = widget.pageFormats[key];  
408 - return DropdownMenuItem<PdfPageFormat>(  
409 - value: val,  
410 - child: Text(key, style: TextStyle(color: iconColor)),  
411 - );  
412 - },  
413 - ),  
414 - onChanged: (PdfPageFormat? pageFormat) {  
415 - setState(() {  
416 - if (pageFormat != null) {  
417 - _pageFormat = pageFormat;  
418 - raster();  
419 - }  
420 - });  
421 - },  
422 - ),  
423 - ); 273 + actions.add(PdfPageFormatAction(
  274 + pageFormats: widget.pageFormats,
  275 + ));
424 276
425 if (widget.canChangeOrientation) { 277 if (widget.canChangeOrientation) {
426 - horizontal ??= pageFormat.width > pageFormat.height;  
427 -  
428 - final disabledColor = iconColor.withAlpha(120);  
429 - actions.add(  
430 - ToggleButtons(  
431 - renderBorder: false,  
432 - borderColor: disabledColor,  
433 - color: disabledColor,  
434 - selectedBorderColor: iconColor,  
435 - selectedColor: iconColor,  
436 - onPressed: (int index) {  
437 - setState(() {  
438 - horizontal = index == 1;  
439 - raster();  
440 - });  
441 - },  
442 - isSelected: <bool>[horizontal == false, horizontal == true],  
443 - children: <Widget>[  
444 - Transform.rotate(  
445 - angle: -pi / 2, child: const Icon(Icons.note_outlined)),  
446 - const Icon(Icons.note_outlined),  
447 - ],  
448 - ),  
449 - ); 278 + actions.add(const PdfPageOrientationAction());
450 } 279 }
451 } 280 }
452 281
453 - if (widget.actions != null) {  
454 - for (final action in widget.actions!) {  
455 - actions.add(  
456 - IconButton(  
457 - icon: action.icon,  
458 - onPressed: action.onPressed == null  
459 - ? null  
460 - : () => action.onPressed!(  
461 - context,  
462 - widget.build,  
463 - computedPageFormat,  
464 - ),  
465 - ),  
466 - );  
467 - }  
468 - } 282 + widget.actions?.forEach(actions.add);
469 283
470 assert(() { 284 assert(() {
471 if (actions.isNotEmpty && widget.canDebug) { 285 if (actions.isNotEmpty && widget.canDebug) {
@@ -474,12 +288,10 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster { @@ -474,12 +288,10 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster {
474 activeColor: Colors.red, 288 activeColor: Colors.red,
475 value: pw.Document.debug, 289 value: pw.Document.debug,
476 onChanged: (bool value) { 290 onChanged: (bool value) {
477 - setState(  
478 - () { 291 + setState(() {
479 pw.Document.debug = value; 292 pw.Document.debug = value;
480 - raster();  
481 - },  
482 - ); 293 + });
  294 + previewWidget.currentState?.raster();
483 }, 295 },
484 ), 296 ),
485 ); 297 );
@@ -488,10 +300,27 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster { @@ -488,10 +300,27 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster {
488 return true; 300 return true;
489 }()); 301 }());
490 302
491 - return Column( 303 + return PdfPreviewController(
  304 + data: this,
  305 + child: Column(
492 mainAxisAlignment: MainAxisAlignment.center, 306 mainAxisAlignment: MainAxisAlignment.center,
493 children: <Widget>[ 307 children: <Widget>[
494 - Expanded(child: scrollView), 308 + Expanded(
  309 + child: PdfPreviewCustom(
  310 + key: previewWidget,
  311 + build: widget.build,
  312 + loadingWidget: widget.loadingWidget,
  313 + maxPageWidth: widget.maxPageWidth,
  314 + onError: widget.onError,
  315 + padding: widget.padding,
  316 + pageFormat: computedPageFormat,
  317 + pages: widget.pages,
  318 + pdfPreviewPageDecoration: widget.pdfPreviewPageDecoration,
  319 + previewPageMargin: widget.previewPageMargin,
  320 + scrollViewDecoration: widget.scrollViewDecoration,
  321 + shouldRepaint: widget.shouldRepaint,
  322 + ),
  323 + ),
495 if (actions.isNotEmpty && widget.useActions) 324 if (actions.isNotEmpty && widget.useActions)
496 IconTheme.merge( 325 IconTheme.merge(
497 data: IconThemeData( 326 data: IconThemeData(
@@ -510,79 +339,9 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster { @@ -510,79 +339,9 @@ class _PdfPreviewState extends State<PdfPreview> with PdfPreviewRaster {
510 ), 339 ),
511 ), 340 ),
512 ), 341 ),
513 - ) 342 + ),
514 ], 343 ],
  344 + ),
515 ); 345 );
516 } 346 }
517 -  
518 - Future<void> _print() async {  
519 - var format = computedPageFormat;  
520 -  
521 - if (!widget.canChangePageFormat && pages.isNotEmpty) {  
522 - format = PdfPageFormat(  
523 - pages.first.page!.width * 72 / dpi,  
524 - pages.first.page!.height * 72 / dpi,  
525 - marginAll: 5 * PdfPageFormat.mm,  
526 - );  
527 - }  
528 -  
529 - try {  
530 - final result = await Printing.layoutPdf(  
531 - onLayout: widget.build,  
532 - name: widget.pdfFileName ?? 'Document',  
533 - format: format,  
534 - dynamicLayout: widget.dynamicLayout,  
535 - );  
536 -  
537 - if (result && widget.onPrinted != null) {  
538 - widget.onPrinted!(context);  
539 - }  
540 - } catch (exception, stack) {  
541 - InformationCollector? collector;  
542 -  
543 - assert(() {  
544 - collector = () sync* {  
545 - yield StringProperty('PageFormat', computedPageFormat.toString());  
546 - };  
547 - return true;  
548 - }());  
549 -  
550 - FlutterError.reportError(FlutterErrorDetails(  
551 - exception: exception,  
552 - stack: stack,  
553 - library: 'printing',  
554 - context: ErrorDescription('while printing a PDF'),  
555 - informationCollector: collector,  
556 - ));  
557 -  
558 - if (widget.onPrintError != null) {  
559 - widget.onPrintError!(context, exception);  
560 - }  
561 - }  
562 - }  
563 -  
564 - Future<void> _share() async {  
565 - // Calculate the widget center for iPad sharing popup position  
566 - final referenceBox =  
567 - shareWidget.currentContext!.findRenderObject() as RenderBox;  
568 - final topLeft =  
569 - referenceBox.localToGlobal(referenceBox.paintBounds.topLeft);  
570 - final bottomRight =  
571 - referenceBox.localToGlobal(referenceBox.paintBounds.bottomRight);  
572 - final bounds = Rect.fromPoints(topLeft, bottomRight);  
573 -  
574 - final bytes = await widget.build(computedPageFormat);  
575 - final result = await Printing.sharePdf(  
576 - bytes: bytes,  
577 - bounds: bounds,  
578 - filename: widget.pdfFileName ?? 'document.pdf',  
579 - body: widget.shareActionExtraBody,  
580 - subject: widget.shareActionExtraSubject,  
581 - emails: widget.shareActionExtraEmails,  
582 - );  
583 -  
584 - if (result && widget.onShared != null) {  
585 - widget.onShared!(context);  
586 - }  
587 - }  
588 } 347 }
1 -/*  
2 - * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>  
3 - *  
4 - * Licensed under the Apache License, Version 2.0 (the "License");  
5 - * you may not use this file except in compliance with the License.  
6 - * You may obtain a copy of the License at  
7 - *  
8 - * http://www.apache.org/licenses/LICENSE-2.0  
9 - *  
10 - * Unless required by applicable law or agreed to in writing, software  
11 - * distributed under the License is distributed on an "AS IS" BASIS,  
12 - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
13 - * See the License for the specific language governing permissions and  
14 - * limitations under the License.  
15 - */  
16 -  
17 -import 'package:flutter/material.dart';  
18 -import 'package:pdf/pdf.dart';  
19 -  
20 -import '../callback.dart';  
21 -  
22 -/// Base Action callback  
23 -typedef OnPdfPreviewActionPressed = void Function(  
24 - BuildContext context,  
25 - LayoutCallback build,  
26 - PdfPageFormat pageFormat,  
27 -);  
28 -  
29 -/// Action to add the the [PdfPreview] widget  
30 -class PdfPreviewAction {  
31 - /// Represents an icon to add to [PdfPreview]  
32 - const PdfPreviewAction({  
33 - required this.icon,  
34 - required this.onPressed,  
35 - });  
36 -  
37 - /// The icon to display  
38 - final Icon icon;  
39 -  
40 - /// The callback called when the user tap on the icon  
41 - final OnPdfPreviewActionPressed? onPressed;  
42 -}  
@@ -25,18 +25,15 @@ import 'package:pdf/pdf.dart'; @@ -25,18 +25,15 @@ import 'package:pdf/pdf.dart';
25 import '../printing.dart'; 25 import '../printing.dart';
26 import '../printing_info.dart'; 26 import '../printing_info.dart';
27 import '../raster.dart'; 27 import '../raster.dart';
28 -import 'pdf_preview.dart';  
29 -import 'pdf_preview_page.dart'; 28 +import 'custom.dart';
  29 +import 'page.dart';
30 30
31 /// Raster PDF documents 31 /// Raster PDF documents
32 -mixin PdfPreviewRaster on State<PdfPreview> { 32 +mixin PdfPreviewRaster on State<PdfPreviewCustom> {
33 static const _updateTime = Duration(milliseconds: 300); 33 static const _updateTime = Duration(milliseconds: 300);
34 34
35 /// Configured page format 35 /// Configured page format
36 - PdfPageFormat get pageFormat;  
37 -  
38 - /// Is the print horizontal  
39 - bool? horizontal; 36 + PdfPageFormat get pageFormat => widget.pageFormat;
40 37
41 /// Resulting pages 38 /// Resulting pages
42 final List<PdfPreviewPage> pages = <PdfPreviewPage>[]; 39 final List<PdfPreviewPage> pages = <PdfPreviewPage>[];
@@ -60,11 +57,6 @@ mixin PdfPreviewRaster on State<PdfPreview> { @@ -60,11 +57,6 @@ mixin PdfPreviewRaster on State<PdfPreview> {
60 super.dispose(); 57 super.dispose();
61 } 58 }
62 59
63 - /// Computed page format  
64 - PdfPageFormat get computedPageFormat => horizontal != null  
65 - ? (horizontal! ? pageFormat.landscape : pageFormat.portrait)  
66 - : pageFormat;  
67 -  
68 /// Rasterize the document 60 /// Rasterize the document
69 void raster() { 61 void raster() {
70 _previewUpdate?.cancel(); 62 _previewUpdate?.cancel();
@@ -72,7 +64,7 @@ mixin PdfPreviewRaster on State<PdfPreview> { @@ -72,7 +64,7 @@ mixin PdfPreviewRaster on State<PdfPreview> {
72 final mq = MediaQuery.of(context); 64 final mq = MediaQuery.of(context);
73 dpi = (min(mq.size.width - 16, widget.maxPageWidth ?? double.infinity)) * 65 dpi = (min(mq.size.width - 16, widget.maxPageWidth ?? double.infinity)) *
74 mq.devicePixelRatio / 66 mq.devicePixelRatio /
75 - computedPageFormat.width * 67 + pageFormat.width *
76 72; 68 72;
77 69
78 _raster(); 70 _raster();
@@ -107,13 +99,13 @@ mixin PdfPreviewRaster on State<PdfPreview> { @@ -107,13 +99,13 @@ mixin PdfPreviewRaster on State<PdfPreview> {
107 } 99 }
108 100
109 try { 101 try {
110 - _doc = await widget.build(computedPageFormat); 102 + _doc = await widget.build(pageFormat);
111 } catch (exception, stack) { 103 } catch (exception, stack) {
112 InformationCollector? collector; 104 InformationCollector? collector;
113 105
114 assert(() { 106 assert(() {
115 collector = () sync* { 107 collector = () sync* {
116 - yield StringProperty('PageFormat', computedPageFormat.toString()); 108 + yield StringProperty('PageFormat', pageFormat.toString());
117 }; 109 };
118 return true; 110 return true;
119 }()); 111 }());
@@ -174,7 +166,7 @@ mixin PdfPreviewRaster on State<PdfPreview> { @@ -174,7 +166,7 @@ mixin PdfPreviewRaster on State<PdfPreview> {
174 166
175 assert(() { 167 assert(() {
176 collector = () sync* { 168 collector = () sync* {
177 - yield StringProperty('PageFormat', computedPageFormat.toString()); 169 + yield StringProperty('PageFormat', pageFormat.toString());
178 }; 170 };
179 return true; 171 return true;
180 }()); 172 }());