D* (minus photos) and Friendica loading in UI.

This commit is contained in:
Hank Grabowski 2022-03-08 16:22:17 -05:00
parent b8b40ebe5b
commit 1deaebd94e
47 changed files with 970 additions and 581 deletions

View file

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

View file

@ -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<TimeElement> entries;
final FriendicaConnections connections;
final ConnectionsManager connections;
const TopInteractorsWidget(this.entries, this.connections, {Key? key})
: super(key: key);

View file

@ -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<String, dynamic> json) {
Connection contactFromDiasporaJson(Map<String, dynamic> json) {
const network = "Diaspora";
final accountId = json['account_id'] ?? '';
final profileUrl = _profileUrlFromAccountId(accountId);
@ -17,7 +17,7 @@ Contact friendicaContactFromDiasporaJson(Map<String, dynamic> json) {
status = ConnectionStatus.theyFollowYou;
}
return Contact(
return Connection(
status: status,
name: name,
id: id,

View file

@ -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<TimelineEntry, ExecError> timelineItemFromDiasporaPostJson(
Map<String, dynamic> 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<TimelineEntry, ExecError> _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<TimelineEntry, ExecError> _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';
}

View file

@ -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<String, ImageEntry> _imagesByRequestUrl = {};
final List<FriendicaEntryTreeItem> _postEntries = [];
final List<FriendicaEntryTreeItem> _orphanedCommentEntries = [];
final List<FriendicaEntryTreeItem> _allComments = [];
final FriendicaConnections connections = FriendicaConnections();
final List<EntryTreeItem> _postEntries = [];
final List<EntryTreeItem> _orphanedCommentEntries = [];
final List<EntryTreeItem> _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<List<FriendicaEntryTreeItem>, ExecError> getPosts() async {
FutureResult<List<EntryTreeItem>, ExecError> getPosts() async {
if (_postEntries.isEmpty && _allComments.isEmpty) {
_loadEntries();
_loadProfileFile();
}
return Result.ok(_postEntries);
}
FutureResult<List<FriendicaEntryTreeItem>, ExecError> getAllComments() async {
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() async {
if (_postEntries.isEmpty && _allComments.isEmpty) {
_loadEntries();
_loadProfileFile();
}
return Result.ok(_allComments);
}
FutureResult<List<FriendicaEntryTreeItem>, ExecError>
getOrphanedComments() async {
FutureResult<List<EntryTreeItem>, 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<dynamic>;
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 = <String, FriendicaEntryTreeItem>{};
final postTreeEntries = <FriendicaEntryTreeItem>[];
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 = <FriendicaEntryTreeItem>[];
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));
}
}

View file

@ -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 = <FileSystemEntity>[];
DiasporaPathMappingService(this.settings) {
refresh();
}
@override
String get rootFolder => settings.rootFolder;
@override
List<FileSystemEntity> 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;
}
}

View file

@ -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 = <String, dynamic>{};
DiasporaProfileJsonReader(this.jsonFilePath);
DiasporaProfileJsonReader(this.jsonFilePath, this.connectionsManager);
Map<String, dynamic> get jsonData {
if (_jsonData.isNotEmpty) {
return _jsonData;
}
List<Contact> readContacts() {
final jsonFile = File(jsonFilePath);
if (jsonFile.existsSync()) {
final json =
jsonDecode(jsonFile.readAsStringSync()) as Map<String, dynamic>;
final contactsJson = json['user']?['contacts'] as List<dynamic>;
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<Connection> 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<dynamic>;
final contacts = contactsJson.map((j) => contactFromDiasporaJson(j));
connectionsManager.addAllConnections(contacts);
return contacts.toList();
}
List<TimelineEntry> readPosts() {
if (connectionsManager.length == 0) {
readContacts();
}
final json = jsonData;
final postsJson = json['user']?['posts'] as List<dynamic>;
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;
}
}

View file

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

View file

@ -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<FriendicaTimelineEntry> posts;
final List<TimelineEntry> posts;
final Offset pos;
final Color color;

View file

@ -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<FriendicaMediaAttachment> mediaAttachments;
final List<MediaAttachment> 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<FriendicaPathMappingService>(context);
final archiveService = Provider.of<FriendicaArchiveService>(context);
final archiveService = Provider.of<ArchiveServiceProvider>(context);
final settingsController = Provider.of<SettingsController>(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(

View file

@ -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<SettingsController>(context);
final pathMapper = Provider.of<FriendicaPathMappingService>(context);
final archiveService = Provider.of<FriendicaArchiveService>(context);
final archiveService = Provider.of<ArchiveServiceProvider>(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);
}
}

View file

@ -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<SettingsController>(context).dateTimeFormatter;
final mapper = Provider.of<FriendicaPathMappingService>(context);
final archiveService = Provider.of<ArchiveServiceProvider>(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),

View file

@ -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 = <String, FriendicaEntryTreeItem>{};
FriendicaEntryTreeItem(this.entry, this.isOrphaned);
String get id => entry.id;
void addChild(FriendicaEntryTreeItem child) {
_children[child.id] = child;
}
List<FriendicaEntryTreeItem> get children =>
List.unmodifiable(_children.values);
}

View file

@ -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<FriendicaMediaAttachment> mediaAttachments;
final LocationData locationData;
final List<Uri> links;
final List<Contact> likes;
final List<Contact> 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 <Contact>[],
this.dislikes = const <Contact>[],
List<FriendicaMediaAttachment>? mediaAttachments,
List<Uri>? links})
: mediaAttachments = mediaAttachments ?? <FriendicaMediaAttachment>[],
links = links ?? <Uri>[];
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 <Contact>[],
dislikes = const <Contact>[],
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<FriendicaMediaAttachment>? mediaAttachments,
List<Contact>? likes,
List<Contact>? dislikes,
List<Uri>? 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<String, dynamic> 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 = <Uri>[];
final mediaAttachments = (json['attachments'] as List<dynamic>? ?? [])
.map((j) => FriendicaMediaAttachment.fromJson(j))
.toList();
final likes =
(json['friendica_activities']?['like'] as List<dynamic>? ?? [])
.map((json) => Contact.fromJson(json))
.toList();
final dislikes =
(json['friendica_activities']?['dislike'] as List<dynamic>? ?? [])
.map((json) => Contact.fromJson(json))
.toList();
final announce =
(json['friendica_activities']?['announce'] as List<dynamic>? ?? [])
.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,
);
}
}

View file

@ -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<List<FriendicaEntryTreeItem>, ExecError> Function()
populator;
final FutureResult<List<EntryTreeItem>, 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<SettingsController>(context);
return FutureBuilder<Result<List<FriendicaEntryTreeItem>, ExecError>>(
return FutureBuilder<Result<List<EntryTreeItem>, 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<FriendicaEntryTreeItem> posts;
final List<EntryTreeItem> 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<FriendicaEntryTreeItem, dynamic>(
return FilterControl<EntryTreeItem, dynamic>(
allItems: posts,
commentsOnlyFilterFunction: (post) => post.children.isNotEmpty,
imagesOnlyFilterFunction: (post) => post.entry.hasImages(),

View file

@ -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<FriendicaArchiveService>(context);
final service = Provider.of<ArchiveServiceProvider>(context);
return FutureBuilder<Result<List<FriendicaEntryTreeItem>, ExecError>>(
return FutureBuilder<Result<List<EntryTreeItem>, 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<FriendicaTimelineEntry> posts;
final List<TimelineEntry> posts;
const GeospatialView({Key? key, required this.posts}) : super(key: key);
@ -88,8 +88,8 @@ class _GeospatialViewState extends State<GeospatialView> {
);
Offset? dragStart;
final postsInList = <FriendicaTimelineEntry>[];
final postsInView = <FriendicaTimelineEntry>[];
final postsInList = <TimelineEntry>[];
final postsInView = <TimelineEntry>[];
double scaleStart = 1.0;
@override
@ -203,8 +203,8 @@ class _GeospatialViewState extends State<GeospatialView> {
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<GeospatialView> {
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<SettingsController>(context);
final shouldDebugCache =
@ -332,8 +332,8 @@ class _GeospatialViewState extends State<GeospatialView> {
);
}
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,

View file

@ -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<FriendicaMediaAttachment> mediaAttachments;
final List<MediaAttachment> mediaAttachments;
final int initialIndex;
const MediaSlideShowScreen(
@ -27,7 +27,7 @@ class MediaSlideShowScreen extends StatefulWidget {
class _MediaSlideShowScreenState extends State<MediaSlideShowScreen> {
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<MediaSlideShowScreen> {
}
Future<void> _saveFile(BuildContext context) async {
final pathMapper = Provider.of<FriendicaPathMappingService>(context, listen: false);
final pathMapper =
Provider.of<FriendicaPathMappingService>(context, listen: false);
final filename = media.uri.pathSegments.last;
final initialPath = pathMapper.toFullPath(media.uri.toFilePath());

View file

@ -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<StatsScreen> {
static final _logger = Logger("$_StatsScreenState");
FriendicaArchiveService? archiveDataService;
ArchiveServiceProvider? archiveDataService;
final allItems = <TimeElement>[];
StatType statType = StatType.selectType;
bool hasText = true;
@ -78,7 +78,7 @@ class _StatsScreenState extends State<StatsScreen> {
@override
Widget build(BuildContext context) {
archiveDataService = Provider.of<FriendicaArchiveService>(context);
archiveDataService = Provider.of<ArchiveServiceProvider>(context);
return FilterControl<TimeElement, dynamic>(
allItems: allItems,
@ -147,7 +147,7 @@ class _StatsScreenState extends State<StatsScreen> {
child: Column(children: [
..._buildGraphScreens(context, items),
const Divider(),
TopInteractorsWidget(items, archiveDataService!.connections),
TopInteractorsWidget(items, archiveDataService!.connectionsManager),
const Divider(),
WordFrequencyWidget(items),
]),

View file

@ -0,0 +1,18 @@
import '../../models/connection.dart';
Connection contactFromFriendicaJson(Map<String, dynamic> 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);
}

View file

@ -0,0 +1,25 @@
import '../../models/media_attachment.dart';
MediaAttachment mediaAttachmentfromFriendicaJson(Map<String, dynamic> json) {
final uri = Uri.parse(json['url']);
const creationTimestamp = 0;
final metadata = (json['metadata'] as Map<String, dynamic>? ?? {})
.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);
}

View file

@ -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<String, dynamic> 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<dynamic>? ?? [])
.map((j) => mediaAttachmentfromFriendicaJson(j))
.toList();
final likes = (json['friendica_activities']?['like'] as List<dynamic>? ?? [])
.map((json) => contactFromFriendicaJson(json))
.toList();
final dislikes =
(json['friendica_activities']?['dislike'] as List<dynamic>? ?? [])
.map((json) => contactFromFriendicaJson(json))
.toList();
final announce =
(json['friendica_activities']?['announce'] as List<dynamic>? ?? [])
.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,
);
}

View file

@ -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<String, ImageEntry> _imagesByRequestUrl = {};
final List<FriendicaEntryTreeItem> _postEntries = [];
final List<FriendicaEntryTreeItem> _orphanedCommentEntries = [];
final List<FriendicaEntryTreeItem> _allComments = [];
final FriendicaConnections connections = FriendicaConnections();
final List<EntryTreeItem> _postEntries = [];
final List<EntryTreeItem> _orphanedCommentEntries = [];
final List<EntryTreeItem> _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<List<FriendicaEntryTreeItem>, ExecError> getPosts() async {
FutureResult<List<EntryTreeItem>, ExecError> getPosts() async {
if (_postEntries.isEmpty && _allComments.isEmpty) {
_loadEntries();
}
@ -49,7 +53,7 @@ class FriendicaArchiveService extends ChangeNotifier {
return Result.ok(_postEntries);
}
FutureResult<List<FriendicaEntryTreeItem>, ExecError> getAllComments() async {
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() async {
if (_postEntries.isEmpty && _allComments.isEmpty) {
_loadEntries();
}
@ -57,8 +61,7 @@ class FriendicaArchiveService extends ChangeNotifier {
return Result.ok(_allComments);
}
FutureResult<List<FriendicaEntryTreeItem>, ExecError>
getOrphanedComments() async {
FutureResult<List<EntryTreeItem>, 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<dynamic>;
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 = <String, FriendicaEntryTreeItem>{};
final entryTrees = <String, EntryTreeItem>{};
final postTreeEntries = <FriendicaEntryTreeItem>[];
final postTreeEntries = <EntryTreeItem>[];
for (final entry in topLevelEntries) {
final treeEntry = FriendicaEntryTreeItem(entry, false);
final treeEntry = EntryTreeItem(entry, false);
entryTrees[entry.id] = treeEntry;
postTreeEntries.add(treeEntry);
}
final commentTreeEntries = <FriendicaEntryTreeItem>[];
final commentTreeEntries = <EntryTreeItem>[];
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);

View file

@ -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 = <String, Contact>{};
final _connectionsByName = <String, Contact>{};
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<Contact, String> getById(String id) {
final result = _connectionsById[id];
return result != null ? Result.ok(result) : Result.error('$id not found');
}
Result<Contact, String> getByName(String name) {
final result = _connectionsByName[name];
return result != null ? Result.ok(result) : Result.error('$name not found');
}
}

View file

@ -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 = <FileSystemEntity>[];

View file

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

View file

@ -0,0 +1,5 @@
enum ArchiveType {
unknown,
diaspora,
friendica,
}

View file

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

View file

@ -0,0 +1,18 @@
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
class EntryTreeItem {
final TimelineEntry entry;
final bool isOrphaned;
final _children = <String, EntryTreeItem>{};
EntryTreeItem(this.entry, this.isOrphaned);
String get id => entry.id;
void addChild(EntryTreeItem child) {
_children[child.id] = child;
}
List<EntryTreeItem> get children => List.unmodifiable(_children.values);
}

View file

@ -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<String, dynamic> 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";
}
}
}

View file

@ -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<String, String> 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<String, dynamic> json)
: uri = Uri.parse(json['url']),
creationTimestamp = 0,
metadata = (json['metadata'] as Map<String, dynamic>? ?? {})
.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<String, dynamic> 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;
}
}

View file

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

View file

@ -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<MediaAttachment> mediaAttachments;
final LocationData locationData;
final List<Connection> likes;
final List<Connection> 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 <Connection>[],
this.dislikes = const <Connection>[],
List<MediaAttachment>? mediaAttachments,
}) : mediaAttachments = mediaAttachments ?? <MediaAttachment>[];
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 <Connection>[],
dislikes = const <Connection>[],
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<MediaAttachment>? mediaAttachments,
List<Connection>? likes,
List<Connection>? dislikes,
List<Uri>? 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;
}

View file

@ -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<List<EntryTreeItem>, ExecError> getPosts() async =>
throw Exception('Not implemented');
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() =>
throw Exception('Not implemented');
FutureResult<List<EntryTreeItem>, ExecError> getOrphanedComments() =>
throw Exception('Not implemented');
Result<ImageEntry, ExecError> getImageByUrl(String url) =>
throw Exception('Not implemented');
}

View file

@ -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<List<EntryTreeItem>, ExecError> getPosts() async {
return _archiveService.getPosts();
}
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() async {
return _archiveService.getAllComments();
}
FutureResult<List<EntryTreeItem>, ExecError> getOrphanedComments() async {
return _archiveService.getOrphanedComments();
}
Result<ImageEntry, ExecError> 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;
}

View file

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

View file

@ -0,0 +1,12 @@
import 'dart:io';
class PathMappingService {
String get rootFolder => throw Exception('Not implemented');
List<FileSystemEntity> get archiveDirectories =>
throw Exception('Not implemented');
void refresh() => throw Exception('Not implemented');
String toFullPath(String relPath) => throw Exception('Not implemented');
}

View file

@ -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<void> 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<void> updateArchiveType(ArchiveType newArchiveType) async {
if (newArchiveType == _archiveType) return;
_archiveType = newArchiveType;
notifyListeners();
await _settingsService.updateArchiveType(newArchiveType);
}
late ThemeMode _themeMode;
ThemeMode get themeMode => _themeMode;

View file

@ -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> 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<void> updateArchiveType(ArchiveType archiveType) async {
final prefs = await SharedPreferences.getInstance();
prefs.setInt(archiveTypeKey, archiveType.index);
}
Future<String> rootFolder() async {
final prefs = await SharedPreferences.getInstance();
final result = prefs.getString(rootFolderKey) ?? '';

View file

@ -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<SettingsView> {
Text('Archive Folder: ',
style: Theme.of(context).textTheme.bodyText1),
const SizedBox(width: 10),
DropdownButton<ArchiveType>(
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,

View file

@ -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(
static final _offsetTimeParser =
OffsetDateTimePattern.createWithInvariantCulture(
'ddd MMM dd HH:mm:ss o<+HHmm> yyyy');
static Result<int, ExecError> 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<int, ExecError> epochSecTimeFromTimeZoneString(
String dateString) {
final offsetDateTime = OffsetDateTimePattern.generalIso.parse(dateString);
if (!offsetDateTime.success) {
return Result.error(ExecError.message(offsetDateTime.error.toString()));
}

View file

@ -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 = <String, InteractorItem>{};
@ -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}) {

View file

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

View file

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

View file

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

View file

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