💄 Optimize the embed view experience
This commit is contained in:
@@ -1012,6 +1012,11 @@
|
||||
"expandPoll": "Expand Poll",
|
||||
"collapsePoll": "Collapse Poll",
|
||||
"embedView": "Embed View",
|
||||
"auto": "Auto",
|
||||
"manual": "Manual",
|
||||
"iframeCode": "Iframe Code",
|
||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||
"parseIframe": "Parse Iframe",
|
||||
"embedUri": "Embed URI",
|
||||
"aspectRatio": "Aspect Ratio",
|
||||
"renderer": "Renderer",
|
||||
@@ -1023,5 +1028,6 @@
|
||||
"noEmbed": "No embed yet",
|
||||
"save": "Save",
|
||||
"webView": "Web View",
|
||||
"messageActions": "Message Actions"
|
||||
"messageActions": "Message Actions",
|
||||
"viewEmbedLoadHint": "Tap to load"
|
||||
}
|
||||
|
@@ -30,10 +30,13 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
final selectedRenderer = useState<PostEmbedViewRenderer>(
|
||||
PostEmbedViewRenderer.webView,
|
||||
);
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
final iframeController = useTextEditingController();
|
||||
|
||||
void clearForm() {
|
||||
uriController.clear();
|
||||
aspectRatioController.clear();
|
||||
iframeController.clear();
|
||||
selectedRenderer.value = PostEmbedViewRenderer.webView;
|
||||
}
|
||||
|
||||
@@ -77,6 +80,57 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void parseIframe() {
|
||||
final iframe = iframeController.text.trim();
|
||||
if (iframe.isEmpty) return;
|
||||
|
||||
final srcMatch = RegExp(r'src="([^"]*)"').firstMatch(iframe);
|
||||
final widthMatch = RegExp(r'width="([^"]*)"').firstMatch(iframe);
|
||||
final heightMatch = RegExp(r'height="([^"]*)"').firstMatch(iframe);
|
||||
|
||||
if (srcMatch != null) {
|
||||
uriController.text = srcMatch.group(1)!;
|
||||
}
|
||||
|
||||
if (widthMatch != null && heightMatch != null) {
|
||||
final w = double.tryParse(widthMatch.group(1)!);
|
||||
final h = double.tryParse(heightMatch.group(1)!);
|
||||
if (w != null && h != null && h != 0) {
|
||||
aspectRatioController.text = (w / h).toStringAsFixed(3);
|
||||
}
|
||||
}
|
||||
|
||||
tabController.animateTo(1);
|
||||
}
|
||||
|
||||
void deleteEmbed(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(dialogContext) => AlertDialog(
|
||||
title: Text('deleteEmbed').tr(),
|
||||
content: Text('deleteEmbedConfirm').tr(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text('cancel').tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ComposeLogic.deleteEmbedView(state);
|
||||
clearForm();
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
child: Text('delete').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'embedView'.tr(),
|
||||
heightFactor: 0.7,
|
||||
@@ -85,7 +139,7 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
// Header with save button when editing
|
||||
if (currentEmbedView != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -97,187 +151,207 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: saveEmbedView,
|
||||
style: ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
child: Text('save'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Tab bar
|
||||
TabBar(
|
||||
controller: tabController,
|
||||
tabs: [Tab(text: 'auto'.tr()), Tab(text: 'manual'.tr())],
|
||||
),
|
||||
|
||||
// Content area
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Form fields
|
||||
TextField(
|
||||
controller: uriController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'embedUri'.tr(),
|
||||
hintText: 'https://example.com',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
// Auto tab
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: iframeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'iframeCode'.tr(),
|
||||
hintText: 'iframeCodeHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
maxLines: 5,
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: aspectRatioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'aspectRatio'.tr(),
|
||||
hintText: '16/9 = 1.777',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
const Gap(16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: parseIframe,
|
||||
icon: const Icon(Symbols.auto_fix),
|
||||
label: Text('parseIframe'.tr()),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
DropdownButtonFormField2<PostEmbedViewRenderer>(
|
||||
value: selectedRenderer.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'renderer'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
items:
|
||||
PostEmbedViewRenderer.values.map((renderer) {
|
||||
return DropdownMenuItem(
|
||||
value: renderer,
|
||||
child: Text(renderer.name).tr(),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
selectedRenderer.value = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// Current embed view display (when exists)
|
||||
if (currentEmbedView != null) ...[
|
||||
const Gap(32),
|
||||
Text(
|
||||
'currentEmbed'.tr(),
|
||||
style: theme.textTheme.titleMedium,
|
||||
).padding(horizontal: 4),
|
||||
const Gap(8),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 12,
|
||||
top: 4,
|
||||
),
|
||||
// Manual tab
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Form fields
|
||||
TextField(
|
||||
controller: uriController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'embedUri'.tr(),
|
||||
hintText: 'https://example.com',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
keyboardType: TextInputType.url,
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: aspectRatioController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'aspectRatio'.tr(),
|
||||
hintText: '16/9 = 1.777',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d*\.?\d*$'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
DropdownButtonFormField2<PostEmbedViewRenderer>(
|
||||
value: selectedRenderer.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'renderer'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
selectedItemBuilder: (context) {
|
||||
return PostEmbedViewRenderer.values.map((renderer) {
|
||||
return Text(renderer.name).tr();
|
||||
}).toList();
|
||||
},
|
||||
menuItemStyleData: MenuItemStyleData(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
items:
|
||||
PostEmbedViewRenderer.values.map((renderer) {
|
||||
return DropdownMenuItem(
|
||||
value: renderer,
|
||||
child: Text(
|
||||
renderer.name,
|
||||
).tr().padding(horizontal: 20),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
selectedRenderer.value = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// Current embed view display (when exists)
|
||||
if (currentEmbedView != null) ...[
|
||||
const Gap(32),
|
||||
Text(
|
||||
'currentEmbed'.tr(),
|
||||
style: theme.textTheme.titleMedium,
|
||||
).padding(horizontal: 4),
|
||||
const Gap(8),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 12,
|
||||
top: 4,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
currentEmbedView.renderer ==
|
||||
PostEmbedViewRenderer.webView
|
||||
? Symbols.web
|
||||
: Symbols.web,
|
||||
color: colorScheme.primary,
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
currentEmbedView.renderer ==
|
||||
PostEmbedViewRenderer.webView
|
||||
? Symbols.web
|
||||
: Symbols.web,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
currentEmbedView.uri,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () => deleteEmbed(context),
|
||||
tooltip: 'delete'.tr(),
|
||||
color: colorScheme.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
currentEmbedView.uri,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Text(
|
||||
'aspectRatio'.tr(),
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(dialogContext) => AlertDialog(
|
||||
title: Text('deleteEmbed').tr(),
|
||||
content:
|
||||
Text('deleteEmbedConfirm').tr(),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
() =>
|
||||
Navigator.of(
|
||||
dialogContext,
|
||||
).pop(),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ComposeLogic.deleteEmbedView(
|
||||
state,
|
||||
);
|
||||
clearForm();
|
||||
Navigator.of(
|
||||
dialogContext,
|
||||
).pop();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
colorScheme.error,
|
||||
),
|
||||
child: Text('delete').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: 'delete'.tr(),
|
||||
color: colorScheme.error,
|
||||
const Gap(4),
|
||||
Text(
|
||||
currentEmbedView.aspectRatio != null
|
||||
? currentEmbedView.aspectRatio!
|
||||
.toStringAsFixed(2)
|
||||
: 'notSet'.tr(),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
'aspectRatio'.tr(),
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
currentEmbedView.aspectRatio != null
|
||||
? currentEmbedView.aspectRatio!
|
||||
.toStringAsFixed(2)
|
||||
: 'notSet'.tr(),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Save button for new embed
|
||||
const Gap(16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: saveEmbedView,
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('addEmbed'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
const Gap(16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: saveEmbedView,
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('addEmbed'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@@ -82,75 +82,83 @@ class ComposeToolbar extends HookConsumerWidget {
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: pickPhotoMedia,
|
||||
tooltip: 'addPhoto'.tr(),
|
||||
icon: const Icon(Symbols.add_a_photo),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: pickVideoMedia,
|
||||
tooltip: 'addVideo'.tr(),
|
||||
icon: const Icon(Symbols.videocam),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: addAudio,
|
||||
tooltip: 'addAudio'.tr(),
|
||||
icon: const Icon(Symbols.mic),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: pickGeneralFile,
|
||||
tooltip: 'uploadFile'.tr(),
|
||||
icon: const Icon(Symbols.file_upload),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: linkAttachment,
|
||||
icon: const Icon(Symbols.attach_file),
|
||||
tooltip: 'linkAttachment'.tr(),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
// Poll button with visual state when a poll is linked
|
||||
ListenableBuilder(
|
||||
listenable: state.pollId,
|
||||
builder: (context, _) {
|
||||
return IconButton(
|
||||
onPressed: pickPoll,
|
||||
icon: const Icon(Symbols.how_to_vote),
|
||||
tooltip: 'poll'.tr(),
|
||||
color: colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
state.pollId.value != null
|
||||
? colorScheme.primary.withOpacity(0.15)
|
||||
: null,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: pickPhotoMedia,
|
||||
tooltip: 'addPhoto'.tr(),
|
||||
icon: const Icon(Symbols.add_a_photo),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Embed button with visual state when embed is present
|
||||
ListenableBuilder(
|
||||
listenable: state.embedView,
|
||||
builder: (context, _) {
|
||||
return IconButton(
|
||||
onPressed: showEmbedSheet,
|
||||
icon: const Icon(Symbols.web),
|
||||
tooltip: 'embedView'.tr(),
|
||||
color: colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
state.embedView.value != null
|
||||
? colorScheme.primary.withOpacity(0.15)
|
||||
: null,
|
||||
IconButton(
|
||||
onPressed: pickVideoMedia,
|
||||
tooltip: 'addVideo'.tr(),
|
||||
icon: const Icon(Symbols.videocam),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
IconButton(
|
||||
onPressed: addAudio,
|
||||
tooltip: 'addAudio'.tr(),
|
||||
icon: const Icon(Symbols.mic),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: pickGeneralFile,
|
||||
tooltip: 'uploadFile'.tr(),
|
||||
icon: const Icon(Symbols.file_upload),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: linkAttachment,
|
||||
icon: const Icon(Symbols.attach_file),
|
||||
tooltip: 'linkAttachment'.tr(),
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
// Poll button with visual state when a poll is linked
|
||||
ListenableBuilder(
|
||||
listenable: state.pollId,
|
||||
builder: (context, _) {
|
||||
return IconButton(
|
||||
onPressed: pickPoll,
|
||||
icon: const Icon(Symbols.how_to_vote),
|
||||
tooltip: 'poll'.tr(),
|
||||
color: colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
state.pollId.value != null
|
||||
? colorScheme.primary.withOpacity(0.15)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Embed button with visual state when embed is present
|
||||
ListenableBuilder(
|
||||
listenable: state.embedView,
|
||||
builder: (context, _) {
|
||||
return IconButton(
|
||||
onPressed: showEmbedSheet,
|
||||
icon: const Icon(Symbols.iframe),
|
||||
tooltip: 'embedView'.tr(),
|
||||
color: colorScheme.primary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
state.embedView.value != null
|
||||
? colorScheme.primary.withOpacity(0.15)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (originalPost == null && state.isEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.draft),
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
@@ -58,8 +61,8 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
children: [
|
||||
Icon(
|
||||
embedView.renderer == PostEmbedViewRenderer.webView
|
||||
? Symbols.web
|
||||
: Symbols.web,
|
||||
? Symbols.globe
|
||||
: Symbols.iframe,
|
||||
size: 16,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
@@ -74,13 +77,13 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
InkWell(
|
||||
child: Icon(
|
||||
Symbols.open_in_new,
|
||||
size: 16,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () async {
|
||||
onTap: () async {
|
||||
final uri = Uri.parse(embedView.uri);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(
|
||||
@@ -89,10 +92,6 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
tooltip: 'Open in browser',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -106,6 +105,20 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
? Stack(
|
||||
children: [
|
||||
InAppWebView(
|
||||
gestureRecognizers: {
|
||||
Factory<VerticalDragGestureRecognizer>(
|
||||
() => VerticalDragGestureRecognizer(),
|
||||
),
|
||||
Factory<HorizontalDragGestureRecognizer>(
|
||||
() => HorizontalDragGestureRecognizer(),
|
||||
),
|
||||
Factory<ScaleGestureRecognizer>(
|
||||
() => ScaleGestureRecognizer(),
|
||||
),
|
||||
Factory<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(),
|
||||
),
|
||||
},
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri(embedView.uri),
|
||||
),
|
||||
@@ -256,14 +269,14 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.play_arrow,
|
||||
fill: 1,
|
||||
size: 48,
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(
|
||||
0.6,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap to load content',
|
||||
'embedViewLoadHint'.tr(),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant
|
||||
.withOpacity(0.6),
|
||||
|
Reference in New Issue
Block a user