Refactor out all Facebook/Kyanite references or specific code.

This commit is contained in:
Hank Grabowski 2022-01-19 12:58:28 -05:00
parent aaa41c9131
commit 1f5a312c0e
54 changed files with 50 additions and 2513 deletions

View file

@ -1,33 +1,3 @@
# Kyanite Changelog
# Friendica Archive Browser Changelog
## Version 0.1.2 (2021-12-07)
### New Features
* Make Photo Details an image carousel on posts/albums with multiple images
* Let users navigate photo details carousel with arrow keys and go back to former screen with escape-key
* Added a "copy" button on posts, comments, conversations that copies all the textual data to the clipboard
* Adds a map view for posts/photos that have latitude/longitude data
### Bug Fixes
* Fixes memory leak with images and posts
* Fixes error where default video player was set to empty string on initial startup
* Fix capitalization inconsistencies on buttons
### Changes
* Change log file textbox on settings panel to be single line and overflow with ellipses
## Version 0.1.1 (2021-11-17)
### Bug Fixes
* Add support for update Facebook archive format (versus original one from a year ago)
## Version 0.1.0 (2021-11-14) ** [Initial Release] **
### New Features
* Posts Browsing/filtering (including media and links)
* Comments Browsing/filtering (including media and links)
* Photo Albums Browsing/filtering (and photos attached to posts and comments)
* Video Album Browsing/filtering (and videos attached to posts and comments)
* Facebook Messenger Conversation Browsing/filtering (with media and links)
* Events Browsing/filtering
* Friends list and history browsing
* Ability to export photos from posts/comments/albums/etc.
## Version 1.0.0

View file

@ -6,7 +6,7 @@ generate with the command line tool in this same project
## Installation
To install Kyanite you simply have to download the latest release from
To install the Friendica Archive Browser you simply have to download the latest release from
[the project release directory](https://gitlab.com/HankG/mysocialportal/-/releases)
for your given platform. Then unzip the folder and you are ready to run. On Mac
and Windows you will get a warning about an "unknown publisher" since this is beta

View file

@ -10,7 +10,7 @@ import 'src/settings/settings_controller.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final logPath = await setupLogging();
Logger.root.info('Starting Facebook Archive Viewer');
Logger.root.info('Starting Friendica Archive Browser');
final settingsController = SettingsController(logPath: logPath);
await settingsController.loadSettings();
runApp(FriendicaArchiveBrowser(settingsController: settingsController));

View file

@ -7,7 +7,6 @@ import 'package:friendica_archive_browser/src/themes.dart';
import 'package:friendica_archive_browser/src/utils/scrolling_behavior.dart';
import 'package:provider/provider.dart';
import 'friendica/services/facebook_archive_service.dart';
import 'friendica/services/path_mapping_service.dart';
import 'home.dart';
import 'settings/settings_controller.dart';
@ -27,14 +26,10 @@ class FriendicaArchiveBrowser extends StatelessWidget {
Widget build(BuildContext context) {
DesktopWindow.setMinWindowSize(minAppSize);
final pathMappingService = PathMappingService(settingsController);
final facebookArchiveService = FacebookArchiveDataService(
pathMappingService: pathMappingService,
appDataDirectory: settingsController.appDataDirectory.path);
final friendicaArchiveService =
FriendicaArchiveService(pathMappingService: pathMappingService);
settingsController.addListener(() {
friendicaArchiveService.clearCaches();
facebookArchiveService.clearCaches();
pathMappingService.refresh();
});
return AnimatedBuilder(
@ -60,8 +55,6 @@ class FriendicaArchiveBrowser extends StatelessWidget {
home: MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => settingsController),
ChangeNotifierProvider(
create: (context) => facebookArchiveService),
ChangeNotifierProvider(
create: (context) => friendicaArchiveService),
Provider(create: (context) => pathMappingService),

View file

@ -1,77 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_comment.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:provider/provider.dart';
import 'link_elements_component.dart';
import 'media_timeline_component.dart';
class CommentCard extends StatelessWidget {
final FriendicaComment comment;
const CommentCard({Key? key, required this.comment}) : super(key: key);
@override
Widget build(BuildContext context) {
if (Scrollable.recommendDeferredLoadingForContext(context)) {
return const SizedBox();
}
const double spacingHeight = 5.0;
final formatter = context.read<SettingsController>().dateTimeFormatter;
final title = comment.title.isEmpty ? 'Comment' : comment.title;
final mapper = Provider.of<PathMappingService>(context);
final dateStamp = ' At ' +
formatter.format(DateTime.fromMillisecondsSinceEpoch(
comment.creationTimestamp * 1000)
.toLocal());
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
direction: Axis.horizontal,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Text(dateStamp,
style: const TextStyle(
fontStyle: FontStyle.italic,
)),
Tooltip(
message: 'Copy text version of comment to clipboard',
child: IconButton(
onPressed: () async => await copyToClipboard(
context: context,
text: comment.toHumanString(mapper, formatter),
snackbarMessage: 'Copied Comment to clipboard'),
icon: const Icon(Icons.copy)),
),
]),
if (comment.comment.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
Text(comment.comment)
],
if (comment.links.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
LinkElementsComponent(links: comment.links)
],
if (comment.mediaAttachments.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
MediaTimelineComponent(mediaAttachments: comment.mediaAttachments)
],
],
),
);
}
}

View file

@ -1,63 +0,0 @@
import 'package:logging/logging.dart';
import 'friendica_comment.dart';
import 'friendica_media_attachment.dart';
import 'model_utils.dart';
class FriendicaAlbum {
static final _logger = Logger('$FriendicaAlbum');
final String name;
final String description;
final int lastModifiedTimestamp;
final FriendicaMediaAttachment coverPhoto;
final List<FriendicaMediaAttachment> photos;
final List<FriendicaComment> comments;
FriendicaAlbum(
{required this.name,
required this.description,
required this.lastModifiedTimestamp,
required this.coverPhoto,
required this.photos,
required this.comments});
static FriendicaAlbum fromJson(Map<String, dynamic> json) {
final knownAlbumKeys = [
'name',
'photos',
'cover_photo',
'last_modified_timestamp',
'comments',
'description'
];
logAdditionalKeys(knownAlbumKeys, json.keys, _logger, Level.WARNING,
'Unknown top level album keys');
String name = json['name'] ?? '';
String description = json['description'] ?? '';
int lastModifiedTimestamp = json['last_modified_timestamp'] ?? 0;
FriendicaMediaAttachment coverPhoto = json.containsKey('cover_photo')
? FriendicaMediaAttachment.fromFacebookJson(json['cover_photo'])
: FriendicaMediaAttachment.blank();
final photos = <FriendicaMediaAttachment>[];
for (Map<String, dynamic> photoJson in json['photos'] ?? []) {
photos.add(FriendicaMediaAttachment.fromFacebookJson(photoJson));
}
final comments = <FriendicaComment>[];
for (Map<String, dynamic> commentsJson in json['comments'] ?? []) {
comments.add(FriendicaComment.fromInnerCommentJson(commentsJson));
}
return FriendicaAlbum(
name: name,
description: description,
lastModifiedTimestamp: lastModifiedTimestamp,
coverPhoto: coverPhoto,
photos: photos,
comments: comments);
}
}

View file

@ -1,209 +0,0 @@
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'friendica_media_attachment.dart';
import 'model_utils.dart';
class FriendicaComment {
static final _logger = Logger('$FriendicaComment');
final int creationTimestamp;
final String author;
final String comment;
final String group;
final String title;
final List<FriendicaMediaAttachment> mediaAttachments;
final List<Uri> links;
FriendicaComment(
{this.creationTimestamp = 0,
this.author = '',
this.comment = '',
this.group = '',
this.title = '',
List<FriendicaMediaAttachment>? mediaAttachments,
List<Uri>? links})
: mediaAttachments = mediaAttachments ?? <FriendicaMediaAttachment>[],
links = links ?? <Uri>[];
FriendicaComment.randomBuilt()
: creationTimestamp = DateTime.now().millisecondsSinceEpoch,
author = 'Random Author ${randomId()}',
comment = 'Random comment text ${randomId()}',
group = 'Random Group ${randomId()}',
title = 'Random title ${randomId()}',
links = [
Uri.parse('http://localhost/${randomId()}'),
Uri.parse('http://localhost/${randomId()}')
],
mediaAttachments = [
FriendicaMediaAttachment.randomBuilt(),
FriendicaMediaAttachment.randomBuilt()
];
FriendicaComment copy(
{int? creationTimestamp,
String? author,
String? comment,
String? group,
String? title,
List<FriendicaMediaAttachment>? mediaAttachments,
List<Uri>? links}) {
return FriendicaComment(
creationTimestamp: creationTimestamp ?? this.creationTimestamp,
author: author ?? this.author,
comment: comment ?? this.comment,
group: group ?? this.group,
title: title ?? this.title,
mediaAttachments: mediaAttachments ?? this.mediaAttachments,
links: links ?? this.links);
}
@override
String toString() {
return 'FacebookPost{creationTimestamp: $creationTimestamp, comment: $comment, author, $author, group: $group, title: $title, mediaAttachments: $mediaAttachments, links: $links}';
}
String toHumanString(PathMappingService mapper, DateFormat formatter) {
final creationDateString = formatter.format(
DateTime.fromMillisecondsSinceEpoch(creationTimestamp * 1000)
.toLocal());
return [
'Title: $title',
'Creation At: $creationDateString',
if (group.isNotEmpty) 'Group: $group',
'Text:',
comment,
'',
if (links.isNotEmpty) 'Links:',
...links.map((e) => e.toString()),
'',
if (mediaAttachments.isNotEmpty) 'Photos and Videos:',
...mediaAttachments.map((e) => e.toHumanString(mapper)),
].join('\n');
}
FriendicaComment.fromJson(Map<String, dynamic> json)
: creationTimestamp = json['creationTimeStamp'] ?? 0,
author = json['author'] ?? '',
comment = json['comment'] ?? '',
group = json['group'] ?? '',
title = json['title'] ?? '',
mediaAttachments = (json['mediaAttachments'] as List<dynamic>? ?? [])
.map((j) => FriendicaMediaAttachment.fromJson(j))
.toList(),
links = (json['links'] as List<dynamic>? ?? [])
.map((j) => Uri.parse(j))
.toList();
Map<String, dynamic> toJson() => {
'creationTimestamp': creationTimestamp,
'author': author,
'comment': comment,
'group': group,
'title': title,
'mediaAttachments': mediaAttachments.map((m) => m.toJson()).toList(),
'links': links.map((e) => e.path).toList(),
};
bool hasImages() => mediaAttachments
.where((element) =>
element.explicitType == FriendicaAttachmentMediaType.image)
.isNotEmpty;
bool hasVideos() => mediaAttachments
.where((element) =>
element.explicitType == FriendicaAttachmentMediaType.video)
.isNotEmpty;
static FriendicaComment fromInnerCommentJson(
Map<String, dynamic> commentSubData) {
final knownCommentKeys = ['comment', 'timestamp', 'group', 'author'];
if (_logger.isLoggable(Level.WARNING)) {
logAdditionalKeys(knownCommentKeys, commentSubData.keys, _logger,
Level.WARNING, 'Unknown comment level comment keys');
}
final comment = commentSubData['comment'] ?? '';
final group = commentSubData['group'] ?? '';
final author = commentSubData['author'] ?? '';
final timestamp = commentSubData['timestamp'] ?? 0;
return FriendicaComment(
creationTimestamp: timestamp,
author: author,
group: group,
comment: comment,
);
}
static FriendicaComment fromFacebookJson(Map<String, dynamic> json) {
final knownTopLevelKeys = ['timestamp', 'data', 'title', 'attachments'];
final knownExternalContextKeys = ['external_context', 'media', 'name'];
int timestamp = json['timestamp'] ?? 0;
logAdditionalKeys(knownTopLevelKeys, json.keys, _logger, Level.WARNING,
'Unknown top level comment keys');
FriendicaComment basicCommentData = FriendicaComment();
if (json.containsKey('data')) {
final data = json['data'];
for (var dataItem in data) {
if (dataItem.containsKey('comment')) {
basicCommentData =
FriendicaComment.fromInnerCommentJson(dataItem['comment']);
} else {
_logger.warning(
"No comment or update key sequence in post @$timestamp: ${dataItem.keys}");
}
}
}
final String title = json['title'] ?? '';
final links = <Uri>[];
final mediaAttachments = <FriendicaMediaAttachment>[];
if (json.containsKey('attachments')) {
for (Map<String, dynamic> attachment in json['attachments']) {
if (!attachment.containsKey('data')) {
continue;
}
for (var dataItem in attachment['data']) {
if (_logger.isLoggable(Level.WARNING)) {
logAdditionalKeys(
knownExternalContextKeys,
dataItem.keys,
_logger,
Level.WARNING,
'Unknown comment external context key level keys in attachment data');
}
if (dataItem.containsKey('external_context')) {
final String linkText = dataItem['external_context']['url'] ?? '';
if (linkText.isNotEmpty) {
links.add(Uri.parse(linkText));
}
} else if (dataItem.containsKey('media')) {
mediaAttachments.add(
FriendicaMediaAttachment.fromFacebookJson(dataItem['media']));
}
}
}
}
return FriendicaComment(
creationTimestamp: timestamp,
author: basicCommentData.author,
comment: basicCommentData.comment,
group: basicCommentData.group,
title: title,
links: links,
mediaAttachments: mediaAttachments);
}
}

View file

@ -28,7 +28,7 @@ class FriendicaContact {
@override
String toString() {
return 'FacebookFriend{status: $status, name: $name, contactInfo: $contactInfo, friendSinceTimestamp: $friendSinceTimestamp, receivedTimestamp: $receivedTimestamp, rejectedTimestamp: $rejectedTimestamp, removeTimestamp: $removeTimestamp, sentTimestamp: $sentTimestamp, markedAsSpam: $markedAsSpam}';
return 'FriendicaFriend{status: $status, name: $name, contactInfo: $contactInfo, friendSinceTimestamp: $friendSinceTimestamp, receivedTimestamp: $receivedTimestamp, rejectedTimestamp: $rejectedTimestamp, removeTimestamp: $removeTimestamp, sentTimestamp: $sentTimestamp, markedAsSpam: $markedAsSpam}';
}
static FriendicaContact fromJson(

View file

@ -1,15 +1,12 @@
import 'dart:io';
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:logging/logging.dart';
import 'friendica_comment.dart';
import 'model_utils.dart';
enum FriendicaAttachmentMediaType { unknown, image, video }
class FriendicaMediaAttachment {
static final _logger = Logger('$FriendicaMediaAttachment');
static final _graphicsExtensions = ['jpg', 'png', 'gif', 'tif'];
static final _movieExtensions = ['avi', 'mp4', 'mpg', 'wmv'];
@ -19,8 +16,6 @@ class FriendicaMediaAttachment {
final Map<String, String> metadata;
final List<FriendicaComment> comments;
final FriendicaAttachmentMediaType explicitType;
final Uri thumbnailUri;
@ -36,8 +31,7 @@ class FriendicaMediaAttachment {
required this.thumbnailUri,
required this.title,
required this.explicitType,
required this.description,
required this.comments});
required this.description});
FriendicaMediaAttachment.randomBuilt()
: uri = Uri.parse('http://localhost/${randomId()}'),
@ -46,10 +40,6 @@ class FriendicaMediaAttachment {
thumbnailUri = Uri.parse('${randomId()}.jpg'),
description = 'Random description ${randomId()}',
explicitType = FriendicaAttachmentMediaType.image,
comments = [
FriendicaComment.randomBuilt(),
FriendicaComment.randomBuilt()
],
metadata = {'value1': randomId(), 'value2': randomId()};
FriendicaMediaAttachment.fromUriOnly(this.uri)
@ -58,7 +48,6 @@ class FriendicaMediaAttachment {
title = '',
explicitType = mediaTypeFromString(uri.path),
description = '',
comments = [],
metadata = {};
FriendicaMediaAttachment.fromUriAndTime(this.uri, this.creationTimestamp)
@ -66,7 +55,6 @@ class FriendicaMediaAttachment {
title = '',
explicitType = mediaTypeFromString(uri.path),
description = '',
comments = [],
metadata = {};
FriendicaMediaAttachment.blank()
@ -76,12 +64,11 @@ class FriendicaMediaAttachment {
explicitType = FriendicaAttachmentMediaType.unknown,
title = '',
description = '',
comments = [],
metadata = {};
@override
String toString() {
return 'FacebookMediaAttachment{uri: $uri, creationTimestamp: $creationTimestamp, type: $explicitType, metadata: $metadata, title: $title, description: $description, comments: $comments}';
return 'FriendicaMediaAttachment{uri: $uri, creationTimestamp: $creationTimestamp, type: $explicitType, metadata: $metadata, title: $title, description: $description}';
}
String toHumanString(PathMappingService mapper) {
@ -102,7 +89,6 @@ class FriendicaMediaAttachment {
: (json['mimetype'] ?? '').startsWith('video')
? FriendicaAttachmentMediaType.video
: FriendicaAttachmentMediaType.unknown,
comments = [],
thumbnailUri = Uri(),
title = '',
description = '';
@ -112,50 +98,11 @@ class FriendicaMediaAttachment {
'creationTimestamp': creationTimestamp,
'metadata': metadata,
'type': explicitType,
'comments': comments.map((c) => c.toJson()).toList(),
'thumbnailUri': thumbnailUri.toString(),
'title': title,
'description': description,
};
static FriendicaMediaAttachment fromFacebookJson(Map<String, dynamic> json) {
final Uri uri = Uri.parse(json['uri']);
final int timestamp = json['creation_timestamp'] ?? 0;
final String title = json['title'] ?? '';
final String description = json['description'] ?? '';
final metadata = <String, String>{};
final thumbnailUrlString = json['thumbnail']?['uri'] ?? '';
final thumbnailUri = thumbnailUrlString.startsWith('http')
? Uri.parse(thumbnailUrlString)
: Uri.file(thumbnailUrlString);
final mediaType = json['media_metadata']?.forEach((key, value) {
if (key == 'photo_metadata' || key == 'video_metadata') {
final exifData = value['exif_data'] ?? [];
for (final exif in exifData) {
exif.forEach((k2, v2) => metadata[k2] = v2.toString());
}
} else {
_logger.fine("Unknown media key $key");
metadata[key] = value;
}
});
final comments = <FriendicaComment>[];
for (Map<String, dynamic> commentJson in json['comments'] ?? {}) {
final comment = FriendicaComment.fromInnerCommentJson(commentJson);
comments.add(comment);
}
return FriendicaMediaAttachment(
uri: uri,
creationTimestamp: timestamp,
metadata: metadata,
explicitType: mediaType,
thumbnailUri: thumbnailUri,
title: title,
comments: comments,
description: description);
}
static FriendicaAttachmentMediaType mediaTypeFromString(String path) {
final separator = Platform.isWindows ? '\\' : '/';
final lastSlash = path.lastIndexOf(separator) + 1;

View file

@ -108,7 +108,7 @@ class FriendicaTimelineEntry {
@override
String toString() {
return 'FacebookPost{id: $id, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, timelineType: $timelineType, post: $post, title: $title, author: $author, parentAuthor: $parentAuthor mediaAttachments: $mediaAttachments, links: $links}';
return 'FriendicaTimelineEntry{id: $id, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, timelineType: $timelineType, post: $post, title: $title, author: $author, parentAuthor: $parentAuthor mediaAttachments: $mediaAttachments, links: $links}';
}
String toHumanString(PathMappingService mapper, DateFormat formatter) {

View file

@ -40,7 +40,7 @@ class LocationData {
@override
String toString() {
return 'FacebookLocationData{name: $name, latitude: $latitude, longitude: $longitude, altitude: $altitude, hasPosition: $hasPosition, address: $address, url: $url}';
return 'LocationData{name: $name, latitude: $latitude, longitude: $longitude, altitude: $altitude, hasPosition: $hasPosition, address: $address, url: $url}';
}
String toHumanString() {

View file

@ -1,115 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/components/comment_card.dart';
import 'package:friendica_archive_browser/src/friendica/components/filter_control_component.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_comment.dart';
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_service.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: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';
class CommentsScreen extends StatelessWidget {
static final _logger = Logger('$CommentsScreen');
const CommentsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final service = Provider.of<FacebookArchiveDataService>(context);
final username = Provider.of<SettingsController>(context).facebookName;
_logger.fine('Build FacebookPostListView');
return FutureBuilder<Result<List<FriendicaComment>, ExecError>>(
future: service.getComments(),
builder: (context, snapshot) {
_logger.fine('Future Comment builder called');
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
return const LoadingStatusScreen(title: 'Loading Comments');
}
final commentsResult = snapshot.requireData;
if (commentsResult.isFailure) {
return ErrorScreen(
title: 'Error getting comments', error: commentsResult.error);
}
final comments = commentsResult.value;
if (comments.isEmpty) {
return const StandInStatusScreen(title: 'No comments were found');
}
_logger.fine('Build Comments ListView');
return _FacebookCommentsScreenWidget(
comments: comments, username: username);
});
}
}
class _FacebookCommentsScreenWidget extends StatelessWidget {
static final _logger = Logger('$_FacebookCommentsScreenWidget');
final List<FriendicaComment> comments;
final String username;
const _FacebookCommentsScreenWidget(
{Key? key, required this.comments, required this.username})
: super(key: key);
@override
Widget build(BuildContext context) {
_logger.fine('Redrawing');
return FilterControl<FriendicaComment, dynamic>(
allItems: comments,
imagesOnlyFilterFunction: (comment) => comment.hasImages(),
videosOnlyFilterFunction: (comment) => comment.hasVideos(),
textSearchFilterFunction: (comment, text) =>
comment.title.contains(text) || comment.comment.contains(text),
itemToDateTimeFunction: (comment) =>
DateTime.fromMillisecondsSinceEpoch(
comment.creationTimestamp * 1000),
dateRangeFilterFunction: (comment, start, stop) =>
timestampInRange(comment.creationTimestamp * 1000, start, stop),
builder: (context, items) {
if (items.isEmpty) {
return const StandInStatusScreen(
title: 'No comments meet filter criteria');
}
return ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(scrollbars: false),
child: ListView.separated(
primary: false,
restorationId: 'facebookCommentsListView',
itemCount: items.length,
itemBuilder: (context, index) {
_logger.finer('Rendering FacebookComment List Item');
final comment = items[index];
final newTitle = username.isEmpty
? comment.title
: comment.title
.replaceAll(username, 'You')
.replaceAll(wholeWordRegEx('his'), 'your')
.replaceAll(wholeWordRegEx('her'), 'your');
final cardComment = username.isEmpty
? comment
: comment.copy(title: newTitle);
return CommentCard(comment: cardComment);
},
separatorBuilder: (context, index) {
return const Divider(
color: Colors.black,
thickness: 0.2,
);
}),
);
});
}
}

View file

@ -1,122 +0,0 @@
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_contact.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_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/settings/settings_controller.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
class FriendsScreen extends StatelessWidget {
static final _logger = Logger('$FriendsScreen');
const FriendsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final service = Provider.of<FacebookArchiveDataService>(context);
final rootPath = Provider.of<SettingsController>(context).rootFolder;
_logger.fine('Build FacebookFriendsScreen');
return FutureBuilder<Result<List<FriendicaContact>, ExecError>>(
future: service.getFriends(),
builder: (context, snapshot) {
_logger.fine('Future Friends builder called');
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
return const LoadingStatusScreen(title: 'Loading Friends');
}
final friendsResult = snapshot.requireData;
if (friendsResult.isFailure) {
return ErrorScreen(
title: 'Error getting friends', error: friendsResult.error);
}
final friends = friendsResult.value;
if (friends.isEmpty) {
return const StandInStatusScreen(title: 'No friends were found');
}
_logger.fine('Build Friends Data Grid View');
return _FacebookFriendsScreenWidget(
friends: friends, rootPath: rootPath);
});
}
}
class _FacebookFriendsScreenWidget extends StatelessWidget {
final List<FriendicaContact> friends;
final String rootPath;
const _FacebookFriendsScreenWidget(
{Key? key, required this.friends, required this.rootPath})
: super(key: key);
@override
Widget build(BuildContext context) {
final formatter = Provider.of<SettingsController>(context).dateFormatter;
final headerStyle = Theme.of(context)
.textTheme
.bodyText1
?.copyWith(fontWeight: FontWeight.bold);
const nameSize = 250.0;
const statusSize = 100.0;
const dateSize = 150.0;
return ListView.separated(
restorationId: 'friendListView',
itemCount: friends.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
SizedBox(
width: nameSize, child: Text('Title', style: headerStyle)),
SizedBox(
width: statusSize,
child: Text('Status', style: headerStyle)),
SizedBox(
width: dateSize,
child: Text('Friends Since', style: headerStyle)),
],
),
);
}
final friend = friends[index - 1];
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
SizedBox(width: nameSize, child: SelectableText(friend.name)),
SizedBox(width: statusSize, child: Text(friend.status.name())),
SizedBox(
width: dateSize,
child:
Text(_dateText(friend.friendSinceTimestamp, formatter))),
],
),
);
},
separatorBuilder: (context, index) {
return Divider(
color: Colors.black,
thickness: index == 0 ? 1.0 : 0.2,
);
},
);
}
String _dateText(int timestamp, DateFormat formatter) => timestamp == 0
? 'Not Available'
: formatter.format(DateTime.fromMillisecondsSinceEpoch(timestamp * 1000));
}

