kopia lustrzana https://gitlab.com/mysocialportal/relatica
Merge branch 'status-control-refactoring' into 'main'
Refactor post/comment views to use a more flattened structure. See merge request mysocialportal/friendica_portal!4codemagic-setup
commit
31c3cbaa98
|
@ -47,7 +47,7 @@ android {
|
|||
applicationId "social.myportal.flutter_portal"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||
minSdkVersion 19
|
||||
minSdkVersion 21
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="social.myportal.flutter_portal">
|
||||
<application
|
||||
|
||||
<application
|
||||
android:label="flutter_portal"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
@ -17,12 +18,11 @@
|
|||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
|
@ -31,4 +31,10 @@
|
|||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
@ -48,14 +48,17 @@ PODS:
|
|||
- SDWebImage (5.13.2):
|
||||
- SDWebImage/Core (= 5.13.2)
|
||||
- SDWebImage/Core (5.13.2)
|
||||
- shared_preferences_ios (0.0.1):
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.2):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- SwiftyGif (5.4.3)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- video_player_avfoundation (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
|
@ -64,9 +67,10 @@ DEPENDENCIES:
|
|||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
|
@ -89,17 +93,19 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
shared_preferences_ios:
|
||||
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
video_player_avfoundation:
|
||||
:path: ".symlinks/plugins/video_player_avfoundation/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95
|
||||
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
|
@ -107,10 +113,11 @@ SPEC CHECKSUMS:
|
|||
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||
|
||||
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../globals.dart';
|
||||
import '../models/attachment_media_type_enum.dart';
|
||||
import '../models/media_attachment.dart';
|
||||
import '../screens/image_viewer_screen.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
import 'image_control.dart';
|
||||
import 'video_control.dart';
|
||||
|
||||
class MediaAttachmentViewerControl extends StatefulWidget {
|
||||
final MediaAttachment attachment;
|
||||
|
||||
const MediaAttachmentViewerControl({super.key, required this.attachment});
|
||||
|
||||
@override
|
||||
State<MediaAttachmentViewerControl> createState() =>
|
||||
_MediaAttachmentViewerControlState();
|
||||
}
|
||||
|
||||
class _MediaAttachmentViewerControlState
|
||||
extends State<MediaAttachmentViewerControl> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final item = widget.attachment;
|
||||
const width = 250.0;
|
||||
const height = 250.0;
|
||||
if (item.explicitType == AttachmentMediaType.video) {
|
||||
if (useVideoPlayer) {
|
||||
return VideoControl(
|
||||
videoUrl: widget.attachment.uri.toString(),
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final confirm = await showYesNoDialog(
|
||||
context, 'Open Video Link in external app? ${item.uri}');
|
||||
if (confirm != true) {
|
||||
return;
|
||||
}
|
||||
if (await canLaunchUrl(item.uri)) {
|
||||
if (mounted) {
|
||||
buildSnackbar(
|
||||
context,
|
||||
'Attempting to launch video: ${item.uri}',
|
||||
);
|
||||
}
|
||||
await launchUrl(item.uri);
|
||||
} else {
|
||||
if (mounted) {
|
||||
buildSnackbar(context, 'Unable to launch video: ${item.uri}');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
width: width,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
item.description.isNotEmpty
|
||||
? item.description
|
||||
: 'Video: ${item.uri}',
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
if (item.explicitType != AttachmentMediaType.image) {
|
||||
return Text('${item.explicitType}: ${item.uri}');
|
||||
}
|
||||
|
||||
return ImageControl(
|
||||
width: width,
|
||||
height: height,
|
||||
imageUrl: item.thumbnailUri.toString(),
|
||||
altText: item.description,
|
||||
onTap: () async {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return ImageViewerScreen(attachment: item);
|
||||
}));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,12 +25,13 @@ class NotificationControl extends StatelessWidget {
|
|||
final manager = getIt<TimelineManager>();
|
||||
final existingPostData = manager.getPostTreeEntryBy(notification.iid);
|
||||
if (existingPostData.isSuccess) {
|
||||
context.push('/post/view/${existingPostData.value.id}');
|
||||
context
|
||||
.push('/post/view/${existingPostData.value.id}/${notification.iid}');
|
||||
return;
|
||||
}
|
||||
final loadedPost = await manager.refreshStatusChain(notification.iid);
|
||||
if (loadedPost.isSuccess) {
|
||||
context.push('/post/view/${loadedPost.value.id}');
|
||||
context.push('/post/view/${loadedPost.value.id}/${notification.iid}');
|
||||
return;
|
||||
}
|
||||
buildSnackbar(
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../models/flattened_tree_item.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../utils/url_opening_utils.dart';
|
||||
import '../media_attachment_viewer_control.dart';
|
||||
import '../padding.dart';
|
||||
import 'interactions_bar_control.dart';
|
||||
import 'status_header_control.dart';
|
||||
|
||||
class FlattenedTreeEntryControl extends StatefulWidget {
|
||||
final FlattenedTreeItem originalItem;
|
||||
final bool openRemote;
|
||||
final bool showStatusOpenButton;
|
||||
|
||||
const FlattenedTreeEntryControl(
|
||||
{super.key,
|
||||
required this.originalItem,
|
||||
required this.openRemote,
|
||||
required this.showStatusOpenButton});
|
||||
|
||||
@override
|
||||
State<FlattenedTreeEntryControl> createState() => _StatusControlState();
|
||||
}
|
||||
|
||||
class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
||||
static final _logger = Logger('$FlattenedTreeEntryControl');
|
||||
|
||||
var showContent = true;
|
||||
|
||||
var showComments = false;
|
||||
|
||||
FlattenedTreeItem get item => widget.originalItem;
|
||||
|
||||
TimelineEntry get entry => item.timelineEntry;
|
||||
|
||||
bool get isPublic => entry.isPublic;
|
||||
|
||||
bool get isPost => entry.parentId.isEmpty;
|
||||
|
||||
bool get hasComments => entry.engagementSummary.repliesCount > 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
showContent = entry.spoilerText.isEmpty;
|
||||
showComments = isPost ? false : true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building ${entry.toShortString()}');
|
||||
const otherPadding = 8.0;
|
||||
final leftPadding = otherPadding + (widget.originalItem.level * 15.0);
|
||||
final color = widget.originalItem.level.isOdd
|
||||
? Theme.of(context).splashColor
|
||||
: Theme.of(context).cardColor;
|
||||
final body = Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
StatusHeaderControl(
|
||||
entry: entry,
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (entry.spoilerText.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showContent = !showContent;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Content Summary: ${entry.spoilerText} (Click to ${showContent ? "Hide" : "Show"}}')),
|
||||
if (showContent) ...[
|
||||
buildBody(context),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
buildMediaBar(context),
|
||||
],
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
InteractionsBarControl(
|
||||
entry: entry,
|
||||
isMine: item.isMine,
|
||||
openRemote: widget.openRemote,
|
||||
showOpenControl: widget.showStatusOpenButton,
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: leftPadding,
|
||||
right: otherPadding,
|
||||
top: otherPadding,
|
||||
bottom: otherPadding,
|
||||
),
|
||||
child: isPost ? body : Card(color: color, child: body),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return HtmlWidget(
|
||||
entry.body,
|
||||
onTapUrl: (url) async {
|
||||
return await openUrlStringInSystembrowser(context, url, 'video');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildMediaBar(BuildContext context) {
|
||||
final items = entry.mediaAttachments;
|
||||
if (items.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 250.0,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
return MediaAttachmentViewerControl(attachment: items[index]);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return HorizontalPadding();
|
||||
},
|
||||
itemCount: items.length));
|
||||
}
|
||||
}
|
|
@ -6,14 +6,21 @@ import '../../globals.dart';
|
|||
import '../../models/timeline_entry.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/snackbar_builder.dart';
|
||||
import '../../utils/url_opening_utils.dart';
|
||||
|
||||
class InteractionsBarControl extends StatefulWidget {
|
||||
final TimelineEntry entry;
|
||||
|
||||
final bool openRemote;
|
||||
final bool showOpenControl;
|
||||
final bool isMine;
|
||||
|
||||
const InteractionsBarControl(
|
||||
{super.key, required this.entry, required this.isMine});
|
||||
const InteractionsBarControl({
|
||||
super.key,
|
||||
required this.entry,
|
||||
required this.isMine,
|
||||
required this.showOpenControl,
|
||||
required this.openRemote,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InteractionsBarControl> createState() => _InteractionsBarControlState();
|
||||
|
@ -28,7 +35,7 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
|||
|
||||
bool get isFavorited => widget.entry.isFavorited;
|
||||
|
||||
bool get isReshared => widget.entry.isReshare;
|
||||
bool get youReshared => widget.entry.youReshared;
|
||||
|
||||
int get reshares => widget.entry.engagementSummary.rebloggedCount;
|
||||
|
||||
|
@ -114,6 +121,22 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> openAction(BuildContext context) async {
|
||||
if (widget.openRemote) {
|
||||
final openInBrowser =
|
||||
await showYesNoDialog(context, 'Open in external browser?');
|
||||
if (openInBrowser == true && mounted) {
|
||||
await openUrlStringInSystembrowser(
|
||||
context,
|
||||
widget.entry.externalLink,
|
||||
'Post',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
context.push('/post/view/${widget.entry.id}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Building: ${widget.entry.toShortString()}');
|
||||
|
@ -134,25 +157,39 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
|||
: Icon(Icons.thumb_up_outlined)),
|
||||
if (isPost)
|
||||
IconButton(
|
||||
onPressed: widget.isMine && !widget.entry.isReshare
|
||||
onPressed: widget.isMine && !widget.entry.youReshared
|
||||
? null
|
||||
: isProcessing
|
||||
? null
|
||||
: () async => isReshared
|
||||
: () async => youReshared
|
||||
? await unResharePost()
|
||||
: await resharePost(),
|
||||
icon:
|
||||
Icon(isReshared ? Icons.repeat_on_outlined : Icons.repeat)),
|
||||
icon: Icon(
|
||||
youReshared ? Icons.repeat_on_outlined : Icons.repeat)),
|
||||
IconButton(
|
||||
onPressed: isProcessing ? null : addComment,
|
||||
icon: Icon(Icons.add_comment)),
|
||||
if (widget.isMine &&
|
||||
!widget.entry
|
||||
.isReshare) //TODO Figure out why reshares show up as mine sometimes but not others
|
||||
.youReshared) //TODO Figure out why reshares show up as mine sometimes but not others
|
||||
IconButton(
|
||||
onPressed:
|
||||
isProcessing ? null : () async => await deleteEntry(),
|
||||
icon: Icon(Icons.delete))
|
||||
icon: Icon(Icons.delete)),
|
||||
if (widget.showOpenControl)
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await openAction(context);
|
||||
},
|
||||
icon: const Icon(Icons.launch),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import '../../models/entry_tree_item.dart';
|
||||
import '../../models/flattened_tree_item.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/entry_tree_item_flattening.dart';
|
||||
import 'flattened_tree_entry_control.dart';
|
||||
|
||||
class PostControl extends StatefulWidget {
|
||||
final EntryTreeItem originalItem;
|
||||
final String scrollToId;
|
||||
final bool openRemote;
|
||||
final bool showStatusOpenButton;
|
||||
final bool isRoot;
|
||||
|
||||
const PostControl({
|
||||
super.key,
|
||||
required this.originalItem,
|
||||
required this.scrollToId,
|
||||
required this.openRemote,
|
||||
required this.showStatusOpenButton,
|
||||
required this.isRoot,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostControl> createState() => _PostControlState();
|
||||
}
|
||||
|
||||
class _PostControlState extends State<PostControl> {
|
||||
static final _logger = Logger('$PostControl');
|
||||
|
||||
final ItemScrollController itemScrollController = ItemScrollController();
|
||||
final ItemPositionsListener itemPositionsListener =
|
||||
ItemPositionsListener.create();
|
||||
|
||||
var showContent = true;
|
||||
|
||||
var showComments = false;
|
||||
|
||||
EntryTreeItem get item => widget.originalItem;
|
||||
|
||||
TimelineEntry get entry => item.entry;
|
||||
|
||||
bool get isPublic => item.entry.isPublic;
|
||||
|
||||
bool get hasComments => entry.engagementSummary.repliesCount > 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
showContent = entry.spoilerText.isEmpty;
|
||||
showComments = widget.scrollToId != item.id;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final manager = context.watch<TimelineManager>();
|
||||
_logger.finest('Building ${item.entry.toShortString()}');
|
||||
final items = widget.originalItem.flatten(topLevelOnly: !showComments);
|
||||
|
||||
if (widget.isRoot) {
|
||||
return buildListView(context, items, manager);
|
||||
} else {
|
||||
return buildColumnView(context, items, manager);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildColumnView(
|
||||
BuildContext context,
|
||||
List<FlattenedTreeItem> items,
|
||||
TimelineManager manager,
|
||||
) {
|
||||
final widgets = <Widget>[];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final itemWidget = FlattenedTreeEntryControl(
|
||||
originalItem: items[i],
|
||||
openRemote: widget.openRemote,
|
||||
showStatusOpenButton: widget.showStatusOpenButton,
|
||||
);
|
||||
|
||||
widgets.add(itemWidget);
|
||||
if (i == 0 && hasComments) {
|
||||
widgets.add(
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
showComments = !showComments;
|
||||
});
|
||||
if (showComments) {
|
||||
await manager.refreshStatusChain(entry.id);
|
||||
}
|
||||
},
|
||||
child:
|
||||
Text(showComments ? 'Hide Comments' : 'Load & Show Comments'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Column(children: widgets);
|
||||
}
|
||||
|
||||
Widget buildListView(
|
||||
BuildContext context,
|
||||
List<FlattenedTreeItem> items,
|
||||
TimelineManager manager,
|
||||
) {
|
||||
final int count;
|
||||
final int offset;
|
||||
if (hasComments && showComments) {
|
||||
count = items.length + 1;
|
||||
offset = 1;
|
||||
final scrollToIndex =
|
||||
items.indexWhere((e) => e.timelineEntry.id == widget.scrollToId);
|
||||
Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() async => itemScrollController.jumpTo(index: scrollToIndex),
|
||||
);
|
||||
} else if (hasComments) {
|
||||
count = 2;
|
||||
offset = 0;
|
||||
} else {
|
||||
count = 1;
|
||||
offset = 0;
|
||||
}
|
||||
return ScrollablePositionedList.builder(
|
||||
itemCount: count,
|
||||
itemScrollController: itemScrollController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return FlattenedTreeEntryControl(
|
||||
originalItem: items.first,
|
||||
openRemote: widget.openRemote,
|
||||
showStatusOpenButton: widget.showStatusOpenButton,
|
||||
);
|
||||
}
|
||||
if (index == 1 && hasComments) {
|
||||
return TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
showComments = !showComments;
|
||||
});
|
||||
if (showComments) {
|
||||
await manager.refreshStatusChain(entry.id);
|
||||
}
|
||||
},
|
||||
child:
|
||||
Text(showComments ? 'Hide Comments' : 'Load & Show Comments'),
|
||||
);
|
||||
}
|
||||
return FlattenedTreeEntryControl(
|
||||
originalItem: items[index - offset],
|
||||
openRemote: widget.openRemote,
|
||||
showStatusOpenButton: widget.showStatusOpenButton,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../models/attachment_media_type_enum.dart';
|
||||
import '../../models/entry_tree_item.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../screens/image_viewer_screen.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import '../../utils/snackbar_builder.dart';
|
||||
import '../../utils/url_opening_utils.dart';
|
||||
import '../image_control.dart';
|
||||
import '../padding.dart';
|
||||
import 'interactions_bar_control.dart';
|
||||
import 'status_header_control.dart';
|
||||
|
||||
class StatusControl extends StatefulWidget {
|
||||
final EntryTreeItem originalItem;
|
||||
final bool openRemote;
|
||||
final bool showStatusOpenButton;
|
||||
|
||||
const StatusControl(
|
||||
{super.key,
|
||||
required this.originalItem,
|
||||
required this.openRemote,
|
||||
required this.showStatusOpenButton});
|
||||
|
||||
@override
|
||||
State<StatusControl> createState() => _StatusControlState();
|
||||
}
|
||||
|
||||
class _StatusControlState extends State<StatusControl> {
|
||||
static final _logger = Logger('$StatusControl');
|
||||
|
||||
var showContent = true;
|
||||
|
||||
var showComments = false;
|
||||
|
||||
EntryTreeItem get item => widget.originalItem;
|
||||
|
||||
TimelineEntry get entry => item.entry;
|
||||
|
||||
bool get isPublic => item.entry.isPublic;
|
||||
|
||||
bool get isPost => item.entry.parentId.isEmpty;
|
||||
|
||||
bool get hasComments => entry.engagementSummary.repliesCount > 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
showContent = entry.spoilerText.isEmpty;
|
||||
showComments = isPost ? false : true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final manager = context.watch<TimelineManager>();
|
||||
_logger.finest('Building ${item.entry.toShortString()}');
|
||||
final padding = isPost ? 8.0 : 8.0;
|
||||
final body = Padding(
|
||||
padding: EdgeInsets.all(padding),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
StatusHeaderControl(
|
||||
entry: entry,
|
||||
openRemote: widget.openRemote,
|
||||
showOpenControl: widget.showStatusOpenButton,
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (entry.spoilerText.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showContent = !showContent;
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Content Summary: ${entry.spoilerText} (Click to ${showContent ? "Hide" : "Show"}}')),
|
||||
if (showContent) ...[
|
||||
buildBody(context),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
buildMediaBar(context),
|
||||
],
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
InteractionsBarControl(
|
||||
entry: entry,
|
||||
isMine: item.isMine,
|
||||
),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
if (isPost && hasComments)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
showComments = !showComments;
|
||||
});
|
||||
if (showComments) {
|
||||
await manager.refreshStatusChain(item.id);
|
||||
}
|
||||
},
|
||||
child:
|
||||
Text(showComments ? 'Hide Comments' : 'Load & Show Comments'),
|
||||
),
|
||||
if (item.totalChildren > 0 && showComments)
|
||||
buildChildComments(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return isPost
|
||||
? body
|
||||
: Card(color: Theme.of(context).splashColor, child: body);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return HtmlWidget(
|
||||
entry.body,
|
||||
onTapUrl: (url) async {
|
||||
return await openUrlStringInSystembrowser(context, url, 'video');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildMediaBar(BuildContext context) {
|
||||
final items = entry.mediaAttachments;
|
||||
if (items.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return SizedBox(
|
||||
height: 250.0,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
|
||||
if (item.explicitType == AttachmentMediaType.video) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (await canLaunchUrl(item.uri)) {
|
||||
buildSnackbar(
|
||||
context,
|
||||
'Attempting to launch video: ${item.uri}',
|
||||
);
|
||||
await launchUrl(item.uri);
|
||||
} else {
|
||||
buildSnackbar(
|
||||
context, 'Unable to launch video: ${item.uri}');
|
||||
}
|
||||
},
|
||||
child: Text(item.description.isNotEmpty
|
||||
? item.description
|
||||
: 'Video'));
|
||||
}
|
||||
if (item.explicitType != AttachmentMediaType.image) {
|
||||
return Text('${item.explicitType}: ${item.uri}');
|
||||
}
|
||||
|
||||
return ImageControl(
|
||||
width: 250.0,
|
||||
height: 250.0,
|
||||
imageUrl: item.thumbnailUri.toString(),
|
||||
altText: item.description,
|
||||
onTap: () async {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
return ImageViewerScreen(attachment: item);
|
||||
}));
|
||||
},
|
||||
);
|
||||
// return Text(item.toString());
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return HorizontalPadding();
|
||||
},
|
||||
itemCount: items.length));
|
||||
}
|
||||
|
||||
Widget buildChildComments(BuildContext context) {
|
||||
final comments = widget.originalItem.children;
|
||||
|
||||
if (comments.isEmpty) {
|
||||
return Text('No comments');
|
||||
}
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 5.0, top: 5.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: comments
|
||||
.map((c) => StatusControl(
|
||||
originalItem: c,
|
||||
openRemote: false,
|
||||
showStatusOpenButton: false,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
|
@ -7,20 +7,16 @@ import '../../models/timeline_entry.dart';
|
|||
import '../../routes.dart';
|
||||
import '../../services/connections_manager.dart';
|
||||
import '../../utils/dateutils.dart';
|
||||
import '../../utils/url_opening_utils.dart';
|
||||
import '../image_control.dart';
|
||||
import '../padding.dart';
|
||||
|
||||
class StatusHeaderControl extends StatelessWidget {
|
||||
final TimelineEntry entry;
|
||||
final bool openRemote;
|
||||
final bool showOpenControl;
|
||||
|
||||
const StatusHeaderControl(
|
||||
{super.key,
|
||||
required this.entry,
|
||||
required this.openRemote,
|
||||
required this.showOpenControl});
|
||||
const StatusHeaderControl({
|
||||
super.key,
|
||||
required this.entry,
|
||||
});
|
||||
|
||||
void goToProfile(BuildContext context, String id) {
|
||||
context.pushNamed(ScreenPaths.userProfile, params: {'id': id});
|
||||
|
@ -95,39 +91,7 @@ class StatusHeaderControl extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
],
|
||||
if (showOpenControl) ...[
|
||||
SizedBox(
|
||||
width: 20.0,
|
||||
),
|
||||
buildActionBar(context),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActionBar(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await _openAction(context);
|
||||
},
|
||||
icon: const Icon(Icons.launch),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openAction(BuildContext context) async {
|
||||
if (openRemote) {
|
||||
final openInBrowser =
|
||||
await showYesNoDialog(context, 'Open in external browser?');
|
||||
if (openInBrowser == true) {
|
||||
await openUrlStringInSystembrowser(context, entry.externalLink, 'Post');
|
||||
}
|
||||
} else {
|
||||
context.push('/post/view/${entry.id}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
|||
|
||||
import '../../models/TimelineIdentifiers.dart';
|
||||
import '../../services/timeline_manager.dart';
|
||||
import 'status_control.dart';
|
||||
import 'post_control.dart';
|
||||
|
||||
class TimelinePanel extends StatelessWidget {
|
||||
static final _logger = Logger('$TimelinePanel');
|
||||
|
@ -43,10 +43,12 @@ class TimelinePanel extends StatelessWidget {
|
|||
final item = items[itemIndex];
|
||||
_logger.finest(
|
||||
'Building item: $itemIndex: ${item.entry.toShortString()}');
|
||||
return StatusControl(
|
||||
return PostControl(
|
||||
originalItem: item,
|
||||
scrollToId: item.id,
|
||||
openRemote: false,
|
||||
showStatusOpenButton: true,
|
||||
isRoot: false,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Divider(),
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../services/setting_service.dart';
|
||||
|
||||
final _shownVideos = <String>{};
|
||||
|
||||
class VideoControl extends StatefulWidget {
|
||||
final String videoUrl;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
const VideoControl({
|
||||
super.key,
|
||||
required this.videoUrl,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoControl> createState() => _VideoControlState();
|
||||
}
|
||||
|
||||
class _VideoControlState extends State<VideoControl> {
|
||||
late final VideoPlayerController videoPlayerController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
videoPlayerController = VideoPlayerController.network(widget.videoUrl)
|
||||
..initialize().then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
videoPlayerController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
videoPlayerController.pause();
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
void showVideo() {
|
||||
_shownVideos.add(widget.videoUrl);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void toggleVideoPlay() {
|
||||
videoPlayerController.value.isPlaying
|
||||
? videoPlayerController.pause()
|
||||
: videoPlayerController.play();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shown = !context.watch<SettingsService>().lowBandwidthMode ||
|
||||
_shownVideos.contains(widget.videoUrl);
|
||||
|
||||
final placeHolderBox = SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: const Card(
|
||||
color: Colors.black12,
|
||||
shape: RoundedRectangleBorder(),
|
||||
child: Icon(Icons.movie)),
|
||||
);
|
||||
|
||||
if (!shown) {
|
||||
return GestureDetector(
|
||||
onTap: showVideo,
|
||||
child: placeHolderBox,
|
||||
);
|
||||
}
|
||||
|
||||
_shownVideos.add(widget.videoUrl);
|
||||
if (!videoPlayerController.value.isInitialized) {
|
||||
return placeHolderBox;
|
||||
}
|
||||
final size = videoPlayerController.value.size;
|
||||
final videoWidth = size.width <= widget.width ? size.width : widget.width;
|
||||
final videoHeight = size.width <= widget.width
|
||||
? size.height
|
||||
: size.height * videoWidth / size.width;
|
||||
return GestureDetector(
|
||||
onTap: toggleVideoPlay,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: videoWidth,
|
||||
height: videoHeight,
|
||||
child: AspectRatio(
|
||||
aspectRatio: videoPlayerController.value.aspectRatio,
|
||||
child: VideoPlayer(videoPlayerController),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: videoPlayerController.value.isPlaying
|
||||
? const Icon(Icons.pause)
|
||||
: const Icon(Icons.play_arrow),
|
||||
onPressed: toggleVideoPlay,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
videoPlayerController.seekTo(Duration.zero);
|
||||
},
|
||||
icon: const Icon(Icons.replay)),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,8 @@ final platformHasCamera = Platform.isIOS || Platform.isAndroid;
|
|||
|
||||
final useImagePicker = kIsWeb || Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
final useVideoPlayer = kIsWeb || Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
Future<bool?> showConfirmDialog(BuildContext context, String caption) {
|
||||
return showDialog<bool>(
|
||||
context: context,
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import 'timeline_entry.dart';
|
||||
|
||||
class FlattenedTreeItem {
|
||||
final TimelineEntry timelineEntry;
|
||||
|
||||
final bool isMine;
|
||||
|
||||
final int level;
|
||||
|
||||
FlattenedTreeItem({
|
||||
required this.timelineEntry,
|
||||
required this.isMine,
|
||||
required this.level,
|
||||
});
|
||||
}
|
|
@ -30,7 +30,7 @@ class TimelineEntry {
|
|||
|
||||
final String spoilerText;
|
||||
|
||||
final bool isReshare;
|
||||
final bool youReshared;
|
||||
|
||||
final bool isPublic;
|
||||
|
||||
|
@ -60,7 +60,7 @@ class TimelineEntry {
|
|||
this.creationTimestamp = 0,
|
||||
this.backdatedTimestamp = 0,
|
||||
this.modificationTimestamp = 0,
|
||||
this.isReshare = false,
|
||||
this.youReshared = false,
|
||||
this.isPublic = true,
|
||||
this.body = '',
|
||||
this.title = '',
|
||||
|
@ -86,7 +86,7 @@ class TimelineEntry {
|
|||
backdatedTimestamp = DateTime.now().millisecondsSinceEpoch,
|
||||
modificationTimestamp = DateTime.now().millisecondsSinceEpoch,
|
||||
id = randomId(),
|
||||
isReshare = DateTime.now().second ~/ 2 == 0 ? true : false,
|
||||
youReshared = DateTime.now().second ~/ 2 == 0 ? true : false,
|
||||
isPublic = DateTime.now().second ~/ 2 == 0 ? true : false,
|
||||
parentId = randomId(),
|
||||
externalLink = 'Random external link ${randomId()}',
|
||||
|
@ -138,7 +138,7 @@ class TimelineEntry {
|
|||
modificationTimestamp:
|
||||
modificationTimestamp ?? this.modificationTimestamp,
|
||||
id: id ?? this.id,
|
||||
isReshare: isReshare ?? this.isReshare,
|
||||
youReshared: isReshare ?? this.youReshared,
|
||||
isPublic: isPublic ?? this.isPublic,
|
||||
parentId: parentId ?? this.parentId,
|
||||
externalLink: externalLink ?? this.externalLink,
|
||||
|
@ -163,11 +163,11 @@ class TimelineEntry {
|
|||
|
||||
@override
|
||||
String toString() {
|
||||
return 'TimelineEntry{id: $id, isReshare: $isReshare, isFavorited: $isFavorited, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor externalLink:$externalLink}';
|
||||
return 'TimelineEntry{id: $id, isReshare: $youReshared, isFavorited: $isFavorited, parentId: $parentId, creationTimestamp: $creationTimestamp, modificationTimestamp: $modificationTimestamp, backdatedTimeStamp: $backdatedTimestamp, post: $body, title: $title, author: $author, parentAuthor: $parentAuthor externalLink:$externalLink}';
|
||||
}
|
||||
|
||||
String toShortString() {
|
||||
return 'TimelineEntry{id: $id, isReshare: $isReshare, isFavorited: $isFavorited, parentId: $parentId, $engagementSummary}';
|
||||
return 'TimelineEntry{id: $id, isReshare: $youReshared, isFavorited: $isFavorited, parentId: $parentId, $engagementSummary}';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -187,7 +187,7 @@ class TimelineEntry {
|
|||
body == other.body &&
|
||||
title == other.title &&
|
||||
spoilerText == other.spoilerText &&
|
||||
isReshare == other.isReshare &&
|
||||
youReshared == other.youReshared &&
|
||||
isPublic == other.isPublic &&
|
||||
author == other.author &&
|
||||
authorId == other.authorId &&
|
||||
|
@ -214,7 +214,7 @@ class TimelineEntry {
|
|||
body.hashCode ^
|
||||
title.hashCode ^
|
||||
spoilerText.hashCode ^
|
||||
isReshare.hashCode ^
|
||||
youReshared.hashCode ^
|
||||
isPublic.hashCode ^
|
||||
author.hashCode ^
|
||||
authorId.hashCode ^
|
||||
|
|
|
@ -21,7 +21,7 @@ enum NotificationType {
|
|||
case NotificationType.follow_request:
|
||||
return 'sent follow request to you';
|
||||
case NotificationType.mention:
|
||||
return 'mentioned you on';
|
||||
return 'mentioned you';
|
||||
case NotificationType.reshare:
|
||||
case NotificationType.reblog:
|
||||
return 'reshared';
|
||||
|
|
|
@ -137,9 +137,11 @@ final appRouter = GoRouter(
|
|||
EditorScreen(id: state.params['id'] ?? 'Not Found'),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'view/:id',
|
||||
builder: (context, state) =>
|
||||
PostScreen(id: state.params['id'] ?? 'Not Found'),
|
||||
path: 'view/:id/:goto_id',
|
||||
builder: (context, state) => PostScreen(
|
||||
id: state.params['id'] ?? 'Not Found',
|
||||
goToId: state.params['goto_id'] ?? 'Not Found',
|
||||
),
|
||||
),
|
||||
]),
|
||||
GoRoute(
|
||||
|
|
|
@ -240,8 +240,6 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
children: [
|
||||
StatusHeaderControl(
|
||||
entry: entry,
|
||||
openRemote: false,
|
||||
showOpenControl: false,
|
||||
),
|
||||
const VerticalPadding(height: 3),
|
||||
if (entry.spoilerText.isNotEmpty) ...[
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controls/timeline/status_control.dart';
|
||||
import '../controls/timeline/post_control.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
|
||||
class PostScreen extends StatelessWidget {
|
||||
final String id;
|
||||
|
||||
const PostScreen({super.key, required this.id});
|
||||
final String goToId;
|
||||
|
||||
const PostScreen({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.goToId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -17,13 +23,12 @@ class PostScreen extends StatelessWidget {
|
|||
onRefresh: () async {
|
||||
await manager.refreshStatusChain(id);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: StatusControl(
|
||||
originalItem: post,
|
||||
openRemote: true,
|
||||
showStatusOpenButton: true,
|
||||
),
|
||||
child: PostControl(
|
||||
originalItem: post,
|
||||
scrollToId: goToId,
|
||||
openRemote: true,
|
||||
showStatusOpenButton: true,
|
||||
isRoot: true,
|
||||
),
|
||||
),
|
||||
onError: (error) => Text('Error getting post: $error'));
|
||||
|
|
|
@ -52,7 +52,7 @@ extension TimelineEntryFriendicaExtensions on TimelineEntry {
|
|||
backdatedTimestamp: backdatedTimestamp,
|
||||
locationData: actualLocationData,
|
||||
body: body,
|
||||
isReshare: isReshare,
|
||||
youReshared: isReshare,
|
||||
isPublic: isPublic,
|
||||
id: id,
|
||||
parentId: parentId,
|
||||
|
|
|
@ -4,7 +4,6 @@ import '../../globals.dart';
|
|||
import '../../models/user_notification.dart';
|
||||
import '../../services/connections_manager.dart';
|
||||
import '../../utils/dateutils.dart';
|
||||
import '../../utils/string_utils.dart';
|
||||
import 'connection_mastodon_extensions.dart';
|
||||
import 'timeline_entry_mastodon_extensions.dart';
|
||||
|
||||
|
@ -46,9 +45,18 @@ extension NotificationMastodonExtension on UserNotification {
|
|||
final status = TimelineEntryMastodonExtensions.fromJson(json['status']);
|
||||
statusId = status.id;
|
||||
statusLink = status.externalLink;
|
||||
final referenceType = status.parentId.isEmpty ? 'post' : 'comment';
|
||||
content =
|
||||
"${from.name} ${type.toVerb()} ${status.author}'s $referenceType: ${status.body.truncate()}";
|
||||
final referenceType = type == NotificationType.mention
|
||||
? ''
|
||||
: status.parentId.isEmpty
|
||||
? 'post'
|
||||
: 'comment';
|
||||
final baseContent = type == NotificationType.mention
|
||||
? "${from.name} ${type.toVerb()}"
|
||||
: "${from.name} ${type.toVerb()} ${status.author}'s";
|
||||
final shareInfo = status.reshareAuthorId.isNotEmpty
|
||||
? "reshare of ${status.reshareAuthor}'s"
|
||||
: '';
|
||||
content = "$baseContent $shareInfo $referenceType: ${status.body}";
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
|||
})
|
||||
: 0;
|
||||
final id = json['id'] ?? '';
|
||||
final isReshare = json['reblogged'] ?? false;
|
||||
final youReshared = json['reblogged'] ?? false;
|
||||
final isPublic = json['visibility'] == 'public';
|
||||
final parentId = json['in_reply_to_id'] ?? '';
|
||||
final parentAuthor = json['in_reply_to_account_id'] ?? '';
|
||||
|
@ -88,7 +88,7 @@ extension TimelineEntryMastodonExtensions on TimelineEntry {
|
|||
locationData: actualLocationData,
|
||||
spoilerText: spoilerText,
|
||||
body: body,
|
||||
isReshare: isReshare,
|
||||
youReshared: youReshared,
|
||||
isPublic: isPublic,
|
||||
id: id,
|
||||
parentId: parentId,
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import '../models/entry_tree_item.dart';
|
||||
import '../models/flattened_tree_item.dart';
|
||||
|
||||
extension FlatteningExtensions on EntryTreeItem {
|
||||
static const BaseLevel = 0;
|
||||
|
||||
List<FlattenedTreeItem> flatten(
|
||||
{int level = BaseLevel, bool topLevelOnly = false}) {
|
||||
final items = <FlattenedTreeItem>[];
|
||||
final myEntry = FlattenedTreeItem(
|
||||
timelineEntry: entry,
|
||||
isMine: isMine,
|
||||
level: level,
|
||||
);
|
||||
|
||||
items.add(myEntry);
|
||||
if (topLevelOnly) {
|
||||
return items;
|
||||
}
|
||||
|
||||
final sortedChildren = [...children];
|
||||
sortedChildren.sort((c1, c2) =>
|
||||
c1.entry.creationTimestamp.compareTo(c2.entry.creationTimestamp));
|
||||
for (final child in sortedChildren) {
|
||||
int childLevel = level + 1;
|
||||
if (child.entry.authorId == entry.authorId && level != BaseLevel) {
|
||||
childLevel = level;
|
||||
}
|
||||
|
||||
final childItems = child.flatten(level: childLevel);
|
||||
childItems.sort((c1, c2) {
|
||||
if (c2.level == c1.level) {
|
||||
return c1.timelineEntry.creationTimestamp
|
||||
.compareTo(c2.timelineEntry.creationTimestamp);
|
||||
}
|
||||
|
||||
if (c1.level == childLevel) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (c2.level == childLevel) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
items.addAll(childItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
extension StringUtils on String {
|
||||
String truncate({int length = 32}) {
|
||||
if (length <= length) {
|
||||
if (this.length <= length) {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,13 @@ import Foundation
|
|||
import desktop_window
|
||||
import flutter_secure_storage_macos
|
||||
import path_provider_macos
|
||||
import shared_preferences_macos
|
||||
import shared_preferences_foundation
|
||||
import sqflite
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin"))
|
||||
FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
platform :osx, '10.11'
|
||||
platform :osx, '10.13'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
PODS:
|
||||
- desktop_window (0.0.1):
|
||||
- FlutterMacOS
|
||||
- flutter_secure_storage_macos (3.3.1):
|
||||
- flutter_secure_storage_macos (6.1.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
- FMDB (2.7.5):
|
||||
|
@ -9,7 +9,8 @@ PODS:
|
|||
- FMDB/standard (2.7.5)
|
||||
- path_provider_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_macos (0.0.1):
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.2):
|
||||
- FlutterMacOS
|
||||
|
@ -22,7 +23,7 @@ DEPENDENCIES:
|
|||
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||
- shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
|
||||
|
@ -39,8 +40,8 @@ EXTERNAL SOURCES:
|
|||
:path: Flutter/ephemeral
|
||||
path_provider_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
|
||||
shared_preferences_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
|
||||
url_launcher_macos:
|
||||
|
@ -48,14 +49,14 @@ EXTERNAL SOURCES:
|
|||
|
||||
SPEC CHECKSUMS:
|
||||
desktop_window: fb7c4f12c1129f947ac482296b6f14059d57a3c3
|
||||
flutter_secure_storage_macos: 6ceee8fbc7f484553ad17f79361b556259df89aa
|
||||
flutter_secure_storage_macos: 75c8cadfdba05ca007c0fa4ea0c16e5cf85e521b
|
||||
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
||||
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
|
||||
path_provider_macos: 05fb0ef0cedf3e5bd179b9e41a638682b37133ea
|
||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||
sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
|
||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||
|
||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||
PODFILE CHECKSUM: a884f6dd3f7494f3892ee6c81feea3a3abbf9153
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -204,7 +204,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 1300;
|
||||
LastUpgradeCheck = 1420;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
|
@ -395,6 +395,7 @@
|
|||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
|
@ -405,7 +406,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
|
@ -420,13 +421,18 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = T69YZGT58U;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
@ -436,6 +442,8 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Profile;
|
||||
|
@ -468,6 +476,7 @@
|
|||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
|
@ -484,7 +493,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
|
@ -521,6 +530,7 @@
|
|||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
|
@ -531,7 +541,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
|
@ -546,13 +556,18 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = T69YZGT58U;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
@ -566,13 +581,18 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = T69YZGT58U;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
@ -582,6 +602,8 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
|
@ -590,6 +612,8 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1300"
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -10,5 +10,7 @@
|
|||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -6,5 +6,7 @@
|
|||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -10,5 +10,7 @@
|
|||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
77
pubspec.lock
77
pubspec.lock
|
@ -147,7 +147,7 @@ packages:
|
|||
name: file_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.2.4"
|
||||
version: "5.2.5"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -208,7 +208,7 @@ packages:
|
|||
name: flutter_secure_storage
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "7.0.1"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -222,7 +222,7 @@ packages:
|
|||
name: flutter_secure_storage_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "2.0.1"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -260,7 +260,7 @@ packages:
|
|||
name: flutter_widget_from_html_core
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0+2"
|
||||
version: "0.9.1"
|
||||
functional_listener:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -323,7 +323,7 @@ packages:
|
|||
name: image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
version: "3.3.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -351,7 +351,7 @@ packages:
|
|||
name: image_picker_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.6+4"
|
||||
version: "0.8.6+5"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -484,7 +484,7 @@ packages:
|
|||
name: path_provider_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
version: "2.0.7"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -562,13 +562,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.27.7"
|
||||
scrollable_positioned_list:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: scrollable_positioned_list
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.0.16"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -576,10 +583,10 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.14"
|
||||
shared_preferences_ios:
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_ios
|
||||
name: shared_preferences_foundation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
|
@ -590,13 +597,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
shared_preferences_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -636,14 +636,14 @@ packages:
|
|||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.0+2"
|
||||
version: "2.4.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -678,7 +678,7 @@ packages:
|
|||
name: synchronized
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0+3"
|
||||
version: "3.0.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -777,6 +777,41 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.4.10"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.10"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.8"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -790,7 +825,7 @@ packages:
|
|||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0+2"
|
||||
version: "0.2.0+3"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -14,7 +14,7 @@ dependencies:
|
|||
cupertino_icons: ^1.0.2
|
||||
desktop_window: ^0.4.0
|
||||
email_validator: ^2.1.17
|
||||
flutter_secure_storage: ^6.0.0
|
||||
flutter_secure_storage: ^7.0.1
|
||||
flutter_widget_from_html_core: ^0.9.0
|
||||
get_it: ^7.2.0
|
||||
get_it_mixin: ^3.1.4
|
||||
|
@ -36,6 +36,8 @@ dependencies:
|
|||
image: ^3.2.2
|
||||
flutter_file_dialog: ^2.3.2
|
||||
multi_trigger_autocomplete: ^0.1.1
|
||||
video_player: ^2.4.10
|
||||
scrollable_positioned_list: ^0.3.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -56,8 +58,8 @@ parts:
|
|||
- libsecret-1-dev
|
||||
- libjsoncpp-dev
|
||||
stage-packages:
|
||||
- libsecret-1-dev
|
||||
- libjsoncpp1-dev
|
||||
- libsecret-1-0
|
||||
- libjsoncpp1
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
|
|
|
@ -0,0 +1,271 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:friendica_portal/globals.dart';
|
||||
import 'package:friendica_portal/models/entry_tree_item.dart';
|
||||
import 'package:friendica_portal/models/timeline_entry.dart';
|
||||
import 'package:friendica_portal/utils/entry_tree_item_flattening.dart';
|
||||
|
||||
void main() {
|
||||
group('Flattening Tests', () {
|
||||
test('Single entry no children', () {
|
||||
final entry = TimelineEntry.randomBuilt();
|
||||
final treeItem = EntryTreeItem(entry);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(1));
|
||||
expect(flattened.first.isMine, equals(treeItem.isMine));
|
||||
expect(flattened.first.timelineEntry, equals(treeItem.entry));
|
||||
});
|
||||
|
||||
test('Entry with two children', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(
|
||||
TimelineEntry(id: '1'),
|
||||
),
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2'),
|
||||
),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 1]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry with nesting children different authors', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(TimelineEntry(id: '1', authorId: randomId()),
|
||||
initialChildren: {
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2', authorId: randomId()),
|
||||
),
|
||||
}),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry with nesting children same authors', () {
|
||||
final post = TimelineEntry(id: '0');
|
||||
final children = {
|
||||
'1': EntryTreeItem(TimelineEntry(id: '1'), initialChildren: {
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(id: '2'),
|
||||
),
|
||||
}),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(3));
|
||||
expect(
|
||||
flattened.map((e) => int.parse(e.timelineEntry.id)).toList(),
|
||||
equals([0, 1, 2]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 1]),
|
||||
);
|
||||
});
|
||||
|
||||
test('Entry fully nested children', () {
|
||||
var stamp = 0;
|
||||
final post =
|
||||
TimelineEntry(id: '0', authorId: 'a0', creationTimestamp: stamp++);
|
||||
final children = {
|
||||
'1': EntryTreeItem(
|
||||
TimelineEntry(id: '1', creationTimestamp: stamp++),
|
||||
initialChildren: {
|
||||
'1.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'1.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'1.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '1.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1',
|
||||
authorId: 'a1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.1.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1.1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.1.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.1.2',
|
||||
authorId: 'a1',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'2.2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'2.2.2.1': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.1',
|
||||
creationTimestamp: (stamp++) + 100,
|
||||
),
|
||||
),
|
||||
'2.2.2.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.2',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.2.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.3',
|
||||
authorId: 'a2',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'2.2.2.4': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.2.2.4',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
'2.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '2.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
'3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
initialChildren: {
|
||||
'3.1': EntryTreeItem(TimelineEntry(
|
||||
id: '3.1',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
)),
|
||||
'3.2': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3.2',
|
||||
authorId: 'a0',
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
'3.3': EntryTreeItem(
|
||||
TimelineEntry(
|
||||
id: '3.3',
|
||||
authorId: randomId(),
|
||||
creationTimestamp: stamp++,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
};
|
||||
final treeItem = EntryTreeItem(post, initialChildren: children);
|
||||
final flattened = treeItem.flatten();
|
||||
expect(flattened.length, equals(21));
|
||||
expect(
|
||||
flattened.map((e) => e.timelineEntry.id).toList(),
|
||||
equals([
|
||||
'0',
|
||||
'1',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.3',
|
||||
'2',
|
||||
'2.1',
|
||||
'2.2',
|
||||
'2.2.1',
|
||||
'2.2.1.2',
|
||||
'2.2.1.1',
|
||||
'2.2.2',
|
||||
'2.2.2.2',
|
||||
'2.2.2.3',
|
||||
'2.2.2.4',
|
||||
'2.2.2.1',
|
||||
'2.3',
|
||||
'3',
|
||||
'3.2',
|
||||
'3.1',
|
||||
'3.3',
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
flattened.map((e) => e.level).toList(),
|
||||
equals([0, 1, 2, 2, 2, 1, 2, 2, 3, 3, 4, 3, 3, 3, 4, 4, 2, 1, 1, 2, 2]),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
Ładowanie…
Reference in New Issue