Merge branch 'status-control-refactoring' into 'main'

Refactor post/comment views to use a more flattened structure.

See merge request mysocialportal/friendica_portal!4
codemagic-setup
HankG 2023-01-16 15:28:54 +00:00
commit 31c3cbaa98
35 zmienionych plików z 1095 dodań i 353 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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

Wyświetl plik

@ -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);
}));
},
);
}
}

Wyświetl plik

@ -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(

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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),
),
],
),
),
]),
],
);

Wyświetl plik

@ -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,
);
});
}
}

Wyświetl plik

@ -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(),
),
),
],
));
}
}

Wyświetl plik

@ -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}');
}
}
}

Wyświetl plik

@ -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(),

Wyświetl plik

@ -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)),
],
)
],
),
);
}
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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,
});
}

Wyświetl plik

@ -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 ^

Wyświetl plik

@ -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';

Wyświetl plik

@ -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(

Wyświetl plik

@ -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) ...[

Wyświetl plik

@ -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'));

Wyświetl plik

@ -52,7 +52,7 @@ extension TimelineEntryFriendicaExtensions on TimelineEntry {
backdatedTimestamp: backdatedTimestamp,
locationData: actualLocationData,
body: body,
isReshare: isReshare,
youReshared: isReshare,
isPublic: isPublic,
id: id,
parentId: parentId,

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -1,6 +1,6 @@
extension StringUtils on String {
String truncate({int length = 32}) {
if (length <= length) {
if (this.length <= length) {
return this;
}

Wyświetl plik

@ -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"))

Wyświetl plik

@ -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'

Wyświetl plik

@ -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

Wyświetl plik

@ -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;

Wyświetl plik

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1420"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

Wyświetl plik

@ -10,5 +10,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

Wyświetl plik

@ -6,5 +6,7 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

Wyświetl plik

@ -10,5 +10,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

Wyświetl plik

@ -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:

Wyświetl plik

@ -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

Wyświetl plik

@ -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]),
);
});
});
}