View file

@ -8,12 +8,11 @@ import 'package:friendica_archive_browser/src/friendica/components/geo/marker_da
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/models/model_utils.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_service.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';
@ -33,14 +32,13 @@ class GeospatialViewScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
_logger.info('Build FacebookGeospatialViewScreen');
final service = Provider.of<FacebookArchiveDataService>(context);
final username = Provider.of<SettingsController>(context).facebookName;
_logger.info('Build GeospatialViewScreen');
final service = Provider.of<FriendicaArchiveService>(context);
return FutureBuilder<Result<List<FriendicaTimelineEntry>, ExecError>>(
return FutureBuilder<Result<List<FriendicaEntryTreeItem>, ExecError>>(
future: service.getPosts(),
builder: (context, snapshot) {
_logger.info('FacebookGeospatialViewScreen Future builder called');
_logger.info('GeospatialViewScreen Future builder called');
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
@ -55,30 +53,11 @@ class GeospatialViewScreen extends StatelessWidget {
}
final allPosts = postsResult.value;
final filteredPosts =
allPosts.where((p) => p.locationData.hasPosition);
final filteredPosts = allPosts
.where((p) => p.entry.locationData.hasPosition)
.map((e) => e.entry);
final posts = username.isEmpty
? filteredPosts.toList()
: filteredPosts.map((p) {
var newTitle = p.title;
if (p.title == username) {
newTitle = 'You posted';
} else {
newTitle = p.title
.replaceAll(username, 'You')
.replaceAll(wholeWordRegEx('his'), 'your')
.replaceAll(wholeWordRegEx('her'), 'your');
}
if (newTitle == p.title) {
return p;
} else {
return p.copy(title: newTitle);
}
}).toList();
if (posts.isEmpty) {
return const StandInStatusScreen(title: 'No posts were found');
}
final posts = filteredPosts.toList();
_logger.fine('Build Posts ListView');
return GeospatialView(posts: posts);

View file

@ -1,127 +0,0 @@
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/media_wrapper_component.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_album.dart';
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_service.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/settings/settings_controller.dart';
import 'package:friendica_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 'photo_album_screen.dart';
class PhotoAlbumsBrowserScreen extends StatelessWidget {
static final _logger = Logger('$PhotoAlbumsBrowserScreen');
const PhotoAlbumsBrowserScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
_logger.fine('Build FacebookAlbumListView');
final service = Provider.of<FacebookArchiveDataService>(context);
return FutureBuilder<Result<List<FriendicaAlbum>, ExecError>>(
future: service.getAlbums(),
builder: (futureBuilderContext, snapshot) {
_logger.fine('FacebookAlbumListView Future builder called');
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
return const LoadingStatusScreen(title: 'Loading albums');
}
final albumsResult = snapshot.requireData;
if (albumsResult.isFailure) {
return ErrorScreen(
title: 'Error getting comments', error: albumsResult.error);
}
final albums = albumsResult.value;
if (albums.isEmpty) {
return const StandInStatusScreen(title: 'No albums were found');
}
_logger.fine('Build Photo Albums Grid View');
return _FacebookPhotoAlbumsBrowserScreenWidget(albums: albums);
});
}
}
class _FacebookPhotoAlbumsBrowserScreenWidget extends StatelessWidget {
final List<FriendicaAlbum> albums;
const _FacebookPhotoAlbumsBrowserScreenWidget(
{Key? key, required this.albums})
: super(key: key);
@override
Widget build(BuildContext context) {
final settingsController = Provider.of<SettingsController>(context);
final pathMapper = Provider.of<PathMappingService>(context);
return FilterControl<FriendicaAlbum, dynamic>(
allItems: albums,
textSearchFilterFunction: (album, text) =>
album.name.contains(text) || album.description.contains(text),
itemToDateTimeFunction: (album) => DateTime.fromMillisecondsSinceEpoch(
album.lastModifiedTimestamp * 1000),
dateRangeFilterFunction: (album, start, stop) =>
timestampInRange(album.lastModifiedTimestamp * 1000, start, stop),
builder: (context, albums) {
return Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
),
child: GridView.builder(
itemCount: albums.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 255,
maxCrossAxisExtent: 225,
),
itemBuilder: (itemBuilderContext, index) {
final album = albums[index];
return InkWell(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (routeContext) {
return MultiProvider(providers: [
ChangeNotifierProvider.value(value: settingsController),
Provider.value(value: pathMapper)
], child: PhotoAlbumScreen(album: album));
}));
},
child: SizedBox(
width: 200,
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MediaWrapperComponent(
preferredWidth: 150,
preferredHeight: 150,
mediaAttachment: album.coverPhoto),
const SizedBox(height: 5),
Text(
'${album.name} ',
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 5),
Text('(${album.photos.length} photos)'),
])),
);
},
),
);
});
}
}

View file

