From 1524cc217a3a4f4bdd3f07e1271f61c97a2ab53a Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Fri, 18 Nov 2022 16:50:15 -0500 Subject: [PATCH] First cut of entire timeline elements rendering --- .../timeline/interactions_bar_control.dart | 61 ++++++ lib/controls/timeline/status_control.dart | 188 ++++++++++++++++++ lib/friendica_client.dart | 19 ++ lib/main.dart | 8 +- lib/models/connection.dart | 12 +- lib/models/entry_tree_item.dart | 6 + lib/models/timeline_entry.dart | 6 + lib/screens/home.dart | 41 +--- lib/screens/image_viewer_screen.dart | 28 +++ .../connection_mastodon_extensions.dart | 19 ++ .../timeline_entry_mastodon_extensions.dart | 10 +- lib/services/connections_manager.dart | 56 ++++++ lib/services/entry_manager_service.dart | 48 +++++ lib/services/timeline_manager.dart | 1 + lib/utils/dateutils.dart | 20 ++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 56 ++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 22 files changed, 547 insertions(+), 44 deletions(-) create mode 100644 lib/controls/timeline/interactions_bar_control.dart create mode 100644 lib/controls/timeline/status_control.dart create mode 100644 lib/screens/image_viewer_screen.dart create mode 100644 lib/serializers/mastodon/connection_mastodon_extensions.dart create mode 100644 lib/services/connections_manager.dart diff --git a/lib/controls/timeline/interactions_bar_control.dart b/lib/controls/timeline/interactions_bar_control.dart new file mode 100644 index 0000000..2f20760 --- /dev/null +++ b/lib/controls/timeline/interactions_bar_control.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import '../../globals.dart'; +import '../../models/timeline_entry.dart'; +import '../../services/entry_manager_service.dart'; +import '../../utils/snackbar_builder.dart'; + +class InteractionsBarControl extends StatefulWidget { + final TimelineEntry entry; + const InteractionsBarControl({super.key, required this.entry}); + + @override + State createState() => _InteractionsBarControlState(); +} + +class _InteractionsBarControlState extends State { + bool isFavorited = false; + int reshares = 0; + int comments = 0; + int likes = 0; + + @override + void initState() { + super.initState(); + isFavorited = widget.entry.isFavorited; + comments = widget.entry.engagementSummary.repliesCount; + reshares = widget.entry.engagementSummary.rebloggedCount; + likes = widget.entry.engagementSummary.favoritesCount; + } + + Future toggleFavorited() async { + final newState = !isFavorited; + print('Trying to toggle favorite from $isFavorited to $newState'); + final result = await getIt() + .toggleFavorited(widget.entry.id, newState); + result.match(onSuccess: (update) { + setState(() { + print('Success toggling! $isFavorited -> ${update.entry.isFavorited}'); + isFavorited = update.entry.isFavorited; + }); + }, onError: (error) { + buildSnackbar(context, 'Error toggling like status: $error'); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$likes likes, $reshares reshares, $comments comments'), + IconButton( + onPressed: toggleFavorited, + icon: isFavorited + ? Icon(Icons.thumb_up) + : Icon(Icons.thumb_up_outlined)), + ], + ); + } +} diff --git a/lib/controls/timeline/status_control.dart b/lib/controls/timeline/status_control.dart new file mode 100644 index 0000000..1432e21 --- /dev/null +++ b/lib/controls/timeline/status_control.dart @@ -0,0 +1,188 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../globals.dart'; +import '../../models/attachment_media_type_enum.dart'; +import '../../models/connection.dart'; +import '../../models/entry_tree_item.dart'; +import '../../models/timeline_entry.dart'; +import '../../screens/image_viewer_screen.dart'; +import '../../services/connections_manager.dart'; +import '../../utils/dateutils.dart'; +import '../../utils/snackbar_builder.dart'; +import '../padding.dart'; +import 'interactions_bar_control.dart'; + +class StatusControl extends StatelessWidget { + final EntryTreeItem item; + + TimelineEntry get entry => item.entry; + + const StatusControl({super.key, required this.item}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildHeader(context), + const VerticalPadding( + height: 5, + ), + buildBody(context), + const VerticalPadding( + height: 5, + ), + buildMediaBar(context), + const VerticalPadding( + height: 5, + ), + InteractionsBarControl(entry: entry), + const VerticalPadding( + height: 5, + ), + buildChildComments(context), + ], + ), + ); + } + + Widget buildHeader(BuildContext context) { + final author = getIt() + .getById(entry.authorId) + .getValueOrElse(() => Connection()); + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: author.avatarUrl.toString(), + width: 32.0, + ), + const HorizontalPadding(), + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + author.name, + style: Theme.of(context).textTheme.bodyText1, + ), + Text( + ElapsedDateUtils.epochSecondsToString(entry.backdatedTimestamp), + style: Theme.of(context).textTheme.caption, + ), + ], + ), + ], + ); + } + + Widget buildBody(BuildContext context) { + return HtmlWidget( + entry.body, + onTapUrl: (url) async { + final uri = Uri.tryParse(url); + if (uri == null) { + buildSnackbar(context, 'Bad link: $url'); + return false; + } + if (await canLaunchUrl(uri)) { + buildSnackbar( + context, + 'Attempting to launch video: $url', + ); + await launchUrl(uri); + } else { + buildSnackbar(context, 'Unable to launch video: $url'); + return false; + } + return true; + }, + onTapImage: (imageMetadata) { + print(imageMetadata); + }, + ); + } + + Widget buildMediaBar(BuildContext context) { + final items = entry.mediaAttachments; + if (items.isEmpty) { + return const SizedBox(); + } + return SizedBox( + width: 250.0, + height: 250.0, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final item = items[index]; + + if (item.explicitType == AttachmentMediaType.video) { + return ElevatedButton( + onPressed: () async { + if (await canLaunchUrl(item.uri)) { + buildSnackbar( + context, + 'Attempting to launch video: ${item.uri}', + ); + await launchUrl(item.uri); + } else { + buildSnackbar( + context, 'Unable to launch video: ${item.uri}'); + } + }, + child: Text(item.description.isNotEmpty + ? item.description + : 'Video')); + } + if (item.explicitType != AttachmentMediaType.image) { + return Text('${item.explicitType}: ${item.uri}'); + } + + return InkWell( + onTap: () async { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return ImageViewerScreen(attachment: item); + })); + }, + child: CachedNetworkImage( + width: 250.0, + height: 250.0, + imageUrl: item.thumbnailUri.toString(), + ), + ); + // return Text(item.toString()); + }, + separatorBuilder: (context, index) { + return HorizontalPadding(); + }, + itemCount: items.length)); + } + + Widget buildChildComments(BuildContext context) { + final comments = item.children; + if (comments.isEmpty) { + return Text('No comments'); + } + return Padding( + padding: EdgeInsets.only(left: 20.0, top: 5.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.subdirectory_arrow_right), + Expanded( + child: Column( + children: comments.map((c) => StatusControl(item: c)).toList(), + ), + ), + ], + )); + } +} diff --git a/lib/friendica_client.dart b/lib/friendica_client.dart index b04311b..e3b0cfe 100644 --- a/lib/friendica_client.dart +++ b/lib/friendica_client.dart @@ -109,6 +109,25 @@ class FriendicaClient { }); } + FutureResult changeFavoriteStatus( + String id, bool status) async { + final action = status ? 'favourite' : 'unfavourite'; + final url = Uri.parse('https://$serverName/api/v1/statuses/$id/$action'); + final result = await _postUrl(url, {}); + if (result.isFailure) { + return result.errorCast(); + } + + final responseText = result.value; + + return runCatching(() { + final json = jsonDecode(responseText); + return Result.ok(TimelineEntryMastodonExtensions.fromJson(json)); + }).mapError((error) { + return ExecError(type: ErrorType.parsingError, message: error.toString()); + }); + } + FutureResult getMyProfile() async { _logger.finest(() => 'Getting logged in user profile'); final request = Uri.parse('https://$serverName/api/friendica/profile/show'); diff --git a/lib/main.dart b/lib/main.dart index 42d1142..7a4ecd6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'models/TimelineIdentifiers.dart'; import 'routes.dart'; import 'screens/sign_in.dart'; import 'services/auth_service.dart'; +import 'services/connections_manager.dart'; import 'services/entry_manager_service.dart'; import 'services/secrets_service.dart'; import 'services/timeline_manager.dart'; @@ -25,10 +26,12 @@ void main() async { final authService = AuthService(); final secretsService = SecretsService(); final entryManagerService = EntryManagerService(); + final timelineManager = TimelineManager(); + getIt.registerLazySingleton(() => ConnectionsManager()); getIt.registerSingleton(entryManagerService); getIt.registerSingleton(secretsService); getIt.registerSingleton(authService); - getIt.registerLazySingleton(() => TimelineManager()); + getIt.registerSingleton(timelineManager); await secretsService.initialize().andThenSuccessAsync((credentials) async { if (credentials.isEmpty) { return; @@ -39,7 +42,8 @@ void main() async { final result = await authService.signIn(credentials); print('Startup login result: $result'); if (result.isSuccess) { - await entryManagerService.updateTimeline(TimelineIdentifiers.home()); + print('Getting timeline for ${result.value.credentials.handle}'); + timelineManager.getTimeline(TimelineIdentifiers.home()); } } else { print('Was not logged in'); diff --git a/lib/models/connection.dart b/lib/models/connection.dart index 8ede8fc..be60ce2 100644 --- a/lib/models/connection.dart +++ b/lib/models/connection.dart @@ -9,17 +9,21 @@ class Connection { final String network; + final Uri avatarUrl; + Connection( {this.status = ConnectionStatus.none, this.name = '', this.id = '', - profileUrl, - this.network = ''}) - : profileUrl = profileUrl ?? Uri(); + Uri? profileUrl, + this.network = '', + Uri? avatarUrl}) + : profileUrl = profileUrl ?? Uri(), + avatarUrl = avatarUrl ?? Uri(); @override String toString() { - return 'Connection{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network}'; + return 'Connection{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network, avatar: $avatarUrl}'; } } diff --git a/lib/models/entry_tree_item.dart b/lib/models/entry_tree_item.dart index cc17724..361ec78 100644 --- a/lib/models/entry_tree_item.dart +++ b/lib/models/entry_tree_item.dart @@ -9,6 +9,12 @@ class EntryTreeItem { EntryTreeItem(this.entry, {this.isMine = true, this.isOrphaned = false}); + EntryTreeItem copy({required TimelineEntry entry}) => EntryTreeItem( + entry, + isMine: isMine, + isOrphaned: isOrphaned, + ); + String get id => entry.id; void addChild(EntryTreeItem child) { diff --git a/lib/models/timeline_entry.dart b/lib/models/timeline_entry.dart index 6abd966..b32e157 100644 --- a/lib/models/timeline_entry.dart +++ b/lib/models/timeline_entry.dart @@ -34,6 +34,8 @@ class TimelineEntry { final LocationData locationData; + final bool isFavorited; + final List links; final List likes; @@ -59,6 +61,7 @@ class TimelineEntry { this.parentAuthorId = '', this.externalLink = '', this.locationData = const LocationData(), + this.isFavorited = false, this.links = const [], this.likes = const [], this.dislikes = const [], @@ -81,6 +84,7 @@ class TimelineEntry { parentAuthor = 'Random parent author ${randomId()}', parentAuthorId = 'Random parent author id ${randomId()}', locationData = LocationData.randomBuilt(), + isFavorited = DateTime.now().second ~/ 2 == 0 ? true : false, links = [], likes = [], dislikes = [], @@ -102,6 +106,7 @@ class TimelineEntry { String? parentAuthor, String? parentAuthorId, LocationData? locationData, + bool? isFavorited, List? links, List? likes, List? dislikes, @@ -123,6 +128,7 @@ class TimelineEntry { parentAuthor: parentAuthor ?? this.parentAuthor, parentAuthorId: parentAuthorId ?? this.parentAuthorId, locationData: locationData ?? this.locationData, + isFavorited: isFavorited ?? this.isFavorited, links: links ?? this.links, likes: likes ?? this.likes, dislikes: dislikes ?? this.dislikes, diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 58a3e19..7ad7df4 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import '../controls/timeline/status_control.dart'; import '../models/TimelineIdentifiers.dart'; import '../services/timeline_manager.dart'; @@ -67,48 +67,15 @@ class _HomeScreenState extends State { return Center(child: Text('Error getting timeline: ${result.error}')); } final items = result.value; + print('items count = ${items.length}'); return RefreshIndicator( onRefresh: () async { await manager.refreshTimeline(TimelineIdentifiers.home()); }, child: ListView.separated( itemBuilder: (context, index) { - final item = items[index]; - final entry = item.entry; - return ListTile( - subtitle: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - HtmlWidget( - item.entry.body, - onTapUrl: (url) async { - print(url); - return true; - }, - onTapImage: (imageMetadata) { - print(imageMetadata); - }, - ), - if (entry.links.isNotEmpty) - Text('Preview: ${entry.links.first.url}'), - if (entry.mediaAttachments.isNotEmpty) - ...entry.mediaAttachments - .map((a) => Text('Media: ${a.uri}')), - Text( - 'Engagement -- Likes: ${entry.likes.length}, Dislikes: ${entry.dislikes.length}, Comments:${item.totalChildren} ') - ], - ), - ), - //trailing: Text(item.parentId), - title: Text( - '${entry.id} for ${item.isMine ? 'Me' : entry.author} for post ${entry.parentId}'), - trailing: Text(DateTime.fromMillisecondsSinceEpoch( - entry.creationTimestamp * 1000) - .toIso8601String()), - ); + print('Building item: $index'); + return StatusControl(item: items[index]); }, separatorBuilder: (context, index) => Divider(), itemCount: items.length, diff --git a/lib/screens/image_viewer_screen.dart b/lib/screens/image_viewer_screen.dart new file mode 100644 index 0000000..f1f330c --- /dev/null +++ b/lib/screens/image_viewer_screen.dart @@ -0,0 +1,28 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +import '../models/media_attachment.dart'; + +class ImageViewerScreen extends StatelessWidget { + final MediaAttachment attachment; + + const ImageViewerScreen({super.key, required this.attachment}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Stack( + children: [ + Container( + color: Theme.of(context).backgroundColor, + height: MediaQuery.of(context).size.height, + child: InteractiveViewer( + child: CachedNetworkImage(imageUrl: attachment.uri.toString()), + maxScale: 10.0, + )), + ], + ), + ); + } +} diff --git a/lib/serializers/mastodon/connection_mastodon_extensions.dart b/lib/serializers/mastodon/connection_mastodon_extensions.dart new file mode 100644 index 0000000..cee6fb8 --- /dev/null +++ b/lib/serializers/mastodon/connection_mastodon_extensions.dart @@ -0,0 +1,19 @@ +import '../../models/connection.dart'; + +extension ConnectionMastodonExtensions on Connection { + static Connection fromJson(Map json) { + final name = json['display_name'] ?? ''; + final id = json['id'] ?? ''; + final profileUrl = Uri.parse(json['url'] ?? ''); + const network = 'Mastodon'; + final avatar = Uri.tryParse(json['avatar_static'] ?? '') ?? Uri(); + + return Connection( + name: name, + id: id, + profileUrl: profileUrl, + network: network, + avatarUrl: avatar, + ); + } +} diff --git a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart index 6e66781..2566cdd 100644 --- a/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart +++ b/lib/serializers/mastodon/timeline_entry_mastodon_extensions.dart @@ -1,11 +1,14 @@ import 'package:logging/logging.dart'; +import '../../globals.dart'; import '../../models/engagement_summary.dart'; import '../../models/link_data.dart'; import '../../models/location_data.dart'; import '../../models/media_attachment.dart'; import '../../models/timeline_entry.dart'; +import '../../services/connections_manager.dart'; import '../../utils/dateutils.dart'; +import 'connection_mastodon_extensions.dart'; final _logger = Logger('TimelineEntryMastodonExtensions'); @@ -26,13 +29,14 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { final parentAuthor = json['in_reply_to_account_id'] ?? ''; final parentAuthorId = json['in_reply_to_account_id'] ?? ''; final body = json['content'] ?? ''; - final author = json['account']['acct']; + final author = json['account']['display_name']; final authorId = json['account']['id']; const title = ''; final externalLink = json['uri'] ?? ''; final actualLocationData = LocationData(); final modificationTimestamp = timestamp; final backdatedTimestamp = timestamp; + final isFavorited = json['favourited'] ?? false; final linkData = json['card'] == null ? [] : [LinkData.fromMastodonJson(json['card'])]; @@ -47,6 +51,9 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { rebloggedCount: rebloggedCount, repliesCount: repliesCount, ); + + final connection = ConnectionMastodonExtensions.fromJson(json['account']); + getIt().addConnection(connection); return TimelineEntry( creationTimestamp: timestamp, modificationTimestamp: modificationTimestamp, @@ -57,6 +64,7 @@ extension TimelineEntryMastodonExtensions on TimelineEntry { id: id, parentId: parentId, parentAuthorId: parentAuthorId, + isFavorited: isFavorited, externalLink: externalLink, author: author, authorId: authorId, diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart new file mode 100644 index 0000000..f987932 --- /dev/null +++ b/lib/services/connections_manager.dart @@ -0,0 +1,56 @@ +import 'package:result_monad/result_monad.dart'; + +import '../models/connection.dart'; + +class ConnectionsManager { + final _connectionsById = {}; + final _connectionsByName = {}; + final _connectionsByProfileUrl = {}; + + int get length => _connectionsById.length; + + void clearCaches() { + _connectionsById.clear(); + _connectionsByName.clear(); + _connectionsByProfileUrl.clear(); + } + + bool addConnection(Connection connection) { + if (_connectionsById.containsKey(connection.id)) { + return false; + } + _connectionsById[connection.id] = connection; + _connectionsByName[connection.name] = connection; + _connectionsByProfileUrl[connection.profileUrl] = connection; + + return true; + } + + bool addAllConnections(Iterable newConnections) { + bool result = true; + + for (final connection in newConnections) { + result &= addConnection(connection); + } + + return result; + } + + Result getById(String id) { + final result = _connectionsById[id]; + + return result != null ? Result.ok(result) : Result.error('$id not found'); + } + + Result getByName(String name) { + final result = _connectionsByName[name]; + + return result != null ? Result.ok(result) : Result.error('$name not found'); + } + + Result getByProfileUrl(Uri url) { + final result = _connectionsByProfileUrl[url]; + + return result != null ? Result.ok(result) : Result.error('$url not found'); + } +} diff --git a/lib/services/entry_manager_service.dart b/lib/services/entry_manager_service.dart index ff430b7..ea8cc2c 100644 --- a/lib/services/entry_manager_service.dart +++ b/lib/services/entry_manager_service.dart @@ -104,4 +104,52 @@ class EntryManagerService extends ChangeNotifier { 'Completed processing new items ${client == null ? 'sub level' : 'top level'}'); return updatedPosts; } + + FutureResult toggleFavorited( + String id, bool newStatus, + {bool notify = false}) async { + final auth = getIt(); + final clientResult = auth.currentClient; + if (clientResult.isFailure) { + _logger.severe('Error getting Friendica client: ${clientResult.error}'); + return clientResult.errorCast(); + } + final client = clientResult.value; + final result = await client.changeFavoriteStatus(id, newStatus); + if (result.isFailure) { + return result.errorCast(); + } + + final update = result.value; + late EntryTreeItem rval; + if (_posts.containsKey(update.id)) { + rval = _posts[update.id]!.copy(entry: update); + _posts[update.id] = rval; + _updateChildrenEntities(rval, update); + } + + if (_allComments.containsKey(update.id)) { + rval = _allComments[update.id]!.copy(entry: update); + _allComments[update.id] = rval; + _updateChildrenEntities(rval, update); + } + + if (notify) { + notifyListeners(); + } + return Result.ok(rval); + } + + void _updateChildrenEntities(EntryTreeItem item, TimelineEntry entry) { + final updates = item.children.where((element) => element.id == entry.id); + for (final u in updates) { + final newItem = u.copy(entry: entry); + item.children.remove(u); + item.children.add(newItem); + } + + for (final c in item.children) { + _updateChildrenEntities(c, entry); + } + } } diff --git a/lib/services/timeline_manager.dart b/lib/services/timeline_manager.dart index f1fc2be..6ab07a5 100644 --- a/lib/services/timeline_manager.dart +++ b/lib/services/timeline_manager.dart @@ -30,6 +30,7 @@ class TimelineManager extends ChangeNotifier { (await getIt().updateTimeline(type)).match( onSuccess: (posts) { final timeline = cachedTimelines.putIfAbsent(type, () => Timeline(type)); + _logger.finest('Posts returned for adding to $type: ${posts.length}'); timeline.addPosts(posts); notifyListeners(); }, onError: (error) { diff --git a/lib/utils/dateutils.dart b/lib/utils/dateutils.dart index 11e62e7..b2ee82e 100644 --- a/lib/utils/dateutils.dart +++ b/lib/utils/dateutils.dart @@ -38,3 +38,23 @@ class OffsetDateTimeUtils { 1000); } } + +class ElapsedDateUtils { + static String epochSecondsToString(int epochSeconds) { + final epoch = DateTime.fromMillisecondsSinceEpoch(epochSeconds * 1000); + final elapsed = DateTime.now().difference(epoch); + if (elapsed.inDays > 0) { + return '${elapsed.inDays} days ago'; + } + + if (elapsed.inHours > 0) { + return '${elapsed.inHours} hours ago'; + } + + if (elapsed.inMinutes > 0) { + return '${elapsed.inMinutes} minutes ago'; + } + + return 'seconds ago'; + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 3a812a5..241dc8b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) desktop_window_registrar = @@ -16,4 +17,7 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index c0731f5..fd2ab75 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_window flutter_secure_storage_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6d58128..57c8a20 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import flutter_secure_storage_macos import path_provider_macos import shared_preferences_macos import sqflite +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin")) @@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 51151d7..c515b39 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -588,6 +588,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.21" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" uuid: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e706fd6..8955175 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: shared_preferences: ^2.0.15 uuid: ^3.0.6 time_machine: ^0.9.17 + url_launcher: ^6.1.6 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e823d16..f50c00b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,13 @@ #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { DesktopWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopWindowPlugin")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e2adeed..ddb2474 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_window flutter_secure_storage_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST