Merge branch 'diaspora-browser' into 'main'

Diaspora browser

See merge request mysocialportal/friendica-archiving-tools!4
This commit is contained in:
HankG 2022-03-16 23:10:20 +00:00
commit ff787c8a17
154 changed files with 1595 additions and 644 deletions

View file

@ -1,11 +1,20 @@
# Friendica Archiving Tools
# Fediverse Archiving Tools
This repository contains two tools in support of archiving your Friendica data:
* [Friendica Archiver](https://gitlab.com/mysocialportal/friendica-archiving-tools/-/tree/main/friendica_archiver): a tool to create and update your archive
* [Friendica Archive Browser](https://gitlab.com/mysocialportal/friendica-archiving-tools/-/tree/main/friendica_archive_browser): a desktop application that runs on Windows, Mac, and Linux to browse your archive, extract information, see statistics on it etc.
This repository contains tools for working with locally archived data
from [Fediverse](https://en.wikipedia.org/wiki/Fediverse) accounts.
## Archive Browser
* [Fediverse Archive Browser](https://gitlab.com/mysocialportal/friendica-archiving-tools/-/tree/main/friendica_archive_browser): a desktop application that runs on Windows, Mac, and Linux to browse your archives, extract information, see statistics on it etc.
## Archive Generators/Updaters
For fediverse platforms that don't have the ability to export data or
export it completely/easily. For those there are some archive generators:
* [Friendica Archiver](https://gitlab.com/mysocialportal/friendica-archiving-tools/-/tree/main/friendica_archiver): a tool to create and update your YFriendica archive
Installation can be as simple as going to the desired page and downloading the latest pre-existing binaries. There are build instructions as well though.
# Licensing
Both tools have a license file in their respective folders, as well as this top level one that reflect that these tools are licensed under [The Mozilla Public License Version 2.0 (MPLv2.0)](https://www.mozilla.org/en-US/MPL/2.0/)
All tools have a license file in their respective folders, as well as this top level one that reflect that these tools are licensed under [The Mozilla Public License Version 2.0 (MPLv2.0)](https://www.mozilla.org/en-US/MPL/2.0/)

View file

@ -1,11 +1,12 @@
# Friendica Archive Browser
# Fediverse Archive Browser
A Flutter-based cross platform desktop
application for browsing the Friendica account archive that a user can
A Flutter-based cross platform desktop application for browsing
the archives from Diaspora or Friendica. The Diaspora archive can be exported
directly from your profile on your pod. The Friendica archive can be
generate and update using the [Friendica Archiver](https://gitlab.com/mysocialportal/friendica-archiving-tools/-/tree/main/friendica_archiver).
The archive takes the form of a folder with a series of JSON files and
a local image archive. Simply point the application at this folder and
begin usage
begin usage.
## Installation

View file

Before

Width:  |  Height:  |  Size: 619 B

After

Width:  |  Height:  |  Size: 619 B

View file

Before

Width:  |  Height:  |  Size: 810 B

After

Width:  |  Height:  |  Size: 810 B

View file

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 419 B

View file

@ -1,7 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/utils/temp_file_builder.dart';
import 'package:fediverse_archive_browser/src/utils/temp_file_builder.dart';
import 'package:logging/logging.dart';
import 'src/app.dart';

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/services/friendica_archive_service.dart';
import 'package:friendica_archive_browser/src/themes.dart';
import 'package:friendica_archive_browser/src/utils/scrolling_behavior.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_provider.dart';
import 'package:fediverse_archive_browser/src/themes.dart';
import 'package:fediverse_archive_browser/src/utils/scrolling_behavior.dart';
import 'package:provider/provider.dart';
import 'friendica/services/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 = PathMappingService(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:charts_flutter/flutter.dart' as charts;
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/models/stat_bin.dart';
import 'package:fediverse_archive_browser/src/models/stat_bin.dart';
import 'package:logging/logging.dart';
class BarChartComponent extends StatelessWidget {

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';

View file

@ -1,12 +1,12 @@
import 'dart:ui';
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
import 'package:fediverse_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:fediverse_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,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/components/heatmap/heatmap_tile.dart';
import 'package:friendica_archive_browser/src/components/heatmap/tile_color_map.dart';
import 'package:friendica_archive_browser/src/models/stat_bin.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:friendica_archive_browser/src/utils/time_stat_generator.dart';
import 'package:fediverse_archive_browser/src/components/heatmap/heatmap_tile.dart';
import 'package:fediverse_archive_browser/src/components/heatmap/tile_color_map.dart';
import 'package:fediverse_archive_browser/src/models/stat_bin.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/utils/time_stat_generator.dart';
import 'package:provider/provider.dart';
class HeatMapComponent extends StatelessWidget {

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/components/heatmap/tile_color_map.dart';
import 'package:fediverse_archive_browser/src/components/heatmap/tile_color_map.dart';
class HeatMapTile extends StatelessWidget {
static const width = 12.0;

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.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/utils/time_stat_generator.dart';
import 'package:fediverse_archive_browser/src/models/time_element.dart';
import 'package:fediverse_archive_browser/src/screens/standin_status_screen.dart';
import 'package:fediverse_archive_browser/src/utils/time_stat_generator.dart';
import 'heatmap/heatmap_component.dart';

View file

@ -45,6 +45,16 @@ class _LinkElementsComponentState extends State<LinkElementsComponent> {
var ogData = MetadataParser.openGraph(document);
ogData.url ??= url.toString();
if (!(ogData.image?.startsWith('http') ?? true)) {
try {
final separator = ogData.image?.startsWith('/') ?? true ? '' : '/';
final serverPath = 'https://${url.host}$separator${ogData.image}';
ogData.image = serverPath;
} catch (e) {
_logger.finest(
'Unable to map relative ogData.image data into server url');
}
}
_linkPreviewData.add(ogData);
}

View file

@ -1,11 +1,10 @@
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/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/services/friendica_archive_service.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/models/media_attachment.dart';
import 'package:fediverse_archive_browser/src/screens/media_slideshow_screen.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_provider.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:provider/provider.dart';
import 'media_wrapper_component.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<PathMappingService>(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,11 +2,11 @@ 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/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/services/friendica_archive_service.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
import 'package:fediverse_archive_browser/src/models/media_attachment.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_provider.dart';
import 'package:fediverse_archive_browser/src/services/path_mapper_service_interface.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/utils/snackbar_status_builder.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.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<PathMappingService>(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(
@ -56,13 +55,13 @@ class MediaWrapperComponent extends StatelessWidget {
noImageText: 'No Thumbnail',
noImageOnTapText:
'Click to launch video in external player (No Thumbnail)',
onTap: () async =>
await _attemptToPlay(context, videoPlayerCommand, path));
onTap: () async => await _attemptToPlay(
context, videoPlayerCommand, path.toString()));
}
return TextButton(
onPressed: () async {
await _attemptToPlay(context, videoPlayerCommand, path);
await _attemptToPlay(context, videoPlayerCommand, path.toString());
},
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(
@ -74,8 +73,8 @@ class MediaWrapperComponent extends StatelessWidget {
);
}
if (mediaAttachment.explicitType == FriendicaAttachmentMediaType.image) {
final imageResult = _uriToImage(mediaAttachment.uri, pathMapper);
if (mediaAttachment.explicitType == AttachmentMediaType.image) {
final imageResult = _uriToImage(path, archiveService.pathMappingService);
if (imageResult.image == null) {
final errorPath = imageResult.path.isNotEmpty
? imageResult.path
@ -172,14 +171,13 @@ class MediaWrapperComponent extends StatelessWidget {
return InkWell(onTap: onTap, child: imageWidget);
}
String _calculatePath(
PathMappingService pathMapper, FriendicaArchiveService archiveService) {
Uri _calculatePath(ArchiveServiceProvider archiveService) {
final url = mediaAttachment.uri.toString();
String basePath = '';
if (url.startsWith('http')) {
final localCacheFile = archiveService.getImageByUrl(url);
if (localCacheFile.isFailure) {
return url;
return mediaAttachment.uri;
}
basePath = localCacheFile.value.localFilename;
@ -187,7 +185,7 @@ class MediaWrapperComponent extends StatelessWidget {
basePath = mediaAttachment.uri.path;
}
return pathMapper.toFullPath(basePath);
return Uri.parse(basePath);
}
}

View file

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/components/barchart_panel.dart';
import 'package:friendica_archive_browser/src/models/stat_bin.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/utils/time_stat_generator.dart';
import 'package:fediverse_archive_browser/src/components/barchart_panel.dart';
import 'package:fediverse_archive_browser/src/models/stat_bin.dart';
import 'package:fediverse_archive_browser/src/models/time_element.dart';
import 'package:fediverse_archive_browser/src/screens/standin_status_screen.dart';
import 'package:fediverse_archive_browser/src/utils/time_stat_generator.dart';
import 'package:logging/logging.dart';
class TimeChartWidget extends StatefulWidget {

View file

@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/models/time_element.dart';
import 'package:friendica_archive_browser/src/services/friendica_connections.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:fediverse_archive_browser/src/models/time_element.dart';
import 'package:fediverse_archive_browser/src/services/connections_manager.dart';
import 'package:fediverse_archive_browser/src/utils/snackbar_status_builder.dart';
import 'package:fediverse_archive_browser/src/utils/top_interactors_generator.dart';
import 'package:logging/logging.dart';
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,12 +1,12 @@
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/path_mapping_service.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';
import 'package:fediverse_archive_browser/src/models/entry_tree_item.dart';
import 'package:fediverse_archive_browser/src/models/location_data.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_provider.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/utils/clipboard_helper.dart';
import 'package:fediverse_archive_browser/src/utils/snackbar_status_builder.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.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<PathMappingService>(context);
final archiveService = Provider.of<ArchiveServiceProvider>(context);
final entry = treeEntry.entry;
@ -40,7 +40,7 @@ class TreeEntryCard extends StatelessWidget {
? entry.title
: entry.parentId.isEmpty
? (entry.isReshare ? 'Reshare' : 'Post')
: 'Comment on post by ${entry.parentAuthor}';
: 'Comment on post by ${entry.author}';
final dateStamp = ' At ' +
formatter.format(
DateTime.fromMillisecondsSinceEpoch(entry.creationTimestamp * 1000)
@ -75,12 +75,14 @@ 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)),
),
Tooltip(
message: 'Open link to original item',
message:
'Open link to original item (${entry.externalLink})',
child: IconButton(
onPressed: () async {
await canLaunch(entry.externalLink)
@ -129,9 +131,9 @@ class TreeEntryCard extends StatelessWidget {
),
if (entry.locationData.hasData())
entry.locationData.toWidget(spacingHeight),
if (entry.links.isNotEmpty) ...[
if (treeEntry.entry.links.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
LinkElementsComponent(links: entry.links)
LinkElementsComponent(links: treeEntry.entry.links)
],
if (entry.mediaAttachments.isNotEmpty) ...[
const SizedBox(height: spacingHeight),

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/utils/word_map_generator.dart';
import 'package:fediverse_archive_browser/src/models/time_element.dart';
import 'package:fediverse_archive_browser/src/utils/word_map_generator.dart';
import 'package:logging/logging.dart';
class WordFrequencyWidget extends StatefulWidget {

View file

@ -0,0 +1,55 @@
class DiasporaReaction {
final String authorString;
final String guid;
final String parentGuid;
final ParentType parentType;
final ReactionType reactionType;
DiasporaReaction(
{required this.authorString,
this.guid = '',
required this.parentGuid,
this.parentType = ParentType.post,
required this.reactionType});
static DiasporaReaction fromJson(Map<String, dynamic> json) =>
DiasporaReaction(
authorString: json['author'] ?? '',
guid: json['guid'] ?? '',
parentGuid: json['parent_guid'] ?? '',
parentType: _parentTypeFromString(json['parent_type'] ?? ''),
reactionType: _reactionTypeFromBool(json['positive'] ?? true));
}
enum ParentType {
unknown,
comment,
post,
}
ParentType _parentTypeFromString(String string) {
final stringLower = string.toLowerCase();
if (stringLower == 'post') {
return ParentType.post;
}
if (stringLower == 'comment') {
return ParentType.comment;
}
return ParentType.unknown;
}
enum ReactionType {
unknown,
dislike,
like,
}
ReactionType _reactionTypeFromBool(bool value) {
if (value) {
return ReactionType.like;
}
return ReactionType.dislike;
}

View file

@ -0,0 +1,13 @@
import 'package:fediverse_archive_browser/src/diaspora/models/diaspora_reaction.dart';
import 'package:fediverse_archive_browser/src/models/timeline_entry.dart';
class DiasporaRelayable {
final DiasporaReaction? reaction;
final TimelineEntry? comment;
bool get isReaction => reaction != null;
bool get isComment => comment != null;
DiasporaRelayable({this.reaction, this.comment});
}

View file

@ -0,0 +1,47 @@
import 'package:fediverse_archive_browser/src/models/connection.dart';
import 'package:uuid/uuid.dart';
Connection contactFromDiasporaJson(Map<String, dynamic> json) {
const network = "Diaspora";
final accountId = json['account_id'] ?? '';
final profileUrl = _profileUrlFromAccountId(accountId);
final name = json['person_name'] ?? '';
final id = json['person_guid'] ?? '';
final following = json['following'] ?? false;
final followed = json['followed'] ?? false;
var status = ConnectionStatus.none;
if (following && followed) {
status = ConnectionStatus.mutual;
} else if (following) {
status = ConnectionStatus.youFollowThem;
} else if (followed) {
status = ConnectionStatus.theyFollowYou;
}
return Connection(
status: status,
name: name,
id: id,
profileUrl: profileUrl,
network: network);
}
Connection contactFromDiasporaId(String accountId) => Connection(
id: 'generated_${const Uuid().v4}',
status: ConnectionStatus.none,
profileUrl: _profileUrlFromAccountId(accountId));
Uri _profileUrlFromAccountId(String accountId) {
if (accountId.isEmpty) {
return Uri();
}
final accountIdPieces = accountId.split('@');
if (accountIdPieces.length != 2) {
return Uri();
}
final userName = accountIdPieces[0];
final server = accountIdPieces[1];
return Uri.parse('https://$server/u/$userName');
}

View file

@ -0,0 +1,27 @@
import '../../models/media_attachment.dart';
import '../../utils/offsetdatetime_utils.dart';
MediaAttachment mediaAttachmentfromDiasporaJson(Map<String, dynamic> json) {
final entityData = json['entity_data'] ?? {};
final createdAtString = entityData['created_at'] ?? '';
final creationTimestamp =
OffsetDateTimeUtils.epochSecTimeFromTimeZoneString(createdAtString)
.getValueOrElse(() => -1);
String uriBase = entityData['remote_photo_path'] ?? '';
final photoName = entityData['remote_photo_name'] ?? '';
final separator = uriBase.endsWith('/') ? '' : '/';
final uri = Uri.parse('$uriBase$separator$photoName');
const explicitType = AttachmentMediaType.image;
final thumbnailUri = Uri();
const title = '';
const description = '';
return MediaAttachment(
uri: uri,
creationTimestamp: creationTimestamp,
thumbnailUri: thumbnailUri,
metadata: {},
title: title,
explicitType: explicitType,
description: description);
}

View file

@ -0,0 +1,110 @@
import 'package:fediverse_archive_browser/src/models/connection.dart';
import 'package:fediverse_archive_browser/src/models/timeline_entry.dart';
import 'package:fediverse_archive_browser/src/utils/exec_error.dart';
import 'package:fediverse_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';
import 'diaspora_media_attachment_serializer.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 String parentGuid = entityData['root_guid'] ?? '';
final String parentName = entityData['root_author'] ?? '';
final deletedPost = parentGuid.isEmpty || parentName.isEmpty;
final reshareLink = deletedPost
? ''
: _buildPostOrReshareUrl(authorName, parentName, parentGuid);
final text = deletedPost ? 'Original post deleted by author' : '';
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,
externalLink: _buildPostOrReshareUrl(authorName, '', postId),
isReshare: true,
parentAuthor: parentAuthor.name,
parentAuthorId: parentAuthor.id,
body: text,
links: [Uri.parse(reshareLink)]);
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 photos = (entityData['photos'] as List<dynamic>? ?? [])
.map((e) => mediaAttachmentfromDiasporaJson(e));
final timelineEntry = TimelineEntry(
id: postId,
creationTimestamp: epochTime,
body: postHtml,
author: author.name,
externalLink: _buildPostOrReshareUrl(authorName, '', postId),
mediaAttachments: photos.toList(),
authorId: author.id,
);
return Result.ok(timelineEntry);
}
String _buildPostOrReshareUrl(
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

@ -0,0 +1,78 @@
import 'package:fediverse_archive_browser/src/diaspora/models/diaspora_relayables.dart';
import 'package:fediverse_archive_browser/src/models/timeline_entry.dart';
import 'package:fediverse_archive_browser/src/utils/exec_error.dart';
import 'package:fediverse_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 '../models/diaspora_reaction.dart';
final _logger = Logger('DiasporaPostsSerializer');
const _commentType = 'comment';
const _likeType = 'like';
const _knownRelayableTypes = [_commentType, _likeType];
Result<DiasporaRelayable, ExecError> relayableFromDiasporaPostJson(
Map<String, dynamic> json) {
if (!json.containsKey('entity_data')) {
return Result.error(ExecError.message('Relayable item has no entity data'));
}
final entityType = json['entity_type'] ?? '';
final entityData = json['entity_data'] ?? {};
if (!_knownRelayableTypes.contains(entityType)) {
final guid = entityData['guid'];
final error = 'Unknown entity type $entityType for Relayable ID: $guid';
_logger.severe(error);
return Result.error(ExecError.message(error));
}
if (entityType == _commentType) {
return _buildCommentRelayable(entityData);
}
if (entityType == _likeType) {
return _buildReactionRelayable(entityData);
}
return Result.error(ExecError.message('Unknown type: $entityType'));
}
Result<DiasporaRelayable, ExecError> _buildCommentRelayable(
Map<String, dynamic> entityData) {
final author = entityData['author'] ?? '';
final guid = entityData['guid'] ?? '';
final parentGuid = entityData['parent_guid'] ?? '';
final commentMarkdown = entityData['text'] ?? '';
final commentHtml = markdownToHtml(commentMarkdown);
final epochTime = OffsetDateTimeUtils.epochSecTimeFromTimeZoneString(
entityData['created_at'] ?? '')
.getValueOrElse(() => -1);
final timelineEntry = TimelineEntry(
id: guid,
creationTimestamp: epochTime,
body: commentHtml,
author: author,
parentId: parentGuid,
externalLink: _buildCommentUrl(author, parentGuid, guid),
);
return Result.ok(DiasporaRelayable(comment: timelineEntry));
}
Result<DiasporaRelayable, ExecError> _buildReactionRelayable(
Map<String, dynamic> entityData) {
final reaction = DiasporaReaction.fromJson(entityData);
return Result.ok(DiasporaRelayable(reaction: reaction));
}
String _buildCommentUrl(String author, String parentGuid, String commentGuid) {
final accountIdPieces = author.split('@');
if (accountIdPieces.length != 2) {
return commentGuid;
}
final server = accountIdPieces[1];
return 'https://$server/p/$parentGuid#$commentGuid';
}

View file

@ -0,0 +1,198 @@
import 'dart:io';
import 'package:fediverse_archive_browser/src/diaspora/services/diaspora_path_mapping_service.dart';
import 'package:fediverse_archive_browser/src/diaspora/services/diaspora_profile_json_reader.dart';
import 'package:fediverse_archive_browser/src/models/connection.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_interface.dart';
import 'package:path/path.dart' as p;
import 'package:result_monad/result_monad.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 '../models/diaspora_reaction.dart';
import '../serializers/diaspora_contact_serializer.dart';
class DiasporaArchiveService implements ArchiveService {
@override
final DiasporaPathMappingService pathMappingService;
final Map<String, ImageEntry> _imagesByRequestUrl = {};
final List<FileSystemEntity> _topLevelDirectories = [];
final List<EntryTreeItem> _postEntries = [];
final List<EntryTreeItem> _orphanedCommentEntries = [];
final List<EntryTreeItem> _allComments = [];
@override
final ConnectionsManager connectionsManager = ConnectionsManager();
String _ownersName = '';
DiasporaArchiveService({required this.pathMappingService});
String get ownersName => _ownersName;
void clearCaches() {
connectionsManager.clearCaches();
_imagesByRequestUrl.clear();
_orphanedCommentEntries.clear();
_allComments.clear();
_postEntries.clear();
_topLevelDirectories.clear();
}
FutureResult<List<EntryTreeItem>, ExecError> getPosts() async {
if (_postEntries.isEmpty && _allComments.isEmpty) {
_loadProfileFile();
}
return Result.ok(_postEntries);
}
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() async {
if (_postEntries.isEmpty && _allComments.isEmpty) {
_loadProfileFile();
}
return Result.ok(_allComments);
}
FutureResult<List<EntryTreeItem>, ExecError> getOrphanedComments() async {
if (_postEntries.isEmpty && _allComments.isEmpty) {
_loadProfileFile();
}
return Result.ok(_orphanedCommentEntries);
}
Result<ImageEntry, ExecError> getImageByUrl(String url) {
if (_imagesByRequestUrl.isEmpty) {
_populateTopLevelSubDirectory();
}
var result = _imagesByRequestUrl[url];
if (result == null) {
final localFile = _getLocalVersion(url);
if (localFile.isSuccess) {
_imagesByRequestUrl[url] = localFile.value;
result = localFile.value;
}
}
return result == null
? Result.error(ExecError(errorMessage: '$url not found'))
: Result.ok(result);
}
String get _baseArchiveFolder => pathMappingService.rootFolder;
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 entryTree = <String, EntryTreeItem>{};
final newPosts =
reader.readPosts().map((e) => EntryTreeItem(e, false)).toList();
for (final post in newPosts) {
entryTree[post.id] = post;
}
final userComments = reader
.readUserRelayables()
.where((r) => r.isComment)
.map((r) => r.comment!);
final othersRelayables = reader.readOthersRelayables();
final othersComments =
othersRelayables.where((r) => r.isComment).map((r) => r.comment!);
final othersReactions =
othersRelayables.where((r) => r.isReaction).map((r) => r.reaction!);
final allComments = [...userComments, ...othersComments];
allComments.sort(
(c1, c2) => c1.creationTimestamp.compareTo(c2.creationTimestamp));
for (final comment in allComments) {
final parentId = comment.parentId;
final parent = entryTree[parentId];
if (parent == null) {
final newEntry = EntryTreeItem(comment, true);
entryTree[comment.id] = newEntry;
if (userComments.contains(comment)) {
_orphanedCommentEntries.add(newEntry);
}
} else {
parent.addChild(EntryTreeItem(comment, false));
}
}
for (final reaction in othersReactions) {
final treeEntry = entryTree[reaction.parentGuid];
if (treeEntry == null) {
continue;
}
final builtConnections = <String, Connection>{};
final builtConnection = builtConnections[reaction.authorString] ??
contactFromDiasporaId(reaction.authorString);
builtConnections[reaction.authorString] = builtConnection;
final result =
connectionsManager.getByProfileUrl(builtConnection.profileUrl);
final connection = result.fold(
onSuccess: (c) => c,
onError: (error) {
connectionsManager.addConnection(builtConnection);
return builtConnection;
});
switch (reaction.reactionType) {
case ReactionType.unknown:
break;
case ReactionType.dislike:
treeEntry.entry.dislikes.add(connection);
break;
case ReactionType.like:
treeEntry.entry.likes.add(connection);
break;
}
}
_postEntries.addAll(newPosts);
}
}
_postEntries.sort((p1, p2) => _reverseChronologicalSort(p1, p2));
_orphanedCommentEntries.sort((p1, p2) => _reverseChronologicalSort(p1, p2));
}
int _reverseChronologicalSort(EntryTreeItem p1, EntryTreeItem p2) =>
p2.entry.creationTimestamp.compareTo(p1.entry.creationTimestamp);
void _populateTopLevelSubDirectory() {
final topLevelDirectories = Directory(_baseArchiveFolder)
.listSync(recursive: false)
.where((e) => e.statSync().type == FileSystemEntityType.directory);
_topLevelDirectories.addAll(topLevelDirectories);
}
Result<ImageEntry, ExecError> _getLocalVersion(String url) {
final filename = Uri.parse(url).pathSegments.last;
for (final dir in _topLevelDirectories) {
final newPath = p.join(dir.path, filename);
if (File(newPath).existsSync()) {
final imageEntry =
ImageEntry(postId: '', localFilename: newPath, url: url);
return Result.ok(imageEntry);
}
}
return Result.error(ExecError.message('Local file not found'));
}
}

View file

@ -0,0 +1,58 @@
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) {
if (File(relPath).existsSync()) {
return 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

@ -0,0 +1,89 @@
import 'dart:convert';
import 'dart:io';
import 'package:fediverse_archive_browser/src/diaspora/models/diaspora_relayables.dart';
import 'package:fediverse_archive_browser/src/diaspora/serializers/diaspora_contact_serializer.dart';
import 'package:fediverse_archive_browser/src/diaspora/serializers/diaspora_relayables_serializer.dart';
import 'package:fediverse_archive_browser/src/models/connection.dart';
import 'package:fediverse_archive_browser/src/models/timeline_entry.dart';
import 'package:fediverse_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, this.connectionsManager);
Map<String, dynamic> get jsonData {
if (_jsonData.isNotEmpty) {
return _jsonData;
}
final jsonFile = File(jsonFilePath);
if (jsonFile.existsSync()) {
final json =
jsonDecode(jsonFile.readAsStringSync()) as Map<String, dynamic>;
_jsonData.addAll(json);
}
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;
}
List<DiasporaRelayable> readUserRelayables() {
if (connectionsManager.length == 0) {
readContacts();
}
return _readRelayableJson(jsonData['user']?['relayables'] ?? []);
}
List<DiasporaRelayable> readOthersRelayables() {
if (connectionsManager.length == 0) {
readContacts();
}
return _readRelayableJson(jsonData['others_data']?['relayables'] ?? []);
}
List<DiasporaRelayable> _readRelayableJson(List<dynamic> relayableJsonList) =>
relayableJsonList
.map((e) => relayableFromDiasporaPostJson(e))
.where((r) => r.isSuccess)
.map((r) => r.value)
.toList();
}

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:fediverse_archive_browser/src/friendica/serializers/friendica_contact_serializer.dart';
import 'package:fediverse_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,
body: body,
isReshare: isReshare,
id: id,
parentId: parentId,
parentAuthorId: parentAuthorId,
externalLink: externalLink,
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/services/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/models/local_image_archive_entry.dart';
import 'package:friendica_archive_browser/src/services/friendica_connections.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:fediverse_archive_browser/src/friendica/serializers/friendica_timeline_entry_serializer.dart';
import 'package:fediverse_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
import 'package:fediverse_archive_browser/src/models/entry_tree_item.dart';
import 'package:fediverse_archive_browser/src/models/local_image_archive_entry.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_interface.dart';
import 'package:fediverse_archive_browser/src/services/connections_manager.dart';
import 'package:fediverse_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 {
final PathMappingService pathMappingService;
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,15 +1,17 @@
import 'dart:io';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
class PathMappingService {
static final _logger = Logger('$PathMappingService');
import '../../services/path_mapper_service_interface.dart';
class FriendicaPathMappingService implements PathMappingService {
static final _logger = Logger('$FriendicaPathMappingService');
final SettingsController settings;
final _archiveDirectories = <FileSystemEntity>[];
PathMappingService(this.settings) {
FriendicaPathMappingService(this.settings) {
refresh();
}
@ -27,19 +29,9 @@ class PathMappingService {
}
_archiveDirectories.clear();
try {
if (_calcRootIsSingleArchiveFolder()) {
_archiveDirectories.add(Directory(rootFolder));
return;
}
} catch (e) {
_logger
.severe('Error thrown while trying to calculate root structure: $e');
return;
}
final recursive = !_calcRootIsSingleArchiveFolder();
_archiveDirectories.addAll(Directory(settings.rootFolder)
.listSync(recursive: false)
.listSync(recursive: recursive)
.where((element) =>
element.statSync().type == FileSystemEntityType.directory));
}

View file

@ -1,16 +1,16 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/services/friendica_archive_service.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_provider.dart';
import 'friendica/screens/entries_screen.dart';
import 'friendica/screens/stats_screen.dart';
import 'screens/entries_screen.dart';
import 'screens/stats_screen.dart';
import 'settings/settings_controller.dart';
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:fediverse_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,7 +1,7 @@
import 'dart:math';
import 'package:flutter/widgets.dart';
import 'package:friendica_archive_browser/src/friendica/components/link_elements_component.dart';
import 'package:fediverse_archive_browser/src/components/link_elements_component.dart';
import 'model_utils.dart';

View file

@ -1,12 +1,12 @@
import 'dart:io';
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:fediverse_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 = {};
@ -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:fediverse_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,169 @@
import 'package:fediverse_archive_browser/src/models/connection.dart';
import 'package:fediverse_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;
final List<Uri> links;
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.links = const [],
List<Connection>? likes,
List<Connection>? dislikes,
List<MediaAttachment>? mediaAttachments,
}) : mediaAttachments = mediaAttachments ?? <MediaAttachment>[],
likes = likes ?? <Connection>[],
dislikes = dislikes ?? <Connection>[];
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>[],
links = [],
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,
links: links ?? this.links,
);
}
@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(),
if (links.isNotEmpty) ...['Links:', ...links.map((e) => e.toString())]
].join('\n');
}
bool hasImages() => mediaAttachments
.where((element) => element.explicitType == AttachmentMediaType.image)
.isNotEmpty;
bool hasVideos() => mediaAttachments
.where((element) => element.explicitType == AttachmentMediaType.video)
.isNotEmpty;
}

View file

@ -1,22 +1,21 @@
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/screens/error_screen.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:fediverse_archive_browser/src/components/filter_control_component.dart';
import 'package:fediverse_archive_browser/src/components/tree_entry_card.dart';
import 'package:fediverse_archive_browser/src/models/entry_tree_item.dart';
import 'package:fediverse_archive_browser/src/models/model_utils.dart';
import 'package:fediverse_archive_browser/src/screens/error_screen.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/utils/exec_error.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
import '../../screens/loading_status_screen.dart';
import '../../screens/standin_status_screen.dart';
import 'loading_status_screen.dart';
import '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

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/utils/exec_error.dart';
import 'package:provider/provider.dart';
class ErrorScreen extends StatelessWidget {

View file

@ -2,20 +2,18 @@ import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/components/geo/geo_extensions.dart';
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/path_mapping_service.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/services/friendica_archive_service.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';
import 'package:fediverse_archive_browser/src/components/geo/geo_extensions.dart';
import 'package:fediverse_archive_browser/src/components/tree_entry_card.dart';
import 'package:fediverse_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
import 'package:fediverse_archive_browser/src/models/entry_tree_item.dart';
import 'package:fediverse_archive_browser/src/models/timeline_entry.dart';
import 'package:fediverse_archive_browser/src/screens/error_screen.dart';
import 'package:fediverse_archive_browser/src/screens/loading_status_screen.dart';
import 'package:fediverse_archive_browser/src/screens/standin_status_screen.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_provider.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/utils/exec_error.dart';
import 'package:fediverse_archive_browser/src/utils/temp_file_builder.dart';
import 'package:intl/intl.dart';
import 'package:latlng/latlng.dart';
import 'package:logging/logging.dart';
@ -25,6 +23,9 @@ import 'package:network_to_file_image/network_to_file_image.dart';
import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
import '../components/geo/map_bounds.dart';
import '../components/geo/marker_data.dart';
class GeospatialViewScreen extends StatelessWidget {
static final _logger = Logger('$GeospatialViewScreen');
@ -33,9 +34,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 +67,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 +89,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
@ -178,7 +179,7 @@ class _GeospatialViewState extends State<GeospatialView> {
_logger.finer('Call Geospatial builder');
final formatter =
Provider.of<SettingsController>(context).dateTimeFormatter;
final mapper = Provider.of<PathMappingService>(context);
final mapper = Provider.of<FriendicaPathMappingService>(context);
_updatePostsInBoundsFilter();
final map = _buildMap(context, formatter, mapper);
@ -203,8 +204,8 @@ class _GeospatialViewState extends State<GeospatialView> {
highlightedColor: Colors.indigo[900]!)));
}
Widget _buildPostList(
BuildContext context, DateFormat formatter, PathMappingService 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 +219,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, PathMappingService mapper) {
Widget _buildMap(BuildContext context, DateFormat formatter,
FriendicaPathMappingService mapper) {
final settings = Provider.of<SettingsController>(context);
final shouldDebugCache =
@ -332,8 +333,8 @@ class _GeospatialViewState extends State<GeospatialView> {
);
}
Widget _buildMarkerWidget(
MarkerData data, DateFormat formatter, PathMappingService mapper) {
Widget _buildMarkerWidget(MarkerData data, DateFormat formatter,
FriendicaPathMappingService mapper) {
return Positioned(
left: data.pos.dx - 16,
top: data.pos.dy - 16,

View file

@ -3,18 +3,18 @@ import 'dart:io';
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/path_mapping_service.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';
import 'package:fediverse_archive_browser/src/components/media_wrapper_component.dart';
import 'package:fediverse_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
import 'package:fediverse_archive_browser/src/models/media_attachment.dart';
import 'package:fediverse_archive_browser/src/settings/settings_controller.dart';
import 'package:fediverse_archive_browser/src/themes.dart';
import 'package:fediverse_archive_browser/src/utils/snackbar_status_builder.dart';
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<PathMappingService>(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

@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/components/heatmap_widget.dart';
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/time_element.dart';
import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart';
import 'package:friendica_archive_browser/src/services/friendica_archive_service.dart';
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
import 'package:fediverse_archive_browser/src/components/filter_control_component.dart';
import 'package:fediverse_archive_browser/src/components/heatmap_widget.dart';
import 'package:fediverse_archive_browser/src/components/timechart_widget.dart';
import 'package:fediverse_archive_browser/src/components/top_interactactors_widget.dart';
import 'package:fediverse_archive_browser/src/components/word_frequency_widget.dart';
import 'package:fediverse_archive_browser/src/models/model_utils.dart';
import 'package:fediverse_archive_browser/src/models/time_element.dart';
import 'package:fediverse_archive_browser/src/screens/standin_status_screen.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_provider.dart';
import 'package:fediverse_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,31 @@
import 'package:fediverse_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:fediverse_archive_browser/src/diaspora/services/diaspora_archive_service.dart';
import 'package:fediverse_archive_browser/src/diaspora/services/diaspora_path_mapping_service.dart';
import 'package:fediverse_archive_browser/src/friendica/services/friendica_archive_service.dart';
import 'package:fediverse_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
import 'package:fediverse_archive_browser/src/services/archive_service_interface.dart';
import 'package:fediverse_archive_browser/src/services/connections_manager.dart';
import 'package:fediverse_archive_browser/src/services/path_mapper_service_interface.dart';
import 'package:fediverse_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,55 @@
import 'package:fediverse_archive_browser/src/models/connection.dart';
import 'package:result_monad/result_monad.dart';
class ConnectionsManager {
final _connectionsById = <String, Connection>{};
final _connectionsByName = <String, Connection>{};
final _connectionsByProfileUrl = <Uri, Connection>{};
int get length => _connectionsById.length;
void clearCaches() {
_connectionsById.clear();
_connectionsByName.clear();
_connectionsByProfileUrl.clear();
}
bool addConnection(Connection connection) {
if (_connectionsById.containsKey(connection.id)) {
return false;
}
_connectionsById[connection.id] = connection;
_connectionsByName[connection.name] = connection;
_connectionsByProfileUrl[connection.profileUrl] = connection;
return true;
}
bool addAllConnections(Iterable<Connection> newConnections) {
bool result = true;
for (final connection in newConnections) {
result &= addConnection(connection);
}
return result;
}
Result<Connection, String> getById(String id) {
final result = _connectionsById[id];
return result != null ? Result.ok(result) : Result.error('$id not found');
}
Result<Connection, String> getByName(String name) {
final result = _connectionsByName[name];
return result != null ? Result.ok(result) : Result.error('$name not found');
}
Result<Connection, String> getByProfileUrl(Uri url) {
final result = _connectionsByProfileUrl[url];
return result != null ? Result.ok(result) : Result.error('$url not found');
}
}

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,11 +1,13 @@
import 'dart:io';
import 'package:flutter/material.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:fediverse_archive_browser/src/models/archive_types_enum.dart';
import 'package:fediverse_archive_browser/src/settings/video_player_settings.dart';
import 'package:fediverse_archive_browser/src/utils/temp_file_builder.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:result_monad/result_monad.dart';
import 'settings_service.dart';
@ -17,8 +19,15 @@ 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;
if (!canReadRootDir) {
_rootFolder = '';
}
_videoPlayerSettingType = await _settingsService.videoPlayerSettingType();
_videoPlayerCommand = await _settingsService.videoPlayerCommand();
_dateTimeFormatter = DateFormat('MMMM dd yyyy h:mm a');
@ -69,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:fediverse_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,9 +2,10 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.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';
import 'package:fediverse_archive_browser/src/models/archive_types_enum.dart';
import 'package:fediverse_archive_browser/src/settings/video_player_settings.dart';
import 'package:fediverse_archive_browser/src/utils/clipboard_helper.dart';
import 'package:fediverse_archive_browser/src/utils/snackbar_status_builder.dart';
import 'package:logging/logging.dart';
import 'settings_controller.dart';
@ -30,6 +31,7 @@ class _SettingsViewState extends State<SettingsView> {
VideoPlayerSettingType _videoPlayerTypeOption = VideoPlayerSettingType.custom;
bool _validRootFolder = false;
bool _differentSettingValues = false;
ArchiveType _archiveType = ArchiveType.unknown;
Level _logLevel = Level.SEVERE;
@override
@ -157,6 +159,20 @@ class _SettingsViewState extends State<SettingsView> {
Text('Archive Folder: ',
style: Theme.of(context).textTheme.bodyText1),
const SizedBox(width: 10),
DropdownButton<ArchiveType>(
value: _archiveType,
onChanged: (newArchiveType) async {
_archiveType = newArchiveType!;
setState(() {});
},
items: ArchiveType.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.name),
))
.toList(),
),
const SizedBox(width: 15),
Expanded(
child: TextField(
controller: _folderPathController,
@ -266,6 +282,7 @@ class _SettingsViewState extends State<SettingsView> {
.updateVideoPlayerCommand(_videoPlayerPathController.text);
}
await widget._settingsController.updateLogLevel(_logLevel);
await widget._settingsController.updateArchiveType(_archiveType);
setState(() {});
}
@ -275,6 +292,7 @@ class _SettingsViewState extends State<SettingsView> {
_videoPlayerTypeOption = widget._settingsController.videoPlayerSettingType;
_videoPlayerPathController.text =
widget._settingsController.videoPlayerCommand;
_archiveType = widget._settingsController.archiveType;
_logLevel = widget._settingsController.logLevel;
}
@ -289,6 +307,7 @@ class _SettingsViewState extends State<SettingsView> {
newValue |= (_videoPlayerPathController.text !=
widget._settingsController.videoPlayerCommand);
newValue |= (_logLevel != widget._settingsController.logLevel);
newValue |= (_archiveType != widget._settingsController.archiveType);
if (oldValue == newValue) return;
setState(() {
_differentSettingValues = newValue;

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
import 'package:fediverse_archive_browser/src/utils/snackbar_status_builder.dart';
Future<void> copyToClipboard(
{required BuildContext context,

View file

@ -0,0 +1,35 @@
import 'package:fediverse_archive_browser/src/utils/exec_error.dart';
import 'package:result_monad/result_monad.dart';
import 'package:time_machine/time_machine_text_patterns.dart';
class OffsetDateTimeUtils {
static final _offsetTimeParser =
OffsetDateTimePattern.createWithInvariantCulture(
'ddd MMM dd HH:mm:ss o<+HHmm> yyyy');
static Result<int, ExecError> epochSecTimeFromFriendicaString(
String 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()));
}
return Result.ok(offsetDateTime.value.localDateTime
.toDateTimeLocal()
.millisecondsSinceEpoch ~/
1000);
}
}

View file

@ -1,5 +1,5 @@
import 'package:friendica_archive_browser/src/models/stat_bin.dart';
import 'package:friendica_archive_browser/src/models/time_element.dart';
import 'package:fediverse_archive_browser/src/models/stat_bin.dart';
import 'package:fediverse_archive_browser/src/models/time_element.dart';
class TimeStatGenerator {
final List<TimeElement> _elements;

View file

@ -1,6 +1,6 @@
import 'package:friendica_archive_browser/src/friendica/models/friendica_contact.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
import 'package:friendica_archive_browser/src/services/friendica_connections.dart';
import 'package:fediverse_archive_browser/src/models/connection.dart';
import 'package:fediverse_archive_browser/src/models/timeline_entry.dart';
import 'package:fediverse_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) => FriendicaContact(
onError: (error) => Connection(
status: ConnectionStatus.none,
name: '',
id: id,
@ -77,7 +76,7 @@ class TopInteractorsGenerator {
}
class InteractorItem {
final FriendicaContact contact;
final Connection contact;
final int resharedOrCommentedOn;
final int likeCount;
final int dislikeCount;
@ -94,7 +93,7 @@ class InteractorItem {
}
InteractorItem copy(
{FriendicaContact? contact,
{Connection? contact,
int? resharedOrCommentedOn,
int? likeCount,
int? dislikeCount}) {

View file

@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) {
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "Friendica Archive Browser");
gtk_header_bar_set_title(header_bar, "Fediverse Archive Browser");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "Friendica Archive Browser");
gtk_window_set_title(window, "Fediverse Archive Browser");
}
gtk_window_set_default_size(window, 900, 700);

Some files were not shown because too many files have changed in this diff Show more