First cut of entire timeline elements rendering

codemagic-setup
Hank Grabowski 2022-11-18 16:50:15 -05:00
rodzic bac580935c
commit 1524cc217a
22 zmienionych plików z 547 dodań i 44 usunięć

Wyświetl plik

@ -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<InteractionsBarControl> createState() => _InteractionsBarControlState();
}
class _InteractionsBarControlState extends State<InteractionsBarControl> {
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<void> toggleFavorited() async {
final newState = !isFavorited;
print('Trying to toggle favorite from $isFavorited to $newState');
final result = await getIt<EntryManagerService>()
.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)),
],
);
}
}

Wyświetl plik

@ -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<ConnectionsManager>()
.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(),
),
),
],
));
}
}

Wyświetl plik

@ -109,6 +109,25 @@ class FriendicaClient {
});
}
FutureResult<TimelineEntry, ExecError> 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<TimelineEntry>(() {
final json = jsonDecode(responseText);
return Result.ok(TimelineEntryMastodonExtensions.fromJson(json));
}).mapError((error) {
return ExecError(type: ErrorType.parsingError, message: error.toString());
});
}
FutureResult<String, ExecError> getMyProfile() async {
_logger.finest(() => 'Getting logged in user profile');
final request = Uri.parse('https://$serverName/api/friendica/profile/show');

Wyświetl plik

@ -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>(() => ConnectionsManager());
getIt.registerSingleton<EntryManagerService>(entryManagerService);
getIt.registerSingleton<SecretsService>(secretsService);
getIt.registerSingleton<AuthService>(authService);
getIt.registerLazySingleton<TimelineManager>(() => TimelineManager());
getIt.registerSingleton<TimelineManager>(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');

Wyświetl plik

@ -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}';
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -34,6 +34,8 @@ class TimelineEntry {
final LocationData locationData;
final bool isFavorited;
final List<LinkData> links;
final List<Connection> 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<LinkData>? links,
List<Connection>? likes,
List<Connection>? 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,

Wyświetl plik

@ -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<HomeScreen> {
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,

Wyświetl plik

@ -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,
)),
],
),
);
}
}

Wyświetl plik

@ -0,0 +1,19 @@
import '../../models/connection.dart';
extension ConnectionMastodonExtensions on Connection {
static Connection fromJson(Map<String, dynamic> 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,
);
}
}

Wyświetl plik

@ -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>[]
: [LinkData.fromMastodonJson(json['card'])];
@ -47,6 +51,9 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
rebloggedCount: rebloggedCount,
repliesCount: repliesCount,
);
final connection = ConnectionMastodonExtensions.fromJson(json['account']);
getIt<ConnectionsManager>().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,

Wyświetl plik

@ -0,0 +1,56 @@
import 'package:result_monad/result_monad.dart';
import '../models/connection.dart';
class ConnectionsManager {
final _connectionsById = <String, Connection>{};
final _connectionsByName = <String, Connection>{};
final _connectionsByProfileUrl = <Uri, Connection>{};
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<Connection> newConnections) {
bool result = true;
for (final connection in newConnections) {
result &= addConnection(connection);
}
return result;
}
Result<Connection, String> getById(String id) {
final result = _connectionsById[id];
return result != null ? Result.ok(result) : Result.error('$id not found');
}
Result<Connection, String> getByName(String name) {
final result = _connectionsByName[name];
return result != null ? Result.ok(result) : Result.error('$name not found');
}
Result<Connection, String> getByProfileUrl(Uri url) {
final result = _connectionsByProfileUrl[url];
return result != null ? Result.ok(result) : Result.error('$url not found');
}
}

Wyświetl plik

@ -104,4 +104,52 @@ class EntryManagerService extends ChangeNotifier {
'Completed processing new items ${client == null ? 'sub level' : 'top level'}');
return updatedPosts;
}
FutureResult<EntryTreeItem, ExecError> toggleFavorited(
String id, bool newStatus,
{bool notify = false}) async {
final auth = getIt<AuthService>();
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);
}
}
}

Wyświetl plik

@ -30,6 +30,7 @@ class TimelineManager extends ChangeNotifier {
(await getIt<EntryManagerService>().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) {

Wyświetl plik

@ -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';
}
}

Wyświetl plik

@ -8,6 +8,7 @@
#include <desktop_window/desktop_window_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
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);
}

Wyświetl plik

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_window
flutter_secure_storage_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

Wyświetl plik

@ -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"))
}

Wyświetl plik

@ -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:

Wyświetl plik

@ -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:

Wyświetl plik

@ -8,10 +8,13 @@
#include <desktop_window/desktop_window_plugin.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DesktopWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopWindowPlugin"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

Wyświetl plik

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_window
flutter_secure_storage_windows
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST