diff --git a/lib/view/common/misskey_notes/link_preview.dart b/lib/view/common/misskey_notes/link_preview.dart index 096b55efe..e4360ab46 100644 --- a/lib/view/common/misskey_notes/link_preview.dart +++ b/lib/view/common/misskey_notes/link_preview.dart @@ -1,4 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -6,6 +7,8 @@ import 'package:miria/model/account.dart'; import 'package:miria/model/summaly_result.dart'; import 'package:miria/providers.dart'; import 'package:miria/view/common/misskey_notes/player_embed.dart'; +import 'package:miria/view/common/misskey_notes/twitter_embed.dart'; +import 'package:miria/view/themes/app_theme.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -84,8 +87,26 @@ class LinkPreviewItem extends StatefulWidget { class _LinkPreviewItemState extends State { bool isPlayerOpen = false; + String? extractTweetId(String link) { + final url = Uri.parse(link); + if (!["twitter.com", "mobile.twitter.com", "x.com", "mobile.x.com"] + .contains(url.host)) { + return null; + } + final index = url.pathSegments.indexWhere( + (segment) => ["status", "statuses"].contains(segment), + ); + if (index < 0 || url.pathSegments.length - 1 <= index) { + return null; + } + final tweetId = url.pathSegments[index + 1]; + return int.tryParse(tweetId)?.toString(); + } + @override Widget build(BuildContext context) { + final playerUrl = widget.summalyResult.player.url; + final tweetId = extractTweetId(widget.link); return Column( children: [ if (!isPlayerOpen) @@ -93,16 +114,24 @@ class _LinkPreviewItemState extends State { link: widget.link, summalyResult: widget.summalyResult, ), - if (widget.summalyResult.player.url != null && - WebViewPlatform.instance != null) + if (WebViewPlatform.instance != null && + (playerUrl != null || tweetId != null)) if (isPlayerOpen) ...[ - PlayerEmbed(player: widget.summalyResult.player), + if (playerUrl != null) + PlayerEmbed(player: widget.summalyResult.player), + if (tweetId != null) + TwitterEmbed( + tweetId: tweetId, + isDark: AppTheme.of(context).isDarkMode, + // TODO: l10n + lang: "ja", + ), OutlinedButton.icon( onPressed: () => setState(() { isPlayerOpen = false; }), icon: const Icon(Icons.close), - label: const Text("プレイヤーを閉じる"), + label: Text(playerUrl != null ? "プレイヤーを閉じる" : "ツイートを閉じる"), ), ] else OutlinedButton.icon( @@ -110,7 +139,7 @@ class _LinkPreviewItemState extends State { isPlayerOpen = true; }), icon: const Icon(Icons.play_arrow), - label: const Text("プレイヤーを開く"), + label: Text(playerUrl != null ? "プレイヤーを開く" : "ツイートを開く"), ), ], ); diff --git a/lib/view/common/misskey_notes/twitter_embed.dart b/lib/view/common/misskey_notes/twitter_embed.dart new file mode 100644 index 000000000..a26a37d5a --- /dev/null +++ b/lib/view/common/misskey_notes/twitter_embed.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class TwitterEmbed extends StatefulWidget { + const TwitterEmbed({ + super.key, + required this.tweetId, + this.isDark = false, + + // https://developer.twitter.com/en/docs/twitter-for-websites/supported-languages + this.lang, + }); + + final String tweetId; + final bool isDark; + final String? lang; + + @override + State createState() => _TwitterEmbedState(); +} + +class _TwitterEmbedState extends State { + WebViewController? controller; + double? height; + + @override + void initState() { + super.initState(); + + controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.transparent) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (request) async { + final url = Uri.tryParse(request.url); + if (url != null && await canLaunchUrl(url)) { + launchUrl(url, mode: LaunchMode.externalApplication); + } + return NavigationDecision.prevent; + }, + ), + ) + ..addJavaScriptChannel( + "Twitter", + onMessageReceived: (message) { + setState(() { + // そのままだと下が見切れる + height = double.parse(message.message) + 10; + }); + }, + ) + // https://developer.twitter.com/en/docs/twitter-for-websites/embedded-tweets/guides/embedded-tweet-javascript-factory-function + ..loadHtmlString( + """ + + + + + + + + +
+ + + + +""", + ); + } + + @override + Widget build(BuildContext context) { + final controller = this.controller; + if (controller == null) { + return const SizedBox.shrink(); + } + return SizedBox( + height: height ?? 200, + child: WebViewWidget(controller: controller), + ); + } +}