first implementation finished
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:kagi_bang_bang/domain/entities/web_page_info.dart';
|
||||
import 'package:kagi_bang_bang/extensions/web_uri_favicon.dart';
|
||||
|
||||
class FaviconImage extends StatelessWidget {
|
||||
final double size;
|
||||
final Icon _iconPlaceholder;
|
||||
|
||||
final WebPageInfo webPageInfo;
|
||||
|
||||
FaviconImage({
|
||||
required this.webPageInfo,
|
||||
this.size = 16,
|
||||
super.key,
|
||||
}) : _iconPlaceholder = Icon(
|
||||
MdiIcons.webBox,
|
||||
size: size,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (webPageInfo.favicon != null) {
|
||||
return FadeInImage(
|
||||
placeholder: NetworkImage(
|
||||
webPageInfo.url.guessFavicon().toString(),
|
||||
),
|
||||
image: NetworkImage(webPageInfo.favicon!.url.toString()),
|
||||
placeholderErrorBuilder: (_, __, ___) => _iconPlaceholder,
|
||||
imageErrorBuilder: (_, __, ___) => _iconPlaceholder,
|
||||
height: size,
|
||||
width: size,
|
||||
);
|
||||
} else {
|
||||
return Image.network(
|
||||
webPageInfo.url.guessFavicon().toString(),
|
||||
errorBuilder: (_, __, ___) => _iconPlaceholder,
|
||||
height: size,
|
||||
width: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:kagi_bang_bang/domain/entities/web_page_info.dart';
|
||||
import 'package:kagi_bang_bang/features/search_browser/domain/entities/modes.dart';
|
||||
import 'package:kagi_bang_bang/features/search_browser/utils/url_builder.dart'
|
||||
as uri_builder;
|
||||
import 'package:kagi_bang_bang/features/share_intent/domain/entities/shared_content.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/presentation/controllers/switch_new_tab.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/presentation/widgets/favicon.dart';
|
||||
import 'package:kagi_bang_bang/presentation/controllers/website_title.dart';
|
||||
import 'package:kagi_bang_bang/utils/ui_helper.dart' as ui_helper;
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class LoadingWebPageDialog extends HookConsumerWidget {
|
||||
final Uri url;
|
||||
|
||||
final void Function()? onDismiss;
|
||||
|
||||
const LoadingWebPageDialog(this.url, {this.onDismiss});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pageInfoAsync = ref.watch(pageInfoProvider(url));
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: pageInfoAsync.isLoading,
|
||||
child: WebPageDialog(
|
||||
page: pageInfoAsync.valueOrNull ??
|
||||
WebPageInfo(url: url, favicon: null, title: ''),
|
||||
onDismiss: onDismiss,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WebPageDialog extends HookConsumerWidget {
|
||||
final WebPageInfo page;
|
||||
final InAppWebViewController? webViewController;
|
||||
|
||||
final void Function()? onDismiss;
|
||||
|
||||
const WebPageDialog({
|
||||
required this.page,
|
||||
this.webViewController,
|
||||
this.onDismiss,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
final urlTextController =
|
||||
useTextEditingController(text: page.url.toString());
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
ModalBarrier(
|
||||
color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54,
|
||||
onDismiss: onDismiss,
|
||||
),
|
||||
SimpleDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 0.0),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20.0,
|
||||
vertical: 24.0,
|
||||
),
|
||||
title: ListTile(
|
||||
leading: FaviconImage(
|
||||
webPageInfo: page,
|
||||
size: 24,
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(page.title ?? 'Unknown Title'),
|
||||
subtitle: Text(page.url.authority),
|
||||
),
|
||||
children: [
|
||||
SizedBox(
|
||||
//We need this to stretch the dialog, then padding from dialog is applied
|
||||
width: double.maxFinite,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: TextFormField(
|
||||
controller: urlTextController,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: (webViewController != null)
|
||||
? IconButton(
|
||||
onPressed: () async {
|
||||
if (formKey.currentState?.validate() ?? false) {
|
||||
await webViewController!.loadUrl(
|
||||
urlRequest: URLRequest(
|
||||
url: WebUri.uri(
|
||||
Uri.parse(
|
||||
urlTextController.text,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
onDismiss?.call();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.send),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value != null) {
|
||||
if (Uri.tryParse(value) case final Uri url) {
|
||||
if (url.hasScheme && url.hasAuthority) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Invalid URL';
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(MdiIcons.contentCopy),
|
||||
title: const Text('Copy address'),
|
||||
onTap: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: page.url.toString()),
|
||||
);
|
||||
onDismiss?.call();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
if (!await launchUrl(
|
||||
page.url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
)) {
|
||||
if (context.mounted) {
|
||||
ui_helper.showErrorMessage(
|
||||
context,
|
||||
'Could not launch URL (${page.url})',
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Launch External'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(MdiIcons.tabPlus),
|
||||
title: const Text('Open in new tab'),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(switchNewTabControllerProvider.notifier)
|
||||
.add(page.url);
|
||||
|
||||
onDismiss?.call();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share),
|
||||
title: const Text('Share link'),
|
||||
onTap: () async {
|
||||
await Share.shareUri(page.url);
|
||||
|
||||
onDismiss?.call();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(MdiIcons.text),
|
||||
title: const Text('Summarize'),
|
||||
onTap: () async {
|
||||
final url = uri_builder.summarizerUri(
|
||||
document: SharedUrl(page.url),
|
||||
mode: SummarizerMode.keyMoments,
|
||||
);
|
||||
|
||||
await ref
|
||||
.read(switchNewTabControllerProvider.notifier)
|
||||
.add(url);
|
||||
|
||||
onDismiss?.call();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:kagi_bang_bang/core/logger.dart';
|
||||
import 'package:kagi_bang_bang/features/search_browser/domain/entities/modes.dart';
|
||||
import 'package:kagi_bang_bang/features/search_browser/domain/entities/sheet.dart';
|
||||
import 'package:kagi_bang_bang/features/search_browser/domain/providers.dart';
|
||||
import 'package:kagi_bang_bang/features/search_browser/utils/url_builder.dart'
|
||||
as uri_builder;
|
||||
import 'package:kagi_bang_bang/features/settings/data/repositories/settings_repository.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/domain/entities/web_view_page.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/presentation/controllers/switch_new_tab.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/presentation/widgets/web_page_dialog.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/utils/favicon_helper.dart';
|
||||
import 'package:kagi_bang_bang/utils/platform_util.dart' as platform_util;
|
||||
import 'package:kagi_bang_bang/utils/ui_helper.dart' as ui_helper;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
const _webViewSupportedSchemes = [
|
||||
"http",
|
||||
"https",
|
||||
"file",
|
||||
"chrome",
|
||||
"data",
|
||||
"javascript",
|
||||
"about",
|
||||
];
|
||||
|
||||
class WebView extends StatefulHookConsumerWidget {
|
||||
final ValueNotifier<WebViewPage> _valueNotifier;
|
||||
|
||||
ValueListenable<WebViewPage> get page => _valueNotifier;
|
||||
|
||||
void updatePage(WebViewPage Function(WebViewPage page) update) {
|
||||
_valueNotifier.value = update(_valueNotifier.value);
|
||||
}
|
||||
|
||||
WebView({required WebViewPage tab})
|
||||
: _valueNotifier = ValueNotifier(tab),
|
||||
super(key: tab.key);
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _WebViewState();
|
||||
}
|
||||
|
||||
class _WebViewState extends ConsumerState<WebView> {
|
||||
Timer? _onLoadStopDebounce;
|
||||
Timer? _periodicScreenshotUpdate;
|
||||
|
||||
Future<void> _updateScreenshot() async {
|
||||
final screenshot = await widget.page.value.controller
|
||||
?.takeScreenshot(
|
||||
screenshotConfiguration: ScreenshotConfiguration(
|
||||
compressFormat: CompressFormat.JPEG,
|
||||
quality: 20,
|
||||
),
|
||||
)
|
||||
.timeout(
|
||||
const Duration(milliseconds: 1500),
|
||||
onTimeout: () {
|
||||
logger.w('Screenshot timed out');
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
widget.updatePage(
|
||||
(page) => page.copyWith.screenshot(screenshot),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
super.dispose();
|
||||
|
||||
_onLoadStopDebounce?.cancel();
|
||||
_periodicScreenshotUpdate?.cancel();
|
||||
|
||||
widget._valueNotifier.dispose();
|
||||
logger.i('Disposed ${widget.key} (${widget.page.value.title})');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final initialSettings = useMemoized(
|
||||
() => InAppWebViewSettings(
|
||||
// isInspectable: kDebugMode,
|
||||
useOnDownloadStart: true,
|
||||
allowsLinkPreview: false,
|
||||
disableLongPressContextMenuOnLinks: true,
|
||||
useShouldOverrideUrlLoading: true,
|
||||
javaScriptEnabled: ref
|
||||
.read(settingsRepositoryProvider)
|
||||
.valueOrNull
|
||||
?.enableJavascript ??
|
||||
true,
|
||||
saveFormData: false,
|
||||
disabledActionModeMenuItems: ActionModeMenuItem.MENU_ITEM_WEB_SEARCH,
|
||||
),
|
||||
);
|
||||
|
||||
final webViewProgress = useValueNotifier(100);
|
||||
|
||||
useOnAppLifecycleStateChange((previous, current) async {
|
||||
switch (current) {
|
||||
case AppLifecycleState.paused:
|
||||
if (platform_util.isAndroid()) {
|
||||
await widget.page.value.controller?.pause();
|
||||
}
|
||||
if (platform_util.isAndroid() || platform_util.isIOS()) {
|
||||
await widget.page.value.controller?.pauseTimers();
|
||||
}
|
||||
case AppLifecycleState.resumed:
|
||||
if (platform_util.isAndroid()) {
|
||||
await widget.page.value.controller?.resume();
|
||||
}
|
||||
if (platform_util.isAndroid() || platform_util.isIOS()) {
|
||||
await widget.page.value.controller?.resumeTimers();
|
||||
}
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
ref.listen(
|
||||
settingsRepositoryProvider
|
||||
.select((value) => value.valueOrNull?.enableJavascript),
|
||||
(previous, next) async {
|
||||
await widget.page.value.controller?.setSettings(
|
||||
settings: initialSettings.copy()..javaScriptEnabled = next,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
InAppWebView(
|
||||
initialUrlRequest: URLRequest(url: WebUri.uri(widget.page.value.url)),
|
||||
initialSettings: initialSettings,
|
||||
contextMenu: ContextMenu(
|
||||
menuItems: [
|
||||
ContextMenuItem(
|
||||
id: 1,
|
||||
title: "Search",
|
||||
action: () async {
|
||||
final selectedText =
|
||||
await widget.page.value.controller?.getSelectedText();
|
||||
|
||||
if (selectedText != null && selectedText.isNotEmpty) {
|
||||
final url = uri_builder.searchUri(
|
||||
searchQuery: selectedText,
|
||||
);
|
||||
|
||||
await ref
|
||||
.read(switchNewTabControllerProvider.notifier)
|
||||
.add(url);
|
||||
}
|
||||
},
|
||||
),
|
||||
ContextMenuItem(
|
||||
id: 2,
|
||||
title: "Assistant",
|
||||
action: () async {
|
||||
final selectedText =
|
||||
await widget.page.value.controller?.getSelectedText();
|
||||
|
||||
if (selectedText != null && selectedText.isNotEmpty) {
|
||||
ref.read(bottomSheetProvider.notifier).show(
|
||||
CreateTab(
|
||||
content: selectedText,
|
||||
preferredTool: KagiTool.assistant,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onWebViewCreated: (controller) async {
|
||||
if (platform_util.isAndroid()) {
|
||||
await controller.startSafeBrowsing();
|
||||
}
|
||||
|
||||
widget.updatePage((page) => page.copyWith.controller(controller));
|
||||
},
|
||||
onReceivedServerTrustAuthRequest: (controller, challenge) async {
|
||||
final sslError = challenge.protectionSpace.sslError;
|
||||
|
||||
if (sslError != null && sslError.code != null) {
|
||||
if (challenge.protectionSpace.host ==
|
||||
await controller.getUrl().then((value) => value?.host)) {
|
||||
widget.updatePage((page) => page.copyWith.sslError(sslError));
|
||||
if (context.mounted) {
|
||||
ui_helper.showErrorMessage(
|
||||
context,
|
||||
'We detected an security issue and did not continue to ${widget.page.value.url.authority}: ${sslError.message}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ServerTrustAuthResponse(
|
||||
// ignore: avoid_redundant_argument_values
|
||||
action: ServerTrustAuthResponseAction.CANCEL,
|
||||
);
|
||||
}
|
||||
|
||||
widget.updatePage((page) => page.copyWith.sslError(null));
|
||||
return ServerTrustAuthResponse(
|
||||
action: ServerTrustAuthResponseAction.PROCEED,
|
||||
);
|
||||
},
|
||||
onProgressChanged: (controller, progress) {
|
||||
webViewProgress.value = progress;
|
||||
},
|
||||
onLoadStart: (controller, url) {
|
||||
if (url != null) {
|
||||
widget.updatePage(
|
||||
(page) => page.copyWith(
|
||||
url: url,
|
||||
// ignore: avoid_redundant_argument_values
|
||||
sslError: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLoadStop: (controller, url) {
|
||||
if (url != null) {
|
||||
widget.updatePage((page) => page.copyWith.url(url));
|
||||
}
|
||||
|
||||
_onLoadStopDebounce?.cancel();
|
||||
_onLoadStopDebounce =
|
||||
Timer(const Duration(milliseconds: 150), () async {
|
||||
final favicon = await widget.page.value.controller
|
||||
?.getFavicons()
|
||||
.then((icons) => choseFavicon(icons));
|
||||
widget.updatePage((page) => page.copyWith.favicon(favicon));
|
||||
|
||||
await _updateScreenshot().whenComplete(() {
|
||||
_periodicScreenshotUpdate?.cancel();
|
||||
_periodicScreenshotUpdate =
|
||||
Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||
await _updateScreenshot().onError((error, stackTrace) {
|
||||
logger.e(error, stackTrace: stackTrace);
|
||||
timer.cancel();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
onUpdateVisitedHistory: (controller, url, isReload) async {
|
||||
if (isReload != true) {
|
||||
final history = (
|
||||
canGoBack: await controller.canGoBack(),
|
||||
canGoForward: await controller.canGoForward()
|
||||
);
|
||||
|
||||
widget.updatePage((page) => page.copyWith.pageHistory(history));
|
||||
}
|
||||
},
|
||||
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||
final url = navigationAction.request.url;
|
||||
if (url != null) {
|
||||
final launchExternal = ref
|
||||
.read(settingsRepositoryProvider)
|
||||
.valueOrNull
|
||||
?.launchUrlExternal ??
|
||||
false;
|
||||
|
||||
final unhandledScheme =
|
||||
!_webViewSupportedSchemes.contains(url.scheme);
|
||||
|
||||
if (unhandledScheme ||
|
||||
(launchExternal && url.host != 'kagi.com')) {
|
||||
if (await canLaunchUrl(url)) {
|
||||
var success = false;
|
||||
if (unhandledScheme) {
|
||||
success = await launchUrl(url);
|
||||
} else if (launchExternal) {
|
||||
success = await launchUrl(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
if (context.mounted) {
|
||||
ui_helper.showErrorMessage(
|
||||
context,
|
||||
'Could not launch URL ($url)',
|
||||
);
|
||||
}
|
||||
}
|
||||
// and cancel the request
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NavigationActionPolicy.ALLOW;
|
||||
},
|
||||
onLongPressHitTestResult: (controller, hitTestResult) async {
|
||||
if (switch (hitTestResult.type) {
|
||||
InAppWebViewHitTestResultType.SRC_IMAGE_ANCHOR_TYPE ||
|
||||
InAppWebViewHitTestResultType.SRC_ANCHOR_TYPE ||
|
||||
InAppWebViewHitTestResultType.IMAGE_TYPE =>
|
||||
true,
|
||||
_ => false,
|
||||
}) {
|
||||
final requestFocusNodeHrefResult =
|
||||
await controller.requestFocusNodeHref();
|
||||
|
||||
final url = requestFocusNodeHrefResult?.url ??
|
||||
Uri.tryParse(requestFocusNodeHrefResult?.src ?? '');
|
||||
if (url?.hasScheme == true && url?.hasAuthority == true) {
|
||||
ref.read(overlayDialogProvider.notifier).show(
|
||||
LoadingWebPageDialog(
|
||||
url!,
|
||||
onDismiss:
|
||||
ref.watch(overlayDialogProvider.notifier).dismiss,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTitleChanged: (controller, title) {
|
||||
widget.updatePage((page) => page.copyWith.title(title));
|
||||
},
|
||||
|
||||
// onDownloadStartRequest: (controller, downloadStartRequest) {
|
||||
// final regex = RegExp(
|
||||
// r"filename\*=UTF-8''([\w%\-\.]+)(?:; ?|$)",
|
||||
// caseSensitive: false,
|
||||
// );
|
||||
|
||||
// final math =
|
||||
// regex.firstMatch(downloadStartRequest.contentDisposition!);
|
||||
|
||||
// print(math);
|
||||
// },
|
||||
),
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final value = useValueListenable(webViewProgress);
|
||||
|
||||
return Visibility(
|
||||
visible: value < 100,
|
||||
child: LinearProgressIndicator(
|
||||
value: value / 100,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/domain/repositories/web_view.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/presentation/widgets/favicon.dart';
|
||||
import 'package:kagi_bang_bang/features/web_view/presentation/widgets/web_view.dart';
|
||||
|
||||
class WebViewTab extends HookConsumerWidget {
|
||||
final WebView webView;
|
||||
final bool isActive;
|
||||
|
||||
final VoidCallback onClose;
|
||||
|
||||
const WebViewTab({
|
||||
required this.webView,
|
||||
required this.isActive,
|
||||
required this.onClose,
|
||||
super.key,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final page = useValueListenable(webView.page);
|
||||
|
||||
return Container(
|
||||
key: UniqueKey(),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isActive ? colorScheme.primary : colorScheme.outline,
|
||||
width: isActive ? 2.0 : 1.0,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
),
|
||||
child: Material(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
onTap: () {
|
||||
if (!isActive) {
|
||||
ref.read(webViewTabControllerProvider.notifier).showTab(page.key);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 6.0),
|
||||
child: Text(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
page.title ?? 'New Tab',
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4.0, vertical: -4.0),
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(webViewRepositoryProvider.notifier)
|
||||
.closeTab(page.key);
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 6.0,
|
||||
),
|
||||
FaviconImage(
|
||||
webPageInfo: page,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 6.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
page.url.authority,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
if (page.screenshot != null)
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16.0),
|
||||
bottomRight: Radius.circular(16.0),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Image.memory(
|
||||
fit: BoxFit.fitWidth,
|
||||
page.screenshot!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user