💄 Optimize the embed view experience
This commit is contained in:
@@ -1012,6 +1012,11 @@
|
|||||||
"expandPoll": "Expand Poll",
|
"expandPoll": "Expand Poll",
|
||||||
"collapsePoll": "Collapse Poll",
|
"collapsePoll": "Collapse Poll",
|
||||||
"embedView": "Embed View",
|
"embedView": "Embed View",
|
||||||
|
"auto": "Auto",
|
||||||
|
"manual": "Manual",
|
||||||
|
"iframeCode": "Iframe Code",
|
||||||
|
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||||
|
"parseIframe": "Parse Iframe",
|
||||||
"embedUri": "Embed URI",
|
"embedUri": "Embed URI",
|
||||||
"aspectRatio": "Aspect Ratio",
|
"aspectRatio": "Aspect Ratio",
|
||||||
"renderer": "Renderer",
|
"renderer": "Renderer",
|
||||||
@@ -1023,5 +1028,6 @@
|
|||||||
"noEmbed": "No embed yet",
|
"noEmbed": "No embed yet",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"webView": "Web View",
|
"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>(
|
final selectedRenderer = useState<PostEmbedViewRenderer>(
|
||||||
PostEmbedViewRenderer.webView,
|
PostEmbedViewRenderer.webView,
|
||||||
);
|
);
|
||||||
|
final tabController = useTabController(initialLength: 2);
|
||||||
|
final iframeController = useTextEditingController();
|
||||||
|
|
||||||
void clearForm() {
|
void clearForm() {
|
||||||
uriController.clear();
|
uriController.clear();
|
||||||
aspectRatioController.clear();
|
aspectRatioController.clear();
|
||||||
|
iframeController.clear();
|
||||||
selectedRenderer.value = PostEmbedViewRenderer.webView;
|
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(
|
return SheetScaffold(
|
||||||
titleText: 'embedView'.tr(),
|
titleText: 'embedView'.tr(),
|
||||||
heightFactor: 0.7,
|
heightFactor: 0.7,
|
||||||
@@ -85,7 +139,7 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
// Header with save button when editing
|
// Header with save button when editing
|
||||||
if (currentEmbedView != null)
|
if (currentEmbedView != null)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -97,15 +151,55 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: saveEmbedView,
|
onPressed: saveEmbedView,
|
||||||
|
style: ButtonStyle(visualDensity: VisualDensity.compact),
|
||||||
child: Text('save'.tr()),
|
child: Text('save'.tr()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Tab bar
|
||||||
|
TabBar(
|
||||||
|
controller: tabController,
|
||||||
|
tabs: [Tab(text: 'auto'.tr()), Tab(text: 'manual'.tr())],
|
||||||
|
),
|
||||||
|
|
||||||
// Content area
|
// Content area
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
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,
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: parseIframe,
|
||||||
|
icon: const Icon(Symbols.auto_fix),
|
||||||
|
label: Text('parseIframe'.tr()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Manual tab
|
||||||
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -136,7 +230,9 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
decimal: true,
|
decimal: true,
|
||||||
),
|
),
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')),
|
FilteringTextInputFormatter.allow(
|
||||||
|
RegExp(r'^\d*\.?\d*$'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
@@ -148,11 +244,21 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
selectedItemBuilder: (context) {
|
||||||
|
return PostEmbedViewRenderer.values.map((renderer) {
|
||||||
|
return Text(renderer.name).tr();
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
menuItemStyleData: MenuItemStyleData(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
items:
|
items:
|
||||||
PostEmbedViewRenderer.values.map((renderer) {
|
PostEmbedViewRenderer.values.map((renderer) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(
|
||||||
value: renderer,
|
value: renderer,
|
||||||
child: Text(renderer.name).tr(),
|
child: Text(
|
||||||
|
renderer.name,
|
||||||
|
).tr().padding(horizontal: 20),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -172,7 +278,10 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
Card(
|
Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHigh,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -203,43 +312,7 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.delete),
|
icon: const Icon(Symbols.delete),
|
||||||
onPressed: () {
|
onPressed: () => deleteEmbed(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:
|
|
||||||
colorScheme.error,
|
|
||||||
),
|
|
||||||
child: Text('delete').tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
tooltip: 'delete'.tr(),
|
tooltip: 'delete'.tr(),
|
||||||
color: colorScheme.error,
|
color: colorScheme.error,
|
||||||
),
|
),
|
||||||
@@ -265,7 +338,6 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
// Save button for new embed
|
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -279,6 +351,8 @@ class ComposeEmbedSheet extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@@ -80,6 +80,11 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -137,7 +142,7 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: showEmbedSheet,
|
onPressed: showEmbedSheet,
|
||||||
icon: const Icon(Symbols.web),
|
icon: const Icon(Symbols.iframe),
|
||||||
tooltip: 'embedView'.tr(),
|
tooltip: 'embedView'.tr(),
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
@@ -150,7 +155,10 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Spacer(),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (originalPost == null && state.isEmpty)
|
if (originalPost == null && state.isEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.draft),
|
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/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
@@ -58,8 +61,8 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
embedView.renderer == PostEmbedViewRenderer.webView
|
embedView.renderer == PostEmbedViewRenderer.webView
|
||||||
? Symbols.web
|
? Symbols.globe
|
||||||
: Symbols.web,
|
: Symbols.iframe,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
@@ -74,13 +77,13 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
InkWell(
|
||||||
icon: Icon(
|
child: Icon(
|
||||||
Symbols.open_in_new,
|
Symbols.open_in_new,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onTap: () async {
|
||||||
final uri = Uri.parse(embedView.uri);
|
final uri = Uri.parse(embedView.uri);
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(
|
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(
|
? Stack(
|
||||||
children: [
|
children: [
|
||||||
InAppWebView(
|
InAppWebView(
|
||||||
|
gestureRecognizers: {
|
||||||
|
Factory<VerticalDragGestureRecognizer>(
|
||||||
|
() => VerticalDragGestureRecognizer(),
|
||||||
|
),
|
||||||
|
Factory<HorizontalDragGestureRecognizer>(
|
||||||
|
() => HorizontalDragGestureRecognizer(),
|
||||||
|
),
|
||||||
|
Factory<ScaleGestureRecognizer>(
|
||||||
|
() => ScaleGestureRecognizer(),
|
||||||
|
),
|
||||||
|
Factory<TapGestureRecognizer>(
|
||||||
|
() => TapGestureRecognizer(),
|
||||||
|
),
|
||||||
|
},
|
||||||
initialUrlRequest: URLRequest(
|
initialUrlRequest: URLRequest(
|
||||||
url: WebUri(embedView.uri),
|
url: WebUri(embedView.uri),
|
||||||
),
|
),
|
||||||
@@ -256,14 +269,14 @@ class EmbedViewRenderer extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Symbols.play_arrow,
|
Symbols.play_arrow,
|
||||||
|
fill: 1,
|
||||||
size: 48,
|
size: 48,
|
||||||
color: colorScheme.onSurfaceVariant.withOpacity(
|
color: colorScheme.onSurfaceVariant.withOpacity(
|
||||||
0.6,
|
0.6,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
Text(
|
||||||
'Tap to load content',
|
'embedViewLoadHint'.tr(),
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant
|
color: colorScheme.onSurfaceVariant
|
||||||
.withOpacity(0.6),
|
.withOpacity(0.6),
|
||||||
|
Reference in New Issue
Block a user