mirror of
https://gitlab.com/mysocialportal/fediverse-archiving-tools.git
synced 2024-10-18 08:53:31 +00:00
D* (minus photos) and Friendica loading in UI.
This commit is contained in:
parent
b8b40ebe5b
commit
1deaebd94e
47 changed files with 970 additions and 581 deletions
|
@ -2,12 +2,11 @@ import 'package:desktop_window/desktop_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart';
|
import 'package:friendica_archive_browser/src/services/archive_service_provider.dart';
|
||||||
import 'package:friendica_archive_browser/src/themes.dart';
|
import 'package:friendica_archive_browser/src/themes.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/scrolling_behavior.dart';
|
import 'package:friendica_archive_browser/src/utils/scrolling_behavior.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'friendica/services/friendica_path_mapping_service.dart';
|
|
||||||
import 'home.dart';
|
import 'home.dart';
|
||||||
import 'settings/settings_controller.dart';
|
import 'settings/settings_controller.dart';
|
||||||
|
|
||||||
|
@ -25,12 +24,9 @@ class FriendicaArchiveBrowser extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
DesktopWindow.setMinWindowSize(minAppSize);
|
DesktopWindow.setMinWindowSize(minAppSize);
|
||||||
final pathMappingService = FriendicaPathMappingService(settingsController);
|
final archiveService = ArchiveServiceProvider(settingsController);
|
||||||
final friendicaArchiveService =
|
|
||||||
FriendicaArchiveService(pathMappingService: pathMappingService);
|
|
||||||
settingsController.addListener(() {
|
settingsController.addListener(() {
|
||||||
friendicaArchiveService.clearCaches();
|
archiveService.clearCaches();
|
||||||
pathMappingService.refresh();
|
|
||||||
});
|
});
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: settingsController,
|
animation: settingsController,
|
||||||
|
@ -55,13 +51,11 @@ class FriendicaArchiveBrowser extends StatelessWidget {
|
||||||
home: MultiProvider(
|
home: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (context) => settingsController),
|
ChangeNotifierProvider(create: (context) => settingsController),
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(create: (context) => archiveService),
|
||||||
create: (context) => friendicaArchiveService),
|
|
||||||
Provider(create: (context) => pathMappingService),
|
|
||||||
],
|
],
|
||||||
child: Home(
|
child: Home(
|
||||||
settingsController: settingsController,
|
settingsController: settingsController,
|
||||||
archiveService: friendicaArchiveService),
|
archiveService: archiveService),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:friendica_archive_browser/src/models/time_element.dart';
|
import 'package:friendica_archive_browser/src/models/time_element.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_connections.dart';
|
import 'package:friendica_archive_browser/src/services/connections_manager.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/top_interactors_generator.dart';
|
import 'package:friendica_archive_browser/src/utils/top_interactors_generator.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -8,7 +8,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class TopInteractorsWidget extends StatefulWidget {
|
class TopInteractorsWidget extends StatefulWidget {
|
||||||
final List<TimeElement> entries;
|
final List<TimeElement> entries;
|
||||||
final FriendicaConnections connections;
|
final ConnectionsManager connections;
|
||||||
|
|
||||||
const TopInteractorsWidget(this.entries, this.connections, {Key? key})
|
const TopInteractorsWidget(this.entries, this.connections, {Key? key})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:friendica_archive_browser/src/models/friendica_contact.dart';
|
import 'package:friendica_archive_browser/src/models/connection.dart';
|
||||||
|
|
||||||
Contact friendicaContactFromDiasporaJson(Map<String, dynamic> json) {
|
Connection contactFromDiasporaJson(Map<String, dynamic> json) {
|
||||||
const network = "Diaspora";
|
const network = "Diaspora";
|
||||||
final accountId = json['account_id'] ?? '';
|
final accountId = json['account_id'] ?? '';
|
||||||
final profileUrl = _profileUrlFromAccountId(accountId);
|
final profileUrl = _profileUrlFromAccountId(accountId);
|
||||||
|
@ -17,7 +17,7 @@ Contact friendicaContactFromDiasporaJson(Map<String, dynamic> json) {
|
||||||
status = ConnectionStatus.theyFollowYou;
|
status = ConnectionStatus.theyFollowYou;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Contact(
|
return Connection(
|
||||||
status: status,
|
status: status,
|
||||||
name: name,
|
name: name,
|
||||||
id: id,
|
id: id,
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
import 'package:friendica_archive_browser/src/models/connection.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:markdown/markdown.dart';
|
||||||
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
|
import '../../services/connections_manager.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('DiasporaPostsSerializer');
|
||||||
|
const _statusMessageType = 'status_message';
|
||||||
|
const _reshareType = 'reshare';
|
||||||
|
const _knownPostTypes = [_statusMessageType, _reshareType];
|
||||||
|
|
||||||
|
Result<TimelineEntry, ExecError> timelineItemFromDiasporaPostJson(
|
||||||
|
Map<String, dynamic> json, ConnectionsManager connections) {
|
||||||
|
if (!json.containsKey('entity_data')) {
|
||||||
|
return Result.error(ExecError.message('Timeline item has no entity data'));
|
||||||
|
}
|
||||||
|
final entityData = json['entity_data'] ?? {};
|
||||||
|
final postId = entityData['guid'] ?? '';
|
||||||
|
final entityType = json['entity_type'] ?? '';
|
||||||
|
if (!_knownPostTypes.contains(entityType)) {
|
||||||
|
final error = 'Unknown entity type $entityType for Post ID: $postId';
|
||||||
|
_logger.severe(error);
|
||||||
|
return Result.error(ExecError.message(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityType == _statusMessageType) {
|
||||||
|
return _buildStatusMessageType(entityData, connections);
|
||||||
|
} else {
|
||||||
|
return _buildReshareMessageType(entityData, connections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<TimelineEntry, ExecError> _buildReshareMessageType(
|
||||||
|
entityData, ConnectionsManager connections) {
|
||||||
|
final createdAtString = entityData['created_at'] ?? '';
|
||||||
|
final epochTime =
|
||||||
|
OffsetDateTimeUtils.epochSecTimeFromTimeZoneString(createdAtString)
|
||||||
|
.getValueOrElse(() => -1);
|
||||||
|
final postId = entityData['guid'] ?? '';
|
||||||
|
final authorName = entityData['author'] ?? '';
|
||||||
|
final parentGuid = entityData['root_guid'] ?? '';
|
||||||
|
final parentName = entityData['root_author'] ?? '';
|
||||||
|
final externalLink = _buildReshareUrl(authorName, parentName, parentGuid);
|
||||||
|
final author =
|
||||||
|
connections.getByName(authorName).getValueOrElse(() => Connection());
|
||||||
|
final parentAuthor = connections
|
||||||
|
.getByName(parentName)
|
||||||
|
.getValueOrElse(() => Connection(name: parentName));
|
||||||
|
final timelineEntry = TimelineEntry(
|
||||||
|
id: postId,
|
||||||
|
creationTimestamp: epochTime,
|
||||||
|
author: author.name,
|
||||||
|
authorId: author.id,
|
||||||
|
isReshare: true,
|
||||||
|
parentAuthor: parentAuthor.name,
|
||||||
|
parentAuthorId: parentAuthor.id,
|
||||||
|
externalLink: externalLink,
|
||||||
|
);
|
||||||
|
return Result.ok(timelineEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<TimelineEntry, ExecError> _buildStatusMessageType(
|
||||||
|
entityData, ConnectionsManager connections) {
|
||||||
|
final createdAtString = entityData['created_at'] ?? '';
|
||||||
|
final epochTime =
|
||||||
|
OffsetDateTimeUtils.epochSecTimeFromTimeZoneString(createdAtString)
|
||||||
|
.getValueOrElse(() => -1);
|
||||||
|
final postId = entityData['guid'] ?? '';
|
||||||
|
final postMarkdown = entityData['text'] ?? '';
|
||||||
|
final postHtml = markdownToHtml(postMarkdown);
|
||||||
|
final authorName = entityData['author'] ?? '';
|
||||||
|
final author =
|
||||||
|
connections.getByName(authorName).getValueOrElse(() => Connection());
|
||||||
|
final timelineEntry = TimelineEntry(
|
||||||
|
id: postId,
|
||||||
|
creationTimestamp: epochTime,
|
||||||
|
body: postHtml,
|
||||||
|
author: author.name,
|
||||||
|
authorId: author.id);
|
||||||
|
return Result.ok(timelineEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildReshareUrl(String author, String rootAuthor, String rootGuid) {
|
||||||
|
final accountId = rootAuthor.isNotEmpty ? rootAuthor : author;
|
||||||
|
final accountIdPieces = accountId.split('@');
|
||||||
|
if (accountIdPieces.length != 2) {
|
||||||
|
return rootGuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
final server = accountIdPieces[1];
|
||||||
|
|
||||||
|
return 'https://$server/p/$rootGuid';
|
||||||
|
}
|
|
@ -1,23 +1,28 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:friendica_archive_browser/src/diaspora/services/diaspora_path_mapping_service.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/diaspora/services/diaspora_profile_json_reader.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/services/archive_service_interface.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
import '../../friendica/models/friendica_entry_tree_item.dart';
|
import '../../models/entry_tree_item.dart';
|
||||||
import '../../friendica/models/friendica_timeline_entry.dart';
|
|
||||||
import '../../friendica/services/friendica_path_mapping_service.dart';
|
|
||||||
import '../../models/local_image_archive_entry.dart';
|
import '../../models/local_image_archive_entry.dart';
|
||||||
|
import '../../services/connections_manager.dart';
|
||||||
import '../../utils/exec_error.dart';
|
import '../../utils/exec_error.dart';
|
||||||
import '../../friendica/services/friendica_connections.dart';
|
|
||||||
|
|
||||||
class DiasporaArchiveService {
|
class DiasporaArchiveService implements ArchiveService {
|
||||||
final FriendicaPathMappingService pathMappingService;
|
@override
|
||||||
|
final DiasporaPathMappingService pathMappingService;
|
||||||
|
|
||||||
final Map<String, ImageEntry> _imagesByRequestUrl = {};
|
final Map<String, ImageEntry> _imagesByRequestUrl = {};
|
||||||
final List<FriendicaEntryTreeItem> _postEntries = [];
|
final List<EntryTreeItem> _postEntries = [];
|
||||||
final List<FriendicaEntryTreeItem> _orphanedCommentEntries = [];
|
final List<EntryTreeItem> _orphanedCommentEntries = [];
|
||||||
final List<FriendicaEntryTreeItem> _allComments = [];
|
final List<EntryTreeItem> _allComments = [];
|
||||||
final FriendicaConnections connections = FriendicaConnections();
|
|
||||||
|
@override
|
||||||
|
final ConnectionsManager connectionsManager = ConnectionsManager();
|
||||||
String _ownersName = '';
|
String _ownersName = '';
|
||||||
|
|
||||||
DiasporaArchiveService({required this.pathMappingService});
|
DiasporaArchiveService({required this.pathMappingService});
|
||||||
|
@ -25,33 +30,32 @@ class DiasporaArchiveService {
|
||||||
String get ownersName => _ownersName;
|
String get ownersName => _ownersName;
|
||||||
|
|
||||||
void clearCaches() {
|
void clearCaches() {
|
||||||
connections.clearCaches();
|
connectionsManager.clearCaches();
|
||||||
_imagesByRequestUrl.clear();
|
_imagesByRequestUrl.clear();
|
||||||
_orphanedCommentEntries.clear();
|
_orphanedCommentEntries.clear();
|
||||||
_allComments.clear();
|
_allComments.clear();
|
||||||
_postEntries.clear();
|
_postEntries.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<FriendicaEntryTreeItem>, ExecError> getPosts() async {
|
FutureResult<List<EntryTreeItem>, ExecError> getPosts() async {
|
||||||
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
||||||
_loadEntries();
|
_loadProfileFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(_postEntries);
|
return Result.ok(_postEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<FriendicaEntryTreeItem>, ExecError> getAllComments() async {
|
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() async {
|
||||||
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
||||||
_loadEntries();
|
_loadProfileFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(_allComments);
|
return Result.ok(_allComments);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<FriendicaEntryTreeItem>, ExecError>
|
FutureResult<List<EntryTreeItem>, ExecError> getOrphanedComments() async {
|
||||||
getOrphanedComments() async {
|
|
||||||
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
||||||
_loadEntries();
|
_loadProfileFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok(_orphanedCommentEntries);
|
return Result.ok(_orphanedCommentEntries);
|
||||||
|
@ -70,46 +74,21 @@ class DiasporaArchiveService {
|
||||||
|
|
||||||
String get _baseArchiveFolder => pathMappingService.rootFolder;
|
String get _baseArchiveFolder => pathMappingService.rootFolder;
|
||||||
|
|
||||||
void _loadEntries() {
|
void _loadProfileFile() {
|
||||||
final entriesJsonPath = p.join(_baseArchiveFolder, 'postsAndComments.json');
|
_ownersName = '';
|
||||||
final jsonFile = File(entriesJsonPath);
|
final archiveDir = Directory(_baseArchiveFolder);
|
||||||
if (jsonFile.existsSync()) {
|
final jsonFiles = archiveDir.listSync().where((element) =>
|
||||||
final json = jsonDecode(jsonFile.readAsStringSync()) as List<dynamic>;
|
element.statSync().type == FileSystemEntityType.file &&
|
||||||
final entries =
|
element.path.toLowerCase().endsWith('json'));
|
||||||
json.map((j) => FriendicaTimelineEntry.fromJson(j, connections));
|
for (final file in jsonFiles) {
|
||||||
final topLevelEntries =
|
final reader =
|
||||||
entries.where((element) => element.parentId.isEmpty);
|
DiasporaProfileJsonReader(file.absolute.path, connectionsManager);
|
||||||
final commentEntries =
|
if (_ownersName.isEmpty) {
|
||||||
entries.where((element) => element.parentId.isNotEmpty).toList();
|
_ownersName = reader.readOwnersName();
|
||||||
final entryTrees = <String, FriendicaEntryTreeItem>{};
|
reader.readContacts();
|
||||||
|
final newPosts = reader.readPosts().map((e) => EntryTreeItem(e, false));
|
||||||
final postTreeEntries = <FriendicaEntryTreeItem>[];
|
_postEntries.addAll(newPosts);
|
||||||
for (final entry in topLevelEntries) {
|
|
||||||
final treeEntry = FriendicaEntryTreeItem(entry, false);
|
|
||||||
entryTrees[entry.id] = treeEntry;
|
|
||||||
postTreeEntries.add(treeEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final commentTreeEntries = <FriendicaEntryTreeItem>[];
|
|
||||||
commentEntries.sort(
|
|
||||||
(c1, c2) => c1.creationTimestamp.compareTo(c2.creationTimestamp));
|
|
||||||
for (final entry in commentEntries) {
|
|
||||||
final parent = entryTrees[entry.parentId];
|
|
||||||
final treeEntry = FriendicaEntryTreeItem(entry, parent == null);
|
|
||||||
parent?.addChild(treeEntry);
|
|
||||||
entryTrees[entry.id] = treeEntry;
|
|
||||||
commentTreeEntries.add(treeEntry);
|
|
||||||
}
|
|
||||||
|
|
||||||
_postEntries.clear();
|
|
||||||
_postEntries.addAll(postTreeEntries);
|
|
||||||
|
|
||||||
_allComments.clear();
|
|
||||||
_allComments.addAll(commentTreeEntries);
|
|
||||||
|
|
||||||
_orphanedCommentEntries.clear();
|
|
||||||
_orphanedCommentEntries
|
|
||||||
.addAll(entryTrees.values.where((element) => element.isOrphaned));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import '../../services/path_mapper_service_interface.dart';
|
||||||
|
import '../../settings/settings_controller.dart';
|
||||||
|
|
||||||
|
class DiasporaPathMappingService implements PathMappingService {
|
||||||
|
static final _logger = Logger('$DiasporaPathMappingService');
|
||||||
|
final SettingsController settings;
|
||||||
|
final _photoDirectories = <FileSystemEntity>[];
|
||||||
|
|
||||||
|
DiasporaPathMappingService(this.settings) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get rootFolder => settings.rootFolder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<FileSystemEntity> get archiveDirectories =>
|
||||||
|
List.unmodifiable(_photoDirectories);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void refresh() {
|
||||||
|
_logger.fine('Refreshing path mapping service directory data.');
|
||||||
|
if (!Directory(settings.rootFolder).existsSync()) {
|
||||||
|
_logger.severe(
|
||||||
|
"Base directory does not exist! can't do mapping of ${settings.rootFolder}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_photoDirectories.clear();
|
||||||
|
|
||||||
|
_photoDirectories.addAll(Directory(settings.rootFolder)
|
||||||
|
.listSync(recursive: false)
|
||||||
|
.where((element) =>
|
||||||
|
element.statSync().type == FileSystemEntityType.directory));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toFullPath(String relPath) {
|
||||||
|
for (final file in _photoDirectories) {
|
||||||
|
final fullPath = p.join(file.path, relPath);
|
||||||
|
if (File(fullPath).existsSync()) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.fine(
|
||||||
|
'Did not find a file with this relPath anywhere therefore returning the relPath');
|
||||||
|
return relPath;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,24 +2,63 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:friendica_archive_browser/src/diaspora/serializers/diaspora_contact_serializer.dart';
|
import 'package:friendica_archive_browser/src/diaspora/serializers/diaspora_contact_serializer.dart';
|
||||||
import 'package:friendica_archive_browser/src/models/friendica_contact.dart';
|
import 'package:friendica_archive_browser/src/models/connection.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/services/connections_manager.dart';
|
||||||
|
|
||||||
|
import '../serializers/diaspora_posts_serializer.dart';
|
||||||
|
|
||||||
class DiasporaProfileJsonReader {
|
class DiasporaProfileJsonReader {
|
||||||
final String jsonFilePath;
|
final String jsonFilePath;
|
||||||
|
final ConnectionsManager connectionsManager;
|
||||||
|
final _jsonData = <String, dynamic>{};
|
||||||
|
|
||||||
DiasporaProfileJsonReader(this.jsonFilePath);
|
DiasporaProfileJsonReader(this.jsonFilePath, this.connectionsManager);
|
||||||
|
|
||||||
|
Map<String, dynamic> get jsonData {
|
||||||
|
if (_jsonData.isNotEmpty) {
|
||||||
|
return _jsonData;
|
||||||
|
}
|
||||||
|
|
||||||
List<Contact> readContacts() {
|
|
||||||
final jsonFile = File(jsonFilePath);
|
final jsonFile = File(jsonFilePath);
|
||||||
if (jsonFile.existsSync()) {
|
if (jsonFile.existsSync()) {
|
||||||
final json =
|
final json =
|
||||||
jsonDecode(jsonFile.readAsStringSync()) as Map<String, dynamic>;
|
jsonDecode(jsonFile.readAsStringSync()) as Map<String, dynamic>;
|
||||||
final contactsJson = json['user']?['contacts'] as List<dynamic>;
|
_jsonData.addAll(json);
|
||||||
final contacts =
|
|
||||||
contactsJson.map((j) => friendicaContactFromDiasporaJson(j)).toList();
|
|
||||||
return contacts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return _jsonData;
|
||||||
|
}
|
||||||
|
|
||||||
|
String readOwnersName() =>
|
||||||
|
jsonData['user']?['profile']?['entity_data']?['author'] ?? 'Unknown';
|
||||||
|
|
||||||
|
List<Connection> readContacts() {
|
||||||
|
final json = jsonData;
|
||||||
|
final userName = json['user']?['profile']?['entity_data']?['author'] ?? '';
|
||||||
|
final userContact = Connection(name: userName, id: '0');
|
||||||
|
connectionsManager.addConnection(userContact);
|
||||||
|
final contactsJson = json['user']?['contacts'] as List<dynamic>;
|
||||||
|
final contacts = contactsJson.map((j) => contactFromDiasporaJson(j));
|
||||||
|
connectionsManager.addAllConnections(contacts);
|
||||||
|
return contacts.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TimelineEntry> readPosts() {
|
||||||
|
if (connectionsManager.length == 0) {
|
||||||
|
readContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
final json = jsonData;
|
||||||
|
final postsJson = json['user']?['posts'] as List<dynamic>;
|
||||||
|
final posts = postsJson
|
||||||
|
.map((j) => timelineItemFromDiasporaPostJson(j, connectionsManager))
|
||||||
|
.where((element) => element.isSuccess)
|
||||||
|
.map((e) => e.value)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
posts
|
||||||
|
.sort((p1, p2) => p2.creationTimestamp.compareTo(p1.creationTimestamp));
|
||||||
|
return posts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
import 'package:latlng/latlng.dart';
|
import 'package:latlng/latlng.dart';
|
||||||
import 'package:map/map.dart';
|
import 'package:map/map.dart';
|
||||||
|
|
||||||
import 'marker_data.dart';
|
import 'marker_data.dart';
|
||||||
|
|
||||||
extension GeoSpatialPostExtensions on FriendicaTimelineEntry {
|
extension GeoSpatialPostExtensions on TimelineEntry {
|
||||||
MarkerData toMarkerData(MapTransformer transformer, Color color) {
|
MarkerData toMarkerData(MapTransformer transformer, Color color) {
|
||||||
final latLon = LatLng(locationData.latitude, locationData.longitude);
|
final latLon = LatLng(locationData.latitude, locationData.longitude);
|
||||||
final offset = transformer.fromLatLngToXYCoords(latLon);
|
final offset = transformer.fromLatLngToXYCoords(latLon);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
|
|
||||||
class MarkerData {
|
class MarkerData {
|
||||||
final List<FriendicaTimelineEntry> posts;
|
final List<TimelineEntry> posts;
|
||||||
final Offset pos;
|
final Offset pos;
|
||||||
final Color color;
|
final Color color;
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/screens/media_slideshow_screen.dart';
|
import 'package:friendica_archive_browser/src/friendica/screens/media_slideshow_screen.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
import 'package:friendica_archive_browser/src/models/media_attachment.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart';
|
import 'package:friendica_archive_browser/src/services/archive_service_provider.dart';
|
||||||
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -13,7 +12,7 @@ import 'media_wrapper_component.dart';
|
||||||
class MediaTimelineComponent extends StatelessWidget {
|
class MediaTimelineComponent extends StatelessWidget {
|
||||||
static const double _maxHeightWidth = 400.0;
|
static const double _maxHeightWidth = 400.0;
|
||||||
|
|
||||||
final List<FriendicaMediaAttachment> mediaAttachments;
|
final List<MediaAttachment> mediaAttachments;
|
||||||
|
|
||||||
const MediaTimelineComponent({Key? key, required this.mediaAttachments})
|
const MediaTimelineComponent({Key? key, required this.mediaAttachments})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
@ -28,8 +27,7 @@ class MediaTimelineComponent extends StatelessWidget {
|
||||||
final double singleWidth = MediaQuery.of(context).size.width / 2.0;
|
final double singleWidth = MediaQuery.of(context).size.width / 2.0;
|
||||||
final double threeAcrossWidth = MediaQuery.of(context).size.width / 3.0;
|
final double threeAcrossWidth = MediaQuery.of(context).size.width / 3.0;
|
||||||
final double preferredMultiWidth = min(threeAcrossWidth, _maxHeightWidth);
|
final double preferredMultiWidth = min(threeAcrossWidth, _maxHeightWidth);
|
||||||
final pathMapper = Provider.of<FriendicaPathMappingService>(context);
|
final archiveService = Provider.of<ArchiveServiceProvider>(context);
|
||||||
final archiveService = Provider.of<FriendicaArchiveService>(context);
|
|
||||||
final settingsController = Provider.of<SettingsController>(context);
|
final settingsController = Provider.of<SettingsController>(context);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
@ -48,7 +46,6 @@ class MediaTimelineComponent extends StatelessWidget {
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider.value(value: settingsController),
|
ChangeNotifierProvider.value(value: settingsController),
|
||||||
Provider.value(value: pathMapper),
|
|
||||||
ChangeNotifierProvider.value(value: archiveService),
|
ChangeNotifierProvider.value(value: archiveService),
|
||||||
],
|
],
|
||||||
child: MediaSlideShowScreen(
|
child: MediaSlideShowScreen(
|
||||||
|
|
|
@ -2,9 +2,9 @@ import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.dart';
|
import 'package:friendica_archive_browser/src/models/media_attachment.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
import 'package:friendica_archive_browser/src/services/archive_service_provider.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart';
|
import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart';
|
||||||
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
@ -14,7 +14,7 @@ class MediaWrapperComponent extends StatelessWidget {
|
||||||
static final _logger = Logger('$MediaWrapperComponent');
|
static final _logger = Logger('$MediaWrapperComponent');
|
||||||
|
|
||||||
static const double _noPreferredValue = -1.0;
|
static const double _noPreferredValue = -1.0;
|
||||||
final FriendicaMediaAttachment mediaAttachment;
|
final MediaAttachment mediaAttachment;
|
||||||
final double preferredWidth;
|
final double preferredWidth;
|
||||||
final double preferredHeight;
|
final double preferredHeight;
|
||||||
|
|
||||||
|
@ -28,24 +28,23 @@ class MediaWrapperComponent extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final settingsController = Provider.of<SettingsController>(context);
|
final settingsController = Provider.of<SettingsController>(context);
|
||||||
final pathMapper = Provider.of<FriendicaPathMappingService>(context);
|
final archiveService = Provider.of<ArchiveServiceProvider>(context);
|
||||||
final archiveService = Provider.of<FriendicaArchiveService>(context);
|
|
||||||
final videoPlayerCommand = settingsController.videoPlayerCommand;
|
final videoPlayerCommand = settingsController.videoPlayerCommand;
|
||||||
final path = _calculatePath(pathMapper, archiveService);
|
final path = _calculatePath(archiveService);
|
||||||
final width =
|
final width =
|
||||||
preferredWidth > 0 ? preferredWidth : MediaQuery.of(context).size.width;
|
preferredWidth > 0 ? preferredWidth : MediaQuery.of(context).size.width;
|
||||||
final height = preferredHeight > 0
|
final height = preferredHeight > 0
|
||||||
? preferredHeight
|
? preferredHeight
|
||||||
: MediaQuery.of(context).size.height;
|
: MediaQuery.of(context).size.height;
|
||||||
|
|
||||||
if (mediaAttachment.explicitType == FriendicaAttachmentMediaType.unknown) {
|
if (mediaAttachment.explicitType == AttachmentMediaType.unknown) {
|
||||||
return Text('Unable to resolve type for ${mediaAttachment.uri.path}');
|
return Text('Unable to resolve type for ${mediaAttachment.uri.path}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaAttachment.explicitType == FriendicaAttachmentMediaType.video) {
|
if (mediaAttachment.explicitType == AttachmentMediaType.video) {
|
||||||
final title = "Video (click to play): " + mediaAttachment.title;
|
final title = "Video (click to play): " + mediaAttachment.title;
|
||||||
final thumbnailImageResult = _uriToImage(
|
final thumbnailImageResult = _uriToImage(
|
||||||
mediaAttachment.thumbnailUri, pathMapper,
|
mediaAttachment.thumbnailUri, archiveService.pathMappingService,
|
||||||
imageTypeName: 'thumbnail image');
|
imageTypeName: 'thumbnail image');
|
||||||
if (thumbnailImageResult.image != null) {
|
if (thumbnailImageResult.image != null) {
|
||||||
return _createFinalWidget(
|
return _createFinalWidget(
|
||||||
|
@ -74,8 +73,9 @@ class MediaWrapperComponent extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaAttachment.explicitType == FriendicaAttachmentMediaType.image) {
|
if (mediaAttachment.explicitType == AttachmentMediaType.image) {
|
||||||
final imageResult = _uriToImage(mediaAttachment.uri, pathMapper);
|
final imageResult =
|
||||||
|
_uriToImage(mediaAttachment.uri, archiveService.pathMappingService);
|
||||||
if (imageResult.image == null) {
|
if (imageResult.image == null) {
|
||||||
final errorPath = imageResult.path.isNotEmpty
|
final errorPath = imageResult.path.isNotEmpty
|
||||||
? imageResult.path
|
? imageResult.path
|
||||||
|
@ -111,7 +111,7 @@ class MediaWrapperComponent extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ImageAndPathResult _uriToImage(Uri uri, FriendicaPathMappingService mapper,
|
_ImageAndPathResult _uriToImage(Uri uri, PathMappingService mapper,
|
||||||
{String imageTypeName = 'image'}) {
|
{String imageTypeName = 'image'}) {
|
||||||
if (uri.toString().startsWith('https://interncache')) {
|
if (uri.toString().startsWith('https://interncache')) {
|
||||||
return _ImageAndPathResult.none();
|
return _ImageAndPathResult.none();
|
||||||
|
@ -172,8 +172,7 @@ class MediaWrapperComponent extends StatelessWidget {
|
||||||
return InkWell(onTap: onTap, child: imageWidget);
|
return InkWell(onTap: onTap, child: imageWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _calculatePath(
|
String _calculatePath(ArchiveServiceProvider archiveService) {
|
||||||
FriendicaPathMappingService pathMapper, FriendicaArchiveService archiveService) {
|
|
||||||
final url = mediaAttachment.uri.toString();
|
final url = mediaAttachment.uri.toString();
|
||||||
String basePath = '';
|
String basePath = '';
|
||||||
if (url.startsWith('http')) {
|
if (url.startsWith('http')) {
|
||||||
|
@ -187,7 +186,7 @@ class MediaWrapperComponent extends StatelessWidget {
|
||||||
basePath = mediaAttachment.uri.path;
|
basePath = mediaAttachment.uri.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathMapper.toFullPath(basePath);
|
return archiveService.pathMappingService.toFullPath(basePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart';
|
import 'package:friendica_archive_browser/src/models/entry_tree_item.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/location_data.dart';
|
import 'package:friendica_archive_browser/src/models/location_data.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
import 'package:friendica_archive_browser/src/services/archive_service_provider.dart';
|
||||||
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart';
|
import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
||||||
|
@ -16,7 +16,7 @@ import 'media_timeline_component.dart';
|
||||||
|
|
||||||
class TreeEntryCard extends StatelessWidget {
|
class TreeEntryCard extends StatelessWidget {
|
||||||
static final _logger = Logger("$TreeEntryCard");
|
static final _logger = Logger("$TreeEntryCard");
|
||||||
final FriendicaEntryTreeItem treeEntry;
|
final EntryTreeItem treeEntry;
|
||||||
final bool isTopLevel;
|
final bool isTopLevel;
|
||||||
|
|
||||||
const TreeEntryCard(
|
const TreeEntryCard(
|
||||||
|
@ -32,7 +32,7 @@ class TreeEntryCard extends StatelessWidget {
|
||||||
const double spacingHeight = 5.0;
|
const double spacingHeight = 5.0;
|
||||||
final formatter =
|
final formatter =
|
||||||
Provider.of<SettingsController>(context).dateTimeFormatter;
|
Provider.of<SettingsController>(context).dateTimeFormatter;
|
||||||
final mapper = Provider.of<FriendicaPathMappingService>(context);
|
final archiveService = Provider.of<ArchiveServiceProvider>(context);
|
||||||
|
|
||||||
final entry = treeEntry.entry;
|
final entry = treeEntry.entry;
|
||||||
|
|
||||||
|
@ -75,7 +75,8 @@ class TreeEntryCard extends StatelessWidget {
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () async => await copyToClipboard(
|
onPressed: () async => await copyToClipboard(
|
||||||
context: context,
|
context: context,
|
||||||
text: entry.toHumanString(mapper, formatter),
|
text: entry.toHumanString(
|
||||||
|
archiveService.pathMappingService, formatter),
|
||||||
snackbarMessage: 'Copied Post to clipboard'),
|
snackbarMessage: 'Copied Post to clipboard'),
|
||||||
icon: const Icon(Icons.copy)),
|
icon: const Icon(Icons.copy)),
|
||||||
),
|
),
|
||||||
|
@ -129,9 +130,10 @@ class TreeEntryCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
if (entry.locationData.hasData())
|
if (entry.locationData.hasData())
|
||||||
entry.locationData.toWidget(spacingHeight),
|
entry.locationData.toWidget(spacingHeight),
|
||||||
if (entry.links.isNotEmpty) ...[
|
if (treeEntry.entry.externalLink.isNotEmpty) ...[
|
||||||
const SizedBox(height: spacingHeight),
|
const SizedBox(height: spacingHeight),
|
||||||
LinkElementsComponent(links: entry.links)
|
LinkElementsComponent(
|
||||||
|
links: [Uri.parse(treeEntry.entry.externalLink)])
|
||||||
],
|
],
|
||||||
if (entry.mediaAttachments.isNotEmpty) ...[
|
if (entry.mediaAttachments.isNotEmpty) ...[
|
||||||
const SizedBox(height: spacingHeight),
|
const SizedBox(height: spacingHeight),
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
|
|
||||||
|
|
||||||
class FriendicaEntryTreeItem {
|
|
||||||
final FriendicaTimelineEntry entry;
|
|
||||||
final bool isOrphaned;
|
|
||||||
|
|
||||||
final _children = <String, FriendicaEntryTreeItem>{};
|
|
||||||
|
|
||||||
FriendicaEntryTreeItem(this.entry, this.isOrphaned);
|
|
||||||
|
|
||||||
String get id => entry.id;
|
|
||||||
|
|
||||||
void addChild(FriendicaEntryTreeItem child) {
|
|
||||||
_children[child.id] = child;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<FriendicaEntryTreeItem> get children =>
|
|
||||||
List.unmodifiable(_children.values);
|
|
||||||
}
|
|
|
@ -1,246 +0,0 @@
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/models/friendica_contact.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_connections.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
import 'friendica_media_attachment.dart';
|
|
||||||
import 'location_data.dart';
|
|
||||||
import 'model_utils.dart';
|
|
||||||
|
|
||||||
class FriendicaTimelineEntry {
|
|
||||||
static final _logger = Logger('$FriendicaTimelineEntry');
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
final String parentId;
|
|
||||||
|
|
||||||
final String parentAuthor;
|
|
||||||
|
|
||||||
final String parentAuthorId;
|
|
||||||
|
|
||||||
final int creationTimestamp;
|
|
||||||
|
|
||||||
final int backdatedTimestamp;
|
|
||||||
|
|
||||||
final int modificationTimestamp;
|
|
||||||
|
|
||||||
final String body;
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
final bool isReshare;
|
|
||||||
|
|
||||||
final String author;
|
|
||||||
|
|
||||||
final String authorId;
|
|
||||||
|
|
||||||
final String externalLink;
|
|
||||||
|
|
||||||
final List<FriendicaMediaAttachment> mediaAttachments;
|
|
||||||
|
|
||||||
final LocationData locationData;
|
|
||||||
|
|
||||||
final List<Uri> links;
|
|
||||||
|
|
||||||
final List<Contact> likes;
|
|
||||||
|
|
||||||
final List<Contact> dislikes;
|
|
||||||
|
|
||||||
FriendicaTimelineEntry(
|
|
||||||
{this.id = '',
|
|
||||||
this.parentId = '',
|
|
||||||
this.creationTimestamp = 0,
|
|
||||||
this.backdatedTimestamp = 0,
|
|
||||||
this.modificationTimestamp = 0,
|
|
||||||
this.isReshare = false,
|
|
||||||
this.body = '',
|
|
||||||
this.title = '',
|
|
||||||
this.author = '',
|
|
||||||
this.authorId = '',
|
|
||||||
this.parentAuthor = '',
|
|
||||||
this.parentAuthorId = '',
|
|
||||||
this.externalLink = '',
|
|
||||||
this.locationData = const LocationData(),
|
|
||||||
this.likes = const <Contact>[],
|
|
||||||
this.dislikes = const <Contact>[],
|
|
||||||
List<FriendicaMediaAttachment>? mediaAttachments,
|
|
||||||
List<Uri>? links})
|
|
||||||
: mediaAttachments = mediaAttachments ?? <FriendicaMediaAttachment>[],
|
|
||||||
links = links ?? <Uri>[];
|
|
||||||
|
|
||||||
FriendicaTimelineEntry.randomBuilt()
|
|
||||||
: creationTimestamp = DateTime.now().millisecondsSinceEpoch,
|
|
||||||
backdatedTimestamp = DateTime.now().millisecondsSinceEpoch,
|
|
||||||
modificationTimestamp = DateTime.now().millisecondsSinceEpoch,
|
|
||||||
id = randomId(),
|
|
||||||
isReshare = false,
|
|
||||||
parentId = randomId(),
|
|
||||||
externalLink = 'Random external link ${randomId()}',
|
|
||||||
body = 'Random post text ${randomId()}',
|
|
||||||
title = 'Random title ${randomId()}',
|
|
||||||
author = 'Random author ${randomId()}',
|
|
||||||
authorId = 'Random authorId ${randomId()}',
|
|
||||||
parentAuthor = 'Random parent author ${randomId()}',
|
|
||||||
parentAuthorId = 'Random parent author id ${randomId()}',
|
|
||||||
locationData = LocationData.randomBuilt(),
|
|
||||||
likes = const <Contact>[],
|
|
||||||
dislikes = const <Contact>[],
|
|
||||||
links = [
|
|
||||||
Uri.parse('http://localhost/${randomId()}'),
|
|
||||||
Uri.parse('http://localhost/${randomId()}')
|
|
||||||
],
|
|
||||||
mediaAttachments = [
|
|
||||||
FriendicaMediaAttachment.randomBuilt(),
|
|
||||||
FriendicaMediaAttachment.randomBuilt()
|
|
||||||
];
|
|
||||||
|
|
||||||
FriendicaTimelineEntry copy(
|
|
||||||
{int? creationTimestamp,
|
|
||||||
int? backdatedTimestamp,
|
|
||||||
int? modificationTimestamp,
|
|
||||||
bool? isReshare,
|
|
||||||
String? id,
|
|
||||||
String? parentId,
|
|
||||||
String? externalLink,
|
|
||||||
String? body,
|
|
||||||
String? title,
|
|
||||||
String? author,
|
|
||||||
String? authorId,
|
|
||||||
String? parentAuthor,
|
|
||||||
String? parentAuthorId,
|
|
||||||
LocationData? locationData,
|
|
||||||
List<FriendicaMediaAttachment>? mediaAttachments,
|
|
||||||
List<Contact>? likes,
|
|
||||||
List<Contact>? dislikes,
|
|
||||||
List<Uri>? links}) {
|
|
||||||
return FriendicaTimelineEntry(
|
|
||||||
creationTimestamp: creationTimestamp ?? this.creationTimestamp,
|
|
||||||
backdatedTimestamp: backdatedTimestamp ?? this.backdatedTimestamp,
|
|
||||||
modificationTimestamp:
|
|
||||||
modificationTimestamp ?? this.modificationTimestamp,
|
|
||||||
id: id ?? this.id,
|
|
||||||
isReshare: isReshare ?? this.isReshare,
|
|
||||||
parentId: parentId ?? this.parentId,
|
|
||||||
externalLink: externalLink ?? this.externalLink,
|
|
||||||
body: body ?? this.body,
|
|
||||||
title: title ?? this.title,
|
|
||||||
author: author ?? this.author,
|
|
||||||
authorId: authorId ?? this.authorId,
|
|
||||||
parentAuthor: parentAuthor ?? this.parentAuthor,
|
|
||||||
parentAuthorId: parentAuthorId ?? this.parentAuthorId,
|
|
||||||
locationData: locationData ?? this.locationData,
|
|
||||||
mediaAttachments: mediaAttachments ?? this.mediaAttachments,
|
|
||||||
likes: likes ?? this.likes,
|
|
||||||
dislikes: dislikes ?? this.dislikes,
|
|
||||||
links: links ?? this.links);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'FriendicaTimelineEntry{id: $id, isReshare: $isReshare, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor mediaAttachments: $mediaAttachments, links: $links}';
|
|
||||||
}
|
|
||||||
|
|
||||||
String toHumanString(FriendicaPathMappingService mapper, DateFormat formatter) {
|
|
||||||
final creationDateString = formatter.format(
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(creationTimestamp * 1000)
|
|
||||||
.toLocal());
|
|
||||||
return [
|
|
||||||
'Title: $title',
|
|
||||||
'Creation At: $creationDateString',
|
|
||||||
'Text:',
|
|
||||||
'Author: $author',
|
|
||||||
'Reshare: $isReshare',
|
|
||||||
if (externalLink.isNotEmpty) 'External Link: $externalLink',
|
|
||||||
body,
|
|
||||||
'',
|
|
||||||
if (parentId.isNotEmpty)
|
|
||||||
"Comment on post/comment by ${parentAuthor.isNotEmpty ? parentAuthor : 'unknown author'}",
|
|
||||||
if (links.isNotEmpty) 'Links:',
|
|
||||||
...links.map((e) => e.toString()),
|
|
||||||
'',
|
|
||||||
if (mediaAttachments.isNotEmpty) 'Photos and Videos:',
|
|
||||||
...mediaAttachments.map((e) => e.toHumanString(mapper)),
|
|
||||||
if (locationData.hasPosition) locationData.toHumanString(),
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasImages() => mediaAttachments
|
|
||||||
.where((element) =>
|
|
||||||
element.explicitType == FriendicaAttachmentMediaType.image)
|
|
||||||
.isNotEmpty;
|
|
||||||
|
|
||||||
bool hasVideos() => mediaAttachments
|
|
||||||
.where((element) =>
|
|
||||||
element.explicitType == FriendicaAttachmentMediaType.video)
|
|
||||||
.isNotEmpty;
|
|
||||||
|
|
||||||
static FriendicaTimelineEntry fromJson(
|
|
||||||
Map<String, dynamic> json, FriendicaConnections connections) {
|
|
||||||
final int timestamp = json.containsKey('created_at')
|
|
||||||
? OffsetDateTimeUtils.epochSecTimeFromFriendicaString(
|
|
||||||
json['created_at'])
|
|
||||||
.fold(
|
|
||||||
onSuccess: (value) => value,
|
|
||||||
onError: (error) {
|
|
||||||
_logger.severe("Couldn't read date time string: $error");
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
: 0;
|
|
||||||
final id = json['id_str'] ?? '';
|
|
||||||
final isReshare = json.containsKey('retweeted_status');
|
|
||||||
final parentId = json['in_reply_to_status_id_str'] ?? '';
|
|
||||||
final parentAuthor = json['in_reply_to_screen_name'] ?? '';
|
|
||||||
final parentAuthorId = json['in_reply_to_user_id_str'] ?? '';
|
|
||||||
final body = json['friendica_html'] ?? '';
|
|
||||||
final author = json['user']['name'];
|
|
||||||
final authorId = json['user']['id_str'];
|
|
||||||
final title = json['friendica_title'] ?? '';
|
|
||||||
final externalLink = json['external_url'] ?? '';
|
|
||||||
final actualLocationData = LocationData();
|
|
||||||
final modificationTimestamp = timestamp;
|
|
||||||
final backdatedTimestamp = timestamp;
|
|
||||||
final links = <Uri>[];
|
|
||||||
final mediaAttachments = (json['attachments'] as List<dynamic>? ?? [])
|
|
||||||
.map((j) => FriendicaMediaAttachment.fromJson(j))
|
|
||||||
.toList();
|
|
||||||
final likes =
|
|
||||||
(json['friendica_activities']?['like'] as List<dynamic>? ?? [])
|
|
||||||
.map((json) => Contact.fromJson(json))
|
|
||||||
.toList();
|
|
||||||
final dislikes =
|
|
||||||
(json['friendica_activities']?['dislike'] as List<dynamic>? ?? [])
|
|
||||||
.map((json) => Contact.fromJson(json))
|
|
||||||
.toList();
|
|
||||||
final announce =
|
|
||||||
(json['friendica_activities']?['announce'] as List<dynamic>? ?? [])
|
|
||||||
.map((json) => Contact.fromJson(json))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
for (final contact in [...likes, ...dislikes, ...announce]) {
|
|
||||||
connections.addConnection(contact);
|
|
||||||
}
|
|
||||||
|
|
||||||
return FriendicaTimelineEntry(
|
|
||||||
creationTimestamp: timestamp,
|
|
||||||
modificationTimestamp: modificationTimestamp,
|
|
||||||
backdatedTimestamp: backdatedTimestamp,
|
|
||||||
locationData: actualLocationData,
|
|
||||||
externalLink: externalLink,
|
|
||||||
body: body,
|
|
||||||
isReshare: isReshare,
|
|
||||||
id: id,
|
|
||||||
parentId: parentId,
|
|
||||||
parentAuthorId: parentAuthorId,
|
|
||||||
author: author,
|
|
||||||
authorId: authorId,
|
|
||||||
parentAuthor: parentAuthor,
|
|
||||||
title: title,
|
|
||||||
links: links,
|
|
||||||
likes: likes,
|
|
||||||
dislikes: dislikes,
|
|
||||||
mediaAttachments: mediaAttachments,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
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/filter_control_component.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/components/tree_entry_card.dart';
|
import 'package:friendica_archive_browser/src/friendica/components/tree_entry_card.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart';
|
import 'package:friendica_archive_browser/src/models/entry_tree_item.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
|
import 'package:friendica_archive_browser/src/models/model_utils.dart';
|
||||||
import 'package:friendica_archive_browser/src/screens/error_screen.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/settings/settings_controller.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
||||||
|
@ -15,8 +15,7 @@ import '../../screens/standin_status_screen.dart';
|
||||||
|
|
||||||
class EntriesScreen extends StatelessWidget {
|
class EntriesScreen extends StatelessWidget {
|
||||||
static final _logger = Logger('$EntriesScreen');
|
static final _logger = Logger('$EntriesScreen');
|
||||||
final FutureResult<List<FriendicaEntryTreeItem>, ExecError> Function()
|
final FutureResult<List<EntryTreeItem>, ExecError> Function() populator;
|
||||||
populator;
|
|
||||||
|
|
||||||
const EntriesScreen({Key? key, required this.populator}) : super(key: key);
|
const EntriesScreen({Key? key, required this.populator}) : super(key: key);
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@ class EntriesScreen extends StatelessWidget {
|
||||||
_logger.info('Build FriendicaEntriesScreen');
|
_logger.info('Build FriendicaEntriesScreen');
|
||||||
Provider.of<SettingsController>(context);
|
Provider.of<SettingsController>(context);
|
||||||
|
|
||||||
return FutureBuilder<Result<List<FriendicaEntryTreeItem>, ExecError>>(
|
return FutureBuilder<Result<List<EntryTreeItem>, ExecError>>(
|
||||||
future: populator(),
|
future: populator(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
_logger.info('FriendicaEntriesScreen Future builder called');
|
_logger.info('FriendicaEntriesScreen Future builder called');
|
||||||
|
@ -58,7 +57,7 @@ class EntriesScreen extends StatelessWidget {
|
||||||
class _FriendicaEntriesScreenWidget extends StatelessWidget {
|
class _FriendicaEntriesScreenWidget extends StatelessWidget {
|
||||||
static final _logger = Logger('$_FriendicaEntriesScreenWidget');
|
static final _logger = Logger('$_FriendicaEntriesScreenWidget');
|
||||||
|
|
||||||
final List<FriendicaEntryTreeItem> posts;
|
final List<EntryTreeItem> posts;
|
||||||
|
|
||||||
const _FriendicaEntriesScreenWidget({Key? key, required this.posts})
|
const _FriendicaEntriesScreenWidget({Key? key, required this.posts})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
@ -66,7 +65,7 @@ class _FriendicaEntriesScreenWidget extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.fine('Redrawing');
|
_logger.fine('Redrawing');
|
||||||
return FilterControl<FriendicaEntryTreeItem, dynamic>(
|
return FilterControl<EntryTreeItem, dynamic>(
|
||||||
allItems: posts,
|
allItems: posts,
|
||||||
commentsOnlyFilterFunction: (post) => post.children.isNotEmpty,
|
commentsOnlyFilterFunction: (post) => post.children.isNotEmpty,
|
||||||
imagesOnlyFilterFunction: (post) => post.entry.hasImages(),
|
imagesOnlyFilterFunction: (post) => post.entry.hasImages(),
|
||||||
|
|
|
@ -6,13 +6,13 @@ import 'package:friendica_archive_browser/src/friendica/components/geo/geo_exten
|
||||||
import 'package:friendica_archive_browser/src/friendica/components/geo/map_bounds.dart';
|
import 'package:friendica_archive_browser/src/friendica/components/geo/map_bounds.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/components/geo/marker_data.dart';
|
import 'package:friendica_archive_browser/src/friendica/components/geo/marker_data.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/components/tree_entry_card.dart';
|
import 'package:friendica_archive_browser/src/friendica/components/tree_entry_card.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/entry_tree_item.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
import 'package:friendica_archive_browser/src/screens/error_screen.dart';
|
import 'package:friendica_archive_browser/src/screens/error_screen.dart';
|
||||||
import 'package:friendica_archive_browser/src/screens/loading_status_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/screens/standin_status_screen.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart';
|
import 'package:friendica_archive_browser/src/services/archive_service_provider.dart';
|
||||||
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/temp_file_builder.dart';
|
import 'package:friendica_archive_browser/src/utils/temp_file_builder.dart';
|
||||||
|
@ -33,9 +33,9 @@ class GeospatialViewScreen extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.info('Build GeospatialViewScreen');
|
_logger.info('Build GeospatialViewScreen');
|
||||||
final service = Provider.of<FriendicaArchiveService>(context);
|
final service = Provider.of<ArchiveServiceProvider>(context);
|
||||||
|
|
||||||
return FutureBuilder<Result<List<FriendicaEntryTreeItem>, ExecError>>(
|
return FutureBuilder<Result<List<EntryTreeItem>, ExecError>>(
|
||||||
future: service.getPosts(),
|
future: service.getPosts(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
_logger.info('GeospatialViewScreen Future builder called');
|
_logger.info('GeospatialViewScreen Future builder called');
|
||||||
|
@ -66,7 +66,7 @@ class GeospatialViewScreen extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeospatialView extends StatefulWidget {
|
class GeospatialView extends StatefulWidget {
|
||||||
final List<FriendicaTimelineEntry> posts;
|
final List<TimelineEntry> posts;
|
||||||
|
|
||||||
const GeospatialView({Key? key, required this.posts}) : super(key: key);
|
const GeospatialView({Key? key, required this.posts}) : super(key: key);
|
||||||
|
|
||||||
|
@ -88,8 +88,8 @@ class _GeospatialViewState extends State<GeospatialView> {
|
||||||
);
|
);
|
||||||
|
|
||||||
Offset? dragStart;
|
Offset? dragStart;
|
||||||
final postsInList = <FriendicaTimelineEntry>[];
|
final postsInList = <TimelineEntry>[];
|
||||||
final postsInView = <FriendicaTimelineEntry>[];
|
final postsInView = <TimelineEntry>[];
|
||||||
double scaleStart = 1.0;
|
double scaleStart = 1.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -203,8 +203,8 @@ class _GeospatialViewState extends State<GeospatialView> {
|
||||||
highlightedColor: Colors.indigo[900]!)));
|
highlightedColor: Colors.indigo[900]!)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPostList(
|
Widget _buildPostList(BuildContext context, DateFormat formatter,
|
||||||
BuildContext context, DateFormat formatter, FriendicaPathMappingService mapper) {
|
FriendicaPathMappingService mapper) {
|
||||||
_logger.finest(() => 'Building PostList with ${postsInList.length} items');
|
_logger.finest(() => 'Building PostList with ${postsInList.length} items');
|
||||||
if (postsInList.isEmpty) {
|
if (postsInList.isEmpty) {
|
||||||
return const StandInStatusScreen(
|
return const StandInStatusScreen(
|
||||||
|
@ -218,14 +218,14 @@ class _GeospatialViewState extends State<GeospatialView> {
|
||||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
itemBuilder: (context, index) => TreeEntryCard(
|
itemBuilder: (context, index) => TreeEntryCard(
|
||||||
treeEntry: FriendicaEntryTreeItem(postsInList[index], false)),
|
treeEntry: EntryTreeItem(postsInList[index], false)),
|
||||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||||
itemCount: postsInList.length),
|
itemCount: postsInList.length),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMap(
|
Widget _buildMap(BuildContext context, DateFormat formatter,
|
||||||
BuildContext context, DateFormat formatter, FriendicaPathMappingService mapper) {
|
FriendicaPathMappingService mapper) {
|
||||||
final settings = Provider.of<SettingsController>(context);
|
final settings = Provider.of<SettingsController>(context);
|
||||||
|
|
||||||
final shouldDebugCache =
|
final shouldDebugCache =
|
||||||
|
@ -332,8 +332,8 @@ class _GeospatialViewState extends State<GeospatialView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMarkerWidget(
|
Widget _buildMarkerWidget(MarkerData data, DateFormat formatter,
|
||||||
MarkerData data, DateFormat formatter, FriendicaPathMappingService mapper) {
|
FriendicaPathMappingService mapper) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
left: data.pos.dx - 16,
|
left: data.pos.dx - 16,
|
||||||
top: data.pos.dy - 16,
|
top: data.pos.dy - 16,
|
||||||
|
|
|
@ -4,8 +4,8 @@ import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/components/media_wrapper_component.dart';
|
import 'package:friendica_archive_browser/src/friendica/components/media_wrapper_component.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_media_attachment.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/media_attachment.dart';
|
||||||
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
||||||
import 'package:friendica_archive_browser/src/themes.dart';
|
import 'package:friendica_archive_browser/src/themes.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
||||||
|
@ -14,7 +14,7 @@ import 'package:provider/provider.dart';
|
||||||
class MediaSlideShowScreen extends StatefulWidget {
|
class MediaSlideShowScreen extends StatefulWidget {
|
||||||
static const _spacing = 5.0;
|
static const _spacing = 5.0;
|
||||||
|
|
||||||
final List<FriendicaMediaAttachment> mediaAttachments;
|
final List<MediaAttachment> mediaAttachments;
|
||||||
final int initialIndex;
|
final int initialIndex;
|
||||||
|
|
||||||
const MediaSlideShowScreen(
|
const MediaSlideShowScreen(
|
||||||
|
@ -27,7 +27,7 @@ class MediaSlideShowScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _MediaSlideShowScreenState extends State<MediaSlideShowScreen> {
|
class _MediaSlideShowScreenState extends State<MediaSlideShowScreen> {
|
||||||
static const fastestChangeMS = 250;
|
static const fastestChangeMS = 250;
|
||||||
FriendicaMediaAttachment media = FriendicaMediaAttachment.blank();
|
MediaAttachment media = MediaAttachment.blank();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
int lastKeyInducedIndexChange = 0;
|
int lastKeyInducedIndexChange = 0;
|
||||||
|
|
||||||
|
@ -155,7 +155,8 @@ class _MediaSlideShowScreenState extends State<MediaSlideShowScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveFile(BuildContext context) async {
|
Future<void> _saveFile(BuildContext context) async {
|
||||||
final pathMapper = Provider.of<FriendicaPathMappingService>(context, listen: false);
|
final pathMapper =
|
||||||
|
Provider.of<FriendicaPathMappingService>(context, listen: false);
|
||||||
|
|
||||||
final filename = media.uri.pathSegments.last;
|
final filename = media.uri.pathSegments.last;
|
||||||
final initialPath = pathMapper.toFullPath(media.uri.toFilePath());
|
final initialPath = pathMapper.toFullPath(media.uri.toFilePath());
|
||||||
|
|
|
@ -4,10 +4,10 @@ import 'package:friendica_archive_browser/src/components/timechart_widget.dart';
|
||||||
import 'package:friendica_archive_browser/src/components/top_interactactors_widget.dart';
|
import 'package:friendica_archive_browser/src/components/top_interactactors_widget.dart';
|
||||||
import 'package:friendica_archive_browser/src/components/word_frequency_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/components/filter_control_component.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
|
import 'package:friendica_archive_browser/src/models/model_utils.dart';
|
||||||
import 'package:friendica_archive_browser/src/models/time_element.dart';
|
import 'package:friendica_archive_browser/src/models/time_element.dart';
|
||||||
import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart';
|
import 'package:friendica_archive_browser/src/screens/standin_status_screen.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart';
|
import 'package:friendica_archive_browser/src/services/archive_service_provider.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -21,7 +21,7 @@ class StatsScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _StatsScreenState extends State<StatsScreen> {
|
class _StatsScreenState extends State<StatsScreen> {
|
||||||
static final _logger = Logger("$_StatsScreenState");
|
static final _logger = Logger("$_StatsScreenState");
|
||||||
FriendicaArchiveService? archiveDataService;
|
ArchiveServiceProvider? archiveDataService;
|
||||||
final allItems = <TimeElement>[];
|
final allItems = <TimeElement>[];
|
||||||
StatType statType = StatType.selectType;
|
StatType statType = StatType.selectType;
|
||||||
bool hasText = true;
|
bool hasText = true;
|
||||||
|
@ -78,7 +78,7 @@ class _StatsScreenState extends State<StatsScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
archiveDataService = Provider.of<FriendicaArchiveService>(context);
|
archiveDataService = Provider.of<ArchiveServiceProvider>(context);
|
||||||
|
|
||||||
return FilterControl<TimeElement, dynamic>(
|
return FilterControl<TimeElement, dynamic>(
|
||||||
allItems: allItems,
|
allItems: allItems,
|
||||||
|
@ -147,7 +147,7 @@ class _StatsScreenState extends State<StatsScreen> {
|
||||||
child: Column(children: [
|
child: Column(children: [
|
||||||
..._buildGraphScreens(context, items),
|
..._buildGraphScreens(context, items),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
TopInteractorsWidget(items, archiveDataService!.connections),
|
TopInteractorsWidget(items, archiveDataService!.connectionsManager),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
WordFrequencyWidget(items),
|
WordFrequencyWidget(items),
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import '../../models/connection.dart';
|
||||||
|
|
||||||
|
Connection contactFromFriendicaJson(Map<String, dynamic> json) {
|
||||||
|
final status = (json['following'] ?? '') == 'true'
|
||||||
|
? ConnectionStatus.youFollowThem
|
||||||
|
: ConnectionStatus.none;
|
||||||
|
final name = json['name'] ?? '';
|
||||||
|
final id = json['id_str'] ?? '';
|
||||||
|
final profileUrl = Uri.parse(json['url'] ?? '');
|
||||||
|
final network = json['network'] ?? 'unkn';
|
||||||
|
|
||||||
|
return Connection(
|
||||||
|
status: status,
|
||||||
|
name: name,
|
||||||
|
id: id,
|
||||||
|
profileUrl: profileUrl,
|
||||||
|
network: network);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import '../../models/media_attachment.dart';
|
||||||
|
|
||||||
|
MediaAttachment mediaAttachmentfromFriendicaJson(Map<String, dynamic> json) {
|
||||||
|
final uri = Uri.parse(json['url']);
|
||||||
|
const creationTimestamp = 0;
|
||||||
|
final metadata = (json['metadata'] as Map<String, dynamic>? ?? {})
|
||||||
|
.map((key, value) => MapEntry(key, value.toString()));
|
||||||
|
final explicitType = (json['mimetype'] ?? '').startsWith('image')
|
||||||
|
? AttachmentMediaType.image
|
||||||
|
: (json['mimetype'] ?? '').startsWith('video')
|
||||||
|
? AttachmentMediaType.video
|
||||||
|
: AttachmentMediaType.unknown;
|
||||||
|
final thumbnailUri = Uri();
|
||||||
|
const title = '';
|
||||||
|
const description = '';
|
||||||
|
|
||||||
|
return MediaAttachment(
|
||||||
|
uri: uri,
|
||||||
|
creationTimestamp: creationTimestamp,
|
||||||
|
metadata: metadata,
|
||||||
|
thumbnailUri: thumbnailUri,
|
||||||
|
title: title,
|
||||||
|
explicitType: explicitType,
|
||||||
|
description: description);
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import 'package:friendica_archive_browser/src/friendica/serializers/friendica_contact_serializer.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/friendica/serializers/friendica_media_attachment_serializer.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
import '../../models/location_data.dart';
|
||||||
|
import '../../models/timeline_entry.dart';
|
||||||
|
import '../../services/connections_manager.dart';
|
||||||
|
import '../../utils/offsetdatetime_utils.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('FriendicaTimelineEntrySerializer');
|
||||||
|
|
||||||
|
TimelineEntry timelineEntryFromFriendicaJson(
|
||||||
|
Map<String, dynamic> json, ConnectionsManager connections) {
|
||||||
|
final int timestamp = json.containsKey('created_at')
|
||||||
|
? OffsetDateTimeUtils.epochSecTimeFromFriendicaString(json['created_at'])
|
||||||
|
.fold(
|
||||||
|
onSuccess: (value) => value,
|
||||||
|
onError: (error) {
|
||||||
|
_logger.severe("Couldn't read date time string: $error");
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
: 0;
|
||||||
|
final id = json['id_str'] ?? '';
|
||||||
|
final isReshare = json.containsKey('retweeted_status');
|
||||||
|
final parentId = json['in_reply_to_status_id_str'] ?? '';
|
||||||
|
final parentAuthor = json['in_reply_to_screen_name'] ?? '';
|
||||||
|
final parentAuthorId = json['in_reply_to_user_id_str'] ?? '';
|
||||||
|
final body = json['friendica_html'] ?? '';
|
||||||
|
final author = json['user']['name'];
|
||||||
|
final authorId = json['user']['id_str'];
|
||||||
|
final title = json['friendica_title'] ?? '';
|
||||||
|
final externalLink = json['external_url'] ?? '';
|
||||||
|
final actualLocationData = LocationData();
|
||||||
|
final modificationTimestamp = timestamp;
|
||||||
|
final backdatedTimestamp = timestamp;
|
||||||
|
final mediaAttachments = (json['attachments'] as List<dynamic>? ?? [])
|
||||||
|
.map((j) => mediaAttachmentfromFriendicaJson(j))
|
||||||
|
.toList();
|
||||||
|
final likes = (json['friendica_activities']?['like'] as List<dynamic>? ?? [])
|
||||||
|
.map((json) => contactFromFriendicaJson(json))
|
||||||
|
.toList();
|
||||||
|
final dislikes =
|
||||||
|
(json['friendica_activities']?['dislike'] as List<dynamic>? ?? [])
|
||||||
|
.map((json) => contactFromFriendicaJson(json))
|
||||||
|
.toList();
|
||||||
|
final announce =
|
||||||
|
(json['friendica_activities']?['announce'] as List<dynamic>? ?? [])
|
||||||
|
.map((json) => contactFromFriendicaJson(json))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
connections.addAllConnections([...likes, ...dislikes, ...announce]);
|
||||||
|
|
||||||
|
return TimelineEntry(
|
||||||
|
creationTimestamp: timestamp,
|
||||||
|
modificationTimestamp: modificationTimestamp,
|
||||||
|
backdatedTimestamp: backdatedTimestamp,
|
||||||
|
locationData: actualLocationData,
|
||||||
|
externalLink: externalLink,
|
||||||
|
body: body,
|
||||||
|
isReshare: isReshare,
|
||||||
|
id: id,
|
||||||
|
parentId: parentId,
|
||||||
|
parentAuthorId: parentAuthorId,
|
||||||
|
author: author,
|
||||||
|
authorId: authorId,
|
||||||
|
parentAuthor: parentAuthor,
|
||||||
|
title: title,
|
||||||
|
likes: likes,
|
||||||
|
dislikes: dislikes,
|
||||||
|
mediaAttachments: mediaAttachments,
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,23 +1,27 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:friendica_archive_browser/src/friendica/serializers/friendica_timeline_entry_serializer.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_entry_tree_item.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/entry_tree_item.dart';
|
||||||
import 'package:friendica_archive_browser/src/models/local_image_archive_entry.dart';
|
import 'package:friendica_archive_browser/src/models/local_image_archive_entry.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_connections.dart';
|
import 'package:friendica_archive_browser/src/services/archive_service_interface.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/services/connections_manager.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
class FriendicaArchiveService extends ChangeNotifier {
|
class FriendicaArchiveService implements ArchiveService {
|
||||||
|
@override
|
||||||
final FriendicaPathMappingService pathMappingService;
|
final FriendicaPathMappingService pathMappingService;
|
||||||
|
|
||||||
final Map<String, ImageEntry> _imagesByRequestUrl = {};
|
final Map<String, ImageEntry> _imagesByRequestUrl = {};
|
||||||
final List<FriendicaEntryTreeItem> _postEntries = [];
|
final List<EntryTreeItem> _postEntries = [];
|
||||||
final List<FriendicaEntryTreeItem> _orphanedCommentEntries = [];
|
final List<EntryTreeItem> _orphanedCommentEntries = [];
|
||||||
final List<FriendicaEntryTreeItem> _allComments = [];
|
final List<EntryTreeItem> _allComments = [];
|
||||||
final FriendicaConnections connections = FriendicaConnections();
|
@override
|
||||||
|
final ConnectionsManager connectionsManager = ConnectionsManager();
|
||||||
|
|
||||||
String _ownersName = '';
|
String _ownersName = '';
|
||||||
|
|
||||||
FriendicaArchiveService({required this.pathMappingService});
|
FriendicaArchiveService({required this.pathMappingService});
|
||||||
|
@ -34,14 +38,14 @@ class FriendicaArchiveService extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearCaches() {
|
void clearCaches() {
|
||||||
connections.clearCaches();
|
connectionsManager.clearCaches();
|
||||||
_imagesByRequestUrl.clear();
|
_imagesByRequestUrl.clear();
|
||||||
_orphanedCommentEntries.clear();
|
_orphanedCommentEntries.clear();
|
||||||
_allComments.clear();
|
_allComments.clear();
|
||||||
_postEntries.clear();
|
_postEntries.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<FriendicaEntryTreeItem>, ExecError> getPosts() async {
|
FutureResult<List<EntryTreeItem>, ExecError> getPosts() async {
|
||||||
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
||||||
_loadEntries();
|
_loadEntries();
|
||||||
}
|
}
|
||||||
|
@ -49,7 +53,7 @@ class FriendicaArchiveService extends ChangeNotifier {
|
||||||
return Result.ok(_postEntries);
|
return Result.ok(_postEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<FriendicaEntryTreeItem>, ExecError> getAllComments() async {
|
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() async {
|
||||||
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
||||||
_loadEntries();
|
_loadEntries();
|
||||||
}
|
}
|
||||||
|
@ -57,8 +61,7 @@ class FriendicaArchiveService extends ChangeNotifier {
|
||||||
return Result.ok(_allComments);
|
return Result.ok(_allComments);
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureResult<List<FriendicaEntryTreeItem>, ExecError>
|
FutureResult<List<EntryTreeItem>, ExecError> getOrphanedComments() async {
|
||||||
getOrphanedComments() async {
|
|
||||||
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
if (_postEntries.isEmpty && _allComments.isEmpty) {
|
||||||
_loadEntries();
|
_loadEntries();
|
||||||
}
|
}
|
||||||
|
@ -84,27 +87,27 @@ class FriendicaArchiveService extends ChangeNotifier {
|
||||||
final jsonFile = File(entriesJsonPath);
|
final jsonFile = File(entriesJsonPath);
|
||||||
if (jsonFile.existsSync()) {
|
if (jsonFile.existsSync()) {
|
||||||
final json = jsonDecode(jsonFile.readAsStringSync()) as List<dynamic>;
|
final json = jsonDecode(jsonFile.readAsStringSync()) as List<dynamic>;
|
||||||
final entries =
|
final entries = json
|
||||||
json.map((j) => FriendicaTimelineEntry.fromJson(j, connections));
|
.map((j) => timelineEntryFromFriendicaJson(j, connectionsManager));
|
||||||
final topLevelEntries =
|
final topLevelEntries =
|
||||||
entries.where((element) => element.parentId.isEmpty);
|
entries.where((element) => element.parentId.isEmpty);
|
||||||
final commentEntries =
|
final commentEntries =
|
||||||
entries.where((element) => element.parentId.isNotEmpty).toList();
|
entries.where((element) => element.parentId.isNotEmpty).toList();
|
||||||
final entryTrees = <String, FriendicaEntryTreeItem>{};
|
final entryTrees = <String, EntryTreeItem>{};
|
||||||
|
|
||||||
final postTreeEntries = <FriendicaEntryTreeItem>[];
|
final postTreeEntries = <EntryTreeItem>[];
|
||||||
for (final entry in topLevelEntries) {
|
for (final entry in topLevelEntries) {
|
||||||
final treeEntry = FriendicaEntryTreeItem(entry, false);
|
final treeEntry = EntryTreeItem(entry, false);
|
||||||
entryTrees[entry.id] = treeEntry;
|
entryTrees[entry.id] = treeEntry;
|
||||||
postTreeEntries.add(treeEntry);
|
postTreeEntries.add(treeEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
final commentTreeEntries = <FriendicaEntryTreeItem>[];
|
final commentTreeEntries = <EntryTreeItem>[];
|
||||||
commentEntries.sort(
|
commentEntries.sort(
|
||||||
(c1, c2) => c1.creationTimestamp.compareTo(c2.creationTimestamp));
|
(c1, c2) => c1.creationTimestamp.compareTo(c2.creationTimestamp));
|
||||||
for (final entry in commentEntries) {
|
for (final entry in commentEntries) {
|
||||||
final parent = entryTrees[entry.parentId];
|
final parent = entryTrees[entry.parentId];
|
||||||
final treeEntry = FriendicaEntryTreeItem(entry, parent == null);
|
final treeEntry = EntryTreeItem(entry, parent == null);
|
||||||
parent?.addChild(treeEntry);
|
parent?.addChild(treeEntry);
|
||||||
entryTrees[entry.id] = treeEntry;
|
entryTrees[entry.id] = treeEntry;
|
||||||
commentTreeEntries.add(treeEntry);
|
commentTreeEntries.add(treeEntry);
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import 'package:friendica_archive_browser/src/models/friendica_contact.dart';
|
|
||||||
import 'package:result_monad/result_monad.dart';
|
|
||||||
|
|
||||||
class FriendicaConnections {
|
|
||||||
final _connectionsById = <String, Contact>{};
|
|
||||||
final _connectionsByName = <String, Contact>{};
|
|
||||||
|
|
||||||
void clearCaches() {
|
|
||||||
_connectionsById.clear();
|
|
||||||
_connectionsByName.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool addConnection(Contact contact) {
|
|
||||||
if (_connectionsById.containsKey(contact.id)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_connectionsById[contact.id] = contact;
|
|
||||||
_connectionsByName[contact.name] = contact;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<Contact, String> getById(String id) {
|
|
||||||
final result = _connectionsById[id];
|
|
||||||
|
|
||||||
return result != null ? Result.ok(result) : Result.error('$id not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<Contact, String> getByName(String name) {
|
|
||||||
final result = _connectionsByName[name];
|
|
||||||
|
|
||||||
return result != null ? Result.ok(result) : Result.error('$name not found');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,9 @@ import 'package:friendica_archive_browser/src/settings/settings_controller.dart'
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
class FriendicaPathMappingService {
|
import '../../services/path_mapper_service_interface.dart';
|
||||||
|
|
||||||
|
class FriendicaPathMappingService implements PathMappingService {
|
||||||
static final _logger = Logger('$FriendicaPathMappingService');
|
static final _logger = Logger('$FriendicaPathMappingService');
|
||||||
final SettingsController settings;
|
final SettingsController settings;
|
||||||
final _archiveDirectories = <FileSystemEntity>[];
|
final _archiveDirectories = <FileSystemEntity>[];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart';
|
import 'package:friendica_archive_browser/src/services/archive_service_provider.dart';
|
||||||
|
|
||||||
import 'friendica/screens/entries_screen.dart';
|
import 'friendica/screens/entries_screen.dart';
|
||||||
import 'friendica/screens/stats_screen.dart';
|
import 'friendica/screens/stats_screen.dart';
|
||||||
|
@ -10,7 +10,7 @@ import 'settings/settings_view.dart';
|
||||||
|
|
||||||
class Home extends StatefulWidget {
|
class Home extends StatefulWidget {
|
||||||
final SettingsController settingsController;
|
final SettingsController settingsController;
|
||||||
final FriendicaArchiveService archiveService;
|
final ArchiveServiceProvider archiveService;
|
||||||
|
|
||||||
const Home(
|
const Home(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
enum ArchiveType {
|
||||||
|
unknown,
|
||||||
|
diaspora,
|
||||||
|
friendica,
|
||||||
|
}
|
46
friendica_archive_browser/lib/src/models/connection.dart
Normal file
46
friendica_archive_browser/lib/src/models/connection.dart
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
class Connection {
|
||||||
|
final ConnectionStatus status;
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
final Uri profileUrl;
|
||||||
|
|
||||||
|
final String network;
|
||||||
|
|
||||||
|
Connection(
|
||||||
|
{this.status = ConnectionStatus.none,
|
||||||
|
this.name = '',
|
||||||
|
this.id = '',
|
||||||
|
profileUrl,
|
||||||
|
this.network = ''})
|
||||||
|
: profileUrl = profileUrl ?? Uri();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Connection{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnectionStatus {
|
||||||
|
youFollowThem,
|
||||||
|
theyFollowYou,
|
||||||
|
mutual,
|
||||||
|
none,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FriendStatusWriter on ConnectionStatus {
|
||||||
|
String name() {
|
||||||
|
switch (this) {
|
||||||
|
case ConnectionStatus.youFollowThem:
|
||||||
|
return "You Follow Them";
|
||||||
|
case ConnectionStatus.theyFollowYou:
|
||||||
|
return "They Follow You";
|
||||||
|
case ConnectionStatus.mutual:
|
||||||
|
return "Follow each other";
|
||||||
|
case ConnectionStatus.none:
|
||||||
|
return "Not connected";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
|
|
||||||
|
class EntryTreeItem {
|
||||||
|
final TimelineEntry entry;
|
||||||
|
final bool isOrphaned;
|
||||||
|
|
||||||
|
final _children = <String, EntryTreeItem>{};
|
||||||
|
|
||||||
|
EntryTreeItem(this.entry, this.isOrphaned);
|
||||||
|
|
||||||
|
String get id => entry.id;
|
||||||
|
|
||||||
|
void addChild(EntryTreeItem child) {
|
||||||
|
_children[child.id] = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EntryTreeItem> get children => List.unmodifiable(_children.values);
|
||||||
|
}
|
|
@ -1,62 +0,0 @@
|
||||||
class Contact {
|
|
||||||
final ConnectionStatus status;
|
|
||||||
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
|
|
||||||
final Uri profileUrl;
|
|
||||||
|
|
||||||
final String network;
|
|
||||||
|
|
||||||
Contact(
|
|
||||||
{required this.status,
|
|
||||||
required this.name,
|
|
||||||
required this.id,
|
|
||||||
required this.profileUrl,
|
|
||||||
required this.network});
|
|
||||||
|
|
||||||
static Contact fromJson(Map<String, dynamic> json) {
|
|
||||||
final status = (json['following'] ?? '') == 'true'
|
|
||||||
? ConnectionStatus.youFollowThem
|
|
||||||
: ConnectionStatus.none;
|
|
||||||
final name = json['name'] ?? '';
|
|
||||||
final id = json['id_str'] ?? '';
|
|
||||||
final profileUrl = Uri.parse(json['url'] ?? '');
|
|
||||||
final network = json['network'] ?? 'unkn';
|
|
||||||
|
|
||||||
return Contact(
|
|
||||||
status: status,
|
|
||||||
name: name,
|
|
||||||
id: id,
|
|
||||||
profileUrl: profileUrl,
|
|
||||||
network: network);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'FriendicaContact{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ConnectionStatus {
|
|
||||||
youFollowThem,
|
|
||||||
theyFollowYou,
|
|
||||||
mutual,
|
|
||||||
none,
|
|
||||||
}
|
|
||||||
|
|
||||||
extension FriendStatusWriter on ConnectionStatus {
|
|
||||||
String name() {
|
|
||||||
switch (this) {
|
|
||||||
case ConnectionStatus.youFollowThem:
|
|
||||||
return "You Follow Them";
|
|
||||||
case ConnectionStatus.theyFollowYou:
|
|
||||||
return "They Follow You";
|
|
||||||
case ConnectionStatus.mutual:
|
|
||||||
return "Follow each other";
|
|
||||||
case ConnectionStatus.none:
|
|
||||||
return "Not connected";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart';
|
||||||
|
|
||||||
import 'model_utils.dart';
|
import 'model_utils.dart';
|
||||||
|
|
||||||
enum FriendicaAttachmentMediaType { unknown, image, video }
|
enum AttachmentMediaType { unknown, image, video }
|
||||||
|
|
||||||
class FriendicaMediaAttachment {
|
class MediaAttachment {
|
||||||
static final _graphicsExtensions = ['jpg', 'png', 'gif', 'tif'];
|
static final _graphicsExtensions = ['jpg', 'png', 'gif', 'tif'];
|
||||||
static final _movieExtensions = ['avi', 'mp4', 'mpg', 'wmv'];
|
static final _movieExtensions = ['avi', 'mp4', 'mpg', 'wmv'];
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ class FriendicaMediaAttachment {
|
||||||
|
|
||||||
final Map<String, String> metadata;
|
final Map<String, String> metadata;
|
||||||
|
|
||||||
final FriendicaAttachmentMediaType explicitType;
|
final AttachmentMediaType explicitType;
|
||||||
|
|
||||||
final Uri thumbnailUri;
|
final Uri thumbnailUri;
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class FriendicaMediaAttachment {
|
||||||
|
|
||||||
final String description;
|
final String description;
|
||||||
|
|
||||||
FriendicaMediaAttachment(
|
MediaAttachment(
|
||||||
{required this.uri,
|
{required this.uri,
|
||||||
required this.creationTimestamp,
|
required this.creationTimestamp,
|
||||||
required this.metadata,
|
required this.metadata,
|
||||||
|
@ -33,16 +33,16 @@ class FriendicaMediaAttachment {
|
||||||
required this.explicitType,
|
required this.explicitType,
|
||||||
required this.description});
|
required this.description});
|
||||||
|
|
||||||
FriendicaMediaAttachment.randomBuilt()
|
MediaAttachment.randomBuilt()
|
||||||
: uri = Uri.parse('http://localhost/${randomId()}'),
|
: uri = Uri.parse('http://localhost/${randomId()}'),
|
||||||
creationTimestamp = DateTime.now().millisecondsSinceEpoch,
|
creationTimestamp = DateTime.now().millisecondsSinceEpoch,
|
||||||
title = 'Random title ${randomId()}',
|
title = 'Random title ${randomId()}',
|
||||||
thumbnailUri = Uri.parse('${randomId()}.jpg'),
|
thumbnailUri = Uri.parse('${randomId()}.jpg'),
|
||||||
description = 'Random description ${randomId()}',
|
description = 'Random description ${randomId()}',
|
||||||
explicitType = FriendicaAttachmentMediaType.image,
|
explicitType = AttachmentMediaType.image,
|
||||||
metadata = {'value1': randomId(), 'value2': randomId()};
|
metadata = {'value1': randomId(), 'value2': randomId()};
|
||||||
|
|
||||||
FriendicaMediaAttachment.fromUriOnly(this.uri)
|
MediaAttachment.fromUriOnly(this.uri)
|
||||||
: creationTimestamp = 0,
|
: creationTimestamp = 0,
|
||||||
thumbnailUri = Uri.file(''),
|
thumbnailUri = Uri.file(''),
|
||||||
title = '',
|
title = '',
|
||||||
|
@ -50,18 +50,18 @@ class FriendicaMediaAttachment {
|
||||||
description = '',
|
description = '',
|
||||||
metadata = {};
|
metadata = {};
|
||||||
|
|
||||||
FriendicaMediaAttachment.fromUriAndTime(this.uri, this.creationTimestamp)
|
MediaAttachment.fromUriAndTime(this.uri, this.creationTimestamp)
|
||||||
: thumbnailUri = Uri.file(''),
|
: thumbnailUri = Uri.file(''),
|
||||||
title = '',
|
title = '',
|
||||||
explicitType = mediaTypeFromString(uri.path),
|
explicitType = mediaTypeFromString(uri.path),
|
||||||
description = '',
|
description = '',
|
||||||
metadata = {};
|
metadata = {};
|
||||||
|
|
||||||
FriendicaMediaAttachment.blank()
|
MediaAttachment.blank()
|
||||||
: uri = Uri(),
|
: uri = Uri(),
|
||||||
creationTimestamp = 0,
|
creationTimestamp = 0,
|
||||||
thumbnailUri = Uri.file(''),
|
thumbnailUri = Uri.file(''),
|
||||||
explicitType = FriendicaAttachmentMediaType.unknown,
|
explicitType = AttachmentMediaType.unknown,
|
||||||
title = '',
|
title = '',
|
||||||
description = '',
|
description = '',
|
||||||
metadata = {};
|
metadata = {};
|
||||||
|
@ -71,7 +71,7 @@ class FriendicaMediaAttachment {
|
||||||
return 'FriendicaMediaAttachment{uri: $uri, creationTimestamp: $creationTimestamp, type: $explicitType, metadata: $metadata, title: $title, description: $description}';
|
return 'FriendicaMediaAttachment{uri: $uri, creationTimestamp: $creationTimestamp, type: $explicitType, metadata: $metadata, title: $title, description: $description}';
|
||||||
}
|
}
|
||||||
|
|
||||||
String toHumanString(FriendicaPathMappingService mapper) {
|
String toHumanString(PathMappingService mapper) {
|
||||||
if (uri.scheme.startsWith('http')) {
|
if (uri.scheme.startsWith('http')) {
|
||||||
return uri.toString();
|
return uri.toString();
|
||||||
}
|
}
|
||||||
|
@ -79,20 +79,6 @@ class FriendicaMediaAttachment {
|
||||||
return mapper.toFullPath(uri.toString());
|
return mapper.toFullPath(uri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
FriendicaMediaAttachment.fromJson(Map<String, dynamic> json)
|
|
||||||
: uri = Uri.parse(json['url']),
|
|
||||||
creationTimestamp = 0,
|
|
||||||
metadata = (json['metadata'] as Map<String, dynamic>? ?? {})
|
|
||||||
.map((key, value) => MapEntry(key, value.toString())),
|
|
||||||
explicitType = (json['mimetype'] ?? '').startsWith('image')
|
|
||||||
? FriendicaAttachmentMediaType.image
|
|
||||||
: (json['mimetype'] ?? '').startsWith('video')
|
|
||||||
? FriendicaAttachmentMediaType.video
|
|
||||||
: FriendicaAttachmentMediaType.unknown,
|
|
||||||
thumbnailUri = Uri(),
|
|
||||||
title = '',
|
|
||||||
description = '';
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'uri': uri.toString(),
|
'uri': uri.toString(),
|
||||||
'creationTimestamp': creationTimestamp,
|
'creationTimestamp': creationTimestamp,
|
||||||
|
@ -103,25 +89,25 @@ class FriendicaMediaAttachment {
|
||||||
'description': description,
|
'description': description,
|
||||||
};
|
};
|
||||||
|
|
||||||
static FriendicaAttachmentMediaType mediaTypeFromString(String path) {
|
static AttachmentMediaType mediaTypeFromString(String path) {
|
||||||
final separator = Platform.isWindows ? '\\' : '/';
|
final separator = Platform.isWindows ? '\\' : '/';
|
||||||
final lastSlash = path.lastIndexOf(separator) + 1;
|
final lastSlash = path.lastIndexOf(separator) + 1;
|
||||||
final filename = path.substring(lastSlash);
|
final filename = path.substring(lastSlash);
|
||||||
final lastPeriod = filename.lastIndexOf('.') + 1;
|
final lastPeriod = filename.lastIndexOf('.') + 1;
|
||||||
if (lastPeriod == 0) {
|
if (lastPeriod == 0) {
|
||||||
return FriendicaAttachmentMediaType.unknown;
|
return AttachmentMediaType.unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
final extension = filename.substring(lastPeriod).toLowerCase();
|
final extension = filename.substring(lastPeriod).toLowerCase();
|
||||||
|
|
||||||
if (_graphicsExtensions.contains(extension)) {
|
if (_graphicsExtensions.contains(extension)) {
|
||||||
return FriendicaAttachmentMediaType.image;
|
return AttachmentMediaType.image;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_movieExtensions.contains(extension)) {
|
if (_movieExtensions.contains(extension)) {
|
||||||
return FriendicaAttachmentMediaType.video;
|
return AttachmentMediaType.video;
|
||||||
}
|
}
|
||||||
|
|
||||||
return FriendicaAttachmentMediaType.unknown;
|
return AttachmentMediaType.unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
|
|
||||||
class TimeElement {
|
class TimeElement {
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
final FriendicaTimelineEntry entry;
|
final TimelineEntry entry;
|
||||||
|
|
||||||
TimeElement({int timeInMS = 0, required this.entry})
|
TimeElement({int timeInMS = 0, required this.entry})
|
||||||
: timestamp = DateTime.fromMillisecondsSinceEpoch(timeInMS);
|
: timestamp = DateTime.fromMillisecondsSinceEpoch(timeInMS);
|
||||||
|
|
160
friendica_archive_browser/lib/src/models/timeline_entry.dart
Normal file
160
friendica_archive_browser/lib/src/models/timeline_entry.dart
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import 'package:friendica_archive_browser/src/models/connection.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'location_data.dart';
|
||||||
|
import 'media_attachment.dart';
|
||||||
|
import 'model_utils.dart';
|
||||||
|
|
||||||
|
class TimelineEntry {
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
final String parentId;
|
||||||
|
|
||||||
|
final String parentAuthor;
|
||||||
|
|
||||||
|
final String parentAuthorId;
|
||||||
|
|
||||||
|
final int creationTimestamp;
|
||||||
|
|
||||||
|
final int backdatedTimestamp;
|
||||||
|
|
||||||
|
final int modificationTimestamp;
|
||||||
|
|
||||||
|
final String body;
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final bool isReshare;
|
||||||
|
|
||||||
|
final String author;
|
||||||
|
|
||||||
|
final String authorId;
|
||||||
|
|
||||||
|
final String externalLink;
|
||||||
|
|
||||||
|
final List<MediaAttachment> mediaAttachments;
|
||||||
|
|
||||||
|
final LocationData locationData;
|
||||||
|
|
||||||
|
final List<Connection> likes;
|
||||||
|
|
||||||
|
final List<Connection> dislikes;
|
||||||
|
|
||||||
|
TimelineEntry({
|
||||||
|
this.id = '',
|
||||||
|
this.parentId = '',
|
||||||
|
this.creationTimestamp = 0,
|
||||||
|
this.backdatedTimestamp = 0,
|
||||||
|
this.modificationTimestamp = 0,
|
||||||
|
this.isReshare = false,
|
||||||
|
this.body = '',
|
||||||
|
this.title = '',
|
||||||
|
this.author = '',
|
||||||
|
this.authorId = '',
|
||||||
|
this.parentAuthor = '',
|
||||||
|
this.parentAuthorId = '',
|
||||||
|
this.externalLink = '',
|
||||||
|
this.locationData = const LocationData(),
|
||||||
|
this.likes = const <Connection>[],
|
||||||
|
this.dislikes = const <Connection>[],
|
||||||
|
List<MediaAttachment>? mediaAttachments,
|
||||||
|
}) : mediaAttachments = mediaAttachments ?? <MediaAttachment>[];
|
||||||
|
|
||||||
|
TimelineEntry.randomBuilt()
|
||||||
|
: creationTimestamp = DateTime.now().millisecondsSinceEpoch,
|
||||||
|
backdatedTimestamp = DateTime.now().millisecondsSinceEpoch,
|
||||||
|
modificationTimestamp = DateTime.now().millisecondsSinceEpoch,
|
||||||
|
id = randomId(),
|
||||||
|
isReshare = false,
|
||||||
|
parentId = randomId(),
|
||||||
|
externalLink = 'Random external link ${randomId()}',
|
||||||
|
body = 'Random post text ${randomId()}',
|
||||||
|
title = 'Random title ${randomId()}',
|
||||||
|
author = 'Random author ${randomId()}',
|
||||||
|
authorId = 'Random authorId ${randomId()}',
|
||||||
|
parentAuthor = 'Random parent author ${randomId()}',
|
||||||
|
parentAuthorId = 'Random parent author id ${randomId()}',
|
||||||
|
locationData = LocationData.randomBuilt(),
|
||||||
|
likes = const <Connection>[],
|
||||||
|
dislikes = const <Connection>[],
|
||||||
|
mediaAttachments = [
|
||||||
|
MediaAttachment.randomBuilt(),
|
||||||
|
MediaAttachment.randomBuilt()
|
||||||
|
];
|
||||||
|
|
||||||
|
TimelineEntry copy(
|
||||||
|
{int? creationTimestamp,
|
||||||
|
int? backdatedTimestamp,
|
||||||
|
int? modificationTimestamp,
|
||||||
|
bool? isReshare,
|
||||||
|
String? id,
|
||||||
|
String? parentId,
|
||||||
|
String? externalLink,
|
||||||
|
String? body,
|
||||||
|
String? title,
|
||||||
|
String? author,
|
||||||
|
String? authorId,
|
||||||
|
String? parentAuthor,
|
||||||
|
String? parentAuthorId,
|
||||||
|
LocationData? locationData,
|
||||||
|
List<MediaAttachment>? mediaAttachments,
|
||||||
|
List<Connection>? likes,
|
||||||
|
List<Connection>? dislikes,
|
||||||
|
List<Uri>? links}) {
|
||||||
|
return TimelineEntry(
|
||||||
|
creationTimestamp: creationTimestamp ?? this.creationTimestamp,
|
||||||
|
backdatedTimestamp: backdatedTimestamp ?? this.backdatedTimestamp,
|
||||||
|
modificationTimestamp:
|
||||||
|
modificationTimestamp ?? this.modificationTimestamp,
|
||||||
|
id: id ?? this.id,
|
||||||
|
isReshare: isReshare ?? this.isReshare,
|
||||||
|
parentId: parentId ?? this.parentId,
|
||||||
|
externalLink: externalLink ?? this.externalLink,
|
||||||
|
body: body ?? this.body,
|
||||||
|
title: title ?? this.title,
|
||||||
|
author: author ?? this.author,
|
||||||
|
authorId: authorId ?? this.authorId,
|
||||||
|
parentAuthor: parentAuthor ?? this.parentAuthor,
|
||||||
|
parentAuthorId: parentAuthorId ?? this.parentAuthorId,
|
||||||
|
locationData: locationData ?? this.locationData,
|
||||||
|
mediaAttachments: mediaAttachments ?? this.mediaAttachments,
|
||||||
|
likes: likes ?? this.likes,
|
||||||
|
dislikes: dislikes ?? this.dislikes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'TimelineEntry{id: $id, isReshare: $isReshare, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor mediaAttachments: $mediaAttachments, externalLink:$externalLink}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String toHumanString(PathMappingService mapper, DateFormat formatter) {
|
||||||
|
final creationDateString = formatter.format(
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(creationTimestamp * 1000)
|
||||||
|
.toLocal());
|
||||||
|
return [
|
||||||
|
'Title: $title',
|
||||||
|
'Creation At: $creationDateString',
|
||||||
|
'Text:',
|
||||||
|
'Author: $author',
|
||||||
|
'Reshare: $isReshare',
|
||||||
|
if (externalLink.isNotEmpty) 'External Link: $externalLink',
|
||||||
|
body,
|
||||||
|
'',
|
||||||
|
if (parentId.isNotEmpty)
|
||||||
|
"Comment on post/comment by ${parentAuthor.isNotEmpty ? parentAuthor : 'unknown author'}",
|
||||||
|
'',
|
||||||
|
if (mediaAttachments.isNotEmpty) 'Photos and Videos:',
|
||||||
|
...mediaAttachments.map((e) => e.toHumanString(mapper)),
|
||||||
|
if (locationData.hasPosition) locationData.toHumanString(),
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasImages() => mediaAttachments
|
||||||
|
.where((element) => element.explicitType == AttachmentMediaType.image)
|
||||||
|
.isNotEmpty;
|
||||||
|
|
||||||
|
bool hasVideos() => mediaAttachments
|
||||||
|
.where((element) => element.explicitType == AttachmentMediaType.video)
|
||||||
|
.isNotEmpty;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart';
|
||||||
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
|
import '../models/entry_tree_item.dart';
|
||||||
|
import '../models/local_image_archive_entry.dart';
|
||||||
|
import '../utils/exec_error.dart';
|
||||||
|
import 'connections_manager.dart';
|
||||||
|
|
||||||
|
class ArchiveService {
|
||||||
|
ConnectionsManager get connectionsManager =>
|
||||||
|
throw Exception('Not implemented');
|
||||||
|
|
||||||
|
String get ownersName => throw Exception('Not implemented');
|
||||||
|
|
||||||
|
PathMappingService get pathMappingService =>
|
||||||
|
throw Exception('Not Implemented');
|
||||||
|
|
||||||
|
void clearCaches() => throw Exception('Not implemented');
|
||||||
|
|
||||||
|
FutureResult<List<EntryTreeItem>, ExecError> getPosts() async =>
|
||||||
|
throw Exception('Not implemented');
|
||||||
|
|
||||||
|
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() =>
|
||||||
|
throw Exception('Not implemented');
|
||||||
|
|
||||||
|
FutureResult<List<EntryTreeItem>, ExecError> getOrphanedComments() =>
|
||||||
|
throw Exception('Not implemented');
|
||||||
|
|
||||||
|
Result<ImageEntry, ExecError> getImageByUrl(String url) =>
|
||||||
|
throw Exception('Not implemented');
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/diaspora/services/diaspora_archive_service.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/diaspora/services/diaspora_path_mapping_service.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/friendica/services/friendica_archive_service.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/friendica/services/friendica_path_mapping_service.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/services/archive_service_interface.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/services/connections_manager.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/services/path_mapper_service_interface.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
||||||
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
|
import '../models/archive_types_enum.dart';
|
||||||
|
import '../models/entry_tree_item.dart';
|
||||||
|
import '../models/local_image_archive_entry.dart';
|
||||||
|
import '../utils/exec_error.dart';
|
||||||
|
|
||||||
|
class ArchiveServiceProvider extends ChangeNotifier implements ArchiveService {
|
||||||
|
final SettingsController settings;
|
||||||
|
late DiasporaArchiveService _diasporaArchiveService;
|
||||||
|
late FriendicaArchiveService _friendicaArchiveService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConnectionsManager get connectionsManager =>
|
||||||
|
_archiveService.connectionsManager;
|
||||||
|
|
||||||
|
ArchiveServiceProvider(this.settings) {
|
||||||
|
_diasporaArchiveService = DiasporaArchiveService(
|
||||||
|
pathMappingService: DiasporaPathMappingService(settings));
|
||||||
|
_friendicaArchiveService = FriendicaArchiveService(
|
||||||
|
pathMappingService: FriendicaPathMappingService(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
String get ownersName => _archiveService.ownersName;
|
||||||
|
|
||||||
|
void clearCaches() {
|
||||||
|
_friendicaArchiveService.clearCaches();
|
||||||
|
_diasporaArchiveService.clearCaches();
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureResult<List<EntryTreeItem>, ExecError> getPosts() async {
|
||||||
|
return _archiveService.getPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureResult<List<EntryTreeItem>, ExecError> getAllComments() async {
|
||||||
|
return _archiveService.getAllComments();
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureResult<List<EntryTreeItem>, ExecError> getOrphanedComments() async {
|
||||||
|
return _archiveService.getOrphanedComments();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<ImageEntry, ExecError> getImageByUrl(String url) {
|
||||||
|
return _archiveService.getImageByUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArchiveService get _archiveService {
|
||||||
|
switch (settings.archiveType) {
|
||||||
|
case ArchiveType.diaspora:
|
||||||
|
return _diasporaArchiveService;
|
||||||
|
case ArchiveType.friendica:
|
||||||
|
return _friendicaArchiveService;
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown archive type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
PathMappingService get pathMappingService =>
|
||||||
|
_archiveService.pathMappingService;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import 'package:friendica_archive_browser/src/models/connection.dart';
|
||||||
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
|
class ConnectionsManager {
|
||||||
|
final _connectionsById = <String, Connection>{};
|
||||||
|
final _connectionsByName = <String, Connection>{};
|
||||||
|
|
||||||
|
int get length => _connectionsById.length;
|
||||||
|
|
||||||
|
void clearCaches() {
|
||||||
|
_connectionsById.clear();
|
||||||
|
_connectionsByName.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool addConnection(Connection connection) {
|
||||||
|
if (_connectionsById.containsKey(connection.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_connectionsById[connection.id] = connection;
|
||||||
|
_connectionsByName[connection.name] = connection;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool addAllConnections(Iterable<Connection> newConnections) {
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
for (final connection in newConnections) {
|
||||||
|
result &= addConnection(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Connection, String> getById(String id) {
|
||||||
|
final result = _connectionsById[id];
|
||||||
|
|
||||||
|
return result != null ? Result.ok(result) : Result.error('$id not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Connection, String> getByName(String name) {
|
||||||
|
final result = _connectionsByName[name];
|
||||||
|
|
||||||
|
return result != null ? Result.ok(result) : Result.error('$name not found');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
class PathMappingService {
|
||||||
|
String get rootFolder => throw Exception('Not implemented');
|
||||||
|
|
||||||
|
List<FileSystemEntity> get archiveDirectories =>
|
||||||
|
throw Exception('Not implemented');
|
||||||
|
|
||||||
|
void refresh() => throw Exception('Not implemented');
|
||||||
|
|
||||||
|
String toFullPath(String relPath) => throw Exception('Not implemented');
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/archive_types_enum.dart';
|
||||||
import 'package:friendica_archive_browser/src/settings/video_player_settings.dart';
|
import 'package:friendica_archive_browser/src/settings/video_player_settings.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/temp_file_builder.dart';
|
import 'package:friendica_archive_browser/src/utils/temp_file_builder.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
@ -18,9 +19,12 @@ class SettingsController with ChangeNotifier {
|
||||||
: _settingsService = SettingsService();
|
: _settingsService = SettingsService();
|
||||||
|
|
||||||
Future<void> loadSettings() async {
|
Future<void> loadSettings() async {
|
||||||
|
_archiveType = await _settingsService.archiveType();
|
||||||
_themeMode = await _settingsService.themeMode();
|
_themeMode = await _settingsService.themeMode();
|
||||||
_rootFolder = await _settingsService.rootFolder();
|
_rootFolder = await _settingsService.rootFolder();
|
||||||
var canReadRootDir = runCatching(()=>Result.ok(Directory(_rootFolder).listSync())).isSuccess;
|
var canReadRootDir =
|
||||||
|
runCatching(() => Result.ok(Directory(_rootFolder).listSync()))
|
||||||
|
.isSuccess;
|
||||||
if (!canReadRootDir) {
|
if (!canReadRootDir) {
|
||||||
_rootFolder = '';
|
_rootFolder = '';
|
||||||
}
|
}
|
||||||
|
@ -74,6 +78,17 @@ class SettingsController with ChangeNotifier {
|
||||||
await _settingsService.updateRootFolder(newPath);
|
await _settingsService.updateRootFolder(newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late ArchiveType _archiveType;
|
||||||
|
|
||||||
|
ArchiveType get archiveType => _archiveType;
|
||||||
|
|
||||||
|
Future<void> updateArchiveType(ArchiveType newArchiveType) async {
|
||||||
|
if (newArchiveType == _archiveType) return;
|
||||||
|
_archiveType = newArchiveType;
|
||||||
|
notifyListeners();
|
||||||
|
await _settingsService.updateArchiveType(newArchiveType);
|
||||||
|
}
|
||||||
|
|
||||||
late ThemeMode _themeMode;
|
late ThemeMode _themeMode;
|
||||||
|
|
||||||
ThemeMode get themeMode => _themeMode;
|
ThemeMode get themeMode => _themeMode;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/archive_types_enum.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import 'video_player_settings.dart';
|
import 'video_player_settings.dart';
|
||||||
|
|
||||||
class SettingsService {
|
class SettingsService {
|
||||||
|
static const archiveTypeKey = "archiveType";
|
||||||
static const themeDarknessKey = 'themeDarkness';
|
static const themeDarknessKey = 'themeDarkness';
|
||||||
static const rootFolderKey = 'rootFolder';
|
static const rootFolderKey = 'rootFolder';
|
||||||
static const videoPlayerSettingTypeKey = 'videoPlayerSettingType';
|
static const videoPlayerSettingTypeKey = 'videoPlayerSettingType';
|
||||||
|
@ -45,6 +47,22 @@ class SettingsService {
|
||||||
prefs.setInt(themeDarknessKey, theme.index);
|
prefs.setInt(themeDarknessKey, theme.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ArchiveType> archiveType() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final archiveTypeIndex = prefs.getInt(archiveTypeKey) ?? 0;
|
||||||
|
if (archiveTypeIndex > ArchiveType.values.length - 1 ||
|
||||||
|
archiveTypeIndex < 0) {
|
||||||
|
return ArchiveType.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ArchiveType.values[archiveTypeIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateArchiveType(ArchiveType archiveType) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setInt(archiveTypeKey, archiveType.index);
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> rootFolder() async {
|
Future<String> rootFolder() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final result = prefs.getString(rootFolderKey) ?? '';
|
final result = prefs.getString(rootFolderKey) ?? '';
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/archive_types_enum.dart';
|
||||||
import 'package:friendica_archive_browser/src/settings/video_player_settings.dart';
|
import 'package:friendica_archive_browser/src/settings/video_player_settings.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart';
|
import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart';
|
||||||
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
import 'package:friendica_archive_browser/src/utils/snackbar_status_builder.dart';
|
||||||
|
@ -157,6 +158,20 @@ class _SettingsViewState extends State<SettingsView> {
|
||||||
Text('Archive Folder: ',
|
Text('Archive Folder: ',
|
||||||
style: Theme.of(context).textTheme.bodyText1),
|
style: Theme.of(context).textTheme.bodyText1),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
|
DropdownButton<ArchiveType>(
|
||||||
|
value: widget._settingsController.archiveType,
|
||||||
|
onChanged: (newArchiveType) async {
|
||||||
|
await widget._settingsController
|
||||||
|
.updateArchiveType(newArchiveType!);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
items: ArchiveType.values
|
||||||
|
.map((e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Text(e.name),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _folderPathController,
|
controller: _folderPathController,
|
||||||
|
|
|
@ -3,12 +3,26 @@ import 'package:result_monad/result_monad.dart';
|
||||||
import 'package:time_machine/time_machine_text_patterns.dart';
|
import 'package:time_machine/time_machine_text_patterns.dart';
|
||||||
|
|
||||||
class OffsetDateTimeUtils {
|
class OffsetDateTimeUtils {
|
||||||
static final _parser = OffsetDateTimePattern.createWithInvariantCulture(
|
static final _offsetTimeParser =
|
||||||
'ddd MMM dd HH:mm:ss o<+HHmm> yyyy');
|
OffsetDateTimePattern.createWithInvariantCulture(
|
||||||
|
'ddd MMM dd HH:mm:ss o<+HHmm> yyyy');
|
||||||
|
|
||||||
static Result<int, ExecError> epochSecTimeFromFriendicaString(
|
static Result<int, ExecError> epochSecTimeFromFriendicaString(
|
||||||
String dateString) {
|
String dateString) {
|
||||||
final offsetDateTime = _parser.parse(dateString);
|
final offsetDateTime = _offsetTimeParser.parse(dateString);
|
||||||
|
if (!offsetDateTime.success) {
|
||||||
|
return Result.error(ExecError.message(offsetDateTime.error.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(offsetDateTime.value.localDateTime
|
||||||
|
.toDateTimeLocal()
|
||||||
|
.millisecondsSinceEpoch ~/
|
||||||
|
1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result<int, ExecError> epochSecTimeFromTimeZoneString(
|
||||||
|
String dateString) {
|
||||||
|
final offsetDateTime = OffsetDateTimePattern.generalIso.parse(dateString);
|
||||||
if (!offsetDateTime.success) {
|
if (!offsetDateTime.success) {
|
||||||
return Result.error(ExecError.message(offsetDateTime.error.toString()));
|
return Result.error(ExecError.message(offsetDateTime.error.toString()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_timeline_entry.dart';
|
import 'package:friendica_archive_browser/src/models/connection.dart';
|
||||||
import 'package:friendica_archive_browser/src/models/friendica_contact.dart';
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/services/friendica_connections.dart';
|
import 'package:friendica_archive_browser/src/services/connections_manager.dart';
|
||||||
|
|
||||||
class TopInteractorsGenerator {
|
class TopInteractorsGenerator {
|
||||||
final _interactors = <String, InteractorItem>{};
|
final _interactors = <String, InteractorItem>{};
|
||||||
|
@ -11,8 +11,7 @@ class TopInteractorsGenerator {
|
||||||
_processedEntryIds.clear();
|
_processedEntryIds.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void processEntry(
|
void processEntry(TimelineEntry item, ConnectionsManager contacts) {
|
||||||
FriendicaTimelineEntry item, FriendicaConnections contacts) {
|
|
||||||
if (_processedEntryIds.contains(item.id)) {
|
if (_processedEntryIds.contains(item.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -59,14 +58,14 @@ class TopInteractorsGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractorItem _getInteractorItemById(
|
InteractorItem _getInteractorItemById(
|
||||||
String id, FriendicaConnections contacts) {
|
String id, ConnectionsManager contacts) {
|
||||||
if (_interactors.containsKey(id)) {
|
if (_interactors.containsKey(id)) {
|
||||||
return _interactors[id]!;
|
return _interactors[id]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
final contact = contacts.getById(id).fold(
|
final contact = contacts.getById(id).fold(
|
||||||
onSuccess: (contact) => contact,
|
onSuccess: (contact) => contact,
|
||||||
onError: (error) => Contact(
|
onError: (error) => Connection(
|
||||||
status: ConnectionStatus.none,
|
status: ConnectionStatus.none,
|
||||||
name: '',
|
name: '',
|
||||||
id: id,
|
id: id,
|
||||||
|
@ -77,7 +76,7 @@ class TopInteractorsGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
class InteractorItem {
|
class InteractorItem {
|
||||||
final Contact contact;
|
final Connection contact;
|
||||||
final int resharedOrCommentedOn;
|
final int resharedOrCommentedOn;
|
||||||
final int likeCount;
|
final int likeCount;
|
||||||
final int dislikeCount;
|
final int dislikeCount;
|
||||||
|
@ -94,7 +93,7 @@ class InteractorItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractorItem copy(
|
InteractorItem copy(
|
||||||
{Contact? contact,
|
{Connection? contact,
|
||||||
int? resharedOrCommentedOn,
|
int? resharedOrCommentedOn,
|
||||||
int? likeCount,
|
int? likeCount,
|
||||||
int? dislikeCount}) {
|
int? dislikeCount}) {
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -217,6 +224,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
markdown:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: markdown
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.1"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -22,6 +22,7 @@ dependencies:
|
||||||
logging: ^1.0.2
|
logging: ^1.0.2
|
||||||
latlng: ^0.1.0
|
latlng: ^0.1.0
|
||||||
map: ^1.0.0
|
map: ^1.0.0
|
||||||
|
markdown: ^4.0.1
|
||||||
metadata_fetch: ^0.4.1
|
metadata_fetch: ^0.4.1
|
||||||
multi_split_view: ^1.10.0+1
|
multi_split_view: ^1.10.0+1
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// ignore_for_file: avoid_print
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
|
import 'package:friendica_archive_browser/src/models/model_utils.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
@ -2,13 +2,32 @@
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:friendica_archive_browser/src/diaspora/services/diaspora_profile_json_reader.dart';
|
import 'package:friendica_archive_browser/src/diaspora/services/diaspora_profile_json_reader.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/models/timeline_entry.dart';
|
||||||
|
import 'package:friendica_archive_browser/src/services/connections_manager.dart';
|
||||||
|
|
||||||
|
const jsonPath = '/Users/hankdev/Desktop/diaspora_pretty.json';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('Diaspora Connections Test', () {
|
test('Diaspora Connections Test', () {
|
||||||
final reader = DiasporaProfileJsonReader(
|
final reader = DiasporaProfileJsonReader(jsonPath, ConnectionsManager());
|
||||||
'/Users/hankdev/Desktop/diaspora_pretty.json');
|
|
||||||
final contacts = reader.readContacts();
|
final contacts = reader.readContacts();
|
||||||
print(contacts.length);
|
print(contacts.length);
|
||||||
print(contacts.first);
|
print(contacts.first);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Diaspora Posts Test', () {
|
||||||
|
final reader = DiasporaProfileJsonReader(jsonPath, ConnectionsManager());
|
||||||
|
final posts = reader.readPosts();
|
||||||
|
|
||||||
|
print(posts.length);
|
||||||
|
print(posts.first);
|
||||||
|
|
||||||
|
final postsWithImage = posts.firstWhere((element) => element.mediaAttachments.isNotEmpty, orElse: ()=>TimelineEntry());
|
||||||
|
print(postsWithImage);
|
||||||
|
|
||||||
|
final resharePost = posts.firstWhere((element) => element.externalLink.isNotEmpty, orElse: ()=>TimelineEntry());
|
||||||
|
print(resharePost);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue