From 1deaebd94e843b5405db04461337e1e0312ab7bd Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 8 Mar 2022 16:22:17 -0500 Subject: [PATCH] D* (minus photos) and Friendica loading in UI. --- friendica_archive_browser/lib/src/app.dart | 16 +- .../components/top_interactactors_widget.dart | 4 +- .../diaspora_contact_serializer.dart | 6 +- .../diaspora_posts_serializer.dart | 97 +++++++ .../services/diaspora_archive_service.dart | 93 +++---- .../diaspora_path_mapping_service.dart | 54 ++++ .../diaspora_profile_json_reader.dart | 55 +++- .../components/geo/geo_extensions.dart | 4 +- .../friendica/components/geo/marker_data.dart | 4 +- .../components/media_timeline_component.dart | 11 +- .../components/media_wrapper_component.dart | 31 ++- .../friendica/components/tree_entry_card.dart | 18 +- .../models/friendica_entry_tree_item.dart | 19 -- .../models/friendica_timeline_entry.dart | 246 ------------------ .../src/friendica/screens/entries_screen.dart | 13 +- .../friendica/screens/geospatial_screen.dart | 30 +-- .../screens/media_slideshow_screen.dart | 9 +- .../src/friendica/screens/stats_screen.dart | 10 +- .../friendica_contact_serializer.dart | 18 ++ ...friendica_media_attachment_serializer.dart | 25 ++ .../friendica_timeline_entry_serializer.dart | 72 +++++ .../services/friendica_archive_service.dart | 45 ++-- .../services/friendica_connections.dart | 34 --- .../friendica_path_mapping_service.dart | 4 +- friendica_archive_browser/lib/src/home.dart | 4 +- .../lib/src/models/archive_types_enum.dart | 5 + .../lib/src/models/connection.dart | 46 ++++ .../lib/src/models/entry_tree_item.dart | 18 ++ .../lib/src/models/friendica_contact.dart | 62 ----- .../{friendica => }/models/location_data.dart | 0 .../media_attachment.dart} | 48 ++-- .../{friendica => }/models/model_utils.dart | 0 .../lib/src/models/time_element.dart | 4 +- .../lib/src/models/timeline_entry.dart | 160 ++++++++++++ .../services/archive_service_interface.dart | 31 +++ .../services/archive_service_provider.dart | 70 +++++ .../lib/src/services/connections_manager.dart | 46 ++++ .../path_mapper_service_interface.dart | 12 + .../lib/src/settings/settings_controller.dart | 17 +- .../lib/src/settings/settings_service.dart | 18 ++ .../lib/src/settings/settings_view.dart | 15 ++ .../lib/src/utils/offsetdatetime_utils.dart | 20 +- .../src/utils/top_interactors_generator.dart | 17 +- friendica_archive_browser/pubspec.lock | 14 + friendica_archive_browser/pubspec.yaml | 1 + .../test/additional_key_logger_test.dart | 2 +- .../disapora_profile_json_reader_test.dart | 23 +- 47 files changed, 970 insertions(+), 581 deletions(-) create mode 100644 friendica_archive_browser/lib/src/diaspora/serializers/diaspora_posts_serializer.dart create mode 100644 friendica_archive_browser/lib/src/diaspora/services/diaspora_path_mapping_service.dart delete mode 100644 friendica_archive_browser/lib/src/friendica/models/friendica_entry_tree_item.dart delete mode 100644 friendica_archive_browser/lib/src/friendica/models/friendica_timeline_entry.dart create mode 100644 friendica_archive_browser/lib/src/friendica/serializers/friendica_contact_serializer.dart create mode 100644 friendica_archive_browser/lib/src/friendica/serializers/friendica_media_attachment_serializer.dart create mode 100644 friendica_archive_browser/lib/src/friendica/serializers/friendica_timeline_entry_serializer.dart delete mode 100644 friendica_archive_browser/lib/src/friendica/services/friendica_connections.dart create mode 100644 friendica_archive_browser/lib/src/models/archive_types_enum.dart create mode 100644 friendica_archive_browser/lib/src/models/connection.dart create mode 100644 friendica_archive_browser/lib/src/models/entry_tree_item.dart delete mode 100644 friendica_archive_browser/lib/src/models/friendica_contact.dart rename friendica_archive_browser/lib/src/{friendica => }/models/location_data.dart (100%) rename friendica_archive_browser/lib/src/{friendica/models/friendica_media_attachment.dart => models/media_attachment.dart} (61%) rename friendica_archive_browser/lib/src/{friendica => }/models/model_utils.dart (100%) create mode 100644 friendica_archive_browser/lib/src/models/timeline_entry.dart create mode 100644 friendica_archive_browser/lib/src/services/archive_service_interface.dart create mode 100644 friendica_archive_browser/lib/src/services/archive_service_provider.dart create mode 100644 friendica_archive_browser/lib/src/services/connections_manager.dart create mode 100644 friendica_archive_browser/lib/src/services/path_mapper_service_interface.dart diff --git a/friendica_archive_browser/lib/src/app.dart b/friendica_archive_browser/lib/src/app.dart index e195243..2a7b61f 100644 --- a/friendica_archive_browser/lib/src/app.dart +++ b/friendica_archive_browser/lib/src/app.dart @@ -2,12 +2,11 @@ import 'package:desktop_window/desktop_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_provider.dart'; import 'package:friendica_archive_browser/src/themes.dart'; import 'package:friendica_archive_browser/src/utils/scrolling_behavior.dart'; import 'package:provider/provider.dart'; -import 'friendica/services/friendica_path_mapping_service.dart'; import 'home.dart'; import 'settings/settings_controller.dart'; @@ -25,12 +24,9 @@ class FriendicaArchiveBrowser extends StatelessWidget { @override Widget build(BuildContext context) { DesktopWindow.setMinWindowSize(minAppSize); - final pathMappingService = FriendicaPathMappingService(settingsController); - final friendicaArchiveService = - FriendicaArchiveService(pathMappingService: pathMappingService); + final archiveService = ArchiveServiceProvider(settingsController); settingsController.addListener(() { - friendicaArchiveService.clearCaches(); - pathMappingService.refresh(); + archiveService.clearCaches(); }); return AnimatedBuilder( animation: settingsController, @@ -55,13 +51,11 @@ class FriendicaArchiveBrowser extends StatelessWidget { home: MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => settingsController), - ChangeNotifierProvider( - create: (context) => friendicaArchiveService), - Provider(create: (context) => pathMappingService), + ChangeNotifierProvider(create: (context) => archiveService), ], child: Home( settingsController: settingsController, - archiveService: friendicaArchiveService), + archiveService: archiveService), ), ); }, diff --git a/friendica_archive_browser/lib/src/components/top_interactactors_widget.dart b/friendica_archive_browser/lib/src/components/top_interactactors_widget.dart index faf9414..b35795d 100644 --- a/friendica_archive_browser/lib/src/components/top_interactactors_widget.dart +++ b/friendica_archive_browser/lib/src/components/top_interactactors_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:friendica_archive_browser/src/models/time_element.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_connections.dart'; +import 'package:friendica_archive_browser/src/services/connections_manager.dart'; import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart'; import 'package:friendica_archive_browser/src/utils/top_interactors_generator.dart'; import 'package:logging/logging.dart'; @@ -8,7 +8,7 @@ import 'package:url_launcher/url_launcher.dart'; class TopInteractorsWidget extends StatefulWidget { final List entries; - final FriendicaConnections connections; + final ConnectionsManager connections; const TopInteractorsWidget(this.entries, this.connections, {Key? key}) : super(key: key); diff --git a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart index d60d00b..6f54bd2 100644 --- a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart +++ b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_contact_serializer.dart @@ -1,6 +1,6 @@ -import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; +import 'package:friendica_archive_browser/src/models/connection.dart'; -Contact friendicaContactFromDiasporaJson(Map json) { +Connection contactFromDiasporaJson(Map json) { const network = "Diaspora"; final accountId = json['account_id'] ?? ''; final profileUrl = _profileUrlFromAccountId(accountId); @@ -17,7 +17,7 @@ Contact friendicaContactFromDiasporaJson(Map json) { status = ConnectionStatus.theyFollowYou; } - return Contact( + return Connection( status: status, name: name, id: id, diff --git a/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_posts_serializer.dart b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_posts_serializer.dart new file mode 100644 index 0000000..c398397 --- /dev/null +++ b/friendica_archive_browser/lib/src/diaspora/serializers/diaspora_posts_serializer.dart @@ -0,0 +1,97 @@ +import 'package:friendica_archive_browser/src/models/connection.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; +import 'package:friendica_archive_browser/src/utils/exec_error.dart'; +import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart'; +import 'package:logging/logging.dart'; +import 'package:markdown/markdown.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../../services/connections_manager.dart'; + +final _logger = Logger('DiasporaPostsSerializer'); +const _statusMessageType = 'status_message'; +const _reshareType = 'reshare'; +const _knownPostTypes = [_statusMessageType, _reshareType]; + +Result timelineItemFromDiasporaPostJson( + Map json, ConnectionsManager connections) { + if (!json.containsKey('entity_data')) { + return Result.error(ExecError.message('Timeline item has no entity data')); + } + final entityData = json['entity_data'] ?? {}; + final postId = entityData['guid'] ?? ''; + final entityType = json['entity_type'] ?? ''; + if (!_knownPostTypes.contains(entityType)) { + final error = 'Unknown entity type $entityType for Post ID: $postId'; + _logger.severe(error); + return Result.error(ExecError.message(error)); + } + + if (entityType == _statusMessageType) { + return _buildStatusMessageType(entityData, connections); + } else { + return _buildReshareMessageType(entityData, connections); + } +} + +Result _buildReshareMessageType( + entityData, ConnectionsManager connections) { + final createdAtString = entityData['created_at'] ?? ''; + final epochTime = + OffsetDateTimeUtils.epochSecTimeFromTimeZoneString(createdAtString) + .getValueOrElse(() => -1); + final postId = entityData['guid'] ?? ''; + final authorName = entityData['author'] ?? ''; + final parentGuid = entityData['root_guid'] ?? ''; + final parentName = entityData['root_author'] ?? ''; + final externalLink = _buildReshareUrl(authorName, parentName, parentGuid); + final author = + connections.getByName(authorName).getValueOrElse(() => Connection()); + final parentAuthor = connections + .getByName(parentName) + .getValueOrElse(() => Connection(name: parentName)); + final timelineEntry = TimelineEntry( + id: postId, + creationTimestamp: epochTime, + author: author.name, + authorId: author.id, + isReshare: true, + parentAuthor: parentAuthor.name, + parentAuthorId: parentAuthor.id, + externalLink: externalLink, + ); + return Result.ok(timelineEntry); +} + +Result _buildStatusMessageType( + entityData, ConnectionsManager connections) { + final createdAtString = entityData['created_at'] ?? ''; + final epochTime = + OffsetDateTimeUtils.epochSecTimeFromTimeZoneString(createdAtString) + .getValueOrElse(() => -1); + final postId = entityData['guid'] ?? ''; + final postMarkdown = entityData['text'] ?? ''; + final postHtml = markdownToHtml(postMarkdown); + final authorName = entityData['author'] ?? ''; + final author = + connections.getByName(authorName).getValueOrElse(() => Connection()); + final timelineEntry = TimelineEntry( + id: postId, + creationTimestamp: epochTime, + body: postHtml, + author: author.name, + authorId: author.id); + return Result.ok(timelineEntry); +} + +String _buildReshareUrl(String author, String rootAuthor, String rootGuid) { + final accountId = rootAuthor.isNotEmpty ? rootAuthor : author; + final accountIdPieces = accountId.split('@'); + if (accountIdPieces.length != 2) { + return rootGuid; + } + + final server = accountIdPieces[1]; + + return 'https://$server/p/$rootGuid'; +} diff --git a/friendica_archive_browser/lib/src/diaspora/services/diaspora_archive_service.dart b/friendica_archive_browser/lib/src/diaspora/services/diaspora_archive_service.dart index c0c13e1..11e44d9 100644 --- a/friendica_archive_browser/lib/src/diaspora/services/diaspora_archive_service.dart +++ b/friendica_archive_browser/lib/src/diaspora/services/diaspora_archive_service.dart @@ -1,23 +1,28 @@ import 'dart:convert'; import 'dart:io'; +import 'package:friendica_archive_browser/src/diaspora/services/diaspora_path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/diaspora/services/diaspora_profile_json_reader.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_interface.dart'; import 'package:path/path.dart' as p; import 'package:result_monad/result_monad.dart'; -import '../../friendica/models/friendica_entry_tree_item.dart'; -import '../../friendica/models/friendica_timeline_entry.dart'; -import '../../friendica/services/friendica_path_mapping_service.dart'; +import '../../models/entry_tree_item.dart'; import '../../models/local_image_archive_entry.dart'; +import '../../services/connections_manager.dart'; import '../../utils/exec_error.dart'; -import '../../friendica/services/friendica_connections.dart'; -class DiasporaArchiveService { - final FriendicaPathMappingService pathMappingService; +class DiasporaArchiveService implements ArchiveService { + @override + final DiasporaPathMappingService pathMappingService; + final Map _imagesByRequestUrl = {}; - final List _postEntries = []; - final List _orphanedCommentEntries = []; - final List _allComments = []; - final FriendicaConnections connections = FriendicaConnections(); + final List _postEntries = []; + final List _orphanedCommentEntries = []; + final List _allComments = []; + + @override + final ConnectionsManager connectionsManager = ConnectionsManager(); String _ownersName = ''; DiasporaArchiveService({required this.pathMappingService}); @@ -25,33 +30,32 @@ class DiasporaArchiveService { String get ownersName => _ownersName; void clearCaches() { - connections.clearCaches(); + connectionsManager.clearCaches(); _imagesByRequestUrl.clear(); _orphanedCommentEntries.clear(); _allComments.clear(); _postEntries.clear(); } - FutureResult, ExecError> getPosts() async { + FutureResult, ExecError> getPosts() async { if (_postEntries.isEmpty && _allComments.isEmpty) { - _loadEntries(); + _loadProfileFile(); } return Result.ok(_postEntries); } - FutureResult, ExecError> getAllComments() async { + FutureResult, ExecError> getAllComments() async { if (_postEntries.isEmpty && _allComments.isEmpty) { - _loadEntries(); + _loadProfileFile(); } return Result.ok(_allComments); } - FutureResult, ExecError> - getOrphanedComments() async { + FutureResult, ExecError> getOrphanedComments() async { if (_postEntries.isEmpty && _allComments.isEmpty) { - _loadEntries(); + _loadProfileFile(); } return Result.ok(_orphanedCommentEntries); @@ -70,46 +74,21 @@ class DiasporaArchiveService { String get _baseArchiveFolder => pathMappingService.rootFolder; - void _loadEntries() { - final entriesJsonPath = p.join(_baseArchiveFolder, 'postsAndComments.json'); - final jsonFile = File(entriesJsonPath); - if (jsonFile.existsSync()) { - final json = jsonDecode(jsonFile.readAsStringSync()) as List; - final entries = - json.map((j) => FriendicaTimelineEntry.fromJson(j, connections)); - final topLevelEntries = - entries.where((element) => element.parentId.isEmpty); - final commentEntries = - entries.where((element) => element.parentId.isNotEmpty).toList(); - final entryTrees = {}; - - final postTreeEntries = []; - for (final entry in topLevelEntries) { - final treeEntry = FriendicaEntryTreeItem(entry, false); - entryTrees[entry.id] = treeEntry; - postTreeEntries.add(treeEntry); + void _loadProfileFile() { + _ownersName = ''; + final archiveDir = Directory(_baseArchiveFolder); + final jsonFiles = archiveDir.listSync().where((element) => + element.statSync().type == FileSystemEntityType.file && + element.path.toLowerCase().endsWith('json')); + for (final file in jsonFiles) { + final reader = + DiasporaProfileJsonReader(file.absolute.path, connectionsManager); + if (_ownersName.isEmpty) { + _ownersName = reader.readOwnersName(); + reader.readContacts(); + final newPosts = reader.readPosts().map((e) => EntryTreeItem(e, false)); + _postEntries.addAll(newPosts); } - - final commentTreeEntries = []; - commentEntries.sort( - (c1, c2) => c1.creationTimestamp.compareTo(c2.creationTimestamp)); - for (final entry in commentEntries) { - final parent = entryTrees[entry.parentId]; - final treeEntry = FriendicaEntryTreeItem(entry, parent == null); - parent?.addChild(treeEntry); - entryTrees[entry.id] = treeEntry; - commentTreeEntries.add(treeEntry); - } - - _postEntries.clear(); - _postEntries.addAll(postTreeEntries); - - _allComments.clear(); - _allComments.addAll(commentTreeEntries); - - _orphanedCommentEntries.clear(); - _orphanedCommentEntries - .addAll(entryTrees.values.where((element) => element.isOrphaned)); } } diff --git a/friendica_archive_browser/lib/src/diaspora/services/diaspora_path_mapping_service.dart b/friendica_archive_browser/lib/src/diaspora/services/diaspora_path_mapping_service.dart new file mode 100644 index 0000000..2e9ec39 --- /dev/null +++ b/friendica_archive_browser/lib/src/diaspora/services/diaspora_path_mapping_service.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as p; + +import '../../services/path_mapper_service_interface.dart'; +import '../../settings/settings_controller.dart'; + +class DiasporaPathMappingService implements PathMappingService { + static final _logger = Logger('$DiasporaPathMappingService'); + final SettingsController settings; + final _photoDirectories = []; + + DiasporaPathMappingService(this.settings) { + refresh(); + } + + @override + String get rootFolder => settings.rootFolder; + + @override + List get archiveDirectories => + List.unmodifiable(_photoDirectories); + + @override + void refresh() { + _logger.fine('Refreshing path mapping service directory data.'); + if (!Directory(settings.rootFolder).existsSync()) { + _logger.severe( + "Base directory does not exist! can't do mapping of ${settings.rootFolder}"); + return; + } + _photoDirectories.clear(); + + _photoDirectories.addAll(Directory(settings.rootFolder) + .listSync(recursive: false) + .where((element) => + element.statSync().type == FileSystemEntityType.directory)); + } + + @override + String toFullPath(String relPath) { + for (final file in _photoDirectories) { + final fullPath = p.join(file.path, relPath); + if (File(fullPath).existsSync()) { + return fullPath; + } + } + + _logger.fine( + 'Did not find a file with this relPath anywhere therefore returning the relPath'); + return relPath; + } +} diff --git a/friendica_archive_browser/lib/src/diaspora/services/diaspora_profile_json_reader.dart b/friendica_archive_browser/lib/src/diaspora/services/diaspora_profile_json_reader.dart index f4dab26..6520207 100644 --- a/friendica_archive_browser/lib/src/diaspora/services/diaspora_profile_json_reader.dart +++ b/friendica_archive_browser/lib/src/diaspora/services/diaspora_profile_json_reader.dart @@ -2,24 +2,63 @@ import 'dart:convert'; import 'dart:io'; import 'package:friendica_archive_browser/src/diaspora/serializers/diaspora_contact_serializer.dart'; -import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; +import 'package:friendica_archive_browser/src/models/connection.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; +import 'package:friendica_archive_browser/src/services/connections_manager.dart'; + +import '../serializers/diaspora_posts_serializer.dart'; class DiasporaProfileJsonReader { final String jsonFilePath; + final ConnectionsManager connectionsManager; + final _jsonData = {}; - DiasporaProfileJsonReader(this.jsonFilePath); + DiasporaProfileJsonReader(this.jsonFilePath, this.connectionsManager); + + Map get jsonData { + if (_jsonData.isNotEmpty) { + return _jsonData; + } - List readContacts() { final jsonFile = File(jsonFilePath); if (jsonFile.existsSync()) { final json = jsonDecode(jsonFile.readAsStringSync()) as Map; - final contactsJson = json['user']?['contacts'] as List; - final contacts = - contactsJson.map((j) => friendicaContactFromDiasporaJson(j)).toList(); - return contacts; + _jsonData.addAll(json); } - return []; + return _jsonData; + } + + String readOwnersName() => + jsonData['user']?['profile']?['entity_data']?['author'] ?? 'Unknown'; + + List readContacts() { + final json = jsonData; + final userName = json['user']?['profile']?['entity_data']?['author'] ?? ''; + final userContact = Connection(name: userName, id: '0'); + connectionsManager.addConnection(userContact); + final contactsJson = json['user']?['contacts'] as List; + final contacts = contactsJson.map((j) => contactFromDiasporaJson(j)); + connectionsManager.addAllConnections(contacts); + return contacts.toList(); + } + + List readPosts() { + if (connectionsManager.length == 0) { + readContacts(); + } + + final json = jsonData; + final postsJson = json['user']?['posts'] as List; + final posts = postsJson + .map((j) => timelineItemFromDiasporaPostJson(j, connectionsManager)) + .where((element) => element.isSuccess) + .map((e) => e.value) + .toList(); + + posts + .sort((p1, p2) => p2.creationTimestamp.compareTo(p1.creationTimestamp)); + return posts; } } diff --git a/friendica_archive_browser/lib/src/friendica/components/geo/geo_extensions.dart b/friendica_archive_browser/lib/src/friendica/components/geo/geo_extensions.dart index c86625d..c5a339f 100644 --- a/friendica_archive_browser/lib/src/friendica/components/geo/geo_extensions.dart +++ b/friendica_archive_browser/lib/src/friendica/components/geo/geo_extensions.dart @@ -1,12 +1,12 @@ import 'dart:ui'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; import 'package:latlng/latlng.dart'; import 'package:map/map.dart'; import 'marker_data.dart'; -extension GeoSpatialPostExtensions on FriendicaTimelineEntry { +extension GeoSpatialPostExtensions on TimelineEntry { MarkerData toMarkerData(MapTransformer transformer, Color color) { final latLon = LatLng(locationData.latitude, locationData.longitude); final offset = transformer.fromLatLngToXYCoords(latLon); diff --git a/friendica_archive_browser/lib/src/friendica/components/geo/marker_data.dart b/friendica_archive_browser/lib/src/friendica/components/geo/marker_data.dart index eb2ce4e..c921c9e 100644 --- a/friendica_archive_browser/lib/src/friendica/components/geo/marker_data.dart +++ b/friendica_archive_browser/lib/src/friendica/components/geo/marker_data.dart @@ -1,9 +1,9 @@ import 'dart:ui'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; class MarkerData { - final List posts; + final List posts; final Offset pos; final Color color; diff --git a/friendica_archive_browser/lib/src/friendica/components/media_timeline_component.dart b/friendica_archive_browser/lib/src/friendica/components/media_timeline_component.dart index edc3e98..d4b08e9 100644 --- a/friendica_archive_browser/lib/src/friendica/components/media_timeline_component.dart +++ b/friendica_archive_browser/lib/src/friendica/components/media_timeline_component.dart @@ -1,10 +1,9 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.dart'; import 'package:friendica_archive_browser/src/friendica/screens/media_slideshow_screen.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart'; +import 'package:friendica_archive_browser/src/models/media_attachment.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_provider.dart'; import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; import 'package:provider/provider.dart'; @@ -13,7 +12,7 @@ import 'media_wrapper_component.dart'; class MediaTimelineComponent extends StatelessWidget { static const double _maxHeightWidth = 400.0; - final List mediaAttachments; + final List mediaAttachments; const MediaTimelineComponent({Key? key, required this.mediaAttachments}) : super(key: key); @@ -28,8 +27,7 @@ class MediaTimelineComponent extends StatelessWidget { final double singleWidth = MediaQuery.of(context).size.width / 2.0; final double threeAcrossWidth = MediaQuery.of(context).size.width / 3.0; final double preferredMultiWidth = min(threeAcrossWidth, _maxHeightWidth); - final pathMapper = Provider.of(context); - final archiveService = Provider.of(context); + final archiveService = Provider.of(context); final settingsController = Provider.of(context); return Container( @@ -48,7 +46,6 @@ class MediaTimelineComponent extends StatelessWidget { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: settingsController), - Provider.value(value: pathMapper), ChangeNotifierProvider.value(value: archiveService), ], child: MediaSlideShowScreen( diff --git a/friendica_archive_browser/lib/src/friendica/components/media_wrapper_component.dart b/friendica_archive_browser/lib/src/friendica/components/media_wrapper_component.dart index 6929695..7cce22a 100644 --- a/friendica_archive_browser/lib/src/friendica/components/media_wrapper_component.dart +++ b/friendica_archive_browser/lib/src/friendica/components/media_wrapper_component.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart'; +import 'package:friendica_archive_browser/src/models/media_attachment.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_provider.dart'; +import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart'; import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart'; import 'package:logging/logging.dart'; @@ -14,7 +14,7 @@ class MediaWrapperComponent extends StatelessWidget { static final _logger = Logger('$MediaWrapperComponent'); static const double _noPreferredValue = -1.0; - final FriendicaMediaAttachment mediaAttachment; + final MediaAttachment mediaAttachment; final double preferredWidth; final double preferredHeight; @@ -28,24 +28,23 @@ class MediaWrapperComponent extends StatelessWidget { @override Widget build(BuildContext context) { final settingsController = Provider.of(context); - final pathMapper = Provider.of(context); - final archiveService = Provider.of(context); + final archiveService = Provider.of(context); final videoPlayerCommand = settingsController.videoPlayerCommand; - final path = _calculatePath(pathMapper, archiveService); + final path = _calculatePath(archiveService); final width = preferredWidth > 0 ? preferredWidth : MediaQuery.of(context).size.width; final height = preferredHeight > 0 ? preferredHeight : MediaQuery.of(context).size.height; - if (mediaAttachment.explicitType == FriendicaAttachmentMediaType.unknown) { + if (mediaAttachment.explicitType == AttachmentMediaType.unknown) { return Text('Unable to resolve type for ${mediaAttachment.uri.path}'); } - if (mediaAttachment.explicitType == FriendicaAttachmentMediaType.video) { + if (mediaAttachment.explicitType == AttachmentMediaType.video) { final title = "Video (click to play): " + mediaAttachment.title; final thumbnailImageResult = _uriToImage( - mediaAttachment.thumbnailUri, pathMapper, + mediaAttachment.thumbnailUri, archiveService.pathMappingService, imageTypeName: 'thumbnail image'); if (thumbnailImageResult.image != null) { return _createFinalWidget( @@ -74,8 +73,9 @@ class MediaWrapperComponent extends StatelessWidget { ); } - if (mediaAttachment.explicitType == FriendicaAttachmentMediaType.image) { - final imageResult = _uriToImage(mediaAttachment.uri, pathMapper); + if (mediaAttachment.explicitType == AttachmentMediaType.image) { + final imageResult = + _uriToImage(mediaAttachment.uri, archiveService.pathMappingService); if (imageResult.image == null) { final errorPath = imageResult.path.isNotEmpty ? imageResult.path @@ -111,7 +111,7 @@ class MediaWrapperComponent extends StatelessWidget { } } - _ImageAndPathResult _uriToImage(Uri uri, FriendicaPathMappingService mapper, + _ImageAndPathResult _uriToImage(Uri uri, PathMappingService mapper, {String imageTypeName = 'image'}) { if (uri.toString().startsWith('https://interncache')) { return _ImageAndPathResult.none(); @@ -172,8 +172,7 @@ class MediaWrapperComponent extends StatelessWidget { return InkWell(onTap: onTap, child: imageWidget); } - String _calculatePath( - FriendicaPathMappingService pathMapper, FriendicaArchiveService archiveService) { + String _calculatePath(ArchiveServiceProvider archiveService) { final url = mediaAttachment.uri.toString(); String basePath = ''; if (url.startsWith('http')) { @@ -187,7 +186,7 @@ class MediaWrapperComponent extends StatelessWidget { basePath = mediaAttachment.uri.path; } - return pathMapper.toFullPath(basePath); + return archiveService.pathMappingService.toFullPath(basePath); } } diff --git a/friendica_archive_browser/lib/src/friendica/components/tree_entry_card.dart b/friendica_archive_browser/lib/src/friendica/components/tree_entry_card.dart index 396e0f5..3059bb0 100644 --- a/friendica_archive_browser/lib/src/friendica/components/tree_entry_card.dart +++ b/friendica_archive_browser/lib/src/friendica/components/tree_entry_card.dart @@ -1,9 +1,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart'; -import 'package:friendica_archive_browser/src/friendica/models/location_data.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/models/entry_tree_item.dart'; +import 'package:friendica_archive_browser/src/models/location_data.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_provider.dart'; import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart'; import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart'; @@ -16,7 +16,7 @@ import 'media_timeline_component.dart'; class TreeEntryCard extends StatelessWidget { static final _logger = Logger("$TreeEntryCard"); - final FriendicaEntryTreeItem treeEntry; + final EntryTreeItem treeEntry; final bool isTopLevel; const TreeEntryCard( @@ -32,7 +32,7 @@ class TreeEntryCard extends StatelessWidget { const double spacingHeight = 5.0; final formatter = Provider.of(context).dateTimeFormatter; - final mapper = Provider.of(context); + final archiveService = Provider.of(context); final entry = treeEntry.entry; @@ -75,7 +75,8 @@ class TreeEntryCard extends StatelessWidget { child: IconButton( onPressed: () async => await copyToClipboard( context: context, - text: entry.toHumanString(mapper, formatter), + text: entry.toHumanString( + archiveService.pathMappingService, formatter), snackbarMessage: 'Copied Post to clipboard'), icon: const Icon(Icons.copy)), ), @@ -129,9 +130,10 @@ class TreeEntryCard extends StatelessWidget { ), if (entry.locationData.hasData()) entry.locationData.toWidget(spacingHeight), - if (entry.links.isNotEmpty) ...[ + if (treeEntry.entry.externalLink.isNotEmpty) ...[ const SizedBox(height: spacingHeight), - LinkElementsComponent(links: entry.links) + LinkElementsComponent( + links: [Uri.parse(treeEntry.entry.externalLink)]) ], if (entry.mediaAttachments.isNotEmpty) ...[ const SizedBox(height: spacingHeight), diff --git a/friendica_archive_browser/lib/src/friendica/models/friendica_entry_tree_item.dart b/friendica_archive_browser/lib/src/friendica/models/friendica_entry_tree_item.dart deleted file mode 100644 index 7dbe457..0000000 --- a/friendica_archive_browser/lib/src/friendica/models/friendica_entry_tree_item.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart'; - -class FriendicaEntryTreeItem { - final FriendicaTimelineEntry entry; - final bool isOrphaned; - - final _children = {}; - - FriendicaEntryTreeItem(this.entry, this.isOrphaned); - - String get id => entry.id; - - void addChild(FriendicaEntryTreeItem child) { - _children[child.id] = child; - } - - List get children => - List.unmodifiable(_children.values); -} diff --git a/friendica_archive_browser/lib/src/friendica/models/friendica_timeline_entry.dart b/friendica_archive_browser/lib/src/friendica/models/friendica_timeline_entry.dart deleted file mode 100644 index af27b6c..0000000 --- a/friendica_archive_browser/lib/src/friendica/models/friendica_timeline_entry.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; -import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_connections.dart'; -import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart'; -import 'package:intl/intl.dart'; -import 'package:logging/logging.dart'; - -import 'friendica_media_attachment.dart'; -import 'location_data.dart'; -import 'model_utils.dart'; - -class FriendicaTimelineEntry { - static final _logger = Logger('$FriendicaTimelineEntry'); - - final String id; - - final String parentId; - - final String parentAuthor; - - final String parentAuthorId; - - final int creationTimestamp; - - final int backdatedTimestamp; - - final int modificationTimestamp; - - final String body; - - final String title; - - final bool isReshare; - - final String author; - - final String authorId; - - final String externalLink; - - final List mediaAttachments; - - final LocationData locationData; - - final List links; - - final List likes; - - final List dislikes; - - FriendicaTimelineEntry( - {this.id = '', - this.parentId = '', - this.creationTimestamp = 0, - this.backdatedTimestamp = 0, - this.modificationTimestamp = 0, - this.isReshare = false, - this.body = '', - this.title = '', - this.author = '', - this.authorId = '', - this.parentAuthor = '', - this.parentAuthorId = '', - this.externalLink = '', - this.locationData = const LocationData(), - this.likes = const [], - this.dislikes = const [], - List? mediaAttachments, - List? links}) - : mediaAttachments = mediaAttachments ?? [], - links = links ?? []; - - FriendicaTimelineEntry.randomBuilt() - : creationTimestamp = DateTime.now().millisecondsSinceEpoch, - backdatedTimestamp = DateTime.now().millisecondsSinceEpoch, - modificationTimestamp = DateTime.now().millisecondsSinceEpoch, - id = randomId(), - isReshare = false, - parentId = randomId(), - externalLink = 'Random external link ${randomId()}', - body = 'Random post text ${randomId()}', - title = 'Random title ${randomId()}', - author = 'Random author ${randomId()}', - authorId = 'Random authorId ${randomId()}', - parentAuthor = 'Random parent author ${randomId()}', - parentAuthorId = 'Random parent author id ${randomId()}', - locationData = LocationData.randomBuilt(), - likes = const [], - dislikes = const [], - links = [ - Uri.parse('http://localhost/${randomId()}'), - Uri.parse('http://localhost/${randomId()}') - ], - mediaAttachments = [ - FriendicaMediaAttachment.randomBuilt(), - FriendicaMediaAttachment.randomBuilt() - ]; - - FriendicaTimelineEntry copy( - {int? creationTimestamp, - int? backdatedTimestamp, - int? modificationTimestamp, - bool? isReshare, - String? id, - String? parentId, - String? externalLink, - String? body, - String? title, - String? author, - String? authorId, - String? parentAuthor, - String? parentAuthorId, - LocationData? locationData, - List? mediaAttachments, - List? likes, - List? dislikes, - List? links}) { - return FriendicaTimelineEntry( - creationTimestamp: creationTimestamp ?? this.creationTimestamp, - backdatedTimestamp: backdatedTimestamp ?? this.backdatedTimestamp, - modificationTimestamp: - modificationTimestamp ?? this.modificationTimestamp, - id: id ?? this.id, - isReshare: isReshare ?? this.isReshare, - parentId: parentId ?? this.parentId, - externalLink: externalLink ?? this.externalLink, - body: body ?? this.body, - title: title ?? this.title, - author: author ?? this.author, - authorId: authorId ?? this.authorId, - parentAuthor: parentAuthor ?? this.parentAuthor, - parentAuthorId: parentAuthorId ?? this.parentAuthorId, - locationData: locationData ?? this.locationData, - mediaAttachments: mediaAttachments ?? this.mediaAttachments, - likes: likes ?? this.likes, - dislikes: dislikes ?? this.dislikes, - links: links ?? this.links); - } - - @override - String toString() { - return 'FriendicaTimelineEntry{id: $id, isReshare: $isReshare, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor mediaAttachments: $mediaAttachments, links: $links}'; - } - - String toHumanString(FriendicaPathMappingService mapper, DateFormat formatter) { - final creationDateString = formatter.format( - DateTime.fromMillisecondsSinceEpoch(creationTimestamp * 1000) - .toLocal()); - return [ - 'Title: $title', - 'Creation At: $creationDateString', - 'Text:', - 'Author: $author', - 'Reshare: $isReshare', - if (externalLink.isNotEmpty) 'External Link: $externalLink', - body, - '', - if (parentId.isNotEmpty) - "Comment on post/comment by ${parentAuthor.isNotEmpty ? parentAuthor : 'unknown author'}", - if (links.isNotEmpty) 'Links:', - ...links.map((e) => e.toString()), - '', - if (mediaAttachments.isNotEmpty) 'Photos and Videos:', - ...mediaAttachments.map((e) => e.toHumanString(mapper)), - if (locationData.hasPosition) locationData.toHumanString(), - ].join('\n'); - } - - bool hasImages() => mediaAttachments - .where((element) => - element.explicitType == FriendicaAttachmentMediaType.image) - .isNotEmpty; - - bool hasVideos() => mediaAttachments - .where((element) => - element.explicitType == FriendicaAttachmentMediaType.video) - .isNotEmpty; - - static FriendicaTimelineEntry fromJson( - Map json, FriendicaConnections connections) { - final int timestamp = json.containsKey('created_at') - ? OffsetDateTimeUtils.epochSecTimeFromFriendicaString( - json['created_at']) - .fold( - onSuccess: (value) => value, - onError: (error) { - _logger.severe("Couldn't read date time string: $error"); - return 0; - }) - : 0; - final id = json['id_str'] ?? ''; - final isReshare = json.containsKey('retweeted_status'); - final parentId = json['in_reply_to_status_id_str'] ?? ''; - final parentAuthor = json['in_reply_to_screen_name'] ?? ''; - final parentAuthorId = json['in_reply_to_user_id_str'] ?? ''; - final body = json['friendica_html'] ?? ''; - final author = json['user']['name']; - final authorId = json['user']['id_str']; - final title = json['friendica_title'] ?? ''; - final externalLink = json['external_url'] ?? ''; - final actualLocationData = LocationData(); - final modificationTimestamp = timestamp; - final backdatedTimestamp = timestamp; - final links = []; - final mediaAttachments = (json['attachments'] as List? ?? []) - .map((j) => FriendicaMediaAttachment.fromJson(j)) - .toList(); - final likes = - (json['friendica_activities']?['like'] as List? ?? []) - .map((json) => Contact.fromJson(json)) - .toList(); - final dislikes = - (json['friendica_activities']?['dislike'] as List? ?? []) - .map((json) => Contact.fromJson(json)) - .toList(); - final announce = - (json['friendica_activities']?['announce'] as List? ?? []) - .map((json) => Contact.fromJson(json)) - .toList(); - - for (final contact in [...likes, ...dislikes, ...announce]) { - connections.addConnection(contact); - } - - return FriendicaTimelineEntry( - creationTimestamp: timestamp, - modificationTimestamp: modificationTimestamp, - backdatedTimestamp: backdatedTimestamp, - locationData: actualLocationData, - externalLink: externalLink, - body: body, - isReshare: isReshare, - id: id, - parentId: parentId, - parentAuthorId: parentAuthorId, - author: author, - authorId: authorId, - parentAuthor: parentAuthor, - title: title, - links: links, - likes: likes, - dislikes: dislikes, - mediaAttachments: mediaAttachments, - ); - } -} diff --git a/friendica_archive_browser/lib/src/friendica/screens/entries_screen.dart b/friendica_archive_browser/lib/src/friendica/screens/entries_screen.dart index 3dc888a..24eeeb5 100644 --- a/friendica_archive_browser/lib/src/friendica/screens/entries_screen.dart +++ b/friendica_archive_browser/lib/src/friendica/screens/entries_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:friendica_archive_browser/src/friendica/components/filter_control_component.dart'; import 'package:friendica_archive_browser/src/friendica/components/tree_entry_card.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart'; -import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart'; +import 'package:friendica_archive_browser/src/models/entry_tree_item.dart'; +import 'package:friendica_archive_browser/src/models/model_utils.dart'; import 'package:friendica_archive_browser/src/screens/error_screen.dart'; import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; import 'package:friendica_archive_browser/src/utils/exec_error.dart'; @@ -15,8 +15,7 @@ import '../../screens/standin_status_screen.dart'; class EntriesScreen extends StatelessWidget { static final _logger = Logger('$EntriesScreen'); - final FutureResult, ExecError> Function() - populator; + final FutureResult, ExecError> Function() populator; const EntriesScreen({Key? key, required this.populator}) : super(key: key); @@ -25,7 +24,7 @@ class EntriesScreen extends StatelessWidget { _logger.info('Build FriendicaEntriesScreen'); Provider.of(context); - return FutureBuilder, ExecError>>( + return FutureBuilder, ExecError>>( future: populator(), builder: (context, snapshot) { _logger.info('FriendicaEntriesScreen Future builder called'); @@ -58,7 +57,7 @@ class EntriesScreen extends StatelessWidget { class _FriendicaEntriesScreenWidget extends StatelessWidget { static final _logger = Logger('$_FriendicaEntriesScreenWidget'); - final List posts; + final List posts; const _FriendicaEntriesScreenWidget({Key? key, required this.posts}) : super(key: key); @@ -66,7 +65,7 @@ class _FriendicaEntriesScreenWidget extends StatelessWidget { @override Widget build(BuildContext context) { _logger.fine('Redrawing'); - return FilterControl( + return FilterControl( allItems: posts, commentsOnlyFilterFunction: (post) => post.children.isNotEmpty, imagesOnlyFilterFunction: (post) => post.entry.hasImages(), diff --git a/friendica_archive_browser/lib/src/friendica/screens/geospatial_screen.dart b/friendica_archive_browser/lib/src/friendica/screens/geospatial_screen.dart index f652321..5ac0ce1 100644 --- a/friendica_archive_browser/lib/src/friendica/screens/geospatial_screen.dart +++ b/friendica_archive_browser/lib/src/friendica/screens/geospatial_screen.dart @@ -6,13 +6,13 @@ import 'package:friendica_archive_browser/src/friendica/components/geo/geo_exten import 'package:friendica_archive_browser/src/friendica/components/geo/map_bounds.dart'; import 'package:friendica_archive_browser/src/friendica/components/geo/marker_data.dart'; import 'package:friendica_archive_browser/src/friendica/components/tree_entry_card.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart'; import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/models/entry_tree_item.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; import 'package:friendica_archive_browser/src/screens/error_screen.dart'; import 'package:friendica_archive_browser/src/screens/loading_status_screen.dart'; import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_provider.dart'; import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; import 'package:friendica_archive_browser/src/utils/exec_error.dart'; import 'package:friendica_archive_browser/src/utils/temp_file_builder.dart'; @@ -33,9 +33,9 @@ class GeospatialViewScreen extends StatelessWidget { @override Widget build(BuildContext context) { _logger.info('Build GeospatialViewScreen'); - final service = Provider.of(context); + final service = Provider.of(context); - return FutureBuilder, ExecError>>( + return FutureBuilder, ExecError>>( future: service.getPosts(), builder: (context, snapshot) { _logger.info('GeospatialViewScreen Future builder called'); @@ -66,7 +66,7 @@ class GeospatialViewScreen extends StatelessWidget { } class GeospatialView extends StatefulWidget { - final List posts; + final List posts; const GeospatialView({Key? key, required this.posts}) : super(key: key); @@ -88,8 +88,8 @@ class _GeospatialViewState extends State { ); Offset? dragStart; - final postsInList = []; - final postsInView = []; + final postsInList = []; + final postsInView = []; double scaleStart = 1.0; @override @@ -203,8 +203,8 @@ class _GeospatialViewState extends State { highlightedColor: Colors.indigo[900]!))); } - Widget _buildPostList( - BuildContext context, DateFormat formatter, FriendicaPathMappingService mapper) { + Widget _buildPostList(BuildContext context, DateFormat formatter, + FriendicaPathMappingService mapper) { _logger.finest(() => 'Building PostList with ${postsInList.length} items'); if (postsInList.isEmpty) { return const StandInStatusScreen( @@ -218,14 +218,14 @@ class _GeospatialViewState extends State { behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), child: ListView.separated( itemBuilder: (context, index) => TreeEntryCard( - treeEntry: FriendicaEntryTreeItem(postsInList[index], false)), + treeEntry: EntryTreeItem(postsInList[index], false)), separatorBuilder: (context, index) => const Divider(height: 1), itemCount: postsInList.length), ); } - Widget _buildMap( - BuildContext context, DateFormat formatter, FriendicaPathMappingService mapper) { + Widget _buildMap(BuildContext context, DateFormat formatter, + FriendicaPathMappingService mapper) { final settings = Provider.of(context); final shouldDebugCache = @@ -332,8 +332,8 @@ class _GeospatialViewState extends State { ); } - Widget _buildMarkerWidget( - MarkerData data, DateFormat formatter, FriendicaPathMappingService mapper) { + Widget _buildMarkerWidget(MarkerData data, DateFormat formatter, + FriendicaPathMappingService mapper) { return Positioned( left: data.pos.dx - 16, top: data.pos.dy - 16, diff --git a/friendica_archive_browser/lib/src/friendica/screens/media_slideshow_screen.dart b/friendica_archive_browser/lib/src/friendica/screens/media_slideshow_screen.dart index 5a23cf1..c50042a 100644 --- a/friendica_archive_browser/lib/src/friendica/screens/media_slideshow_screen.dart +++ b/friendica_archive_browser/lib/src/friendica/screens/media_slideshow_screen.dart @@ -4,8 +4,8 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:friendica_archive_browser/src/friendica/components/media_wrapper_component.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.dart'; import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/models/media_attachment.dart'; import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; import 'package:friendica_archive_browser/src/themes.dart'; import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart'; @@ -14,7 +14,7 @@ import 'package:provider/provider.dart'; class MediaSlideShowScreen extends StatefulWidget { static const _spacing = 5.0; - final List mediaAttachments; + final List mediaAttachments; final int initialIndex; const MediaSlideShowScreen( @@ -27,7 +27,7 @@ class MediaSlideShowScreen extends StatefulWidget { class _MediaSlideShowScreenState extends State { static const fastestChangeMS = 250; - FriendicaMediaAttachment media = FriendicaMediaAttachment.blank(); + MediaAttachment media = MediaAttachment.blank(); int index = 0; int lastKeyInducedIndexChange = 0; @@ -155,7 +155,8 @@ class _MediaSlideShowScreenState extends State { } Future _saveFile(BuildContext context) async { - final pathMapper = Provider.of(context, listen: false); + final pathMapper = + Provider.of(context, listen: false); final filename = media.uri.pathSegments.last; final initialPath = pathMapper.toFullPath(media.uri.toFilePath()); diff --git a/friendica_archive_browser/lib/src/friendica/screens/stats_screen.dart b/friendica_archive_browser/lib/src/friendica/screens/stats_screen.dart index 53cfeae..bdb884c 100644 --- a/friendica_archive_browser/lib/src/friendica/screens/stats_screen.dart +++ b/friendica_archive_browser/lib/src/friendica/screens/stats_screen.dart @@ -4,10 +4,10 @@ import 'package:friendica_archive_browser/src/components/timechart_widget.dart'; import 'package:friendica_archive_browser/src/components/top_interactactors_widget.dart'; import 'package:friendica_archive_browser/src/components/word_frequency_widget.dart'; import 'package:friendica_archive_browser/src/friendica/components/filter_control_component.dart'; -import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart'; +import 'package:friendica_archive_browser/src/models/model_utils.dart'; import 'package:friendica_archive_browser/src/models/time_element.dart'; import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_provider.dart'; import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; @@ -21,7 +21,7 @@ class StatsScreen extends StatefulWidget { class _StatsScreenState extends State { static final _logger = Logger("$_StatsScreenState"); - FriendicaArchiveService? archiveDataService; + ArchiveServiceProvider? archiveDataService; final allItems = []; StatType statType = StatType.selectType; bool hasText = true; @@ -78,7 +78,7 @@ class _StatsScreenState extends State { @override Widget build(BuildContext context) { - archiveDataService = Provider.of(context); + archiveDataService = Provider.of(context); return FilterControl( allItems: allItems, @@ -147,7 +147,7 @@ class _StatsScreenState extends State { child: Column(children: [ ..._buildGraphScreens(context, items), const Divider(), - TopInteractorsWidget(items, archiveDataService!.connections), + TopInteractorsWidget(items, archiveDataService!.connectionsManager), const Divider(), WordFrequencyWidget(items), ]), diff --git a/friendica_archive_browser/lib/src/friendica/serializers/friendica_contact_serializer.dart b/friendica_archive_browser/lib/src/friendica/serializers/friendica_contact_serializer.dart new file mode 100644 index 0000000..5bb7633 --- /dev/null +++ b/friendica_archive_browser/lib/src/friendica/serializers/friendica_contact_serializer.dart @@ -0,0 +1,18 @@ +import '../../models/connection.dart'; + +Connection contactFromFriendicaJson(Map json) { + final status = (json['following'] ?? '') == 'true' + ? ConnectionStatus.youFollowThem + : ConnectionStatus.none; + final name = json['name'] ?? ''; + final id = json['id_str'] ?? ''; + final profileUrl = Uri.parse(json['url'] ?? ''); + final network = json['network'] ?? 'unkn'; + + return Connection( + status: status, + name: name, + id: id, + profileUrl: profileUrl, + network: network); +} diff --git a/friendica_archive_browser/lib/src/friendica/serializers/friendica_media_attachment_serializer.dart b/friendica_archive_browser/lib/src/friendica/serializers/friendica_media_attachment_serializer.dart new file mode 100644 index 0000000..7e500cd --- /dev/null +++ b/friendica_archive_browser/lib/src/friendica/serializers/friendica_media_attachment_serializer.dart @@ -0,0 +1,25 @@ +import '../../models/media_attachment.dart'; + +MediaAttachment mediaAttachmentfromFriendicaJson(Map json) { + final uri = Uri.parse(json['url']); + const creationTimestamp = 0; + final metadata = (json['metadata'] as Map? ?? {}) + .map((key, value) => MapEntry(key, value.toString())); + final explicitType = (json['mimetype'] ?? '').startsWith('image') + ? AttachmentMediaType.image + : (json['mimetype'] ?? '').startsWith('video') + ? AttachmentMediaType.video + : AttachmentMediaType.unknown; + final thumbnailUri = Uri(); + const title = ''; + const description = ''; + + return MediaAttachment( + uri: uri, + creationTimestamp: creationTimestamp, + metadata: metadata, + thumbnailUri: thumbnailUri, + title: title, + explicitType: explicitType, + description: description); +} diff --git a/friendica_archive_browser/lib/src/friendica/serializers/friendica_timeline_entry_serializer.dart b/friendica_archive_browser/lib/src/friendica/serializers/friendica_timeline_entry_serializer.dart new file mode 100644 index 0000000..636c876 --- /dev/null +++ b/friendica_archive_browser/lib/src/friendica/serializers/friendica_timeline_entry_serializer.dart @@ -0,0 +1,72 @@ +import 'package:friendica_archive_browser/src/friendica/serializers/friendica_contact_serializer.dart'; +import 'package:friendica_archive_browser/src/friendica/serializers/friendica_media_attachment_serializer.dart'; +import 'package:logging/logging.dart'; + +import '../../models/location_data.dart'; +import '../../models/timeline_entry.dart'; +import '../../services/connections_manager.dart'; +import '../../utils/offsetdatetime_utils.dart'; + +final _logger = Logger('FriendicaTimelineEntrySerializer'); + +TimelineEntry timelineEntryFromFriendicaJson( + Map json, ConnectionsManager connections) { + final int timestamp = json.containsKey('created_at') + ? OffsetDateTimeUtils.epochSecTimeFromFriendicaString(json['created_at']) + .fold( + onSuccess: (value) => value, + onError: (error) { + _logger.severe("Couldn't read date time string: $error"); + return 0; + }) + : 0; + final id = json['id_str'] ?? ''; + final isReshare = json.containsKey('retweeted_status'); + final parentId = json['in_reply_to_status_id_str'] ?? ''; + final parentAuthor = json['in_reply_to_screen_name'] ?? ''; + final parentAuthorId = json['in_reply_to_user_id_str'] ?? ''; + final body = json['friendica_html'] ?? ''; + final author = json['user']['name']; + final authorId = json['user']['id_str']; + final title = json['friendica_title'] ?? ''; + final externalLink = json['external_url'] ?? ''; + final actualLocationData = LocationData(); + final modificationTimestamp = timestamp; + final backdatedTimestamp = timestamp; + final mediaAttachments = (json['attachments'] as List? ?? []) + .map((j) => mediaAttachmentfromFriendicaJson(j)) + .toList(); + final likes = (json['friendica_activities']?['like'] as List? ?? []) + .map((json) => contactFromFriendicaJson(json)) + .toList(); + final dislikes = + (json['friendica_activities']?['dislike'] as List? ?? []) + .map((json) => contactFromFriendicaJson(json)) + .toList(); + final announce = + (json['friendica_activities']?['announce'] as List? ?? []) + .map((json) => contactFromFriendicaJson(json)) + .toList(); + + connections.addAllConnections([...likes, ...dislikes, ...announce]); + + return TimelineEntry( + creationTimestamp: timestamp, + modificationTimestamp: modificationTimestamp, + backdatedTimestamp: backdatedTimestamp, + locationData: actualLocationData, + externalLink: externalLink, + body: body, + isReshare: isReshare, + id: id, + parentId: parentId, + parentAuthorId: parentAuthorId, + author: author, + authorId: authorId, + parentAuthor: parentAuthor, + title: title, + likes: likes, + dislikes: dislikes, + mediaAttachments: mediaAttachments, + ); +} diff --git a/friendica_archive_browser/lib/src/friendica/services/friendica_archive_service.dart b/friendica_archive_browser/lib/src/friendica/services/friendica_archive_service.dart index 8af914d..0b6660b 100644 --- a/friendica_archive_browser/lib/src/friendica/services/friendica_archive_service.dart +++ b/friendica_archive_browser/lib/src/friendica/services/friendica_archive_service.dart @@ -1,23 +1,27 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart'; -import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart'; +import 'package:friendica_archive_browser/src/friendica/serializers/friendica_timeline_entry_serializer.dart'; import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/models/entry_tree_item.dart'; import 'package:friendica_archive_browser/src/models/local_image_archive_entry.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_connections.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_interface.dart'; +import 'package:friendica_archive_browser/src/services/connections_manager.dart'; import 'package:friendica_archive_browser/src/utils/exec_error.dart'; import 'package:path/path.dart' as p; import 'package:result_monad/result_monad.dart'; -class FriendicaArchiveService extends ChangeNotifier { +class FriendicaArchiveService implements ArchiveService { + @override final FriendicaPathMappingService pathMappingService; + final Map _imagesByRequestUrl = {}; - final List _postEntries = []; - final List _orphanedCommentEntries = []; - final List _allComments = []; - final FriendicaConnections connections = FriendicaConnections(); + final List _postEntries = []; + final List _orphanedCommentEntries = []; + final List _allComments = []; + @override + final ConnectionsManager connectionsManager = ConnectionsManager(); + String _ownersName = ''; FriendicaArchiveService({required this.pathMappingService}); @@ -34,14 +38,14 @@ class FriendicaArchiveService extends ChangeNotifier { } void clearCaches() { - connections.clearCaches(); + connectionsManager.clearCaches(); _imagesByRequestUrl.clear(); _orphanedCommentEntries.clear(); _allComments.clear(); _postEntries.clear(); } - FutureResult, ExecError> getPosts() async { + FutureResult, ExecError> getPosts() async { if (_postEntries.isEmpty && _allComments.isEmpty) { _loadEntries(); } @@ -49,7 +53,7 @@ class FriendicaArchiveService extends ChangeNotifier { return Result.ok(_postEntries); } - FutureResult, ExecError> getAllComments() async { + FutureResult, ExecError> getAllComments() async { if (_postEntries.isEmpty && _allComments.isEmpty) { _loadEntries(); } @@ -57,8 +61,7 @@ class FriendicaArchiveService extends ChangeNotifier { return Result.ok(_allComments); } - FutureResult, ExecError> - getOrphanedComments() async { + FutureResult, ExecError> getOrphanedComments() async { if (_postEntries.isEmpty && _allComments.isEmpty) { _loadEntries(); } @@ -84,27 +87,27 @@ class FriendicaArchiveService extends ChangeNotifier { final jsonFile = File(entriesJsonPath); if (jsonFile.existsSync()) { final json = jsonDecode(jsonFile.readAsStringSync()) as List; - final entries = - json.map((j) => FriendicaTimelineEntry.fromJson(j, connections)); + final entries = json + .map((j) => timelineEntryFromFriendicaJson(j, connectionsManager)); final topLevelEntries = entries.where((element) => element.parentId.isEmpty); final commentEntries = entries.where((element) => element.parentId.isNotEmpty).toList(); - final entryTrees = {}; + final entryTrees = {}; - final postTreeEntries = []; + final postTreeEntries = []; for (final entry in topLevelEntries) { - final treeEntry = FriendicaEntryTreeItem(entry, false); + final treeEntry = EntryTreeItem(entry, false); entryTrees[entry.id] = treeEntry; postTreeEntries.add(treeEntry); } - final commentTreeEntries = []; + final commentTreeEntries = []; commentEntries.sort( (c1, c2) => c1.creationTimestamp.compareTo(c2.creationTimestamp)); for (final entry in commentEntries) { final parent = entryTrees[entry.parentId]; - final treeEntry = FriendicaEntryTreeItem(entry, parent == null); + final treeEntry = EntryTreeItem(entry, parent == null); parent?.addChild(treeEntry); entryTrees[entry.id] = treeEntry; commentTreeEntries.add(treeEntry); diff --git a/friendica_archive_browser/lib/src/friendica/services/friendica_connections.dart b/friendica_archive_browser/lib/src/friendica/services/friendica_connections.dart deleted file mode 100644 index e3a36df..0000000 --- a/friendica_archive_browser/lib/src/friendica/services/friendica_connections.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; -import 'package:result_monad/result_monad.dart'; - -class FriendicaConnections { - final _connectionsById = {}; - final _connectionsByName = {}; - - void clearCaches() { - _connectionsById.clear(); - _connectionsByName.clear(); - } - - bool addConnection(Contact contact) { - if (_connectionsById.containsKey(contact.id)) { - return false; - } - _connectionsById[contact.id] = contact; - _connectionsByName[contact.name] = contact; - - return true; - } - - 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'); - } -} diff --git a/friendica_archive_browser/lib/src/friendica/services/friendica_path_mapping_service.dart b/friendica_archive_browser/lib/src/friendica/services/friendica_path_mapping_service.dart index 9412695..d189bd3 100644 --- a/friendica_archive_browser/lib/src/friendica/services/friendica_path_mapping_service.dart +++ b/friendica_archive_browser/lib/src/friendica/services/friendica_path_mapping_service.dart @@ -4,7 +4,9 @@ import 'package:friendica_archive_browser/src/settings/settings_controller.dart' import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; -class FriendicaPathMappingService { +import '../../services/path_mapper_service_interface.dart'; + +class FriendicaPathMappingService implements PathMappingService { static final _logger = Logger('$FriendicaPathMappingService'); final SettingsController settings; final _archiveDirectories = []; diff --git a/friendica_archive_browser/lib/src/home.dart b/friendica_archive_browser/lib/src/home.dart index 2adfae3..4196779 100644 --- a/friendica_archive_browser/lib/src/home.dart +++ b/friendica_archive_browser/lib/src/home.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_provider.dart'; import 'friendica/screens/entries_screen.dart'; import 'friendica/screens/stats_screen.dart'; @@ -10,7 +10,7 @@ import 'settings/settings_view.dart'; class Home extends StatefulWidget { final SettingsController settingsController; - final FriendicaArchiveService archiveService; + final ArchiveServiceProvider archiveService; const Home( {Key? key, diff --git a/friendica_archive_browser/lib/src/models/archive_types_enum.dart b/friendica_archive_browser/lib/src/models/archive_types_enum.dart new file mode 100644 index 0000000..5801882 --- /dev/null +++ b/friendica_archive_browser/lib/src/models/archive_types_enum.dart @@ -0,0 +1,5 @@ +enum ArchiveType { + unknown, + diaspora, + friendica, +} diff --git a/friendica_archive_browser/lib/src/models/connection.dart b/friendica_archive_browser/lib/src/models/connection.dart new file mode 100644 index 0000000..8ede8fc --- /dev/null +++ b/friendica_archive_browser/lib/src/models/connection.dart @@ -0,0 +1,46 @@ +class Connection { + final ConnectionStatus status; + + final String name; + + final String id; + + final Uri profileUrl; + + final String network; + + Connection( + {this.status = ConnectionStatus.none, + this.name = '', + this.id = '', + profileUrl, + this.network = ''}) + : profileUrl = profileUrl ?? Uri(); + + @override + String toString() { + return 'Connection{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network}'; + } +} + +enum ConnectionStatus { + youFollowThem, + theyFollowYou, + mutual, + none, +} + +extension FriendStatusWriter on ConnectionStatus { + String name() { + switch (this) { + case ConnectionStatus.youFollowThem: + return "You Follow Them"; + case ConnectionStatus.theyFollowYou: + return "They Follow You"; + case ConnectionStatus.mutual: + return "Follow each other"; + case ConnectionStatus.none: + return "Not connected"; + } + } +} diff --git a/friendica_archive_browser/lib/src/models/entry_tree_item.dart b/friendica_archive_browser/lib/src/models/entry_tree_item.dart new file mode 100644 index 0000000..b6b3cff --- /dev/null +++ b/friendica_archive_browser/lib/src/models/entry_tree_item.dart @@ -0,0 +1,18 @@ +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; + +class EntryTreeItem { + final TimelineEntry entry; + final bool isOrphaned; + + final _children = {}; + + EntryTreeItem(this.entry, this.isOrphaned); + + String get id => entry.id; + + void addChild(EntryTreeItem child) { + _children[child.id] = child; + } + + List get children => List.unmodifiable(_children.values); +} diff --git a/friendica_archive_browser/lib/src/models/friendica_contact.dart b/friendica_archive_browser/lib/src/models/friendica_contact.dart deleted file mode 100644 index 62aef8f..0000000 --- a/friendica_archive_browser/lib/src/models/friendica_contact.dart +++ /dev/null @@ -1,62 +0,0 @@ -class Contact { - final ConnectionStatus status; - - final String name; - - final String id; - - final Uri profileUrl; - - final String network; - - Contact( - {required this.status, - required this.name, - required this.id, - required this.profileUrl, - required this.network}); - - static Contact fromJson(Map json) { - final status = (json['following'] ?? '') == 'true' - ? ConnectionStatus.youFollowThem - : ConnectionStatus.none; - final name = json['name'] ?? ''; - final id = json['id_str'] ?? ''; - final profileUrl = Uri.parse(json['url'] ?? ''); - final network = json['network'] ?? 'unkn'; - - return Contact( - status: status, - name: name, - id: id, - profileUrl: profileUrl, - network: network); - } - - @override - String toString() { - return 'FriendicaContact{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network}'; - } -} - -enum ConnectionStatus { - youFollowThem, - theyFollowYou, - mutual, - none, -} - -extension FriendStatusWriter on ConnectionStatus { - String name() { - switch (this) { - case ConnectionStatus.youFollowThem: - return "You Follow Them"; - case ConnectionStatus.theyFollowYou: - return "They Follow You"; - case ConnectionStatus.mutual: - return "Follow each other"; - case ConnectionStatus.none: - return "Not connected"; - } - } -} diff --git a/friendica_archive_browser/lib/src/friendica/models/location_data.dart b/friendica_archive_browser/lib/src/models/location_data.dart similarity index 100% rename from friendica_archive_browser/lib/src/friendica/models/location_data.dart rename to friendica_archive_browser/lib/src/models/location_data.dart diff --git a/friendica_archive_browser/lib/src/friendica/models/friendica_media_attachment.dart b/friendica_archive_browser/lib/src/models/media_attachment.dart similarity index 61% rename from friendica_archive_browser/lib/src/friendica/models/friendica_media_attachment.dart rename to friendica_archive_browser/lib/src/models/media_attachment.dart index a80e21f..a3c92a8 100644 --- a/friendica_archive_browser/lib/src/friendica/models/friendica_media_attachment.dart +++ b/friendica_archive_browser/lib/src/models/media_attachment.dart @@ -1,12 +1,12 @@ import 'dart:io'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart'; import 'model_utils.dart'; -enum FriendicaAttachmentMediaType { unknown, image, video } +enum AttachmentMediaType { unknown, image, video } -class FriendicaMediaAttachment { +class MediaAttachment { static final _graphicsExtensions = ['jpg', 'png', 'gif', 'tif']; static final _movieExtensions = ['avi', 'mp4', 'mpg', 'wmv']; @@ -16,7 +16,7 @@ class FriendicaMediaAttachment { final Map metadata; - final FriendicaAttachmentMediaType explicitType; + final AttachmentMediaType explicitType; final Uri thumbnailUri; @@ -24,7 +24,7 @@ class FriendicaMediaAttachment { final String description; - FriendicaMediaAttachment( + MediaAttachment( {required this.uri, required this.creationTimestamp, required this.metadata, @@ -33,16 +33,16 @@ class FriendicaMediaAttachment { required this.explicitType, required this.description}); - FriendicaMediaAttachment.randomBuilt() + MediaAttachment.randomBuilt() : uri = Uri.parse('http://localhost/${randomId()}'), creationTimestamp = DateTime.now().millisecondsSinceEpoch, title = 'Random title ${randomId()}', thumbnailUri = Uri.parse('${randomId()}.jpg'), description = 'Random description ${randomId()}', - explicitType = FriendicaAttachmentMediaType.image, + explicitType = AttachmentMediaType.image, metadata = {'value1': randomId(), 'value2': randomId()}; - FriendicaMediaAttachment.fromUriOnly(this.uri) + MediaAttachment.fromUriOnly(this.uri) : creationTimestamp = 0, thumbnailUri = Uri.file(''), title = '', @@ -50,18 +50,18 @@ class FriendicaMediaAttachment { description = '', metadata = {}; - FriendicaMediaAttachment.fromUriAndTime(this.uri, this.creationTimestamp) + MediaAttachment.fromUriAndTime(this.uri, this.creationTimestamp) : thumbnailUri = Uri.file(''), title = '', explicitType = mediaTypeFromString(uri.path), description = '', metadata = {}; - FriendicaMediaAttachment.blank() + MediaAttachment.blank() : uri = Uri(), creationTimestamp = 0, thumbnailUri = Uri.file(''), - explicitType = FriendicaAttachmentMediaType.unknown, + explicitType = AttachmentMediaType.unknown, title = '', description = '', metadata = {}; @@ -71,7 +71,7 @@ class FriendicaMediaAttachment { return 'FriendicaMediaAttachment{uri: $uri, creationTimestamp: $creationTimestamp, type: $explicitType, metadata: $metadata, title: $title, description: $description}'; } - String toHumanString(FriendicaPathMappingService mapper) { + String toHumanString(PathMappingService mapper) { if (uri.scheme.startsWith('http')) { return uri.toString(); } @@ -79,20 +79,6 @@ class FriendicaMediaAttachment { return mapper.toFullPath(uri.toString()); } - FriendicaMediaAttachment.fromJson(Map json) - : uri = Uri.parse(json['url']), - creationTimestamp = 0, - metadata = (json['metadata'] as Map? ?? {}) - .map((key, value) => MapEntry(key, value.toString())), - explicitType = (json['mimetype'] ?? '').startsWith('image') - ? FriendicaAttachmentMediaType.image - : (json['mimetype'] ?? '').startsWith('video') - ? FriendicaAttachmentMediaType.video - : FriendicaAttachmentMediaType.unknown, - thumbnailUri = Uri(), - title = '', - description = ''; - Map toJson() => { 'uri': uri.toString(), 'creationTimestamp': creationTimestamp, @@ -103,25 +89,25 @@ class FriendicaMediaAttachment { 'description': description, }; - static FriendicaAttachmentMediaType mediaTypeFromString(String path) { + static AttachmentMediaType mediaTypeFromString(String path) { final separator = Platform.isWindows ? '\\' : '/'; final lastSlash = path.lastIndexOf(separator) + 1; final filename = path.substring(lastSlash); final lastPeriod = filename.lastIndexOf('.') + 1; if (lastPeriod == 0) { - return FriendicaAttachmentMediaType.unknown; + return AttachmentMediaType.unknown; } final extension = filename.substring(lastPeriod).toLowerCase(); if (_graphicsExtensions.contains(extension)) { - return FriendicaAttachmentMediaType.image; + return AttachmentMediaType.image; } if (_movieExtensions.contains(extension)) { - return FriendicaAttachmentMediaType.video; + return AttachmentMediaType.video; } - return FriendicaAttachmentMediaType.unknown; + return AttachmentMediaType.unknown; } } diff --git a/friendica_archive_browser/lib/src/friendica/models/model_utils.dart b/friendica_archive_browser/lib/src/models/model_utils.dart similarity index 100% rename from friendica_archive_browser/lib/src/friendica/models/model_utils.dart rename to friendica_archive_browser/lib/src/models/model_utils.dart diff --git a/friendica_archive_browser/lib/src/models/time_element.dart b/friendica_archive_browser/lib/src/models/time_element.dart index bb30841..a905fa0 100644 --- a/friendica_archive_browser/lib/src/models/time_element.dart +++ b/friendica_archive_browser/lib/src/models/time_element.dart @@ -1,8 +1,8 @@ -import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; class TimeElement { final DateTime timestamp; - final FriendicaTimelineEntry entry; + final TimelineEntry entry; TimeElement({int timeInMS = 0, required this.entry}) : timestamp = DateTime.fromMillisecondsSinceEpoch(timeInMS); diff --git a/friendica_archive_browser/lib/src/models/timeline_entry.dart b/friendica_archive_browser/lib/src/models/timeline_entry.dart new file mode 100644 index 0000000..cad73df --- /dev/null +++ b/friendica_archive_browser/lib/src/models/timeline_entry.dart @@ -0,0 +1,160 @@ +import 'package:friendica_archive_browser/src/models/connection.dart'; +import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart'; +import 'package:intl/intl.dart'; + +import 'location_data.dart'; +import 'media_attachment.dart'; +import 'model_utils.dart'; + +class TimelineEntry { + final String id; + + final String parentId; + + final String parentAuthor; + + final String parentAuthorId; + + final int creationTimestamp; + + final int backdatedTimestamp; + + final int modificationTimestamp; + + final String body; + + final String title; + + final bool isReshare; + + final String author; + + final String authorId; + + final String externalLink; + + final List mediaAttachments; + + final LocationData locationData; + + final List likes; + + final List dislikes; + + TimelineEntry({ + this.id = '', + this.parentId = '', + this.creationTimestamp = 0, + this.backdatedTimestamp = 0, + this.modificationTimestamp = 0, + this.isReshare = false, + this.body = '', + this.title = '', + this.author = '', + this.authorId = '', + this.parentAuthor = '', + this.parentAuthorId = '', + this.externalLink = '', + this.locationData = const LocationData(), + this.likes = const [], + this.dislikes = const [], + List? mediaAttachments, + }) : mediaAttachments = mediaAttachments ?? []; + + TimelineEntry.randomBuilt() + : creationTimestamp = DateTime.now().millisecondsSinceEpoch, + backdatedTimestamp = DateTime.now().millisecondsSinceEpoch, + modificationTimestamp = DateTime.now().millisecondsSinceEpoch, + id = randomId(), + isReshare = false, + parentId = randomId(), + externalLink = 'Random external link ${randomId()}', + body = 'Random post text ${randomId()}', + title = 'Random title ${randomId()}', + author = 'Random author ${randomId()}', + authorId = 'Random authorId ${randomId()}', + parentAuthor = 'Random parent author ${randomId()}', + parentAuthorId = 'Random parent author id ${randomId()}', + locationData = LocationData.randomBuilt(), + likes = const [], + dislikes = const [], + mediaAttachments = [ + MediaAttachment.randomBuilt(), + MediaAttachment.randomBuilt() + ]; + + TimelineEntry copy( + {int? creationTimestamp, + int? backdatedTimestamp, + int? modificationTimestamp, + bool? isReshare, + String? id, + String? parentId, + String? externalLink, + String? body, + String? title, + String? author, + String? authorId, + String? parentAuthor, + String? parentAuthorId, + LocationData? locationData, + List? mediaAttachments, + List? likes, + List? dislikes, + List? links}) { + return TimelineEntry( + creationTimestamp: creationTimestamp ?? this.creationTimestamp, + backdatedTimestamp: backdatedTimestamp ?? this.backdatedTimestamp, + modificationTimestamp: + modificationTimestamp ?? this.modificationTimestamp, + id: id ?? this.id, + isReshare: isReshare ?? this.isReshare, + parentId: parentId ?? this.parentId, + externalLink: externalLink ?? this.externalLink, + body: body ?? this.body, + title: title ?? this.title, + author: author ?? this.author, + authorId: authorId ?? this.authorId, + parentAuthor: parentAuthor ?? this.parentAuthor, + parentAuthorId: parentAuthorId ?? this.parentAuthorId, + locationData: locationData ?? this.locationData, + mediaAttachments: mediaAttachments ?? this.mediaAttachments, + likes: likes ?? this.likes, + dislikes: dislikes ?? this.dislikes); + } + + @override + String toString() { + return 'TimelineEntry{id: $id, isReshare: $isReshare, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor mediaAttachments: $mediaAttachments, externalLink:$externalLink}'; + } + + String toHumanString(PathMappingService mapper, DateFormat formatter) { + final creationDateString = formatter.format( + DateTime.fromMillisecondsSinceEpoch(creationTimestamp * 1000) + .toLocal()); + return [ + 'Title: $title', + 'Creation At: $creationDateString', + 'Text:', + 'Author: $author', + 'Reshare: $isReshare', + if (externalLink.isNotEmpty) 'External Link: $externalLink', + body, + '', + if (parentId.isNotEmpty) + "Comment on post/comment by ${parentAuthor.isNotEmpty ? parentAuthor : 'unknown author'}", + '', + if (mediaAttachments.isNotEmpty) 'Photos and Videos:', + ...mediaAttachments.map((e) => e.toHumanString(mapper)), + if (locationData.hasPosition) locationData.toHumanString(), + ].join('\n'); + } + + bool hasImages() => mediaAttachments + .where((element) => element.explicitType == AttachmentMediaType.image) + .isNotEmpty; + + bool hasVideos() => mediaAttachments + .where((element) => element.explicitType == AttachmentMediaType.video) + .isNotEmpty; +} diff --git a/friendica_archive_browser/lib/src/services/archive_service_interface.dart b/friendica_archive_browser/lib/src/services/archive_service_interface.dart new file mode 100644 index 0000000..a4b994c --- /dev/null +++ b/friendica_archive_browser/lib/src/services/archive_service_interface.dart @@ -0,0 +1,31 @@ +import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../models/entry_tree_item.dart'; +import '../models/local_image_archive_entry.dart'; +import '../utils/exec_error.dart'; +import 'connections_manager.dart'; + +class ArchiveService { + ConnectionsManager get connectionsManager => + throw Exception('Not implemented'); + + String get ownersName => throw Exception('Not implemented'); + + PathMappingService get pathMappingService => + throw Exception('Not Implemented'); + + void clearCaches() => throw Exception('Not implemented'); + + FutureResult, ExecError> getPosts() async => + throw Exception('Not implemented'); + + FutureResult, ExecError> getAllComments() => + throw Exception('Not implemented'); + + FutureResult, ExecError> getOrphanedComments() => + throw Exception('Not implemented'); + + Result getImageByUrl(String url) => + throw Exception('Not implemented'); +} diff --git a/friendica_archive_browser/lib/src/services/archive_service_provider.dart b/friendica_archive_browser/lib/src/services/archive_service_provider.dart new file mode 100644 index 0000000..20d305e --- /dev/null +++ b/friendica_archive_browser/lib/src/services/archive_service_provider.dart @@ -0,0 +1,70 @@ +import 'package:flutter/cupertino.dart'; +import 'package:friendica_archive_browser/src/diaspora/services/diaspora_archive_service.dart'; +import 'package:friendica_archive_browser/src/diaspora/services/diaspora_path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart'; +import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart'; +import 'package:friendica_archive_browser/src/services/archive_service_interface.dart'; +import 'package:friendica_archive_browser/src/services/connections_manager.dart'; +import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart'; +import 'package:friendica_archive_browser/src/settings/settings_controller.dart'; +import 'package:result_monad/result_monad.dart'; + +import '../models/archive_types_enum.dart'; +import '../models/entry_tree_item.dart'; +import '../models/local_image_archive_entry.dart'; +import '../utils/exec_error.dart'; + +class ArchiveServiceProvider extends ChangeNotifier implements ArchiveService { + final SettingsController settings; + late DiasporaArchiveService _diasporaArchiveService; + late FriendicaArchiveService _friendicaArchiveService; + + @override + ConnectionsManager get connectionsManager => + _archiveService.connectionsManager; + + ArchiveServiceProvider(this.settings) { + _diasporaArchiveService = DiasporaArchiveService( + pathMappingService: DiasporaPathMappingService(settings)); + _friendicaArchiveService = FriendicaArchiveService( + pathMappingService: FriendicaPathMappingService(settings)); + } + + String get ownersName => _archiveService.ownersName; + + void clearCaches() { + _friendicaArchiveService.clearCaches(); + _diasporaArchiveService.clearCaches(); + } + + FutureResult, ExecError> getPosts() async { + return _archiveService.getPosts(); + } + + FutureResult, ExecError> getAllComments() async { + return _archiveService.getAllComments(); + } + + FutureResult, ExecError> getOrphanedComments() async { + return _archiveService.getOrphanedComments(); + } + + Result getImageByUrl(String url) { + return _archiveService.getImageByUrl(url); + } + + ArchiveService get _archiveService { + switch (settings.archiveType) { + case ArchiveType.diaspora: + return _diasporaArchiveService; + case ArchiveType.friendica: + return _friendicaArchiveService; + default: + throw Exception('Unknown archive type'); + } + } + + @override + PathMappingService get pathMappingService => + _archiveService.pathMappingService; +} diff --git a/friendica_archive_browser/lib/src/services/connections_manager.dart b/friendica_archive_browser/lib/src/services/connections_manager.dart new file mode 100644 index 0000000..137fccd --- /dev/null +++ b/friendica_archive_browser/lib/src/services/connections_manager.dart @@ -0,0 +1,46 @@ +import 'package:friendica_archive_browser/src/models/connection.dart'; +import 'package:result_monad/result_monad.dart'; + +class ConnectionsManager { + final _connectionsById = {}; + final _connectionsByName = {}; + + int get length => _connectionsById.length; + + void clearCaches() { + _connectionsById.clear(); + _connectionsByName.clear(); + } + + bool addConnection(Connection connection) { + if (_connectionsById.containsKey(connection.id)) { + return false; + } + _connectionsById[connection.id] = connection; + _connectionsByName[connection.name] = 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'); + } +} diff --git a/friendica_archive_browser/lib/src/services/path_mapper_service_interface.dart b/friendica_archive_browser/lib/src/services/path_mapper_service_interface.dart new file mode 100644 index 0000000..86466d3 --- /dev/null +++ b/friendica_archive_browser/lib/src/services/path_mapper_service_interface.dart @@ -0,0 +1,12 @@ +import 'dart:io'; + +class PathMappingService { + String get rootFolder => throw Exception('Not implemented'); + + List get archiveDirectories => + throw Exception('Not implemented'); + + void refresh() => throw Exception('Not implemented'); + + String toFullPath(String relPath) => throw Exception('Not implemented'); +} diff --git a/friendica_archive_browser/lib/src/settings/settings_controller.dart b/friendica_archive_browser/lib/src/settings/settings_controller.dart index 0b746d4..c7fa89b 100644 --- a/friendica_archive_browser/lib/src/settings/settings_controller.dart +++ b/friendica_archive_browser/lib/src/settings/settings_controller.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:friendica_archive_browser/src/models/archive_types_enum.dart'; import 'package:friendica_archive_browser/src/settings/video_player_settings.dart'; import 'package:friendica_archive_browser/src/utils/temp_file_builder.dart'; import 'package:intl/intl.dart'; @@ -18,9 +19,12 @@ class SettingsController with ChangeNotifier { : _settingsService = SettingsService(); Future loadSettings() async { + _archiveType = await _settingsService.archiveType(); _themeMode = await _settingsService.themeMode(); _rootFolder = await _settingsService.rootFolder(); - var canReadRootDir = runCatching(()=>Result.ok(Directory(_rootFolder).listSync())).isSuccess; + var canReadRootDir = + runCatching(() => Result.ok(Directory(_rootFolder).listSync())) + .isSuccess; if (!canReadRootDir) { _rootFolder = ''; } @@ -74,6 +78,17 @@ class SettingsController with ChangeNotifier { await _settingsService.updateRootFolder(newPath); } + late ArchiveType _archiveType; + + ArchiveType get archiveType => _archiveType; + + Future updateArchiveType(ArchiveType newArchiveType) async { + if (newArchiveType == _archiveType) return; + _archiveType = newArchiveType; + notifyListeners(); + await _settingsService.updateArchiveType(newArchiveType); + } + late ThemeMode _themeMode; ThemeMode get themeMode => _themeMode; diff --git a/friendica_archive_browser/lib/src/settings/settings_service.dart b/friendica_archive_browser/lib/src/settings/settings_service.dart index 1e5cea3..92406cd 100644 --- a/friendica_archive_browser/lib/src/settings/settings_service.dart +++ b/friendica_archive_browser/lib/src/settings/settings_service.dart @@ -1,12 +1,14 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:friendica_archive_browser/src/models/archive_types_enum.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'video_player_settings.dart'; class SettingsService { + static const archiveTypeKey = "archiveType"; static const themeDarknessKey = 'themeDarkness'; static const rootFolderKey = 'rootFolder'; static const videoPlayerSettingTypeKey = 'videoPlayerSettingType'; @@ -45,6 +47,22 @@ class SettingsService { prefs.setInt(themeDarknessKey, theme.index); } + Future archiveType() async { + final prefs = await SharedPreferences.getInstance(); + final archiveTypeIndex = prefs.getInt(archiveTypeKey) ?? 0; + if (archiveTypeIndex > ArchiveType.values.length - 1 || + archiveTypeIndex < 0) { + return ArchiveType.unknown; + } + + return ArchiveType.values[archiveTypeIndex]; + } + + Future updateArchiveType(ArchiveType archiveType) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setInt(archiveTypeKey, archiveType.index); + } + Future rootFolder() async { final prefs = await SharedPreferences.getInstance(); final result = prefs.getString(rootFolderKey) ?? ''; diff --git a/friendica_archive_browser/lib/src/settings/settings_view.dart b/friendica_archive_browser/lib/src/settings/settings_view.dart index 64cda30..5ac969a 100644 --- a/friendica_archive_browser/lib/src/settings/settings_view.dart +++ b/friendica_archive_browser/lib/src/settings/settings_view.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:friendica_archive_browser/src/models/archive_types_enum.dart'; import 'package:friendica_archive_browser/src/settings/video_player_settings.dart'; import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart'; import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart'; @@ -157,6 +158,20 @@ class _SettingsViewState extends State { Text('Archive Folder: ', style: Theme.of(context).textTheme.bodyText1), const SizedBox(width: 10), + DropdownButton( + value: widget._settingsController.archiveType, + onChanged: (newArchiveType) async { + await widget._settingsController + .updateArchiveType(newArchiveType!); + setState(() {}); + }, + items: ArchiveType.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.name), + )) + .toList(), + ), Expanded( child: TextField( controller: _folderPathController, diff --git a/friendica_archive_browser/lib/src/utils/offsetdatetime_utils.dart b/friendica_archive_browser/lib/src/utils/offsetdatetime_utils.dart index 9edf531..8f05822 100644 --- a/friendica_archive_browser/lib/src/utils/offsetdatetime_utils.dart +++ b/friendica_archive_browser/lib/src/utils/offsetdatetime_utils.dart @@ -3,12 +3,26 @@ import 'package:result_monad/result_monad.dart'; import 'package:time_machine/time_machine_text_patterns.dart'; class OffsetDateTimeUtils { - static final _parser = OffsetDateTimePattern.createWithInvariantCulture( - 'ddd MMM dd HH:mm:ss o<+HHmm> yyyy'); + static final _offsetTimeParser = + OffsetDateTimePattern.createWithInvariantCulture( + 'ddd MMM dd HH:mm:ss o<+HHmm> yyyy'); static Result epochSecTimeFromFriendicaString( String dateString) { - final offsetDateTime = _parser.parse(dateString); + final offsetDateTime = _offsetTimeParser.parse(dateString); + if (!offsetDateTime.success) { + return Result.error(ExecError.message(offsetDateTime.error.toString())); + } + + return Result.ok(offsetDateTime.value.localDateTime + .toDateTimeLocal() + .millisecondsSinceEpoch ~/ + 1000); + } + + static Result epochSecTimeFromTimeZoneString( + String dateString) { + final offsetDateTime = OffsetDateTimePattern.generalIso.parse(dateString); if (!offsetDateTime.success) { return Result.error(ExecError.message(offsetDateTime.error.toString())); } diff --git a/friendica_archive_browser/lib/src/utils/top_interactors_generator.dart b/friendica_archive_browser/lib/src/utils/top_interactors_generator.dart index 4270dd1..78b1eae 100644 --- a/friendica_archive_browser/lib/src/utils/top_interactors_generator.dart +++ b/friendica_archive_browser/lib/src/utils/top_interactors_generator.dart @@ -1,6 +1,6 @@ -import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart'; -import 'package:friendica_archive_browser/src/models/friendica_contact.dart'; -import 'package:friendica_archive_browser/src/friendica/services/friendica_connections.dart'; +import 'package:friendica_archive_browser/src/models/connection.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; +import 'package:friendica_archive_browser/src/services/connections_manager.dart'; class TopInteractorsGenerator { final _interactors = {}; @@ -11,8 +11,7 @@ class TopInteractorsGenerator { _processedEntryIds.clear(); } - void processEntry( - FriendicaTimelineEntry item, FriendicaConnections contacts) { + void processEntry(TimelineEntry item, ConnectionsManager contacts) { if (_processedEntryIds.contains(item.id)) { return; } @@ -59,14 +58,14 @@ class TopInteractorsGenerator { } InteractorItem _getInteractorItemById( - String id, FriendicaConnections contacts) { + String id, ConnectionsManager contacts) { if (_interactors.containsKey(id)) { return _interactors[id]!; } final contact = contacts.getById(id).fold( onSuccess: (contact) => contact, - onError: (error) => Contact( + onError: (error) => Connection( status: ConnectionStatus.none, name: '', id: id, @@ -77,7 +76,7 @@ class TopInteractorsGenerator { } class InteractorItem { - final Contact contact; + final Connection contact; final int resharedOrCommentedOn; final int likeCount; final int dislikeCount; @@ -94,7 +93,7 @@ class InteractorItem { } InteractorItem copy( - {Contact? contact, + {Connection? contact, int? resharedOrCommentedOn, int? likeCount, int? dislikeCount}) { diff --git a/friendica_archive_browser/pubspec.lock b/friendica_archive_browser/pubspec.lock index 9b70db8..85e5e80 100644 --- a/friendica_archive_browser/pubspec.lock +++ b/friendica_archive_browser/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" async: dependency: transitive description: @@ -217,6 +224,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + markdown: + dependency: "direct main" + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" matcher: dependency: transitive description: diff --git a/friendica_archive_browser/pubspec.yaml b/friendica_archive_browser/pubspec.yaml index 9eaadfe..b2aacfd 100644 --- a/friendica_archive_browser/pubspec.yaml +++ b/friendica_archive_browser/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: logging: ^1.0.2 latlng: ^0.1.0 map: ^1.0.0 + markdown: ^4.0.1 metadata_fetch: ^0.4.1 multi_split_view: ^1.10.0+1 path: ^1.8.0 diff --git a/friendica_archive_browser/test/additional_key_logger_test.dart b/friendica_archive_browser/test/additional_key_logger_test.dart index 07efc65..e51c81a 100644 --- a/friendica_archive_browser/test/additional_key_logger_test.dart +++ b/friendica_archive_browser/test/additional_key_logger_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: avoid_print import 'package:flutter_test/flutter_test.dart'; -import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart'; +import 'package:friendica_archive_browser/src/models/model_utils.dart'; import 'package:logging/logging.dart'; void main() { diff --git a/friendica_archive_browser/test/disapora_profile_json_reader_test.dart b/friendica_archive_browser/test/disapora_profile_json_reader_test.dart index b7366ee..3e1c465 100644 --- a/friendica_archive_browser/test/disapora_profile_json_reader_test.dart +++ b/friendica_archive_browser/test/disapora_profile_json_reader_test.dart @@ -2,13 +2,32 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:friendica_archive_browser/src/diaspora/services/diaspora_profile_json_reader.dart'; +import 'package:friendica_archive_browser/src/models/timeline_entry.dart'; +import 'package:friendica_archive_browser/src/services/connections_manager.dart'; + +const jsonPath = '/Users/hankdev/Desktop/diaspora_pretty.json'; void main() { test('Diaspora Connections Test', () { - final reader = DiasporaProfileJsonReader( - '/Users/hankdev/Desktop/diaspora_pretty.json'); + final reader = DiasporaProfileJsonReader(jsonPath, ConnectionsManager()); final contacts = reader.readContacts(); print(contacts.length); print(contacts.first); }); + + test('Diaspora Posts Test', () { + final reader = DiasporaProfileJsonReader(jsonPath, ConnectionsManager()); + final posts = reader.readPosts(); + + print(posts.length); + print(posts.first); + + final postsWithImage = posts.firstWhere((element) => element.mediaAttachments.isNotEmpty, orElse: ()=>TimelineEntry()); + print(postsWithImage); + + final resharePost = posts.firstWhere((element) => element.externalLink.isNotEmpty, orElse: ()=>TimelineEntry()); + print(resharePost); + + }); + }