added context menu support
This commit is contained in:
@@ -7,6 +7,10 @@ part of 'routes.dart';
|
||||
TypedGoRoute<WebPageRoute>(name: 'WebPageRoute', path: 'page/:url'),
|
||||
TypedGoRoute<SearchRoute>(name: 'SearchRoute', path: 'search/:searchText'),
|
||||
TypedGoRoute<TorProxyRoute>(name: 'TorProxyRoute', path: 'tor_proxy'),
|
||||
TypedGoRoute<ContextMenuRoute>(
|
||||
name: 'ContextMenuRoute',
|
||||
path: 'context_menu',
|
||||
),
|
||||
TypedGoRoute<ContainerListRoute>(
|
||||
name: 'ContainerListRoute',
|
||||
path: 'containers',
|
||||
@@ -97,3 +101,17 @@ class ContainerCreateRoute extends GoRouteData {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextMenuRoute extends GoRouteData {
|
||||
const ContextMenuRoute();
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return DialogPage(
|
||||
builder:
|
||||
(_) => ContextMenuDialog(
|
||||
hitResult: HitResultJson.fromJson(state.extra! as String),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:lensai/features/bangs/presentation/screens/list.dart';
|
||||
import 'package:lensai/features/bangs/presentation/screens/search.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/context_menu_dialog.dart';
|
||||
import 'package:lensai/features/geckoview/features/search/presentation/screens/search.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
|
||||
|
||||
@@ -227,6 +227,11 @@ RouteBase get $browserRoute => GoRouteData.$route(
|
||||
name: 'TorProxyRoute',
|
||||
factory: $TorProxyRouteExtension._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'context_menu',
|
||||
name: 'ContextMenuRoute',
|
||||
factory: $ContextMenuRouteExtension._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'containers',
|
||||
name: 'ContainerListRoute',
|
||||
@@ -320,6 +325,24 @@ extension $TorProxyRouteExtension on TorProxyRoute {
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
extension $ContextMenuRouteExtension on ContextMenuRoute {
|
||||
static ContextMenuRoute _fromState(GoRouterState state) =>
|
||||
const ContextMenuRoute();
|
||||
|
||||
String get location => GoRouteData.$location(
|
||||
'/context_menu',
|
||||
);
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
extension $ContainerListRouteExtension on ContainerListRoute {
|
||||
static ContainerListRoute _fromState(GoRouterState state) =>
|
||||
ContainerListRoute();
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:lensai/data/models/drag_data.dart';
|
||||
import 'package:lensai/extensions/media_query.dart';
|
||||
import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||
import 'package:lensai/features/geckoview/domain/controllers/overlay.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||
@@ -17,6 +18,7 @@ import 'package:lensai/features/geckoview/features/browser/presentation/widgets/
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:lensai/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
||||
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart';
|
||||
import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart';
|
||||
@@ -29,6 +31,8 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final eventService = ref.watch(eventServiceProvider);
|
||||
|
||||
final displayedSheet = ref.watch(bottomSheetControllerProvider);
|
||||
final displayedOverlay = ref.watch(overlayControllerProvider);
|
||||
|
||||
@@ -48,6 +52,16 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
}
|
||||
});
|
||||
|
||||
useOnStreamChange(
|
||||
eventService.longPressEvent,
|
||||
onData: (event) async {
|
||||
await context.push(
|
||||
const ContextMenuRoute().location,
|
||||
extra: event.hitResult.toJson(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return PopScope(
|
||||
//We need this for BackButtonListener to work downstream
|
||||
//No direct pop result will be handled here
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
|
||||
class HitResultConverter {
|
||||
const HitResultConverter();
|
||||
|
||||
Map<String, dynamic> encode(HitResult input) {
|
||||
final (type, data) = switch (input) {
|
||||
final UnknownHitResult result => (result.runtimeType, result.encode()),
|
||||
final ImageHitResult result => (result.runtimeType, result.encode()),
|
||||
final VideoHitResult result => (result.runtimeType, result.encode()),
|
||||
final AudioHitResult result => (result.runtimeType, result.encode()),
|
||||
final ImageSrcHitResult result => (result.runtimeType, result.encode()),
|
||||
final PhoneHitResult result => (result.runtimeType, result.encode()),
|
||||
final EmailHitResult result => (result.runtimeType, result.encode()),
|
||||
final GeoHitResult result => (result.runtimeType, result.encode()),
|
||||
};
|
||||
|
||||
return {'type': type.toString(), 'data': data};
|
||||
}
|
||||
|
||||
HitResult decode(Map<String, dynamic> input) {
|
||||
final {'type': String type, 'data': List<Object?> data} = input;
|
||||
|
||||
switch (type) {
|
||||
case 'UnknownHitResult':
|
||||
return UnknownHitResult.decode(data);
|
||||
case 'ImageHitResult':
|
||||
return ImageHitResult.decode(data);
|
||||
case 'VideoHitResult':
|
||||
return VideoHitResult.decode(data);
|
||||
case 'AudioHitResult':
|
||||
return AudioHitResult.decode(data);
|
||||
case 'ImageSrcHitResult':
|
||||
return ImageSrcHitResult.decode(data);
|
||||
case 'PhoneHitResult':
|
||||
return PhoneHitResult.decode(data);
|
||||
case 'EmailHitResult':
|
||||
return EmailHitResult.decode(data);
|
||||
case 'GeoHitResult':
|
||||
return GeoHitResult.decode(data);
|
||||
default:
|
||||
throw FormatException('Unknown HitResult type: $type');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/domain/converters/hit_result_converter.dart';
|
||||
|
||||
extension HitResultJson on HitResult {
|
||||
String toJson() {
|
||||
const codec = HitResultConverter();
|
||||
return jsonEncode(codec.encode(this));
|
||||
}
|
||||
|
||||
static HitResult fromJson(String json) {
|
||||
const codec = HitResultConverter();
|
||||
return codec.decode(jsonDecode(json) as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
extension HitResultX on HitResult {
|
||||
Uri? tryGetLink() {
|
||||
final uri = switch (this) {
|
||||
UnknownHitResult(src: final src) => src,
|
||||
ImageHitResult(src: final src) => src,
|
||||
VideoHitResult(src: final src) => src,
|
||||
AudioHitResult(src: final src) => src,
|
||||
ImageSrcHitResult(src: final src) => src,
|
||||
PhoneHitResult(src: final src) => src,
|
||||
EmailHitResult(src: final src) => src,
|
||||
GeoHitResult(src: final src) => src,
|
||||
};
|
||||
|
||||
return Uri.tryParse(uri);
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
const maxTitleLength = 2500;
|
||||
|
||||
return switch (this) {
|
||||
UnknownHitResult(src: final src) => src,
|
||||
ImageSrcHitResult(uri: final uri) => uri,
|
||||
ImageHitResult(src: final src, title: final title) =>
|
||||
title?.isEmpty ?? true
|
||||
? (src.length > maxTitleLength ? 'image' : src)
|
||||
: title!,
|
||||
VideoHitResult(src: final src, title: final title) =>
|
||||
(title?.isEmpty ?? true) ? src : title!,
|
||||
AudioHitResult(src: final src, title: final title) =>
|
||||
(title?.isEmpty ?? true) ? src : title!,
|
||||
_ => 'about:blank',
|
||||
};
|
||||
}
|
||||
|
||||
bool get hasSrc => tryGetLink() != null;
|
||||
|
||||
bool isImage() {
|
||||
return (this is ImageHitResult || this is ImageSrcHitResult) && hasSrc;
|
||||
}
|
||||
|
||||
bool isVideoAudio() {
|
||||
return (this is VideoHitResult || this is AudioHitResult) && hasSrc;
|
||||
}
|
||||
|
||||
bool isFile() {
|
||||
return this is UnknownHitResult && hasSrc;
|
||||
}
|
||||
|
||||
bool isUri() {
|
||||
return (this is UnknownHitResult && hasSrc) || this is ImageSrcHitResult;
|
||||
}
|
||||
|
||||
bool isHttpLink() {
|
||||
return isUri() &&
|
||||
switch (tryGetLink()?.scheme) {
|
||||
'http' => true,
|
||||
'https' => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
bool isIntent() {
|
||||
return this is UnknownHitResult &&
|
||||
hasSrc &&
|
||||
tryGetLink()?.scheme == 'intent';
|
||||
}
|
||||
|
||||
bool isMailto() {
|
||||
return this is UnknownHitResult &&
|
||||
hasSrc &&
|
||||
tryGetLink()?.scheme == 'mailto';
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class CopyEmail extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const CopyEmail({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isMailto();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.emailMultiple),
|
||||
title: const Text('Share email address'),
|
||||
onTap: () async {
|
||||
final email = hitResult.tryGetLink().mapNotNull((url) => url.host);
|
||||
if (email != null) {
|
||||
await Clipboard.setData(ClipboardData(text: email));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class CopyImage extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const CopyImage({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isImage();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.imageMultiple),
|
||||
title: const Text('Copy image'),
|
||||
onTap: () async {
|
||||
final currentTab = ref.read(selectedTabStateProvider);
|
||||
final url = hitResult.tryGetLink();
|
||||
|
||||
if (currentTab != null && url != null) {
|
||||
await GeckoDownloadsService().copyInternetResource(
|
||||
currentTab.id,
|
||||
url: url,
|
||||
isPrivate: currentTab.isPrivate,
|
||||
referrerUrl: currentTab.url,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class CopyImageLocation extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const CopyImageLocation({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isImage();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.imageMarker),
|
||||
title: const Text('Copy image location'),
|
||||
onTap: () async {
|
||||
await hitResult.tryGetLink().mapNotNull(
|
||||
(link) => Clipboard.setData(ClipboardData(text: link.toString())),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class CopyLink extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const CopyLink({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isUri() || hitResult.isImage() || hitResult.isVideoAudio();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.contentCopy),
|
||||
title: const Text('Copy link'),
|
||||
onTap: () async {
|
||||
await hitResult.tryGetLink().mapNotNull(
|
||||
(link) => Clipboard.setData(ClipboardData(text: link.toString())),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:lensai/utils/ui_helper.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class LaunchExternal extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const LaunchExternal({super.key, required this.hitResult});
|
||||
|
||||
static Future<bool> isSupported(HitResult hitResult) async {
|
||||
return hitResult.tryGetLink().mapNotNull((url) => canLaunchUrl(url)) ??
|
||||
false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Launch External'),
|
||||
onTap: () async {
|
||||
await hitResult.tryGetLink().mapNotNull(
|
||||
(url) => launchUrlFeedback(context, url),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class OpenImageInNewTab extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const OpenImageInNewTab({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isImage();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.tabPlus),
|
||||
title: const Text('Open image in new tab'),
|
||||
onTap: () async {
|
||||
final currentTabId = ref.read(selectedTabProvider);
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(url: hitResult.tryGetLink(), parentId: currentTabId);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class OpenInNewTab extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const OpenInNewTab({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isHttpLink();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.tabPlus),
|
||||
title: const Text('Open in new tab'),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(url: hitResult.tryGetLink());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class SaveFile extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const SaveFile({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isVideoAudio() || hitResult.isFile();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.fileDownload),
|
||||
title: const Text('Save file'),
|
||||
onTap: () async {
|
||||
final currentTab = ref.read(selectedTabStateProvider);
|
||||
final url = hitResult.tryGetLink();
|
||||
|
||||
if (currentTab != null && url != null) {
|
||||
await GeckoDownloadsService().requestDownload(
|
||||
currentTab.id,
|
||||
url: url,
|
||||
skipConfirmation: true,
|
||||
isPrivate: currentTab.isPrivate,
|
||||
referrerUrl: currentTab.url,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class SaveImage extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const SaveImage({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isImage();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.imageMove),
|
||||
title: const Text('Save image'),
|
||||
onTap: () async {
|
||||
final currentTab = ref.read(selectedTabStateProvider);
|
||||
final url = hitResult.tryGetLink();
|
||||
|
||||
if (currentTab != null && url != null) {
|
||||
await GeckoDownloadsService().requestDownload(
|
||||
currentTab.id,
|
||||
url: url,
|
||||
skipConfirmation: true,
|
||||
isPrivate: currentTab.isPrivate,
|
||||
referrerUrl: currentTab.url,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class ShareEmail extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const ShareEmail({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isMailto();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.emailArrowRight),
|
||||
title: const Text('Share email address'),
|
||||
onTap: () async {
|
||||
final email = hitResult.tryGetLink().mapNotNull((url) => url.path);
|
||||
if (email != null) {
|
||||
await Share.share(email);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
|
||||
class ShareImage extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const ShareImage({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isImage();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.imagePlus),
|
||||
title: const Text('Share image'),
|
||||
onTap: () async {
|
||||
final currentTab = ref.read(selectedTabStateProvider);
|
||||
final url = hitResult.tryGetLink();
|
||||
|
||||
if (currentTab != null && url != null) {
|
||||
await GeckoDownloadsService().shareInternetResource(
|
||||
currentTab.id,
|
||||
url: url,
|
||||
isPrivate: currentTab.isPrivate,
|
||||
referrerUrl: currentTab.url,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/extensions/nullable.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class ShareLink extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const ShareLink({super.key, required this.hitResult});
|
||||
|
||||
static bool isSupported(HitResult hitResult) {
|
||||
return hitResult.isUri() || hitResult.isImage() || hitResult.isVideoAudio();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.share),
|
||||
title: const Text('Share link'),
|
||||
onTap: () async {
|
||||
await hitResult.tryGetLink().mapNotNull((url) => Share.shareUri(url));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_auto_size_text/flutter_auto_size_text.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/copy_email.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/copy_image.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/copy_image_location.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/copy_link.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/launch_external.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/open_image_new_tab.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/open_new_tab.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/save_file.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/save_image.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/share_email.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/share_image.dart';
|
||||
import 'package:lensai/features/geckoview/features/contextmenu/presentation/candidates/share_link.dart';
|
||||
import 'package:lensai/presentation/hooks/cached_future.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class ContextMenuDialog extends HookConsumerWidget {
|
||||
final HitResult hitResult;
|
||||
|
||||
const ContextMenuDialog({super.key, required this.hitResult});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SimpleDialog(
|
||||
title: AutoSizeText(
|
||||
hitResult.getTitle(),
|
||||
minFontSize: 18,
|
||||
maxFontSize: DefaultTextStyle.of(context).style.fontSize,
|
||||
maxLines: 10,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
),
|
||||
children: [
|
||||
if (OpenInNewTab.isSupported(hitResult))
|
||||
OpenInNewTab(hitResult: hitResult),
|
||||
if (CopyLink.isSupported(hitResult)) CopyLink(hitResult: hitResult),
|
||||
if (SaveFile.isSupported(hitResult)) SaveFile(hitResult: hitResult),
|
||||
if (ShareLink.isSupported(hitResult)) ShareLink(hitResult: hitResult),
|
||||
if (ShareImage.isSupported(hitResult)) ShareImage(hitResult: hitResult),
|
||||
if (OpenImageInNewTab.isSupported(hitResult))
|
||||
OpenImageInNewTab(hitResult: hitResult),
|
||||
if (CopyImage.isSupported(hitResult)) CopyImage(hitResult: hitResult),
|
||||
if (SaveImage.isSupported(hitResult)) SaveImage(hitResult: hitResult),
|
||||
if (CopyImageLocation.isSupported(hitResult))
|
||||
CopyImageLocation(hitResult: hitResult),
|
||||
if (ShareEmail.isSupported(hitResult)) ShareEmail(hitResult: hitResult),
|
||||
if (CopyEmail.isSupported(hitResult)) CopyEmail(hitResult: hitResult),
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final isSupported = useCachedFuture(
|
||||
// ignore: discarded_futures useFuture
|
||||
() => LaunchExternal.isSupported(hitResult),
|
||||
);
|
||||
|
||||
if (isSupported.data == false) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: isSupported.connectionState != ConnectionState.done,
|
||||
child: LaunchExternal(hitResult: hitResult),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user