mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 12:23:31 +00:00
First cut of entire timeline elements rendering
This commit is contained in:
parent
bac580935c
commit
1524cc217a
22 changed files with 547 additions and 44 deletions
61
lib/controls/timeline/interactions_bar_control.dart
Normal file
61
lib/controls/timeline/interactions_bar_control.dart
Normal file
|
@ -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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
188
lib/controls/timeline/status_control.dart
Normal file
188
lib/controls/timeline/status_control.dart
Normal file
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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}';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
28
lib/screens/image_viewer_screen.dart
Normal file
28
lib/screens/image_viewer_screen.dart
Normal file
|
@ -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,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
19
lib/serializers/mastodon/connection_mastodon_extensions.dart
Normal file
19
lib/serializers/mastodon/connection_mastodon_extensions.dart
Normal file
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
56
lib/services/connections_manager.dart
Normal file
56
lib/services/connections_manager.dart
Normal file
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_window
|
||||
flutter_secure_storage_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
56
pubspec.lock
56
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_window
|
||||
flutter_secure_storage_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
|
Loading…
Reference in a new issue