@ -1,136 +0,0 @@
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/media_wrapper_component.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_album.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.dart';
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart';
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'media_slideshow_screen.dart';
class PhotoAlbumScreen extends StatelessWidget {
static final _logger = Logger('$PhotoAlbumScreen');
final FriendicaAlbum album;
const PhotoAlbumScreen({Key? key, required this.album}) : super(key: key);
@override
Widget build(BuildContext context) {
_logger.fine(
'Build FacebookPhotoAlbumScreen for ${album.name} w/ ${album.photos.length} photos');
return album.photos.isEmpty
? _buildEmptyGalleryScrene(context)
: FilterControl<FriendicaMediaAttachment, dynamic>(
allItems: album.photos,
textSearchFilterFunction: (photo, text) =>
photo.title.contains(text) || photo.description.contains(text),
itemToDateTimeFunction: (photo) =>
DateTime.fromMillisecondsSinceEpoch(
photo.creationTimestamp * 1000),
dateRangeFilterFunction: (photo, start, stop) =>
timestampInRange(photo.creationTimestamp * 1000, start, stop),
builder: (context, photos) => _FacebookPhotoAlbumScreenWidget(
photos: photos,
albumName: album.name,
albumDescription: album.description,
),
);
}
_buildEmptyGalleryScrene(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(album.name),
backgroundColor: Theme.of(context).canvasColor,
foregroundColor: Theme.of(context).primaryColor,
elevation: 0.0,
),
body: const StandInStatusScreen(title: 'No photos in album'));
}
}
class _FacebookPhotoAlbumScreenWidget extends StatelessWidget {
static final _logger = Logger('$_FacebookPhotoAlbumScreenWidget');
final List<FriendicaMediaAttachment> photos;
final String albumName;
final String albumDescription;
const _FacebookPhotoAlbumScreenWidget(
{Key? key,
required this.photos,
this.albumName = '',
this.albumDescription = ''})
: super(key: key);
@override
Widget build(BuildContext context) {
_logger.fine('Rebuilding album widget w/${photos.length} photos');
final pathMapper = Provider.of<PathMappingService>(context);
final settingsController = Provider.of<SettingsController>(context);
return Scaffold(
appBar: AppBar(
title: Text(albumName),
backgroundColor: Theme.of(context).canvasColor,
foregroundColor: Theme.of(context).primaryColor,
elevation: 0.0,
),
body: Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (albumDescription.isNotEmpty) ...[
Text(
albumDescription,
softWrap: true,
),
const SizedBox(height: 5)
],
Expanded(
child: GridView.builder(
itemCount: photos.length,
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 400.0, maxCrossAxisExtent: 400.0),
itemBuilder: (itemBuilderContext, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
Navigator.push(context,
MaterialPageRoute(builder: (context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: settingsController),
Provider.value(value: pathMapper)
],
child: MediaSlideShowScreen(
mediaAttachments: photos,
initialIndex: index));
}));
},
child: MediaWrapperComponent(
mediaAttachment: photos[index],
preferredWidth: 300,
preferredHeight: 300,
),
),
);
},
),
),
],
)));
}
}

View file

@ -3,11 +3,10 @@ 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/word_frequency_widget.dart';
import 'package:friendica_archive_browser/src/friendica/components/filter_control_component.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.dart';
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_service.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:logging/logging.dart';
import 'package:provider/provider.dart';
@ -21,7 +20,7 @@ class StatsScreen extends StatefulWidget {
class _StatsScreenState extends State<StatsScreen> {
static final _logger = Logger("$_StatsScreenState");
FacebookArchiveDataService? archiveDataService;
FriendicaArchiveService? archiveDataService;
final allItems = <TimeElement>[];
StatType statType = StatType.selectType;
bool hasText = true;
@ -48,57 +47,26 @@ class _StatsScreenState extends State<StatsScreen> {
case StatType.post:
newItems = (await archiveDataService!.getPosts()).fold(
onSuccess: (posts) => posts.map((e) => TimeElement(
timeInMS: e.creationTimestamp * 1000,
hasImages: e.hasImages(),
hasVideos: e.hasVideos(),
title: e.title,
text: e.post)),
timeInMS: e.entry.creationTimestamp * 1000,
hasImages: e.entry.hasImages(),
hasVideos: e.entry.hasVideos(),
title: e.entry.title,
text: e.entry.post)),
onError: (error) {
_logger.severe('Error getting posts: $error');
return [];
});
break;
case StatType.comment:
newItems = (await archiveDataService!.getComments()).fold(
newItems = (await archiveDataService!.getOrphanedComments()).fold(
onSuccess: (comments) => comments.map((e) => TimeElement(
timeInMS: e.creationTimestamp * 1000,
hasImages: e.hasImages(),
hasVideos: e.hasVideos(),
title: e.title,
text: e.comment)),
timeInMS: e.entry.creationTimestamp * 1000,
hasImages: e.entry.hasImages(),
hasVideos: e.entry.hasVideos(),
title: e.entry.title,
text: e.entry.post)),
onError: (error) {
_logger.severe('Error getting comments: $error');
return [];
});
break;
case StatType.photo:
newItems = (await archiveDataService!.getAlbums()).fold(
onSuccess: (albums) => albums.expand((album) => album.photos).map(
(photo) => TimeElement(
timeInMS: photo.creationTimestamp * 1000,
hasImages: true,
hasVideos: false,
title: photo.title,
text: photo.description)),
onError: (error) {
_logger.severe('Error getting photos: $error');
return [];
});
break;
case StatType.video:
newItems = (await archiveDataService!.getPosts()).fold(
onSuccess: (posts) => posts
.where((post) => post.hasVideos())
.expand((post) => post.mediaAttachments.where((m) =>
m.explicitType == FriendicaAttachmentMediaType.video))
.map((e) => TimeElement(
timeInMS: e.creationTimestamp * 1000,
hasImages: false,
hasVideos: true,
title: e.title,
text: e.description)),
onError: (error) {
_logger.severe('Error getting comments: $error');
_logger.severe('Error getting oprhaned comments: $error');
return [];
});
break;
@ -117,7 +85,7 @@ class _StatsScreenState extends State<StatsScreen> {
@override
Widget build(BuildContext context) {
archiveDataService = Provider.of<FacebookArchiveDataService>(context);
archiveDataService = Provider.of<FriendicaArchiveService>(context);
return FilterControl<TimeElement, dynamic>(
allItems: allItems,
@ -204,8 +172,6 @@ class _StatsScreenState extends State<StatsScreen> {
enum StatType {
post,
comment,
photo,
video,
selectType,
}
@ -216,10 +182,6 @@ extension StatTypeString on StatType {
return "Posts";
case StatType.comment:
return "Comments";
case StatType.photo:
return "Photos";
case StatType.video:
return "Videos";
case StatType.selectType:
return "Select Type";
}
@ -234,14 +196,6 @@ extension StatTypeString on StatType {
return StatType.comment;
}
if (text == 'Photos') {
return StatType.photo;
}
if (text == 'Videos') {
return StatType.video;
}
if (text == 'Select Type') {
return StatType.selectType;
}

View file

@ -1,348 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_album.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_comment.dart';
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/friendica/models/timeline_type.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_file_reader.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import '../../utils/temp_file_builder.dart';
class FacebookArchiveFolderReader extends ChangeNotifier {
static final _logger = Logger('$FacebookArchiveFolderReader');
static final expectedDirectories = [
'posts',
'comments_and_reactions',
'saved_items_and_collections',
'posts/media',
'posts/album',
'events',
'messages',
];
String _rootDirectoryPath = '';
String get rootDirectoryPath => _rootDirectoryPath;
set rootDirectoryPath(String value) {
_rootDirectoryPath = value;
notifyListeners();
}
FacebookArchiveFolderReader(String rootDirectoryPath) {
_rootDirectoryPath = rootDirectoryPath;
_logger.fine('Create new FacebookArchiveFolderReader');
}
FutureResult<List<FriendicaTimelineEntry>, ExecError> readPosts() async {
final posts = <FriendicaTimelineEntry>[];
final errors = <ExecError>[];
final yourPostPath = '$rootDirectoryPath/posts/your_posts_1.json';
if (File(yourPostPath).existsSync()) {
(await _getJsonList(yourPostPath))
.andThen((json) => _parsePostResults(json, TimelineType.active))
.match(
onSuccess: (newPosts) => posts.addAll(newPosts),
onError: (error) {
_logger
.severe('Error $error responses json for ${yourPostPath}');
errors.add(error);
});
}
final archivedPostsPath = '$rootDirectoryPath/posts/archive.json';
if (File(archivedPostsPath).existsSync()) {
(await _getJson(archivedPostsPath))
.andThen((json) => json.containsKey('archive_v2')
? Result.ok(json['archive_v2'])
: Result.error(
ExecError.message('No archive_v2 key in $archivedPostsPath')))
.andThen((archivedPostsJson) =>
_parsePostResults(archivedPostsJson, TimelineType.archive))
.match(
onSuccess: (archivedPosts) => posts.addAll(archivedPosts),
onError: (error) {
_logger.severe(
'Error $error responses json for $archivedPostsPath');
errors.add(error);
});
}
final trashPostsPath = '$rootDirectoryPath/posts/trash.json';
if (File(trashPostsPath).existsSync()) {
(await _getJson(trashPostsPath))
.andThen((json) => json.containsKey('trash_v2')
? Result.ok(json['trash_v2'])
: Result.error(
ExecError.message('No trash_v2 key in $trashPostsPath')))
.andThen((archivedPostsJson) =>
_parsePostResults(archivedPostsJson, TimelineType.trash))
.match(
onSuccess: (archivedPosts) => posts.addAll(archivedPosts),
onError: (error) {
_logger
.severe('Error $error responses json for $trashPostsPath');
errors.add(error);
});
}
if (errors.isNotEmpty) {
return Result.error(ExecError.message(
'Error reading one or more present post files. Check logs for more details.'));
}
return Result.ok(posts);
}
FutureResult<List<FriendicaComment>, ExecError> readComments() async {
final path = '$rootDirectoryPath/comments_and_reactions/comments.json';
final jsonResult = await _getJson(path);
if (jsonResult.isFailure) {
return Result.error(jsonResult.error);
}
final jsonData = jsonResult.value;
if (!jsonData.containsKey('comments_v2')) {
return Result.error(
ExecError(errorMessage: 'Comments JSON file is malformed: $path'));
}
final commentsJson = jsonData['comments_v2'] as List<dynamic>;
final commentsResult = runCatching(() => Result.ok(commentsJson
.map((e) => FriendicaComment.fromFacebookJson(e))
.toList()));
commentsResult.match(
onSuccess: (value) => _logger.fine('Comments processed into PODOs'),
onError: (error) =>
_logger.severe('Error mapping JSON to post data: $error'));
return commentsResult.mapExceptionErrorToExecError();
}
FutureResult<List<FriendicaAlbum>, ExecError> readPhotoAlbums() async {
final albumFolderPath = '$rootDirectoryPath/posts/album';
final folder = Directory(albumFolderPath);
final albums = <FriendicaAlbum>[];
if (!folder.existsSync()) {
final msg = 'Photos folder does not exist; $albumFolderPath';
_logger.severe(msg);
return Result.error(ExecError(errorMessage: msg));
}
await for (var entity in folder.list(recursive: true)) {
final filePath = entity.path;
if (entity.statSync().type != FileSystemEntityType.file) {
_logger
.severe("Unexpected file/folder in photo albums folder: $filePath");
continue;
}
if (!entity.path.toLowerCase().endsWith('json')) {
_logger
.severe("Unexpected file type in photo albums folder: $filePath");
continue;
}
final jsonResult = await _getJson(filePath);
jsonResult.match(
onSuccess: (json) {
final albumResult =
runCatching(() => Result.ok(FriendicaAlbum.fromJson(json)));
albumResult.match(
onSuccess: (album) {
albums.add(album);
_logger.fine('Album converted to PODO');
},
onError: (error) =>
_logger.severe('Error parsing album JSON for $filePath'));
},
onError: (error) =>
_logger.severe('Error parsing photo album: $filePath'));
}
return Result.ok(albums);
}
FutureResult<List<FriendicaContact>, ExecError> readFriends() async {
final basePath = '$rootDirectoryPath/friends_and_followers';
final friendsFile = File('$basePath/friends.json');
final receivedFile = File('$basePath/friend_requests_received.json');
final rejectedFile = File('$basePath/rejected_friend_requests.json');
final removedFile = File('$basePath/removed_friends.json');
final sentFile = File('$basePath/friend_requests_sent.json');
final allFriends = <FriendicaContact>[];
if (!Directory(basePath).existsSync()) {
_logger.severe('Friends base folder does not exist: $basePath');
return Result.error(
ExecError(errorMessage: 'Friends data does not exist'));
}
(await _readFriendsJsonFile(
friendsFile, FriendStatus.friends, "friends_v2"))
.match(
onSuccess: (friends) => allFriends.addAll(friends),
onError: (error) => _logger.info(
"Errors processing friends.json, continuing on without that data"));
(await _readFriendsJsonFile(
receivedFile, FriendStatus.requestReceived, "received_requests_v2"))
.match(
onSuccess: (friends) => allFriends.addAll(friends),
onError: (error) => _logger.info(
"Errors processing received_friend_requests.json, continuing on without that data"));
(await _readFriendsJsonFile(
rejectedFile, FriendStatus.rejectedRequest, "rejected_requests_v2"))
.match(
onSuccess: (friends) => allFriends.addAll(friends),
onError: (error) => _logger.info(
"Errors processing rejected_friend_requests.json, continuing on without that data"));
(await _readFriendsJsonFile(
removedFile, FriendStatus.removed, "deleted_friends_v2"))
.match(
onSuccess: (friends) => allFriends.addAll(friends),
onError: (error) => _logger.info(
"Errors processing removed_friends.json, continuing on without that data"));
(await _readFriendsJsonFile(
sentFile, FriendStatus.removed, "sent_requests_v2"))
.match(
onSuccess: (friends) => allFriends.addAll(friends),
onError: (error) => _logger.info(
"Errors processing sent_friend_requests.json, continuing on without that data"));
return Result.ok(allFriends);
}
static bool validateCanReadArchive(String path) {
_logger.fine('Validating whether path is a valid Facebook Archive: $path');
final baseDir = Directory(path);
if (!baseDir.existsSync()) {
_logger.severe('Unable to find base directory: $path');
return false;
}
try {
baseDir.listSync();
} catch (e) {
_logger.severe('Unable to access base directory: $path');
return false;
}
return true;
}
Result<List<FriendicaTimelineEntry>, ExecError> _parsePostResults(
List<dynamic> json, TimelineType timelineType) {
final postsResult = runCatching(() => Result.ok(json
.map((e) => FriendicaTimelineEntry.fromJson(e, timelineType))
.toList()));
postsResult.match(
onSuccess: (value) => _logger.fine('Posts processed into PODOs'),
onError: (error) =>
_logger.severe('Error mapping JSON to post data: $error'));
return postsResult.mapError((error) =>
error is ExecError ? error : ExecError.message(error.toString()));
}
static FutureResult<Map<String, dynamic>, ExecError> _getJson(String path,
{Level level = Level.FINE}) async {
final file = File(path);
final result = await (await _readFacebookFile(file, level)).andThenAsync(
(jsonText) async => await _parseJsonFileText<Map<String, dynamic>>(
jsonText, file, level));
return result.mapError((error) => error as ExecError);
}
static FutureResult<List<dynamic>, ExecError> _getJsonList(String path,
{Level level = Level.FINE}) async {
final file = File(path);
final fileTextResponse = await _readFacebookFile(file, level);
if (fileTextResponse.isFailure) {
return Result.error(fileTextResponse.error);
}
final jsonText = fileTextResponse.value.trim();
if (!jsonText.startsWith('[')) {
final parsedJsonResult =
await _parseJsonFileText<Map<String, dynamic>>(jsonText, file, level);
return parsedJsonResult.mapValue((value) => [value]);
}
return await _parseJsonFileText<List<dynamic>>(jsonText, file, level);
}
static FutureResult<String, ExecError> _readFacebookFile(
File file, Level level) async {
_logger.log(level, 'Attempting to open and read ${file.path}');
final response = await file.readFacebookEncodedFileAsString();
response.match(
onSuccess: (value) => _logger.log(level, 'Text read from ${file.path}'),
onError: (error) async {
final tmpPath =
await getTempFile(file.uri.pathSegments.last, '.fragment.json');
await File(tmpPath).writeAsString(response.error.errorMessage);
_logger.severe('Wrote partial read of ${file.path} to $tmpPath');
});
return response;
}
static FutureResult<T, ExecError> _parseJsonFileText<T>(
String text, File originalFile, Level levelForFullDump) async {
final jsonParseResult = runCatching(() => Result.ok(jsonDecode(text) as T))
.mapExceptionErrorToExecError();
final msg = jsonParseResult.fold(
onSuccess: (value) => 'JSON decoded from ${originalFile.path}',
onError: (error) async {
final tmpPath = await getTempFile(
originalFile.uri.pathSegments.last, '.ingested.json');
await File(tmpPath).writeAsString(text);
_logger.severe(
'Wrote ingested JSON stream text read of ${originalFile.path} to $tmpPath');
return 'Error parsing json for ${originalFile.path}';
});
_logger.log(levelForFullDump, msg);
return jsonParseResult;
}
FutureResult<List<FriendicaContact>, ExecError> _readFriendsJsonFile(
File file, FriendStatus status, String topKey) async {
final friends = <FriendicaContact>[];
if (file.existsSync()) {
final json = (await _getJson(file.path)).fold(
onSuccess: (json) => json,
onError: (error) {
_logger.severe('Error $error reading json for ${file.path}');
return <String, dynamic>{};
});
final List<dynamic> invited = json[topKey] ?? <Map<String, dynamic>>[];
try {
final entries =
invited.map((f) => FriendicaContact.fromJson(f, status));
_logger.fine(
'${entries.length} friends of type $status found in ${file.path}');
friends.addAll(entries);
} catch (e) {
_logger.severe('Error $e processing JSON $topKey file: ${file.path}');
}
} else {
_logger.info('$topKey file does not exist; ${file.path}');
}
return Result.ok(friends);
}
}

View file

@ -1,283 +0,0 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_album.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_comment.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_contact.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.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/utils/exec_error.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import 'facebook_archive_reader.dart';
class FacebookArchiveDataService extends ChangeNotifier {
static final _logger = Logger('$FacebookArchiveDataService');
final PathMappingService pathMappingService;
final String appDataDirectory;
final List<FriendicaAlbum> albums = [];
final List<FriendicaTimelineEntry> posts = [];
final List<FriendicaComment> comments = [];
final List<FriendicaContact> friends = [];
bool canUseConvoCacheFile = true;
FacebookArchiveDataService(
{required this.pathMappingService, required this.appDataDirectory}) {
_logger.info('Facebook Archive Service created');
}
void clearCaches() {
_logger.fine('clearCaches called');
_logger.finer('Clearing caches');
albums.clear();
posts.clear();
comments.clear();
friends.clear();
notifyListeners();
_logger.finer('Deleting files');
_logger.fine('clearCaches complete');
}
FutureResult<List<FriendicaTimelineEntry>, ExecError> getPosts() async {
_logger.fine('Request for posts');
if (posts.isNotEmpty) {
_logger.fine(
'Posts already loaded, returning existing ${posts.length} posts');
return Result.ok(List.unmodifiable(posts));
}
_logger.finer('No previously pulled posts reading from disk');
final postsResult = await _readAllPosts();
postsResult.match(
onSuccess: (newPosts) {
posts.clear();
posts.addAll(newPosts);
posts.sort((p1, p2) =>
-p1.creationTimestamp.compareTo(p2.creationTimestamp));
},
onError: (error) => _logger.severe('Error loading posts: $error'));
_logger.fine('Returning ${posts.length} posts');
return Result.ok(List.unmodifiable(posts));
}
FutureResult<List<FriendicaComment>, ExecError> getComments() async {
_logger.fine('Request for comments');
if (comments.isNotEmpty) {
_logger.fine(
'Comments already loaded, returning existing ${comments.length} comments');
return Result.ok(List.unmodifiable(comments));
}
_logger.finer('No previously pulled comments reading from disk');
final commentsResult = await _readAllComments();
commentsResult.match(
onSuccess: (newComments) {
comments.clear();
comments.addAll(newComments);
comments.sort((c1, c2) =>
-c1.creationTimestamp.compareTo(c2.creationTimestamp));
},
onError: (error) => _logger.severe('Error loading comments: $error'));
_logger.fine('Returning ${comments.length} comments');
return Result.ok(List.unmodifiable(comments));
}
FutureResult<List<FriendicaContact>, ExecError> getFriends() async {
_logger.fine('Request for friends');
if (friends.isNotEmpty) {
_logger.fine(
'Friends already loaded, returning existing ${friends.length} friends');
return Result.ok(List.unmodifiable(friends));
}
_logger.finer('No previously pulled friends reading from disk');
final friendResult = await _readAllFriends();
friendResult.match(
onSuccess: (newFriends) {
friends.clear();
friends.addAll(newFriends);
},
onError: (error) => _logger.severe('Error loading friends: $error'));
_logger.fine('Returning ${friends.length} friends');
return Result.ok(List.unmodifiable(friends));
}
FutureResult<List<FriendicaAlbum>, ExecError> getAlbums() async {
_logger.fine('Request for albums');
if (albums.isNotEmpty) {
_logger.fine(
'Albums already loaded, returning existing ${albums.length} albums');
return Result.ok(List.unmodifiable(albums));
}
_logger.finer('No previously pulled albums reading from disk');
final albumResult = await _readAllAlbums();
albumResult.match(
onSuccess: (newAlbums) {
albums.clear();
albums.addAll(newAlbums);
},
onError: (error) => _logger.severe('Error loading albums: $error'));
final postsAlbum = await _generatePostsAlbum();
postsAlbum.match(
onSuccess: (album) => albums.add(album),
onError: (error) =>
_logger.severe('Error generating posts album: $error'));
albums.sort((a1, a2) =>
-a1.lastModifiedTimestamp.compareTo(a2.lastModifiedTimestamp));
_logger.fine('Returning ${albums.length} albums');
return Result.ok(List.unmodifiable(albums));
}
FutureResult<List<FriendicaTimelineEntry>, ExecError> _readAllPosts() async {
final allPosts = <FriendicaTimelineEntry>[];
bool hadSuccess = false;
for (final topLevelDir in _topLevelDirs) {
try {
_logger.fine(
'Attempting to find/parse Post JSON data in ${topLevelDir.path}');
final reader = FacebookArchiveFolderReader(topLevelDir.path);
final postsResult = await reader.readPosts();
postsResult.match(
onSuccess: (newPosts) {
allPosts.addAll(newPosts);
hadSuccess = true;
},
onError: (error) => _logger.fine(error));
} catch (e) {
_logger.severe('Exception thrown trying to read posts, $e');
}
}
if (hadSuccess) {
return Result.ok(allPosts);
}
return Result.error(ExecError.message(
'Unable to find any post JSON files in $_baseArchiveFolder'));
}
FutureResult<List<FriendicaComment>, ExecError> _readAllComments() async {
final allComments = <FriendicaComment>[];
bool hadSuccess = false;
for (final topLevelDir in _topLevelDirs) {
try {
_logger.fine(
'Attempting to find/parse comment JSON data in ${topLevelDir.path}');
final reader = FacebookArchiveFolderReader(topLevelDir.path);
final commentsResult = await reader.readComments();
commentsResult.match(
onSuccess: (newEvents) {
allComments.addAll(newEvents);
hadSuccess = true;
},
onError: (error) => _logger.fine(error));
} catch (e) {
_logger.severe('Exception thrown trying to read comments, $e');
}
}
if (hadSuccess) {
return Result.ok(allComments);
}
return Result.error(ExecError.message(
'Unable to find any comment JSON files in $_baseArchiveFolder'));
}
FutureResult<List<FriendicaContact>, ExecError> _readAllFriends() async {
final allFriends = <FriendicaContact>[];
bool hadSuccess = false;
for (final topLevelDir in _topLevelDirs) {
try {
_logger.fine(
'Attempting to find/parse friend JSON data in ${topLevelDir.path}');
final reader = FacebookArchiveFolderReader(topLevelDir.path);
final friendsResult = await reader.readFriends();
friendsResult.match(
onSuccess: (newFriends) {
allFriends.addAll(newFriends);
hadSuccess = true;
},
onError: (error) => _logger.fine(error));
} catch (e) {
_logger.severe('Exception thrown trying to read friends, $e');
}
}
if (hadSuccess) {
return Result.ok(allFriends);
}
return Result.error(ExecError.message(
'Unable to find any album JSON files in $_baseArchiveFolder'));
}
FutureResult<List<FriendicaAlbum>, ExecError> _readAllAlbums() async {
final allAlbums = <FriendicaAlbum>[];
bool hadSuccess = false;
for (final topLevelDir in _topLevelDirs) {
try {
_logger.fine(
'Attempting to find/parse album JSON data in ${topLevelDir.path}');
final reader = FacebookArchiveFolderReader(topLevelDir.path);
final albumResult = await reader.readPhotoAlbums();
albumResult.match(
onSuccess: (newAlbums) {
allAlbums.addAll(newAlbums);
hadSuccess = true;
},
onError: (error) => _logger.fine(error));
} catch (e) {
_logger.severe('Exception thrown trying to read albums, $e');
}
}
if (hadSuccess) {
return Result.ok(allAlbums);
}
return Result.error(ExecError.message(
'Unable to find any album JSON files in $_baseArchiveFolder'));
}
FutureResult<FriendicaAlbum, ExecError> _generatePostsAlbum() async {
const name = 'Photos in Posts';
const description = 'Photos that were added to posts';
final posts = await getPosts();
if (posts.isFailure) {
return Result.error(posts.error);
}
final photos = posts.value
.map((p) => p.mediaAttachments)
.expand((m) => m)
.where((m) => m.explicitType == FriendicaAttachmentMediaType.image)
.toList();
photos
.sort((p1, p2) => p1.creationTimestamp.compareTo(p2.creationTimestamp));
final lastModified = photos.isEmpty ? 0 : photos.last.creationTimestamp;
final coverPhoto =
photos.isEmpty ? FriendicaMediaAttachment.blank() : photos.last;
final album = FriendicaAlbum(
name: name,
description: description,
lastModifiedTimestamp: lastModified,
coverPhoto: coverPhoto,
photos: photos,
comments: []);
return Result.ok(album);
}
String get _baseArchiveFolder => pathMappingService.rootFolder;
List<FileSystemEntity> get _topLevelDirs =>
pathMappingService.archiveDirectories;
}

View file

@ -1,50 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
final _facebookFileReadingLogger = Logger('File.FacebookFileReading');
extension FacebookFileReading on File {
FutureResult<String, ExecError> readFacebookEncodedFileAsString() async {
const leadingSlash = 92;
const leadingU = 117;
final data = await readAsBytes();
final buffer = StringBuffer();
int i = 0;
try {
while (i < data.length) {
if (data[i] == leadingSlash && data[i + 1] == leadingU) {
final byteBuffer = <int>[];
while (i < data.length - 1 &&
data[i] == leadingSlash &&
data[i + 1] == leadingU) {
final chars = data
.sublist(i + 2, i + 6)
.map((e) => e < 97 ? e - 48 : e - 87)
.toList(growable: false);
final byte = (chars[0] << 12) +
(chars[1] << 8) +
(chars[2] << 4) +
(chars[3]);
byteBuffer.add(byte);
i += 6;
}
final unicodeChar = utf8.decode(byteBuffer);
buffer.write(unicodeChar);
} else {
buffer.writeCharCode(data[i]);
i++;
}
}
} catch (e) {
_facebookFileReadingLogger.severe('Error parsing $path, $e');
return Result.error(ExecError(
exception: e as Exception, errorMessage: buffer.toString()));
}
return Result.ok(buffer.toString());
}
}

View file

@ -70,52 +70,8 @@ class PathMappingService {
}
static final _knownRootFilesAndFolders = [
"facebook_100000044480872.zip.enc",
"activity_messages",
"ads_information",
"apps_and_websites_off_of_facebook",
"bug_bounty",
"campus",
"comments_and_reactions",
"events",
"facebook_accounts_center",
"facebook_assistant",
"facebook_gaming",
"facebook_marketplace",
"facebook_news",
"facebook_payments",
"friends_and_followers",
"fundraisers",
"groups",
"journalist_registration",
"live_audio_rooms",
"location",
"messages",
"music_recommendations",
"news_feed",
"notifications",
"other_activity",
"other_logged_information",
"other_personal_information",
"pages",
"polls",
"posts",
"preferences",
"privacy_checkup",
"profile_information",
"reviews",
"saved_items_and_collections",
"search",
"security_and_login_information",
"shops_questions_&_answers",
"short_videos",
"soundbites",
"stories",
"volunteering",
"voting_location_and_reminders",
"your_interactions_on_facebook",
"your_places",
"your_problem_reports",
"your_topics",
'images',
'images.json',
'postsAndComments.json'
];
}

View file

@ -1,9 +1,10 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/services/friendica_archive_service.dart';
import 'friendica/screens/entries_screen.dart';
import 'friendica/screens/stats_screen.dart';
import 'friendica/services/facebook_archive_reader.dart';
import 'settings/settings_controller.dart';
import 'settings/settings_view.dart';
@ -49,8 +50,7 @@ class _HomeState extends State<Home> {
_pages.add(notInitialiedWidget);
}
if (FacebookArchiveFolderReader.validateCanReadArchive(
widget.settingsController.rootFolder)) {
if (!Directory(widget.settingsController.rootFolder).existsSync()) {
_setSelectedIndex(0);
} else {
_setSelectedIndex(_pageData.length - 1);

View file

@ -25,7 +25,6 @@ class SettingsController with ChangeNotifier {
_dateFormatter = DateFormat('MMMM dd yyyy');
_logLevel = await _settingsService.logLevel();
_appDataDirectory = await getApplicationSupportDirectory();
_facebookName = await _settingsService.facebookName();
_geoCacheDirectory = await getTileCachedDirectory();
Logger.root.level = _logLevel;
notifyListeners();
@ -70,17 +69,6 @@ class SettingsController with ChangeNotifier {
await _settingsService.updateRootFolder(newPath);
}
late String _facebookName;
String get facebookName => _facebookName;
Future<void> updateFacebookName(String newName) async {
if (newName == _facebookName) return;
_facebookName = newName;
notifyListeners();
await _settingsService.updateFacebookName(newName);
}
late ThemeMode _themeMode;
ThemeMode get themeMode => _themeMode;

View file

@ -12,7 +12,6 @@ class SettingsService {
static const videoPlayerSettingTypeKey = 'videoPlayerSettingType';
static const videoPlayerCommandKey = 'videoPlayerCustomPath';
static const logLevelKey = "logLevel";
static const facebookNameKey = 'facebookName';
Future<Level> logLevel() async {
const defaultLevelIndex = 5; //INFO
@ -57,17 +56,6 @@ class SettingsService {
await prefs.setString(rootFolderKey, folder);
}
Future<String> facebookName() async {
final prefs = await SharedPreferences.getInstance();
final result = prefs.getString(facebookNameKey) ?? '';
return result;
}
Future<void> updateFacebookName(String folder) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(facebookNameKey, folder);
}
Future<VideoPlayerSettingType> videoPlayerSettingType() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey(videoPlayerSettingTypeKey)) {

View file

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_reader.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';
@ -25,7 +24,6 @@ class SettingsView extends StatefulWidget {
class _SettingsViewState extends State<SettingsView> {
static final _logger = Logger('$_SettingsViewState');
final _facebookNameController = TextEditingController();
final _folderPathController = TextEditingController();
final _videoPlayerPathController = TextEditingController();
String? _invalidFolderString;
@ -37,9 +35,6 @@ class _SettingsViewState extends State<SettingsView> {
@override
void initState() {
_folderPathController.addListener(_validateRootFolder);
_facebookNameController.addListener(() {
_updateSettingsValueDiffs();
});
_videoPlayerPathController.addListener(() {
_updateSettingsValueDiffs();
});
@ -66,8 +61,6 @@ class _SettingsViewState extends State<SettingsView> {
const SizedBox(height: 10),
_buildLoggingOptions(context),
const SizedBox(height: 10),
_buildFacebookNameOptions(context),
const SizedBox(height: 10),
_buildLogFilePath(context),
const SizedBox(height: 10),
_buildGeocacheOptions(context),
@ -169,7 +162,7 @@ class _SettingsViewState extends State<SettingsView> {
controller: _folderPathController,
decoration: InputDecoration(
hintText:
'Root folder of the unzipped Facebook archive file',
'Root folder of the unzipped Friendica archive file',
errorText: _invalidFolderString,
))),
const SizedBox(width: 15),
@ -179,23 +172,6 @@ class _SettingsViewState extends State<SettingsView> {
]);
}
Widget _buildFacebookNameOptions(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Facebook User's Name:",
style: Theme.of(context).textTheme.bodyText1),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: _facebookNameController,
decoration: const InputDecoration(
hintText: 'Displayed user name (used for filtering titles)',
))),
]);
}
Widget _buildThemeOptions(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
@ -290,8 +266,6 @@ class _SettingsViewState extends State<SettingsView> {
.updateVideoPlayerCommand(_videoPlayerPathController.text);
}
await widget._settingsController.updateLogLevel(_logLevel);
await widget._settingsController
.updateFacebookName(_facebookNameController.text);
setState(() {});
}
@ -302,7 +276,6 @@ class _SettingsViewState extends State<SettingsView> {
_videoPlayerPathController.text =
widget._settingsController.videoPlayerCommand;
_logLevel = widget._settingsController.logLevel;
_facebookNameController.text = widget._settingsController.facebookName;
}
void _updateSettingsValueDiffs() {
@ -316,8 +289,6 @@ class _SettingsViewState extends State<SettingsView> {
newValue |= (_videoPlayerPathController.text !=
widget._settingsController.videoPlayerCommand);
newValue |= (_logLevel != widget._settingsController.logLevel);
newValue |= (_facebookNameController.text !=
widget._settingsController.facebookName);
if (oldValue == newValue) return;
setState(() {
_differentSettingValues = newValue;
@ -332,13 +303,6 @@ class _SettingsViewState extends State<SettingsView> {
return;
}
if (!FacebookArchiveFolderReader.validateCanReadArchive(
_folderPathController.text)) {
_invalidFolderString =
'Choose a folder that is a Facebook Archive and accessible.\nOn Macs make sure root folder is in Downloads directory.';
return;
}
_invalidFolderString = null;
_validRootFolder = true;
});

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, "Kyanite");
gtk_header_bar_set_title(header_bar, "Friendica 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, "Kyanite");
gtk_window_set_title(window, "Friendica Archive Browser");
}
gtk_window_set_default_size(window, 900, 700);

View file

@ -55,7 +55,7 @@
/* Begin PBXFileReference section */
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* Kyanite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Facebook Archive Viewer.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* FriendicaArchiveBrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Friendica Archive Browser.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@ -123,7 +123,7 @@
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* Kyanite.app */,
33CC10ED2044A3C60003C045 /* FriendicaArchiveBrowser.app */,
);
name = Products;
sourceTree = "<group>";
@ -194,7 +194,7 @@
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* Kyanite.app */;
productReference = 33CC10ED2044A3C60003C045 /* FriendicaArchiveBrowser.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */

View file

@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Kyanite.app"
BuildableName = "FriendicaArchiveBrowser.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@ -31,7 +31,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Kyanite.app"
BuildableName = "FriendicaArchiveBrowser.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@ -54,7 +54,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Kyanite.app"
BuildableName = "FriendicaArchiveBrowser.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@ -71,7 +71,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "Kyanite.app"
BuildableName = "FriendicaArchiveBrowser.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>

View file

@ -1,52 +0,0 @@
// ignore_for_file: avoid_print
import 'package:flutter_test/flutter_test.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_reader.dart';
import 'package:logging/logging.dart';
void main() {
const String rootPath = 'test_assets/test_facebook_archive';
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((event) {
print(
'${event.level.name} - ${event.loggerName} @ ${event.time}: ${event.message}');
});
group('Test Read Posts JSON', () {
test('Read posts from disk', () async {
final posts =
(await FacebookArchiveFolderReader(rootPath).readPosts()).value;
expect(posts.length, equals(6));
posts.forEach(print);
});
});
group('Test Read Comments JSON', () {
test('Read from disk', () async {
final comments =
await FacebookArchiveFolderReader(rootPath).readComments();
expect(comments.value.length, equals(3));
comments.value.forEach(print);
});
});
group('Test Read Photos JSON', () {
test('Read photos from disk', () async {
final albums =
await FacebookArchiveFolderReader(rootPath).readPhotoAlbums();
expect(albums.value.length, equals(1));
albums.value.forEach(print);
});
});
group('Test Read Friends JSON', () {
test('Read from friends disk', () async {
final friendsResult =
await FacebookArchiveFolderReader(rootPath).readFriends();
friendsResult.match(
onSuccess: (friends) => expect(friends.length, equals(13)),
onError: (error) => fail(error.toString()));
});
});
}

View file

@ -1,37 +0,0 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:friendica_archive_browser/src/friendica/services/facebook_file_reader.dart';
import 'package:logging/logging.dart';
void main() {
const String rootPath = 'test_assets';
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((event) {
print(
'${event.level.name} - ${event.loggerName} @ ${event.time}: ${event.message}');
});
group('Test Facebook Reading', () {
test('Read encoded text from disk', () async {
final expected = [
'This is malformed and should be Polish diacritical character ą.',
'This should be a heart ❤.',
'This should be a five stars ★★ ★★ ★.',
];
const path = '$rootPath/mangled.txt';
final result = await File(path).readFacebookEncodedFileAsString();
expect(result.isSuccess, true);
final lines = result.value.split('\n');
lines.forEach(print);
expect(lines.length, equals(expected.length));
for (var i = 0; i < lines.length; i++) {
//expect(lines[i], equals(expected[i]));
print('|${lines[i]}| ?= |${expected[i]}|');
}
});
});
}

View file

@ -1,3 +0,0 @@
This is malformed and should be Polish diacritical character \u00c4\u0085.
This should be a heart \u00e2\u009d\u00a4\u00ef\u00b8\u008f.
This should be five stars \u00e2\u0098\u0085\u00e2\u0098\u0085\u00e2\u0098\u0085\u00e2\u0098\u0085\u00e2\u0098\u0085.

View file

@ -1,73 +0,0 @@
{
"comments_v2": [
{
"timestamp": 1571613236,
"data": [
{
"comment": {
"timestamp": 1571613236,
"comment": "I'm counting...",
"author": "Your Facebook User"
}
}
],
"title": "Your Facebook User replied to Other Facebook User's comment."
},
{
"timestamp": 1571585885,
"attachments": [
{
"data": [
{
"external_context": {
"url": "https://duckduckgo.com"
}
}
]
}
],
"data": [
{
"comment": {
"timestamp": 1571585885,
"comment": "My favorite search engine! https://duckduckgo.com",
"author": "Your Facebook User"
}
}
],
"title": "Your Facebook User replied to Other Facebook User's comment."
},
{
"timestamp": 1571538865,
"attachments": [
{
"data": [
{
"media": {
"uri": "photos_and_videos/your_posts/ZDkxMjdlMzk4Y2FmMDQzYTU5NDdiMmUw.jpg",
"creation_timestamp": 1571538865,
"media_metadata": {
"photo_metadata": {
"orientation": 1,
"upload_ip": "1.2.3.4"
}
},
"title": ""
}
}
]
}
],
"data": [
{
"comment": {
"timestamp": 1571538866,
"comment": "",
"author": "Your Facebook User"
}
}
],
"title": "Your Facebook User replied to Other Facebook User's comment."
}
]
}

View file

@ -1,19 +0,0 @@
{
"events_invited_v2": [
{
"name": "Event 1",
"start_timestamp": 1593100800,
"end_timestamp": 1593446400
},
{
"name": "Event 2",
"start_timestamp": 1575765000,
"end_timestamp": 1575788400
},
{
"name": "E3",
"start_timestamp": 1572562800,
"end_timestamp": 1574022600
}
]
}

View file

@ -1,40 +0,0 @@
{
"event_responses_v2": {
"events_joined": [
{
"name": "Joined Event 1",
"start_timestamp": 1831662300,
"end_timestamp": 0
},
{
"name": "Joined Event 2",
"start_timestamp": 1569790800,
"end_timestamp": 0
}
],
"events_declined": [
{
"name": "Declined Event 1",
"start_timestamp": 1577044800,
"end_timestamp": 1577563200
},
{
"name": "Declined Event 2",
"start_timestamp": 1572130800,
"end_timestamp": 0
}
],
"events_interested": [
{
"name": "Interested Event 1",
"start_timestamp": 1572134400,
"end_timestamp": 0
},
{
"name": "Interested Event 2",
"start_timestamp": 1543710600,
"end_timestamp": 1543734000
}
]
}
}

View file

@ -1,33 +0,0 @@
{
"your_events_v2": [
{
"name": "Event 1",
"start_timestamp": 1451602800,
"end_timestamp": 0,
"place": {
"name": "House Address, 1234 Some Lane, Somewhere, NV 12345",
"coordinate": {
"latitude": 36.0,
"longitude": -115.0
}
},
"description": "Event 1 Description",
"create_timestamp": 1448639852
},
{
"name": "Event 2",
"start_timestamp": 1447804800,
"end_timestamp": 0,
"place": {
"name": "Some Restaurant",
"coordinate": {
"latitude": 37.0,
"longitude": -114.0
},
"address": "Some Restaurant, Somewhere Shopping Center, 1 Somewhere Lane, Somewhere, NV 12345"
},
"description": "Event 2 Description",
"create_timestamp": 1447367349
}
]
}

View file

@ -1,12 +0,0 @@
{
"received_requests_v2": [
{
"name": "Facebook User4",
"timestamp": 1569081606
},
{
"name": "Facebook User5",
"timestamp": 1569082606
}
]
}

View file

@ -1,12 +0,0 @@
{
"sent_requests_v2": [
{
"name": "Facebook User11",
"timestamp": 1569340806
},
{
"name": "Facebook User12",
"timestamp": 1569360806
}
]
}

View file

@ -1,17 +0,0 @@
{
"friends_v2": [
{
"name": "Facebook User1",
"timestamp": 1568995206
},
{
"name": "Facebook User2",
"timestamp": 1568996206
},
{
"name": "Facebook User3",
"timestamp": 1568997206,
"contact_info": "fbu3@email.com"
}
]
}

View file

@ -1,16 +0,0 @@
{
"rejected_requests_v2": [
{
"name": "Facebook User6",
"timestamp": 1569168006
},
{
"name": "Facebook User7",
"timestamp": 1569178006
},
{
"name": "Facebook User8",
"timestamp": 1569188006
}
]
}

View file

@ -1,16 +0,0 @@
{
"deleted_friends_v2": [
{
"name": "Facebook User8",
"timestamp": 1569254406
},
{
"name": "Facebook User9",
"timestamp": 1569264406
},
{
"name": "Facebook User10",
"timestamp": 1569274406
}
]
}

View file

@ -1,63 +0,0 @@
{
"participants": [
{
"name": "Other Facebook User1"
},
{
"name": "Other Facebook User2"
},
{
"name": "Your Facebook User"
}
],
"messages": [
{
"sender_name": "Your Facebook User",
"timestamp_ms": 1417141329770,
"content": "nice!",
"type": "Generic"
},
{
"sender_name": "Other Facebook User2",
"timestamp_ms": 1417141315479,
"content": "It's been easy to find stuff.",
"type": "Generic"
},
{
"sender_name": "Your Facebook User",
"timestamp_ms": 1417140317192,
"content": "https://www.facebook.com/some_facebook_group",
"share": {
"link": "https://www.facebook.com/some_facebook_group/"
},
"type": "Share"
},
{
"sender_name": "Other Facebook User2",
"timestamp_ms": 1417137805157,
"photos": [
{
"uri": "messages/inbox/User1andUser2_DQxMmNhY/photos/OWRkZTNlNjJhNmU4ODZjMDg0MGY3NjEy.jpg",
"creation_timestamp": 1417137804
}
],
"type": "Generic"
},
{
"sender_name": "Other Facebook User1",
"timestamp_ms": 1417141286161,
"content": "wow nice!",
"type": "Generic"
},
{
"sender_name": "Other Facebook User2",
"timestamp_ms": 1417141262808,
"content": "Hello",
"type": "Generic"
}
],
"title": "User1 and User2",
"is_still_participant": true,
"thread_type": "RegularGroup",
"thread_path": "inbox/OWRkZTNlNjJhNmU4ODZjMDg0MGY3NjEy"
}

View file

@ -1,21 +0,0 @@
{
"name": "Album1",
"photos": [
],
"cover_photo": {
"uri": "photos_and_videos/Album1_OGFiZDdkOG/YzFiMzZmODdkOWNkNGI2YmQ1Zjg5NDI5.jpg",
"creation_timestamp": 1322342687,
"media_metadata": {
"photo_metadata": {
"upload_ip": "1.2.3.4"
}
},
"title": "Album 1 Title"
},
"last_modified_timestamp": 1322342714,
"comments": [
],
"description": ""
}

View file

@ -1,147 +0,0 @@
[
{
"timestamp": 1577798240,
"attachments": [
{
"data": [
{
"external_context": {
"url": "https://duckduckgo.com"
}
}
]
}
],
"data": [
{
"post": "Great search engine! https://duckduckgo.com"
},
{
"update_timestamp": 1577798240
}
],
"title": "Your Facebook User"
},
{
"timestamp": 1577590323,
"attachments": [],
"data": [
{
"update_timestamp": 1577590323
}
],
"title": "Your Facebook User"
},
{
"timestamp": 1575129131,
"attachments": [
{
"data": [
{
"media": {
"uri": "photos_and_videos/TimelinePhotos_NzMwNzNlYzI0YT/OWRkMmRiMTVkOWU4ZGVhNGEzN2RiMDFk.jpg",
"creation_timestamp": 1575129121,
"media_metadata": {
"photo_metadata": {
"upload_ip": "1.2.3.4"
}
},
"title": "Timeline Photos",
"description": "The sand mandala is a Buddhist practice of making art out of sand (days to weeks) to then shortly after completion brush it away. It's a reminder of our impermanence, and to me a reminder to enjoy the journey as much as the destination. Both are equally ephemeral."
}
}
]
}
],
"data": [
{
"post": "The sand mandala is a Buddhist practice of making art out of sand (days to weeks) to then shortly after completion brush it away. It's a reminder of our impermanence, and to me a reminder to enjoy the journey as much as the destination. Both are equally ephemeral."
}
]
},
{
"timestamp": 1574803760,
"attachments": [
{
"data": [
{
"media": {
"uri": "photos_and_videos/your_posts/OWRlZTdkNjgwZjI0MzExNjFjMmU2YTBj.jpg",
"creation_timestamp": 1574803743,
"media_metadata": {
"photo_metadata": {
"upload_ip": "1.2.3.4"
}
},
"title": "",
"description": "Y tho meme"
}
}
]
}
],
"data": [
{
"post": "Y tho meme"
}
],
"title": "Your Facebook User posted in Some Club."
},
{
"timestamp": 1574803309,
"data": [
{
"post": "Some thoughts..."
}
],
"title": "Your Facebook User posted in Some Other Club."
},
{
"timestamp": 1575771550,
"attachments": [
{
"data": [
{
"media": {
"uri": "photos_and_videos/videos/NWJlY2M0NWM5ZmUwZmI0NWM5ZjY4MDYx.mp4",
"creation_timestamp": 1575771558,
"media_metadata": {
"video_metadata": {
"upload_timestamp": 0,
"upload_ip": "75.171.6.169"
}
},
"thumbnail": {
"uri": "photos_and_videos/thumbnails/YjVkNjhiY2NiMjBjNzIwNDJhOTMyMTQx.jpg"
},
"comments": [
{
"timestamp": 1575772044,
"comment": "sudo rm -rf /",
"author": "User NTJiZTk2OGJ"
},
{
"timestamp": 1575772171,
"comment": "Machine?",
"author": "User OGE1NmJkYj"
},
{
"timestamp": 1575799409,
"comment": "Woah!",
"author": "User ODA0NmJmMDZ"
}
],
"title": "",
"description": "Neat! I'll have to try that next time I get on a Windows machine :)"
}
}
]
}
],
"data": [
{
"post": "Neat! I'll have to try that next time I get on a Windows machine :)"
}
]
}
]

View file

@ -1,36 +0,0 @@
{
"videos": [
{
"uri": "photos_and_videos/videos/NWJlY2M0NWM5ZmUwZmI0NWM5ZjY4MDYx.mp4",
"creation_timestamp": 1575771558,
"media_metadata": {
"video_metadata": {
"upload_timestamp": 0,
"upload_ip": "1.2.3.4"
}
},
"thumbnail": {
"uri": "photos_and_videos/videos/NWJlY2M0NWM5ZmUwZmI0NWM5ZjY4MDYx.mp4"
},
"comments": [
{
"timestamp": 1575772044,
"comment": "sudo rm -rf /",
"author": "User NTJiZTk2OGJ"
},
{
"timestamp": 1575772171,
"comment": "Machine?",
"author": "User OGE1NmJkYj"
},
{
"timestamp": 1575799409,
"comment": "Woah!",
"author": "User ODA0NmJmMDZ"
}
],
"title": "",
"description": "Neat! I'll have to try that next time I get on a Windows machine :)"
}
]
}

View file

@ -1,75 +0,0 @@
{
"saves_and_collections_v2": [
{
"title": "user1 saved user2's post."
},
{
"timestamp": 1435620001,
"title": "user1 saved user3's post."
},
{
"attachments": [
{
"data": [
{
"external_context": {
"name": "external title",
"source": "Source1",
"url": "http://source1.com/story1.html"
}
}
]
}
],
"title": "user1 saved a link."
},
{
"timestamp": 1435620002,
"attachments": [
{
"data": [
{
"external_context": {
"name": "external title",
"source": "https://source2.com/story22/",
"url": "source2.com"
}
}
]
}
],
"title": "user1 saved a link."
},
{
"timestamp": 1435620003,
"attachments": [
{
"data": [
{
"external_context": {
"name": "Some Facebook Page"
}
}
]
}
],
"title": "user1 saved a page."
},
{
"timestamp": 1435620004,
"attachments": [
{
"data": [
{
"external_context": {
"name": "Story title 3",
"url": "http://source3.com/story3"
}
}
]
}
],
"title": "user1 saved a link from his post."
}
]
}

View file

@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(915, 700);
if (!window.CreateAndShow(L"Kyanite", origin, size)) {
if (!window.CreateAndShow(L"Friendica Archive Browser", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);