export & copy page content as pdf/md

This commit is contained in:
Fabian Freund
2025-12-25 18:36:31 +01:00
parent 2c58029d8a
commit 774da40ddc
7 changed files with 209 additions and 0 deletions
@@ -68,6 +68,10 @@ class TabSession extends _$TabSession {
return _sessionService.requestDesktopSite(enable: enable);
}
Future<void> saveToPdf() {
return _sessionService.saveToPdf();
}
@override
void build({required String? tabId}) {
_sessionService = (tabId != null)
@@ -4,10 +4,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart';
import 'package:share_plus/share_plus.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class ShareMenuItemButton extends HookConsumerWidget {
@@ -58,6 +61,126 @@ class ShowQrCodeMenuItemButton extends HookConsumerWidget {
}
}
class SaveToPdfMenuItemButton extends HookConsumerWidget {
const SaveToPdfMenuItemButton({super.key, required this.selectedTabId});
final String? selectedTabId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuItemButton(
leadingIcon: const Icon(MdiIcons.filePdfBox),
closeOnActivate: false,
onPressed: () async {
await ref
.read(tabSessionProvider(tabId: selectedTabId).notifier)
.saveToPdf();
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
child: const Text('Export as PDF'),
);
}
}
class ShareMarkdownActionMenuItemButton extends HookConsumerWidget {
const ShareMarkdownActionMenuItemButton({
super.key,
required this.selectedTabId,
required this.icon,
required this.title,
required this.shareMarkdownAction,
});
final String selectedTabId;
final Widget icon;
final Widget title;
final Future<void> Function(String content, String? fileName)
shareMarkdownAction;
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuItemButton(
leadingIcon: icon,
closeOnActivate: false,
onPressed: () => _handleShare(context, ref),
child: title,
);
}
Future<void> _handleShare(BuildContext context, WidgetRef ref) async {
final tabData = await ref
.read(tabDataRepositoryProvider.notifier)
.getTabDataById(selectedTabId);
if (tabData == null || tabData.fullContentMarkdown.isEmpty) {
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
return;
}
final shouldShowDialog =
tabData.isProbablyReaderable == true &&
tabData.extractedContentMarkdown.isNotEmpty;
if (shouldShowDialog && context.mounted) {
await _showContentSelectionDialog(context, tabData);
} else {
await shareMarkdownAction(
tabData.fullContentMarkdown!,
tabData.title ?? tabData.url?.authority,
);
}
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
}
Future<void> _showContentSelectionDialog(
BuildContext context,
TabData tabData,
) async {
await showDialog(
context: context,
builder: (context) => SimpleDialog(
title: title,
children: [
ListTile(
title: const Text('Extracted Content'),
subtitle: const Text(
'Reader-optimized content without navigation and ads',
),
onTap: () async {
Navigator.of(context).pop();
await shareMarkdownAction(
tabData.extractedContentMarkdown!,
tabData.title ?? tabData.url?.authority,
);
},
),
ListTile(
title: const Text('Full Content'),
subtitle: const Text(
'Complete page including all elements and structure',
),
onTap: () async {
Navigator.of(context).pop();
await shareMarkdownAction(
tabData.fullContentMarkdown!,
tabData.title ?? tabData.url?.authority,
);
},
),
],
),
);
}
}
class ShareScreenshotMenuItemButton extends HookConsumerWidget {
const ShareScreenshotMenuItemButton({super.key, required this.selectedTabId});
@@ -19,7 +19,9 @@
*/
import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
@@ -320,6 +322,43 @@ class TabMenu extends HookConsumerWidget {
leadingIcon: const Icon(Icons.share),
child: const Text('Share'),
),
SubmenuButton(
menuChildren: [
ShareMarkdownActionMenuItemButton(
selectedTabId: selectedTabId,
title: const Text('Copy as Markdown'),
// ignore: deprecated_member_use
icon: const Icon(MdiIcons.languageMarkdownOutline),
shareMarkdownAction: (content, fileName) async {
await Clipboard.setData(ClipboardData(text: content));
if (context.mounted) {
ui_helper.showInfoMessage(
context,
'Markdown copied to clipboard',
);
}
},
),
ShareMarkdownActionMenuItemButton(
selectedTabId: selectedTabId,
title: const Text('Export as Markdown'),
// ignore: deprecated_member_use
icon: const Icon(MdiIcons.languageMarkdown),
shareMarkdownAction: (content, fileName) async {
await FilePicker.platform.saveFile(
fileName: fileName,
type: FileType.custom,
allowedExtensions: ['md'],
bytes: utf8.encode(content),
);
},
),
SaveToPdfMenuItemButton(selectedTabId: selectedTabId),
],
leadingIcon: const Icon(MdiIcons.fileExport),
child: const Text('Export'),
),
MenuItemButton(
onPressed: () async {
await ref
@@ -102,6 +102,14 @@ class TabDataRepository extends _$TabDataRepository {
return filtered.length;
}
Future<TabData?> getTabDataById(String tabId) {
return ref
.read(tabDatabaseProvider)
.tabDao
.getTabDataById(tabId)
.getSingleOrNull();
}
Future<List<TabData>> getContainerTabsData(String? containerId) {
return ref
.read(tabDatabaseProvider)
+1
View File
@@ -22,6 +22,7 @@ dependencies:
fading_scroll: ^0.9.1
fancy_password_field: ^2.0.8
fast_equatable: ^1.3.1
file_picker: ^10.3.8
flutter:
sdk: flutter
flutter_auto_size_text: ^4.1.0
@@ -21,6 +21,7 @@ import eu.weblibre.flutter_mozilla_components.activities.NotificationActivity
import eu.weblibre.flutter_mozilla_components.R
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
import eu.weblibre.flutter_mozilla_components.middleware.FlutterEventMiddleware
import eu.weblibre.flutter_mozilla_components.middleware.SaveToPDFMiddleware
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
import kotlinx.coroutines.FlowPreview
@@ -191,6 +192,7 @@ class Core(
SessionPrioritizationMiddleware(),
RecordingDevicesMiddleware(context, components.notificationsDelegate),
PromptMiddleware(),
SaveToPDFMiddleware(),
FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner),
LastMediaAccessMiddleware(),
) + EngineMiddleware.create(
@@ -0,0 +1,32 @@
package eu.weblibre.flutter_mozilla_components.middleware
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.feature.addons.logger
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
class SaveToPDFMiddleware : Middleware<BrowserState, BrowserAction> {
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction,
) {
when (action) {
is EngineAction.SaveToPdfAction -> {
// Continue to generate the PDF, passing through here to add telemetry
next(action)
}
is EngineAction.SaveToPdfCompleteAction -> {
logger.info("Saved PDF successfully")
}
is EngineAction.SaveToPdfExceptionAction -> {
logger.error("Unable to save PDF", action.throwable)
}
else -> {
next(action)
}
}
}
}