custom pwa and shortcut support
This commit is contained in:
@@ -36,7 +36,10 @@ final _contentParserTransformer =
|
||||
StreamTransformer<ReceivedIntentParameter, SharedContent>.fromHandlers(
|
||||
handleData: (parameter, sink) {
|
||||
final parsed = parameter.content.mapNotNull(
|
||||
(content) => SharedContent.parse(content),
|
||||
(content) => SharedContent.parse(
|
||||
content,
|
||||
contextId: parameter.contextId,
|
||||
),
|
||||
);
|
||||
|
||||
if (parsed != null) {
|
||||
|
||||
+10
-2
@@ -681,8 +681,10 @@ class _AddToHomeScreenTile extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isInstallable = ref.watch(isCurrentTabInstallableProvider);
|
||||
final isShortcutable = ref.watch(isCurrentTabShortcutableProvider);
|
||||
|
||||
if (!isInstallable) return const SizedBox.shrink();
|
||||
// Show for installable PWAs or any HTTPS page
|
||||
if (!isInstallable && !isShortcutable) return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -691,7 +693,13 @@ class _AddToHomeScreenTile extends ConsumerWidget {
|
||||
leading: const Icon(Icons.add_to_home_screen),
|
||||
title: const Text('Add to Home Screen'),
|
||||
onTap: () async {
|
||||
await showPwaInstallDialog(context, ref);
|
||||
if (isInstallable) {
|
||||
// Site has valid manifest — use existing PWA install flow
|
||||
await showPwaInstallDialog(context, ref);
|
||||
} else {
|
||||
// No manifest — show shortcut choice dialog
|
||||
await showShortcutInstallDialog(context, ref);
|
||||
}
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
|
||||
+27
@@ -32,6 +32,7 @@ import 'package:weblibre/core/providers/router.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
|
||||
import 'package:weblibre/features/bangs/domain/services/search_history_cleanup.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/browser_extension.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
||||
@@ -355,6 +356,12 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
final router = await ref.read(routerProvider.future);
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
// Resolve container from shortcut intent context ID
|
||||
final containerSelection = await _resolveContainerSelection(
|
||||
ref,
|
||||
sharedContent.contextId,
|
||||
);
|
||||
|
||||
switch (settings.tabIntentOpenSetting) {
|
||||
case TabIntentOpenSetting.regular:
|
||||
case TabIntentOpenSetting.private:
|
||||
@@ -371,6 +378,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
: TabMode.regular,
|
||||
launchedFromIntent: true,
|
||||
selectTab: true,
|
||||
containerSelection: containerSelection,
|
||||
);
|
||||
case SharedText():
|
||||
final bang =
|
||||
@@ -642,3 +650,22 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a [TabContainerSelection] from a shortcut intent's context ID.
|
||||
/// Returns [TabContainerSelection.useSelected] if no contextId or container not found.
|
||||
Future<TabContainerSelection> _resolveContainerSelection(
|
||||
WidgetRef ref,
|
||||
String? contextId,
|
||||
) async {
|
||||
if (contextId == null) return const TabContainerSelection.useSelected();
|
||||
|
||||
final container = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerByContextualIdentity(contextId);
|
||||
|
||||
if (container != null) {
|
||||
return TabContainerSelection.specific(container);
|
||||
}
|
||||
|
||||
return const TabContainerSelection.useSelected();
|
||||
}
|
||||
|
||||
@@ -211,15 +211,20 @@ class TabMenu extends HookConsumerWidget {
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final isInstallable = ref.watch(isCurrentTabInstallableProvider);
|
||||
final isShortcutable = ref.watch(isCurrentTabShortcutableProvider);
|
||||
|
||||
return Visibility(
|
||||
visible: isInstallable,
|
||||
visible: isInstallable || isShortcutable,
|
||||
child: MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
leadingIcon: const Icon(Icons.add_to_home_screen),
|
||||
child: const Text('Add to Home Screen'),
|
||||
onPressed: () async {
|
||||
await showPwaInstallDialog(context, ref);
|
||||
if (isInstallable) {
|
||||
await showPwaInstallDialog(context, ref);
|
||||
} else {
|
||||
await showShortcutInstallDialog(context, ref);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
MenuController.maybeOf(context)?.close();
|
||||
|
||||
@@ -24,8 +24,10 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/filesystem.dart' show filesystem;
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/extensions/uri.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/pwa/domain/pwa_installability.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
@@ -121,3 +123,46 @@ Future<bool> installCurrentWebApp(Ref ref) async {
|
||||
Future<List<PwaManifest>> installedWebApps(Ref ref) {
|
||||
return GeckoPwaApi().getInstalledWebApps();
|
||||
}
|
||||
|
||||
/// Whether the current tab is on an HTTPS page (eligible for home screen shortcut).
|
||||
@Riverpod()
|
||||
bool isCurrentTabShortcutable(Ref ref) {
|
||||
final selectedTabId = ref.watch(selectedTabProvider);
|
||||
if (selectedTabId == null) return false;
|
||||
|
||||
final tabState = ref.watch(tabStateProvider(selectedTabId));
|
||||
if (tabState == null) return false;
|
||||
|
||||
return tabState.url.isHttps ||
|
||||
(tabState.url.isHttp && tabState.url.isLocalhost);
|
||||
}
|
||||
|
||||
/// Creates a basic bookmark shortcut on the home screen for the current tab.
|
||||
@Riverpod()
|
||||
Future<bool> installBasicShortcut(Ref ref, {String? overrideName}) async {
|
||||
final selectedTabId = ref.read(selectedTabProvider);
|
||||
|
||||
if (selectedTabId == null) {
|
||||
throw StateError('No tab selected');
|
||||
}
|
||||
|
||||
final profileUuid = filesystem.selectedProfile.uuid;
|
||||
|
||||
final selectedContainerId = ref.read(selectedContainerProvider);
|
||||
|
||||
String? contextId;
|
||||
if (selectedContainerId != null) {
|
||||
final containerRepository = ref.read(containerRepositoryProvider.notifier);
|
||||
final containerData = await containerRepository.getContainerData(
|
||||
selectedContainerId,
|
||||
);
|
||||
contextId = containerData?.metadata.contextualIdentity;
|
||||
}
|
||||
|
||||
return GeckoPwaApi().installBasicShortcut(
|
||||
selectedTabId,
|
||||
profileUuid,
|
||||
contextId,
|
||||
overrideName,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -296,3 +296,129 @@ final class InstalledWebAppsProvider
|
||||
}
|
||||
|
||||
String _$installedWebAppsHash() => r'ff185620ccd25bf6b71415e34e3e0c0f20d5e59d';
|
||||
|
||||
/// Whether the current tab is on an HTTPS page (eligible for home screen shortcut).
|
||||
|
||||
@ProviderFor(isCurrentTabShortcutable)
|
||||
final isCurrentTabShortcutableProvider = IsCurrentTabShortcutableProvider._();
|
||||
|
||||
/// Whether the current tab is on an HTTPS page (eligible for home screen shortcut).
|
||||
|
||||
final class IsCurrentTabShortcutableProvider
|
||||
extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Whether the current tab is on an HTTPS page (eligible for home screen shortcut).
|
||||
IsCurrentTabShortcutableProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'isCurrentTabShortcutableProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$isCurrentTabShortcutableHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return isCurrentTabShortcutable(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$isCurrentTabShortcutableHash() =>
|
||||
r'a18fa837facc92397dacb79f38bdefd303ff5d00';
|
||||
|
||||
/// Creates a basic bookmark shortcut on the home screen for the current tab.
|
||||
|
||||
@ProviderFor(installBasicShortcut)
|
||||
final installBasicShortcutProvider = InstallBasicShortcutFamily._();
|
||||
|
||||
/// Creates a basic bookmark shortcut on the home screen for the current tab.
|
||||
|
||||
final class InstallBasicShortcutProvider
|
||||
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
|
||||
with $FutureModifier<bool>, $FutureProvider<bool> {
|
||||
/// Creates a basic bookmark shortcut on the home screen for the current tab.
|
||||
InstallBasicShortcutProvider._({
|
||||
required InstallBasicShortcutFamily super.from,
|
||||
required String? super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'installBasicShortcutProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$installBasicShortcutHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'installBasicShortcutProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<bool> create(Ref ref) {
|
||||
final argument = this.argument as String?;
|
||||
return installBasicShortcut(ref, overrideName: argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is InstallBasicShortcutProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$installBasicShortcutHash() =>
|
||||
r'aa8a96e94eac19e3e0b2a087bc6dde12d28f48f2';
|
||||
|
||||
/// Creates a basic bookmark shortcut on the home screen for the current tab.
|
||||
|
||||
final class InstallBasicShortcutFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
|
||||
InstallBasicShortcutFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'installBasicShortcutProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Creates a basic bookmark shortcut on the home screen for the current tab.
|
||||
|
||||
InstallBasicShortcutProvider call({String? overrideName}) =>
|
||||
InstallBasicShortcutProvider._(argument: overrideName, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'installBasicShortcutProvider';
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
*/
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/extensions/uri.dart';
|
||||
|
||||
/// Display modes that are valid for installable PWAs per W3C spec.
|
||||
const _validDisplayModes = {
|
||||
@@ -40,11 +42,16 @@ const _validDisplayModes = {
|
||||
/// 3. start_url is within the scope
|
||||
/// 4. prefer_related_applications is not true
|
||||
bool isManifestInstallable(PwaManifest manifest) {
|
||||
final currentUrl = Uri.tryParse(manifest.currentUrl);
|
||||
final startUrl = Uri.tryParse(manifest.startUrl);
|
||||
|
||||
if (currentUrl == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check HTTPS requirement (relaxed for localhost)
|
||||
final isSecure =
|
||||
manifest.currentUrl.startsWith('https://') ||
|
||||
manifest.currentUrl.startsWith('http://localhost') ||
|
||||
manifest.currentUrl.startsWith('http://127.0.0.1');
|
||||
currentUrl.isHttps || (currentUrl.isHttp && currentUrl.isLocalhost);
|
||||
|
||||
if (!isSecure) {
|
||||
return false;
|
||||
@@ -62,24 +69,23 @@ bool isManifestInstallable(PwaManifest manifest) {
|
||||
|
||||
// W3C §1.10.6: start_url must be same-origin as the document URL
|
||||
final hasValidStartUrl =
|
||||
manifest.startUrl.isNotEmpty &&
|
||||
_isSameOrigin(manifest.startUrl, manifest.currentUrl);
|
||||
startUrl != null && _isSameOrigin(startUrl, currentUrl);
|
||||
|
||||
final hasValidDisplay =
|
||||
manifest.display != null &&
|
||||
_validDisplayModes.contains(manifest.display!.toLowerCase());
|
||||
|
||||
// Check that start_url is within scope
|
||||
final isInScope = _isStartUrlInScope(manifest.startUrl, manifest.scope);
|
||||
final isInScope =
|
||||
hasValidStartUrl &&
|
||||
_isStartUrlInScope(startUrl, manifest.scope.mapNotNull(Uri.tryParse));
|
||||
|
||||
return hasValidName && hasValidStartUrl && hasValidDisplay && isInScope;
|
||||
}
|
||||
|
||||
/// Returns true if two URLs share the same origin (scheme + host + port).
|
||||
bool _isSameOrigin(String url1, String url2) {
|
||||
bool _isSameOrigin(Uri uri1, Uri uri2) {
|
||||
try {
|
||||
final uri1 = Uri.parse(url1);
|
||||
final uri2 = Uri.parse(url2);
|
||||
return uri1.scheme == uri2.scheme &&
|
||||
uri1.host == uri2.host &&
|
||||
uri1.port == uri2.port;
|
||||
@@ -93,18 +99,14 @@ bool _isSameOrigin(String url1, String url2) {
|
||||
/// Per W3C spec: when scope is absent, the default scope is the start_url
|
||||
/// with its last path segment, query, and fragment removed.
|
||||
/// Scope matching uses path-prefix comparison on `/` boundaries.
|
||||
bool _isStartUrlInScope(String startUrl, String? scope) {
|
||||
bool _isStartUrlInScope(Uri startUri, Uri? scopeUri) {
|
||||
try {
|
||||
final startUri = Uri.parse(startUrl);
|
||||
|
||||
if (scope == null || scope.isEmpty) {
|
||||
if (scopeUri == null) {
|
||||
// Per W3C: default scope = start_url with filename/query/fragment removed
|
||||
// The start_url is trivially within its own default scope.
|
||||
return true;
|
||||
}
|
||||
|
||||
final scopeUri = Uri.parse(scope);
|
||||
|
||||
// Must be same origin
|
||||
if (startUri.scheme != scopeUri.scheme ||
|
||||
startUri.host != scopeUri.host ||
|
||||
|
||||
+132
-17
@@ -18,26 +18,141 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
|
||||
/// Shows a dialog asking the user to confirm adding a PWA to the home screen.
|
||||
/// The type of home screen shortcut the user chose.
|
||||
enum ShortcutInstallType { shortcut, app }
|
||||
|
||||
/// Shows a bottom sheet to confirm adding a PWA to the home screen.
|
||||
///
|
||||
/// Returns true if the user confirms, false if cancelled.
|
||||
Future<bool?> showPwaInstallConfirmDialog(BuildContext context, String name) {
|
||||
return showDialog<bool>(
|
||||
/// Returns true if the user confirms, null if dismissed.
|
||||
Future<bool?> showPwaInstallBottomSheet(
|
||||
BuildContext context, {
|
||||
required String name,
|
||||
required Uri url,
|
||||
}) {
|
||||
return showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Add to Home Screen'),
|
||||
content: Text('Add "$name" to your home screen?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Add'),
|
||||
),
|
||||
],
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ShortcutSheetHeader(name: name, url: url),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.install_mobile),
|
||||
title: const Text('Install as App'),
|
||||
subtitle: const Text('Runs standalone with its own window.'),
|
||||
onTap: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a bottom sheet for non-manifest sites offering a choice between
|
||||
/// "Add Shortcut" (opens in browser) and "Install as App" (standalone mode).
|
||||
///
|
||||
/// [showAppOption] controls whether the "Install as App" option is visible
|
||||
/// (requires the allowNonManifestPwaInstall setting to be enabled).
|
||||
///
|
||||
/// Returns [ShortcutInstallType] or null if dismissed.
|
||||
Future<ShortcutInstallType?> showShortcutChoiceBottomSheet(
|
||||
BuildContext context, {
|
||||
required String name,
|
||||
required Uri url,
|
||||
required bool showAppOption,
|
||||
}) {
|
||||
return showModalBottomSheet<ShortcutInstallType>(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ShortcutSheetHeader(name: name, url: url),
|
||||
const Divider(height: 1),
|
||||
if (showAppOption)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.install_mobile),
|
||||
title: const Text('Install as App'),
|
||||
subtitle: const Text('Runs standalone with its own window.'),
|
||||
onTap: () =>
|
||||
Navigator.of(context).pop(ShortcutInstallType.app),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.shortcut),
|
||||
title: const Text('Add Shortcut'),
|
||||
subtitle: const Text('Opens as a standard tab in the browser.'),
|
||||
onTap: () =>
|
||||
Navigator.of(context).pop(ShortcutInstallType.shortcut),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ShortcutSheetHeader extends StatelessWidget {
|
||||
final String name;
|
||||
final Uri url;
|
||||
|
||||
const _ShortcutSheetHeader({required this.name, required this.url});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Add to Home Screen', style: textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
RepaintBoundary(child: UrlIcon([url], iconSize: 32)),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
UriBreadcrumb(
|
||||
uri: url,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+70
-1
@@ -21,15 +21,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/pwa/presentation/dialogs/pwa_install_dialog.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart';
|
||||
|
||||
/// Shows install bottom sheet for sites with a valid PWA manifest (existing flow).
|
||||
Future<void> showPwaInstallDialog(BuildContext context, WidgetRef ref) async {
|
||||
final selectedTabId = ref.read(selectedTabProvider);
|
||||
final manifest = ref.read(currentTabManifestProvider);
|
||||
final name = manifest?.name ?? manifest?.shortName ?? 'this web app';
|
||||
final tabState = selectedTabId != null
|
||||
? ref.read(tabStateProvider(selectedTabId))
|
||||
: null;
|
||||
final url = tabState?.url ?? Uri.parse('about:blank');
|
||||
|
||||
final confirmed = await showPwaInstallConfirmDialog(context, name);
|
||||
final confirmed = await showPwaInstallBottomSheet(
|
||||
context,
|
||||
name: name,
|
||||
url: url,
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
@@ -60,3 +73,59 @@ Future<void> showPwaInstallDialog(BuildContext context, WidgetRef ref) async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows choice dialog for sites without a manifest.
|
||||
/// Offers "Add as Shortcut" (always) and "Add as App" (if setting enabled).
|
||||
Future<void> showShortcutInstallDialog(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
final selectedTabId = ref.read(selectedTabProvider);
|
||||
if (selectedTabId == null) return;
|
||||
|
||||
final tabState = ref.read(tabStateProvider(selectedTabId));
|
||||
final name = tabState?.title ?? 'this site';
|
||||
final url = tabState?.url ?? Uri.parse('about:blank');
|
||||
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
final showAppOption = settings.allowNonManifestPwaInstall;
|
||||
|
||||
final choice = await showShortcutChoiceBottomSheet(
|
||||
context,
|
||||
name: name,
|
||||
url: url,
|
||||
showAppOption: showAppOption,
|
||||
);
|
||||
|
||||
if (choice == null) return;
|
||||
|
||||
try {
|
||||
final bool success;
|
||||
switch (choice) {
|
||||
case ShortcutInstallType.shortcut:
|
||||
success = await ref.read(installBasicShortcutProvider().future);
|
||||
case ShortcutInstallType.app:
|
||||
success = await ref.read(installCurrentWebAppProvider.future);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
showInfoMessage(context, '$name added to home screen');
|
||||
} else {
|
||||
showErrorMessage(context, 'Failed to add $name to home screen');
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
logger.e('Failed to create shortcut', error: e, stackTrace: stackTrace);
|
||||
|
||||
if (context.mounted) {
|
||||
var errorMessage = 'Failed to add $name to home screen';
|
||||
|
||||
if (e is StateError) {
|
||||
errorMessage = 'No tab selected. Please try again.';
|
||||
}
|
||||
|
||||
showErrorMessage(context, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,4 +196,12 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
||||
Selectable<String?> containersToClearOnExit() {
|
||||
return db.definitionsDrift.containersToClearOnExit();
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<ContainerData> getContainerByContextualIdentity(
|
||||
String contextId,
|
||||
) {
|
||||
return db.definitionsDrift.containerByContextualIdentity(
|
||||
contextId: contextId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +335,11 @@ allAssignedSites WITH SiteAssignment:
|
||||
CROSS JOIN json_each(container.metadata, '$.assignedSites')
|
||||
WHERE value IS NOT NULL;
|
||||
|
||||
containerByContextualIdentity:
|
||||
SELECT * FROM container
|
||||
WHERE container.metadata ->> '$.contextualIdentity' = :contextId
|
||||
LIMIT 1;
|
||||
|
||||
containersToClearOnExit:
|
||||
SELECT container.metadata ->> '$.contextualIdentity' AS contextual_identity
|
||||
FROM container
|
||||
|
||||
@@ -2601,6 +2601,16 @@ class DefinitionsDrift extends i9.ModularAccessor {
|
||||
);
|
||||
}
|
||||
|
||||
i0.Selectable<i1.ContainerData> containerByContextualIdentity({
|
||||
required String contextId,
|
||||
}) {
|
||||
return customSelect(
|
||||
'SELECT * FROM container WHERE container.metadata ->> \'\$.contextualIdentity\' = ?1 LIMIT 1',
|
||||
variables: [i0.Variable<String>(contextId)],
|
||||
readsFrom: {container},
|
||||
).asyncMap(container.mapFromRow);
|
||||
}
|
||||
|
||||
i0.Selectable<String?> containersToClearOnExit() {
|
||||
return customSelect(
|
||||
'SELECT container.metadata ->> \'\$.contextualIdentity\' AS contextual_identity FROM container WHERE json_extract(container.metadata, \'\$.clearDataOnExit\') = 1 AND container.metadata ->> \'\$.contextualIdentity\' IS NOT NULL',
|
||||
|
||||
@@ -77,6 +77,14 @@ class ContainerRepository extends _$ContainerRepository {
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<ContainerData?> getContainerByContextualIdentity(String contextId) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.containerDao
|
||||
.getContainerByContextualIdentity(contextId)
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<String>> getContainerTabIds(String? id) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ContainerRepositoryProvider
|
||||
}
|
||||
|
||||
String _$containerRepositoryHash() =>
|
||||
r'5e46b6d9d3510aeeb70646ede75d71c2db9efb0f';
|
||||
r'57ee339087a845e54dc79224e25fa0da19568a68';
|
||||
|
||||
abstract class _$ContainerRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
Reference in New Issue
Block a user