Skip to content

Commit 774da40

Browse files
committed
export & copy page content as pdf/md
1 parent 2c58029 commit 774da40

File tree

7 files changed

+209
-0
lines changed

7 files changed

+209
-0
lines changed

app/lib/features/geckoview/domain/providers/tab_session.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ class TabSession extends _$TabSession {
6868
return _sessionService.requestDesktopSite(enable: enable);
6969
}
7070

71+
Future<void> saveToPdf() {
72+
return _sessionService.saveToPdf();
73+
}
74+
7175
@override
7276
void build({required String? tabId}) {
7377
_sessionService = (tabId != null)

app/lib/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import 'package:flutter/material.dart';
44
import 'package:flutter/services.dart';
55
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
66
import 'package:hooks_riverpod/hooks_riverpod.dart';
7+
import 'package:nullability/nullability.dart';
78
import 'package:share_plus/share_plus.dart';
89
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
910
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
1011
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
12+
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
13+
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
1114
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
1215

1316
class ShareMenuItemButton extends HookConsumerWidget {
@@ -58,6 +61,126 @@ class ShowQrCodeMenuItemButton extends HookConsumerWidget {
5861
}
5962
}
6063

64+
class SaveToPdfMenuItemButton extends HookConsumerWidget {
65+
const SaveToPdfMenuItemButton({super.key, required this.selectedTabId});
66+
67+
final String? selectedTabId;
68+
69+
@override
70+
Widget build(BuildContext context, WidgetRef ref) {
71+
return MenuItemButton(
72+
leadingIcon: const Icon(MdiIcons.filePdfBox),
73+
closeOnActivate: false,
74+
onPressed: () async {
75+
await ref
76+
.read(tabSessionProvider(tabId: selectedTabId).notifier)
77+
.saveToPdf();
78+
79+
if (context.mounted) {
80+
MenuController.maybeOf(context)?.close();
81+
}
82+
},
83+
child: const Text('Export as PDF'),
84+
);
85+
}
86+
}
87+
88+
class ShareMarkdownActionMenuItemButton extends HookConsumerWidget {
89+
const ShareMarkdownActionMenuItemButton({
90+
super.key,
91+
required this.selectedTabId,
92+
required this.icon,
93+
required this.title,
94+
required this.shareMarkdownAction,
95+
});
96+
97+
final String selectedTabId;
98+
final Widget icon;
99+
final Widget title;
100+
final Future<void> Function(String content, String? fileName)
101+
shareMarkdownAction;
102+
103+
@override
104+
Widget build(BuildContext context, WidgetRef ref) {
105+
return MenuItemButton(
106+
leadingIcon: icon,
107+
closeOnActivate: false,
108+
onPressed: () => _handleShare(context, ref),
109+
child: title,
110+
);
111+
}
112+
113+
Future<void> _handleShare(BuildContext context, WidgetRef ref) async {
114+
final tabData = await ref
115+
.read(tabDataRepositoryProvider.notifier)
116+
.getTabDataById(selectedTabId);
117+
118+
if (tabData == null || tabData.fullContentMarkdown.isEmpty) {
119+
if (context.mounted) {
120+
MenuController.maybeOf(context)?.close();
121+
}
122+
return;
123+
}
124+
125+
final shouldShowDialog =
126+
tabData.isProbablyReaderable == true &&
127+
tabData.extractedContentMarkdown.isNotEmpty;
128+
129+
if (shouldShowDialog && context.mounted) {
130+
await _showContentSelectionDialog(context, tabData);
131+
} else {
132+
await shareMarkdownAction(
133+
tabData.fullContentMarkdown!,
134+
tabData.title ?? tabData.url?.authority,
135+
);
136+
}
137+
138+
if (context.mounted) {
139+
MenuController.maybeOf(context)?.close();
140+
}
141+
}
142+
143+
Future<void> _showContentSelectionDialog(
144+
BuildContext context,
145+
TabData tabData,
146+
) async {
147+
await showDialog(
148+
context: context,
149+
builder: (context) => SimpleDialog(
150+
title: title,
151+
children: [
152+
ListTile(
153+
title: const Text('Extracted Content'),
154+
subtitle: const Text(
155+
'Reader-optimized content without navigation and ads',
156+
),
157+
onTap: () async {
158+
Navigator.of(context).pop();
159+
await shareMarkdownAction(
160+
tabData.extractedContentMarkdown!,
161+
tabData.title ?? tabData.url?.authority,
162+
);
163+
},
164+
),
165+
ListTile(
166+
title: const Text('Full Content'),
167+
subtitle: const Text(
168+
'Complete page including all elements and structure',
169+
),
170+
onTap: () async {
171+
Navigator.of(context).pop();
172+
await shareMarkdownAction(
173+
tabData.fullContentMarkdown!,
174+
tabData.title ?? tabData.url?.authority,
175+
);
176+
},
177+
),
178+
],
179+
),
180+
);
181+
}
182+
}
183+
61184
class ShareScreenshotMenuItemButton extends HookConsumerWidget {
62185
const ShareScreenshotMenuItemButton({super.key, required this.selectedTabId});
63186

app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
*/
2020
import 'dart:convert';
2121

22+
import 'package:file_picker/file_picker.dart';
2223
import 'package:flutter/material.dart';
24+
import 'package:flutter/services.dart';
2325
import 'package:flutter_hooks/flutter_hooks.dart';
2426
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
2527
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
@@ -320,6 +322,43 @@ class TabMenu extends HookConsumerWidget {
320322
leadingIcon: const Icon(Icons.share),
321323
child: const Text('Share'),
322324
),
325+
SubmenuButton(
326+
menuChildren: [
327+
ShareMarkdownActionMenuItemButton(
328+
selectedTabId: selectedTabId,
329+
title: const Text('Copy as Markdown'),
330+
// ignore: deprecated_member_use
331+
icon: const Icon(MdiIcons.languageMarkdownOutline),
332+
shareMarkdownAction: (content, fileName) async {
333+
await Clipboard.setData(ClipboardData(text: content));
334+
335+
if (context.mounted) {
336+
ui_helper.showInfoMessage(
337+
context,
338+
'Markdown copied to clipboard',
339+
);
340+
}
341+
},
342+
),
343+
ShareMarkdownActionMenuItemButton(
344+
selectedTabId: selectedTabId,
345+
title: const Text('Export as Markdown'),
346+
// ignore: deprecated_member_use
347+
icon: const Icon(MdiIcons.languageMarkdown),
348+
shareMarkdownAction: (content, fileName) async {
349+
await FilePicker.platform.saveFile(
350+
fileName: fileName,
351+
type: FileType.custom,
352+
allowedExtensions: ['md'],
353+
bytes: utf8.encode(content),
354+
);
355+
},
356+
),
357+
SaveToPdfMenuItemButton(selectedTabId: selectedTabId),
358+
],
359+
leadingIcon: const Icon(MdiIcons.fileExport),
360+
child: const Text('Export'),
361+
),
323362
MenuItemButton(
324363
onPressed: () async {
325364
await ref

app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ class TabDataRepository extends _$TabDataRepository {
102102
return filtered.length;
103103
}
104104

105+
Future<TabData?> getTabDataById(String tabId) {
106+
return ref
107+
.read(tabDatabaseProvider)
108+
.tabDao
109+
.getTabDataById(tabId)
110+
.getSingleOrNull();
111+
}
112+
105113
Future<List<TabData>> getContainerTabsData(String? containerId) {
106114
return ref
107115
.read(tabDatabaseProvider)

app/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies:
2222
fading_scroll: ^0.9.1
2323
fancy_password_field: ^2.0.8
2424
fast_equatable: ^1.3.1
25+
file_picker: ^10.3.8
2526
flutter:
2627
sdk: flutter
2728
flutter_auto_size_text: ^4.1.0

packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import eu.weblibre.flutter_mozilla_components.activities.NotificationActivity
2121
import eu.weblibre.flutter_mozilla_components.R
2222
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
2323
import eu.weblibre.flutter_mozilla_components.middleware.FlutterEventMiddleware
24+
import eu.weblibre.flutter_mozilla_components.middleware.SaveToPDFMiddleware
2425
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
2526
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
2627
import kotlinx.coroutines.FlowPreview
@@ -191,6 +192,7 @@ class Core(
191192
SessionPrioritizationMiddleware(),
192193
RecordingDevicesMiddleware(context, components.notificationsDelegate),
193194
PromptMiddleware(),
195+
SaveToPDFMiddleware(),
194196
FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner),
195197
LastMediaAccessMiddleware(),
196198
) + EngineMiddleware.create(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package eu.weblibre.flutter_mozilla_components.middleware
2+
3+
import mozilla.components.browser.state.action.BrowserAction
4+
import mozilla.components.browser.state.action.EngineAction
5+
import mozilla.components.browser.state.state.BrowserState
6+
import mozilla.components.feature.addons.logger
7+
import mozilla.components.lib.state.Middleware
8+
import mozilla.components.lib.state.MiddlewareContext
9+
10+
class SaveToPDFMiddleware : Middleware<BrowserState, BrowserAction> {
11+
override fun invoke(
12+
context: MiddlewareContext<BrowserState, BrowserAction>,
13+
next: (BrowserAction) -> Unit,
14+
action: BrowserAction,
15+
) {
16+
when (action) {
17+
is EngineAction.SaveToPdfAction -> {
18+
// Continue to generate the PDF, passing through here to add telemetry
19+
next(action)
20+
}
21+
is EngineAction.SaveToPdfCompleteAction -> {
22+
logger.info("Saved PDF successfully")
23+
}
24+
is EngineAction.SaveToPdfExceptionAction -> {
25+
logger.error("Unable to save PDF", action.throwable)
26+
}
27+
else -> {
28+
next(action)
29+
}
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)