export & copy page content as pdf/md
This commit is contained in:
@@ -68,6 +68,10 @@ class TabSession extends _$TabSession {
|
|||||||
return _sessionService.requestDesktopSite(enable: enable);
|
return _sessionService.requestDesktopSite(enable: enable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveToPdf() {
|
||||||
|
return _sessionService.saveToPdf();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build({required String? tabId}) {
|
void build({required String? tabId}) {
|
||||||
_sessionService = (tabId != null)
|
_sessionService = (tabId != null)
|
||||||
|
|||||||
+123
@@ -4,10 +4,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:share_plus/share_plus.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_session.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.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/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;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
class ShareMenuItemButton extends HookConsumerWidget {
|
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 {
|
class ShareScreenshotMenuItemButton extends HookConsumerWidget {
|
||||||
const ShareScreenshotMenuItemButton({super.key, required this.selectedTabId});
|
const ShareScreenshotMenuItemButton({super.key, required this.selectedTabId});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
*/
|
*/
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
@@ -320,6 +322,43 @@ class TabMenu extends HookConsumerWidget {
|
|||||||
leadingIcon: const Icon(Icons.share),
|
leadingIcon: const Icon(Icons.share),
|
||||||
child: const Text('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(
|
MenuItemButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ref
|
await ref
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
return filtered.length;
|
return filtered.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<TabData?> getTabDataById(String tabId) {
|
||||||
|
return ref
|
||||||
|
.read(tabDatabaseProvider)
|
||||||
|
.tabDao
|
||||||
|
.getTabDataById(tabId)
|
||||||
|
.getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<TabData>> getContainerTabsData(String? containerId) {
|
Future<List<TabData>> getContainerTabsData(String? containerId) {
|
||||||
return ref
|
return ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies:
|
|||||||
fading_scroll: ^0.9.1
|
fading_scroll: ^0.9.1
|
||||||
fancy_password_field: ^2.0.8
|
fancy_password_field: ^2.0.8
|
||||||
fast_equatable: ^1.3.1
|
fast_equatable: ^1.3.1
|
||||||
|
file_picker: ^10.3.8
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_auto_size_text: ^4.1.0
|
flutter_auto_size_text: ^4.1.0
|
||||||
|
|||||||
+2
@@ -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.R
|
||||||
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
|
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
|
||||||
import eu.weblibre.flutter_mozilla_components.middleware.FlutterEventMiddleware
|
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.BrowserExtensionEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
@@ -191,6 +192,7 @@ class Core(
|
|||||||
SessionPrioritizationMiddleware(),
|
SessionPrioritizationMiddleware(),
|
||||||
RecordingDevicesMiddleware(context, components.notificationsDelegate),
|
RecordingDevicesMiddleware(context, components.notificationsDelegate),
|
||||||
PromptMiddleware(),
|
PromptMiddleware(),
|
||||||
|
SaveToPDFMiddleware(),
|
||||||
FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner),
|
FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner),
|
||||||
LastMediaAccessMiddleware(),
|
LastMediaAccessMiddleware(),
|
||||||
) + EngineMiddleware.create(
|
) + EngineMiddleware.create(
|
||||||
|
|||||||
+32
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user