mirror of
https://gitlab.com/mysocialportal/fediverse-archiving-tools.git
synced 2024-10-18 08:53:31 +00:00
Initial Friendica posts showing up in timeline
This commit is contained in:
parent
0c47ad1432
commit
dc3672a58d
9 changed files with 212 additions and 134 deletions
|
@ -2,6 +2,7 @@ import 'package:desktop_window/desktop_window.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:friendica_archive_browser/src/services/friendica_archive_service.dart';
|
||||
import 'package:friendica_archive_browser/src/themes.dart';
|
||||
import 'package:friendica_archive_browser/src/utils/scrolling_behavior.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -26,11 +27,14 @@ class FriendicaArchiveBrowser extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
DesktopWindow.setMinWindowSize(minAppSize);
|
||||
final pathMappingService = PathMappingService(settingsController);
|
||||
final archiveService = FacebookArchiveDataService(
|
||||
final facebookArchiveService = FacebookArchiveDataService(
|
||||
pathMappingService: pathMappingService,
|
||||
appDataDirectory: settingsController.appDataDirectory.path);
|
||||
final friendicaArchiveService =
|
||||
FriendicaArchiveService(pathMappingService: pathMappingService);
|
||||
settingsController.addListener(() {
|
||||
archiveService.clearCaches();
|
||||
friendicaArchiveService.clearCaches();
|
||||
facebookArchiveService.clearCaches();
|
||||
pathMappingService.refresh();
|
||||
});
|
||||
return AnimatedBuilder(
|
||||
|
@ -56,7 +60,10 @@ class FriendicaArchiveBrowser extends StatelessWidget {
|
|||
home: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => settingsController),
|
||||
ChangeNotifierProvider(create: (context) => archiveService),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => facebookArchiveService),
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => friendicaArchiveService),
|
||||
Provider(create: (context) => pathMappingService),
|
||||
],
|
||||
child: Home(settingsController: settingsController),
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_post.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/models/location_data.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/models/timeline_type.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
|
||||
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
||||
import 'package:friendica_archive_browser/src/utils/clipboard_helper.dart';
|
||||
|
@ -53,19 +52,6 @@ class PostCard extends StatelessWidget {
|
|||
style: const TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
)),
|
||||
if (post.timelineType != TimelineType.active)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Tooltip(
|
||||
message:
|
||||
'Post is in ${post.timelineType == TimelineType.trash ? 'Trash' : 'Archive'}',
|
||||
child: Icon(
|
||||
post.timelineType == TimelineType.trash
|
||||
? Icons.delete_outline
|
||||
: Icons.archive_outlined,
|
||||
color: Theme.of(context).disabledColor,
|
||||
)),
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Copy text version of post to clipboard',
|
||||
child: IconButton(
|
||||
|
@ -76,6 +62,19 @@ class PostCard extends StatelessWidget {
|
|||
icon: const Icon(Icons.copy)),
|
||||
),
|
||||
]),
|
||||
Wrap(
|
||||
direction: Axis.horizontal,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'By: ',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(post.author)
|
||||
],
|
||||
),
|
||||
if (post.post.isNotEmpty) ...[
|
||||
const SizedBox(height: spacingHeight),
|
||||
Text(post.post)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
|
||||
import 'package:friendica_archive_browser/src/utils/offsetdatetime_utils.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
|
@ -20,6 +21,8 @@ class FriendicaPost {
|
|||
|
||||
final String title;
|
||||
|
||||
final String author;
|
||||
|
||||
final List<FriendicaMediaAttachment> mediaAttachments;
|
||||
|
||||
final LocationData locationData;
|
||||
|
@ -34,6 +37,7 @@ class FriendicaPost {
|
|||
this.modificationTimestamp = 0,
|
||||
this.post = '',
|
||||
this.title = '',
|
||||
this.author = '',
|
||||
this.locationData = const LocationData(),
|
||||
required this.timelineType,
|
||||
List<FriendicaMediaAttachment>? mediaAttachments,
|
||||
|
@ -47,6 +51,7 @@ class FriendicaPost {
|
|||
modificationTimestamp = DateTime.now().millisecondsSinceEpoch,
|
||||
post = 'Random post text ${randomId()}',
|
||||
title = 'Random title ${randomId()}',
|
||||
author = 'Random author ${randomId()}',
|
||||
locationData = LocationData.randomBuilt(),
|
||||
timelineType = TimelineType.active,
|
||||
links = [
|
||||
|
@ -64,6 +69,7 @@ class FriendicaPost {
|
|||
int? modificationTimestamp,
|
||||
String? post,
|
||||
String? title,
|
||||
String? author,
|
||||
LocationData? locationData,
|
||||
List<FriendicaMediaAttachment>? mediaAttachments,
|
||||
TimelineType? timelineType,
|
||||
|
@ -75,6 +81,7 @@ class FriendicaPost {
|
|||
modificationTimestamp ?? this.modificationTimestamp,
|
||||
post: post ?? this.post,
|
||||
title: title ?? this.title,
|
||||
author: author ?? this.author,
|
||||
locationData: locationData ?? this.locationData,
|
||||
mediaAttachments: mediaAttachments ?? this.mediaAttachments,
|
||||
timelineType: timelineType ?? this.timelineType,
|
||||
|
@ -83,7 +90,7 @@ class FriendicaPost {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FacebookPost{creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, timelineType: $timelineType, post: $post, title: $title, mediaAttachments: $mediaAttachments, links: $links}';
|
||||
return 'FacebookPost{creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, timelineType: $timelineType, post: $post, title: $title, author: $author, mediaAttachments: $mediaAttachments, links: $links}';
|
||||
}
|
||||
|
||||
String toHumanString(PathMappingService mapper, DateFormat formatter) {
|
||||
|
@ -94,6 +101,7 @@ class FriendicaPost {
|
|||
'Title: $title',
|
||||
'Creation At: $creationDateString',
|
||||
'Text:',
|
||||
'Author: $author',
|
||||
post,
|
||||
'',
|
||||
if (links.isNotEmpty) 'Links:',
|
||||
|
@ -117,95 +125,33 @@ class FriendicaPost {
|
|||
|
||||
static FriendicaPost fromJson(
|
||||
Map<String, dynamic> json, TimelineType timelineType) {
|
||||
final int timestamp = json['timestamp'] ?? 0;
|
||||
var modificationTimestamp = timestamp;
|
||||
var backdatedTimestamp = timestamp;
|
||||
var locationData = const LocationData();
|
||||
String post = '';
|
||||
if (json.containsKey('data')) {
|
||||
final data = json['data'];
|
||||
for (var dataItem in data) {
|
||||
if (dataItem.containsKey('post')) {
|
||||
post = dataItem['post'];
|
||||
} else if (dataItem.containsKey('update_timestamp')) {
|
||||
modificationTimestamp = dataItem['update_timestamp'];
|
||||
} else if (dataItem.containsKey('backdated_timestamp')) {
|
||||
backdatedTimestamp = dataItem['backdated_timestamp'];
|
||||
} else {
|
||||
_logger.fine(
|
||||
"No post or update key sequence in post @$timestamp: ${dataItem.keys}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String title = json['title'] ?? '';
|
||||
final int timestamp = json.containsKey('created_at')
|
||||
? OffsetDateTimeUtils.epochSecTimeFromFriendicaString(
|
||||
json['created_at'])
|
||||
.fold(
|
||||
onSuccess: (value) => value,
|
||||
onError: (error) {
|
||||
_logger.severe("Couldn't read date time string: $error");
|
||||
return 0;
|
||||
})
|
||||
: 0;
|
||||
final post = json['text'] ?? '';
|
||||
final author = json['user']['name'];
|
||||
final title = json['friendica_title'] ?? '';
|
||||
final actualLocationData = LocationData();
|
||||
final modificationTimestamp = timestamp;
|
||||
final backdatedTimestamp = timestamp;
|
||||
final links = <Uri>[];
|
||||
final mediaAttachments = <FriendicaMediaAttachment>[];
|
||||
|
||||
if (json.containsKey('attachments')) {
|
||||
for (Map<String, dynamic> attachment in json['attachments']) {
|
||||
if (!attachment.containsKey('data')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var dataItem in attachment['data']) {
|
||||
if (dataItem.containsKey('external_context')) {
|
||||
final String linkText = dataItem['external_context']['url'] ?? '';
|
||||
if (linkText.isNotEmpty) {
|
||||
links.add(Uri.parse(linkText));
|
||||
}
|
||||
} else if (dataItem.containsKey('media')) {
|
||||
mediaAttachments.add(
|
||||
FriendicaMediaAttachment.fromFacebookJson(dataItem['media']));
|
||||
} else if (dataItem.containsKey('place')) {
|
||||
locationData = LocationData.fromJson(dataItem['place']);
|
||||
} else {
|
||||
//TODO Add Facebook Post Poll Processing
|
||||
if (dataItem.containsKey('poll')) continue;
|
||||
//TODO Add Facebook Post attachment text processing
|
||||
if (dataItem.containsKey('text')) continue;
|
||||
//TODO Add Facebook Post external context detailed link processing (not just the URL)
|
||||
if (dataItem.containsKey('name')) continue;
|
||||
//TODO Add Facebook Post event processing
|
||||
if (dataItem.containsKey('event')) continue;
|
||||
|
||||
_logger.fine('Unknown post key type: ${dataItem.keys}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
late final LocationData actualLocationData;
|
||||
if (locationData.hasPosition) {
|
||||
actualLocationData = locationData;
|
||||
} else {
|
||||
final mediaWithPosition = mediaAttachments.where((m) =>
|
||||
m.metadata.containsKey('latitude') &&
|
||||
m.metadata.containsKey('longitude'));
|
||||
if (mediaWithPosition.isNotEmpty) {
|
||||
final metadata = mediaWithPosition.first.metadata;
|
||||
final latitude = double.tryParse(metadata['latitude'] ?? '') ?? 0.0;
|
||||
final longitude = double.tryParse(metadata['longitude'] ?? '') ?? 0.0;
|
||||
actualLocationData = LocationData(
|
||||
latitude: latitude, longitude: longitude, hasPosition: true);
|
||||
} else {
|
||||
actualLocationData = locationData;
|
||||
}
|
||||
}
|
||||
|
||||
final String actualTitle = title.isNotEmpty
|
||||
? title
|
||||
: mediaAttachments
|
||||
.map((m) => m.title)
|
||||
.firstWhere((t) => t.isNotEmpty, orElse: () => '');
|
||||
|
||||
return FriendicaPost(
|
||||
creationTimestamp: timestamp,
|
||||
modificationTimestamp: modificationTimestamp,
|
||||
backdatedTimestamp: backdatedTimestamp,
|
||||
locationData: actualLocationData,
|
||||
post: post,
|
||||
title: actualTitle,
|
||||
author: author,
|
||||
title: title,
|
||||
links: links,
|
||||
mediaAttachments: mediaAttachments,
|
||||
timelineType: timelineType,
|
||||
|
|
|
@ -3,8 +3,8 @@ import 'package:friendica_archive_browser/src/friendica/components/filter_contro
|
|||
import 'package:friendica_archive_browser/src/friendica/components/post_card.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_post.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/models/model_utils.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/services/facebook_archive_service.dart';
|
||||
import 'package:friendica_archive_browser/src/screens/error_screen.dart';
|
||||
import 'package:friendica_archive_browser/src/services/friendica_archive_service.dart';
|
||||
import 'package:friendica_archive_browser/src/settings/settings_controller.dart';
|
||||
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -21,14 +21,14 @@ class PostsScreen extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.info('Build FacebookPostListView');
|
||||
final service = Provider.of<FacebookArchiveDataService>(context);
|
||||
_logger.info('Build FriendicaPostListView');
|
||||
final service = Provider.of<FriendicaArchiveService>(context);
|
||||
final username = Provider.of<SettingsController>(context).facebookName;
|
||||
|
||||
return FutureBuilder<Result<List<FriendicaPost>, ExecError>>(
|
||||
future: service.getPosts(),
|
||||
builder: (context, snapshot) {
|
||||
_logger.info('FacebookPostListView Future builder called');
|
||||
_logger.info('FriendicaPostListView Future builder called');
|
||||
|
||||
if (!snapshot.hasData ||
|
||||
snapshot.connectionState != ConnectionState.done) {
|
||||
|
@ -43,48 +43,49 @@ class PostsScreen extends StatelessWidget {
|
|||
}
|
||||
|
||||
final allPosts = postsResult.value;
|
||||
final filteredPosts = username.isEmpty
|
||||
? allPosts
|
||||
: allPosts.where((p) =>
|
||||
p.title != username ||
|
||||
p.post.isNotEmpty ||
|
||||
p.mediaAttachments.isNotEmpty ||
|
||||
p.links.isNotEmpty);
|
||||
|
||||
final posts = username.isEmpty
|
||||
? filteredPosts.toList()
|
||||
: filteredPosts.map((p) {
|
||||
var newTitle = p.title;
|
||||
if (p.title == username) {
|
||||
newTitle = 'You posted';
|
||||
} else {
|
||||
newTitle = p.title
|
||||
.replaceAll(username, 'You')
|
||||
.replaceAll(wholeWordRegEx('his'), 'your')
|
||||
.replaceAll(wholeWordRegEx('her'), 'your');
|
||||
}
|
||||
if (newTitle == p.title) {
|
||||
return p;
|
||||
} else {
|
||||
return p.copy(title: newTitle);
|
||||
}
|
||||
}).toList();
|
||||
final posts = allPosts;
|
||||
// final filteredPosts = username.isEmpty
|
||||
// ? allPosts
|
||||
// : allPosts.where((p) =>
|
||||
// p.title != username ||
|
||||
// p.post.isNotEmpty ||
|
||||
// p.mediaAttachments.isNotEmpty ||
|
||||
// p.links.isNotEmpty);
|
||||
//
|
||||
// final posts = username.isEmpty
|
||||
// ? filteredPosts.toList()
|
||||
// : filteredPosts.map((p) {
|
||||
// var newTitle = p.title;
|
||||
// if (p.title == username) {
|
||||
// newTitle = 'You posted';
|
||||
// } else {
|
||||
// newTitle = p.title
|
||||
// .replaceAll(username, 'You')
|
||||
// .replaceAll(wholeWordRegEx('his'), 'your')
|
||||
// .replaceAll(wholeWordRegEx('her'), 'your');
|
||||
// }
|
||||
// if (newTitle == p.title) {
|
||||
// return p;
|
||||
// } else {
|
||||
// return p.copy(title: newTitle);
|
||||
// }
|
||||
// }).toList();
|
||||
if (posts.isEmpty) {
|
||||
return const StandInStatusScreen(title: 'No posts were found');
|
||||
}
|
||||
|
||||
_logger.fine('Build Posts ListView');
|
||||
return _FacebookPostsScreenWidget(posts: posts);
|
||||
return _FriendicaPostsScreenWidget(posts: posts);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _FacebookPostsScreenWidget extends StatelessWidget {
|
||||
static final _logger = Logger('$_FacebookPostsScreenWidget');
|
||||
class _FriendicaPostsScreenWidget extends StatelessWidget {
|
||||
static final _logger = Logger('$_FriendicaPostsScreenWidget');
|
||||
|
||||
final List<FriendicaPost> posts;
|
||||
|
||||
const _FacebookPostsScreenWidget({Key? key, required this.posts})
|
||||
const _FriendicaPostsScreenWidget({Key? key, required this.posts})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -112,10 +113,10 @@ class _FacebookPostsScreenWidget extends StatelessWidget {
|
|||
child: ListView.separated(
|
||||
primary: false,
|
||||
physics: const RangeMaintainingScrollPhysics(),
|
||||
restorationId: 'facebookPostsListView',
|
||||
restorationId: 'friendicaPostsListView',
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
_logger.finer('Rendering FacebookPost List Item');
|
||||
_logger.finer('Rendering Friendica List Item');
|
||||
return PostCard(post: items[index]);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
class ImageEntry {
|
||||
final String postId;
|
||||
final String localFilename;
|
||||
final String url;
|
||||
|
||||
ImageEntry(
|
||||
{required this.postId, required this.localFilename, required this.url});
|
||||
|
||||
ImageEntry.fromJson(Map<String, dynamic> json)
|
||||
: postId = json['postId'] ?? '',
|
||||
localFilename = json['localFilename'] ?? '',
|
||||
url = json['url'] ?? '';
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'postId': postId,
|
||||
'localFilename': localFilename,
|
||||
'url': url,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/models/friendica_post.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/models/timeline_type.dart';
|
||||
import 'package:friendica_archive_browser/src/friendica/services/path_mapping_service.dart';
|
||||
import 'package:friendica_archive_browser/src/models/local_image_archive_entry.dart';
|
||||
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
|
||||
class FriendicaArchiveService extends ChangeNotifier {
|
||||
final PathMappingService pathMappingService;
|
||||
final Map<String, ImageEntry> _imagesByRequestUrl = {};
|
||||
final List<FriendicaPost> _posts = [];
|
||||
|
||||
FriendicaArchiveService({required this.pathMappingService});
|
||||
|
||||
void clearCaches() {
|
||||
_imagesByRequestUrl.clear();
|
||||
_posts.clear();
|
||||
}
|
||||
|
||||
FutureResult<List<FriendicaPost>, ExecError> getPosts() async {
|
||||
if (_posts.isEmpty) {
|
||||
_loadPosts();
|
||||
}
|
||||
|
||||
return Result.ok(_posts);
|
||||
}
|
||||
|
||||
Result<ImageEntry, ExecError> getImageByUrl(String url) {
|
||||
if (_imagesByRequestUrl.isEmpty) {
|
||||
_loadImages();
|
||||
}
|
||||
|
||||
final result = _imagesByRequestUrl[url];
|
||||
return result == null
|
||||
? Result.error(ExecError(errorMessage: '$url not found'))
|
||||
: Result.ok(result);
|
||||
}
|
||||
|
||||
String get _baseArchiveFolder => pathMappingService.rootFolder;
|
||||
|
||||
void _loadPosts() {
|
||||
final entriesJsonPath = p.join(_baseArchiveFolder, 'postsAndComments.json');
|
||||
final jsonFile = File(entriesJsonPath);
|
||||
if (jsonFile.existsSync()) {
|
||||
final json = jsonDecode(jsonFile.readAsStringSync()) as List<dynamic>;
|
||||
final postEntries =
|
||||
json.map((j) => FriendicaPost.fromJson(j, TimelineType.active));
|
||||
_posts.clear();
|
||||
_posts.addAll(postEntries);
|
||||
}
|
||||
|
||||
final uniqueUsers = _posts.map((e) => e.author).toSet();
|
||||
print(uniqueUsers);
|
||||
}
|
||||
|
||||
void _loadImages() {
|
||||
final imageJsonPath = p.join(_baseArchiveFolder, 'images.json');
|
||||
final jsonFile = File(imageJsonPath);
|
||||
if (jsonFile.existsSync()) {
|
||||
final json = jsonDecode(jsonFile.readAsStringSync()) as List<dynamic>;
|
||||
final imageEntries = json.map((j) => ImageEntry.fromJson(j));
|
||||
for (final entry in imageEntries) {
|
||||
_imagesByRequestUrl[entry.url] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:friendica_archive_browser/src/utils/exec_error.dart';
|
||||
import 'package:result_monad/result_monad.dart';
|
||||
import 'package:time_machine/time_machine_text_patterns.dart';
|
||||
|
||||
class OffsetDateTimeUtils {
|
||||
static final _parser = OffsetDateTimePattern.createWithInvariantCulture(
|
||||
'ddd MMM dd HH:mm:ss o<+HHmm> yyyy');
|
||||
|
||||
static Result<int, ExecError> epochSecTimeFromFriendicaString(
|
||||
String dateString) {
|
||||
final offsetDateTime = _parser.parse(dateString);
|
||||
if (!offsetDateTime.success) {
|
||||
return Result.error(ExecError.message(offsetDateTime.error.toString()));
|
||||
}
|
||||
|
||||
return Result.ok(offsetDateTime.value.localDateTime
|
||||
.toDateTimeLocal()
|
||||
.millisecondsSinceEpoch ~/
|
||||
1000);
|
||||
}
|
||||
}
|
|
@ -453,6 +453,15 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
time_machine:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: master
|
||||
resolved-ref: "040de1a261df442538ed97f6de5895465d7ca4dd"
|
||||
url: "https://github.com/Dana-Ferguson/time_machine"
|
||||
source: git
|
||||
version: "0.9.17"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -29,6 +29,10 @@ dependencies:
|
|||
result_monad: ^1.0.2
|
||||
scrollable_positioned_list: ^0.2.2
|
||||
shared_preferences: ^2.0.8
|
||||
time_machine:
|
||||
git:
|
||||
url: https://github.com/Dana-Ferguson/time_machine
|
||||
ref: master
|
||||
url_launcher: ^6.0.12
|
||||
uuid: ^3.0.5
|
||||
network_to_file_image: ^3.0.3
|
||||
|
|
Loading…
Reference in a new issue