Refactor out all Facebook/Kyanite references or specific code.
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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)'),
|
||||
])),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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]}|');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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.
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"received_requests_v2": [
|
||||
{
|
||||
"name": "Facebook User4",
|
||||
"timestamp": 1569081606
|
||||
},
|
||||
{
|
||||
"name": "Facebook User5",
|
||||
"timestamp": 1569082606
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"sent_requests_v2": [
|
||||
{
|
||||
"name": "Facebook User11",
|
||||
"timestamp": 1569340806
|
||||
},
|
||||
{
|
||||
"name": "Facebook User12",
|
||||
"timestamp": 1569360806
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"rejected_requests_v2": [
|
||||
{
|
||||
"name": "Facebook User6",
|
||||
"timestamp": 1569168006
|
||||
},
|
||||
{
|
||||
"name": "Facebook User7",
|
||||
"timestamp": 1569178006
|
||||
},
|
||||
{
|
||||
"name": "Facebook User8",
|
||||
"timestamp": 1569188006
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"deleted_friends_v2": [
|
||||
{
|
||||
"name": "Facebook User8",
|
||||
"timestamp": 1569254406
|
||||
},
|
||||
{
|
||||
"name": "Facebook User9",
|
||||
"timestamp": 1569264406
|
||||
},
|
||||
{
|
||||
"name": "Facebook User10",
|
||||
"timestamp": 1569274406
|
||||
}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 70 KiB |
|
@ -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"
|
||||
}
|
|
@ -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": ""
|
||||
}
|
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 197 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 1.8 KiB |
|
@ -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 :)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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 :)"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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);
|
||||
|
|