Initial Friendica posts showing up in timeline

This commit is contained in:
Hank Grabowski 2022-01-18 13:38:25 -05:00
parent 0c47ad1432
commit dc3672a58d
9 changed files with 212 additions and 134 deletions

View file

@ -2,6 +2,7 @@ import 'package:desktop_window/desktop_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:friendica_archive_browser/src/services/friendica_archive_service.dart';
import 'package:friendica_archive_browser/src/themes.dart';
import 'package:friendica_archive_browser/src/utils/scrolling_behavior.dart';
import 'package:provider/provider.dart';
@ -26,11 +27,14 @@ class FriendicaArchiveBrowser extends StatelessWidget {
Widget build(BuildContext context) {
DesktopWindow.setMinWindowSize(minAppSize);
final pathMappingService = PathMappingService(settingsController);
final archiveService = FacebookArchiveDataService(
final facebookArchiveService = FacebookArchiveDataService(
pathMappingService: pathMappingService,
appDataDirectory: settingsController.appDataDirectory.path);
final friendicaArchiveService =
FriendicaArchiveService(pathMappingService: pathMappingService);
settingsController.addListener(() {
archiveService.clearCaches();
friendicaArchiveService.clearCaches();
facebookArchiveService.clearCaches();
pathMappingService.refresh();
});
return AnimatedBuilder(
@ -56,7 +60,10 @@ class FriendicaArchiveBrowser extends StatelessWidget {
home: MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => settingsController),
ChangeNotifierProvider(create: (context) => archiveService),
ChangeNotifierProvider(
create: (context) => facebookArchiveService),
ChangeNotifierProvider(
create: (context) => friendicaArchiveService),
Provider(create: (context) => pathMappingService),
],
child: Home(settingsController: settingsController),

View file

@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_post.dart';
import 'package:friendica_archive_browser/src/friendica/models/location_data.dart';
import 'package:friendica_archive_browser/src/friendica/models/timeline_type.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';
@ -53,19 +52,6 @@ class PostCard extends StatelessWidget {
style: const TextStyle(
fontStyle: FontStyle.italic,
)),
if (post.timelineType != TimelineType.active)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Tooltip(
message:
'Post is in ${post.timelineType == TimelineType.trash ? 'Trash' : 'Archive'}',
child: Icon(
post.timelineType == TimelineType.trash
? Icons.delete_outline
: Icons.archive_outlined,
color: Theme.of(context).disabledColor,
)),
),
Tooltip(
message: 'Copy text version of post to clipboard',
child: IconButton(
@ -76,6 +62,19 @@ class PostCard extends StatelessWidget {
icon: const Icon(Icons.copy)),
),
]),
Wrap(
direction: Axis.horizontal,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
const Text(
'By: ',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Text(post.author)
],
),
if (post.post.isNotEmpty) ...[
const SizedBox(height: spacingHeight),
Text(post.post)

View file

@ -1,4 +1,5 @@
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
@ -20,6 +21,8 @@ class FriendicaPost {
final String title;
final String author;
final List<FriendicaMediaAttachment> mediaAttachments;
final LocationData locationData;
@ -34,6 +37,7 @@ class FriendicaPost {
this.modificationTimestamp = 0,
this.post = '',
this.title = '',
this.author = '',
this.locationData = const LocationData(),
required this.timelineType,
List<FriendicaMediaAttachment>? mediaAttachments,
@ -47,6 +51,7 @@ class FriendicaPost {
modificationTimestamp = DateTime.now().millisecondsSinceEpoch,
post = 'Random post text ${randomId()}',
title = 'Random title ${randomId()}',
author = 'Random author ${randomId()}',
locationData = LocationData.randomBuilt(),
timelineType = TimelineType.active,
links = [
@ -64,6 +69,7 @@ class FriendicaPost {
int? modificationTimestamp,
String? post,
String? title,
String? author,
LocationData? locationData,
List<FriendicaMediaAttachment>? mediaAttachments,
TimelineType? timelineType,
@ -75,6 +81,7 @@ class FriendicaPost {
modificationTimestamp ?? this.modificationTimestamp,
post: post ?? this.post,
title: title ?? this.title,
author: author ?? this.author,
locationData: locationData ?? this.locationData,
mediaAttachments: mediaAttachments ?? this.mediaAttachments,
timelineType: timelineType ?? this.timelineType,
@ -83,7 +90,7 @@ class FriendicaPost {
@override
String toString() {
return 'FacebookPost{creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, timelineType: $timelineType, post: $post, title: $title, mediaAttachments: $mediaAttachments, links: $links}';
return 'FacebookPost{creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, timelineType: $timelineType, post: $post, title: $title, author: $author, mediaAttachments: $mediaAttachments, links: $links}';
}
String toHumanString(PathMappingService mapper, DateFormat formatter) {
@ -94,6 +101,7 @@ class FriendicaPost {
'Title: $title',
'Creation At: $creationDateString',
'Text:',
'Author: $author',
post,
'',
if (links.isNotEmpty) 'Links:',
@ -117,95 +125,33 @@ class FriendicaPost {
static FriendicaPost fromJson(
Map<String, dynamic> json, TimelineType timelineType) {
final int timestamp = json['timestamp'] ?? 0;
var modificationTimestamp = timestamp;
var backdatedTimestamp = timestamp;
var locationData = const LocationData();
String post = '';
if (json.containsKey('data')) {
final data = json['data'];
for (var dataItem in data) {
if (dataItem.containsKey('post')) {
post = dataItem['post'];
} else if (dataItem.containsKey('update_timestamp')) {
modificationTimestamp = dataItem['update_timestamp'];
} else if (dataItem.containsKey('backdated_timestamp')) {
backdatedTimestamp = dataItem['backdated_timestamp'];
} else {
_logger.fine(
"No post or update key sequence in post @$timestamp: ${dataItem.keys}");
}
}
}
final String title = json['title'] ?? '';
final int timestamp = json.containsKey('created_at')
? OffsetDateTimeUtils.epochSecTimeFromFriendicaString(
json['created_at'])
.fold(
onSuccess: (value) => value,
onError: (error) {
_logger.severe("Couldn't read date time string: $error");
return 0;
})
: 0;
final post = json['text'] ?? '';
final author = json['user']['name'];
final title = json['friendica_title'] ?? '';
final actualLocationData = LocationData();
final modificationTimestamp = timestamp;
final backdatedTimestamp = timestamp;
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 (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']));
} else if (dataItem.containsKey('place')) {
locationData = LocationData.fromJson(dataItem['place']);
} else {
//TODO Add Facebook Post Poll Processing
if (dataItem.containsKey('poll')) continue;
//TODO Add Facebook Post attachment text processing
if (dataItem.containsKey('text')) continue;
//TODO Add Facebook Post external context detailed link processing (not just the URL)
if (dataItem.containsKey('name')) continue;
//TODO Add Facebook Post event processing
if (dataItem.containsKey('event')) continue;
_logger.fine('Unknown post key type: ${dataItem.keys}');
}
}
}
}
late final LocationData actualLocationData;
if (locationData.hasPosition) {
actualLocationData = locationData;
} else {
final mediaWithPosition = mediaAttachments.where((m) =>
m.metadata.containsKey('latitude') &&
m.metadata.containsKey('longitude'));
if (mediaWithPosition.isNotEmpty) {
final metadata = mediaWithPosition.first.metadata;
final latitude = double.tryParse(metadata['latitude'] ?? '') ?? 0.0;
final longitude = double.tryParse(metadata['longitude'] ?? '') ?? 0.0;
actualLocationData = LocationData(
latitude: latitude, longitude: longitude, hasPosition: true);
} else {
actualLocationData = locationData;
}
}
final String actualTitle = title.isNotEmpty
? title
: mediaAttachments
.map((m) => m.title)
.firstWhere((t) => t.isNotEmpty, orElse: () => '');
return FriendicaPost(
creationTimestamp: timestamp,
modificationTimestamp: modificationTimestamp,
backdatedTimestamp: backdatedTimestamp,
locationData: actualLocationData,
post: post,
title: actualTitle,
author: author,
title: title,
links: links,
mediaAttachments: mediaAttachments,
timelineType: timelineType,

View file

@ -3,8 +3,8 @@ import 'package:friendica_archive_browser/src/friendica/components/filter_contro
import 'package:friendica_archive_browser/src/friendica/components/post_card.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_post.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/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:logging/logging.dart';
@ -21,14 +21,14 @@ class PostsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
_logger.info('Build FacebookPostListView');
final service = Provider.of<FacebookArchiveDataService>(context);
_logger.info('Build FriendicaPostListView');
final service = Provider.of<FriendicaArchiveService>(context);
final username = Provider.of<SettingsController>(context).facebookName;
return FutureBuilder<Result<List<FriendicaPost>, ExecError>>(
future: service.getPosts(),
builder: (context, snapshot) {
_logger.info('FacebookPostListView Future builder called');
_logger.info('FriendicaPostListView Future builder called');
if (!snapshot.hasData ||
snapshot.connectionState != ConnectionState.done) {
@ -43,48 +43,49 @@ class PostsScreen extends StatelessWidget {
}
final allPosts = postsResult.value;
final filteredPosts = username.isEmpty
? allPosts
: allPosts.where((p) =>
p.title != username ||
p.post.isNotEmpty ||
p.mediaAttachments.isNotEmpty ||
p.links.isNotEmpty);
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();
final posts = allPosts;
// final filteredPosts = username.isEmpty
// ? allPosts
// : allPosts.where((p) =>
// p.title != username ||
// p.post.isNotEmpty ||
// p.mediaAttachments.isNotEmpty ||
// p.links.isNotEmpty);
//
// 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');
}
_logger.fine('Build Posts ListView');
return _FacebookPostsScreenWidget(posts: posts);
return _FriendicaPostsScreenWidget(posts: posts);
});
}
}
class _FacebookPostsScreenWidget extends StatelessWidget {
static final _logger = Logger('$_FacebookPostsScreenWidget');
class _FriendicaPostsScreenWidget extends StatelessWidget {
static final _logger = Logger('$_FriendicaPostsScreenWidget');
final List<FriendicaPost> posts;
const _FacebookPostsScreenWidget({Key? key, required this.posts})
const _FriendicaPostsScreenWidget({Key? key, required this.posts})
: super(key: key);
@override
@ -112,10 +113,10 @@ class _FacebookPostsScreenWidget extends StatelessWidget {
child: ListView.separated(
primary: false,
physics: const RangeMaintainingScrollPhysics(),
restorationId: 'facebookPostsListView',
restorationId: 'friendicaPostsListView',
itemCount: items.length,
itemBuilder: (context, index) {
_logger.finer('Rendering FacebookPost List Item');
_logger.finer('Rendering Friendica List Item');
return PostCard(post: items[index]);
},
separatorBuilder: (context, index) {

View file

@ -0,0 +1,19 @@
class ImageEntry {
final String postId;
final String localFilename;
final String url;
ImageEntry(
{required this.postId, required this.localFilename, required this.url});
ImageEntry.fromJson(Map<String, dynamic> json)
: postId = json['postId'] ?? '',
localFilename = json['localFilename'] ?? '',
url = json['url'] ?? '';
Map<String, dynamic> toJson() => {
'postId': postId,
'localFilename': localFilename,
'url': url,
};
}

View file

@ -0,0 +1,72 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:friendica_archive_browser/src/friendica/models/friendica_post.dart';
import 'package:friendica_archive_browser/src/friendica/models/timeline_type.dart';
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
import 'package:friendica_archive_browser/src/models/local_image_archive_entry.dart';
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:path/path.dart' as p;
import 'package:result_monad/result_monad.dart';
class FriendicaArchiveService extends ChangeNotifier {
final PathMappingService pathMappingService;
final Map<String, ImageEntry> _imagesByRequestUrl = {};
final List<FriendicaPost> _posts = [];
FriendicaArchiveService({required this.pathMappingService});
void clearCaches() {
_imagesByRequestUrl.clear();
_posts.clear();
}
FutureResult<List<FriendicaPost>, ExecError> getPosts() async {
if (_posts.isEmpty) {
_loadPosts();
}
return Result.ok(_posts);
}
Result<ImageEntry, ExecError> getImageByUrl(String url) {
if (_imagesByRequestUrl.isEmpty) {
_loadImages();
}
final result = _imagesByRequestUrl[url];
return result == null
? Result.error(ExecError(errorMessage: '$url not found'))
: Result.ok(result);
}
String get _baseArchiveFolder => pathMappingService.rootFolder;
void _loadPosts() {
final entriesJsonPath = p.join(_baseArchiveFolder, 'postsAndComments.json');
final jsonFile = File(entriesJsonPath);
if (jsonFile.existsSync()) {
final json = jsonDecode(jsonFile.readAsStringSync()) as List<dynamic>;
final postEntries =
json.map((j) => FriendicaPost.fromJson(j, TimelineType.active));
_posts.clear();
_posts.addAll(postEntries);
}
final uniqueUsers = _posts.map((e) => e.author).toSet();
print(uniqueUsers);
}
void _loadImages() {
final imageJsonPath = p.join(_baseArchiveFolder, 'images.json');
final jsonFile = File(imageJsonPath);
if (jsonFile.existsSync()) {
final json = jsonDecode(jsonFile.readAsStringSync()) as List<dynamic>;
final imageEntries = json.map((j) => ImageEntry.fromJson(j));
for (final entry in imageEntries) {
_imagesByRequestUrl[entry.url] = entry;
}
}
}
}

View file

@ -0,0 +1,21 @@
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
import 'package:result_monad/result_monad.dart';
import 'package:time_machine/time_machine_text_patterns.dart';
class OffsetDateTimeUtils {
static final _parser = OffsetDateTimePattern.createWithInvariantCulture(
'ddd MMM dd HH:mm:ss o<+HHmm> yyyy');
static Result<int, ExecError> epochSecTimeFromFriendicaString(
String dateString) {
final offsetDateTime = _parser.parse(dateString);
if (!offsetDateTime.success) {
return Result.error(ExecError.message(offsetDateTime.error.toString()));
}
return Result.ok(offsetDateTime.value.localDateTime
.toDateTimeLocal()
.millisecondsSinceEpoch ~/
1000);
}
}

View file

@ -453,6 +453,15 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
time_machine:
dependency: "direct main"
description:
path: "."
ref: master
resolved-ref: "040de1a261df442538ed97f6de5895465d7ca4dd"
url: "https://github.com/Dana-Ferguson/time_machine"
source: git
version: "0.9.17"
typed_data:
dependency: transitive
description:

View file

@ -29,6 +29,10 @@ dependencies:
result_monad: ^1.0.2
scrollable_positioned_list: ^0.2.2
shared_preferences: ^2.0.8
time_machine:
git:
url: https://github.com/Dana-Ferguson/time_machine
ref: master
url_launcher: ^6.0.12
uuid: ^3.0.5
network_to_file_image: ^3.0.3