Compare commits

..

7 Commits

Author SHA1 Message Date
d22eac5c10 🚀 Launch 1.4.0+14 2024-10-16 00:02:36 +08:00
e5381dd5e0 Support more mouse related actions 2024-10-16 00:02:18 +08:00
1c26944a05 🐛 Fix draft box 2024-10-15 21:14:56 +08:00
df787f02a1 🚀 Launch 1.3.8+13 2024-10-14 23:48:15 +08:00
db43b7dca5 💄 Better auto save 2024-10-14 23:08:53 +08:00
59c4d667f6 🐛 Bug fixes on edit content truncated post 2024-10-14 23:04:55 +08:00
063c087089 Post show more button 2024-10-14 22:58:37 +08:00
13 changed files with 245 additions and 189 deletions

View File

@@ -140,7 +140,7 @@
"clear": "Clear",
"pinPost": "Pin this post",
"unpinPost": "Unpin this post",
"postRestoreFromLocal": "Restore from local",
"postRestoreFromLocal": "Restored",
"postAutoSaveAt": "Auto saved at @date",
"postCategoriesAndTags": "Categories n' Tags",
"postPublishDate": "Publish Date",

View File

@@ -43,14 +43,17 @@ class PostEditorController extends GetxController {
RxBool isRestoreFromLocal = false.obs;
Rx<DateTime?> lastSaveTime = Rx(null);
Timer? _saveTimer;
Future? _saveFuture;
PostEditorController() {
SharedPreferences.getInstance().then((inst) {
_prefs = inst;
_saveTimer = Timer.periodic(
const Duration(seconds: 3),
(Timer t) {
});
contentController.addListener(() {
contentLength.value = contentController.text.length;
_saveFuture ??= Future.delayed(
const Duration(seconds: 1),
() {
if (isNotEmpty) {
localSave();
lastSaveTime.value = DateTime.now();
@@ -59,12 +62,10 @@ class PostEditorController extends GetxController {
localClear();
lastSaveTime.value = null;
}
_saveFuture = null;
},
);
});
contentController.addListener(() {
contentLength.value = contentController.text.length;
});
}
Future<void> editOverview(BuildContext context) {
@@ -355,8 +356,6 @@ class PostEditorController extends GetxController {
@override
void dispose() {
_saveTimer?.cancel();
titleController.dispose();
descriptionController.dispose();
contentController.dispose();

View File

@@ -47,16 +47,19 @@ class ChatListShell extends StatelessWidget {
direction: Axis.horizontal,
divider: ResizableDivider(
thickness: 0.3,
color: Theme.of(context).dividerColor,
color: Theme.of(context).dividerColor.withOpacity(0.3),
),
children: [
const ResizableChild(
minSize: 280,
maxSize: 520,
size: ResizableSize.pixels(320),
size: ResizableSize.pixels(360),
child: ChatList(),
),
ResizableChild(child: child ?? const EmptyPagePlaceholder()),
ResizableChild(
minSize: 280,
child: child ?? const EmptyPagePlaceholder(),
),
],
),
);

View File

@@ -1,15 +1,16 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_owned_list.dart';
import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class DraftBoxScreen extends StatefulWidget {
const DraftBoxScreen({super.key});
@@ -19,38 +20,50 @@ class DraftBoxScreen extends StatefulWidget {
}
class _DraftBoxScreenState extends State<DraftBoxScreen> {
final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
bool _isBusy = true;
int? _totalPosts;
final List<Post> _posts = List.empty(growable: true);
_getPosts(int pageKey) async {
final PostProvider provider = Get.find();
_getPosts() async {
setState(() => _isBusy = true);
Response resp;
try {
resp = await provider.listDraft(pageKey);
} catch (e) {
_pagingController.error = e;
return;
}
final PostProvider posts = Get.find();
final resp = await posts.listDraft(_posts.length);
final PaginationResult result = PaginationResult.fromJson(resp.body);
if (result.count == 0) {
_pagingController.appendLastPage([]);
return;
}
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) {
_pagingController.appendPage(parsed, pageKey + parsed.length);
} else if (parsed != null) {
_pagingController.appendLastPage(parsed);
}
_totalPosts = result.count;
_posts.addAll(parsed ?? List.empty());
setState(() => _isBusy = false);
}
Future<void> _openActions(Post item) async {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(
item: item,
noReact: true,
),
).then((value) {
if (value is Future) {
value.then((_) {
_posts.clear();
_getPosts();
});
} else if (value != null) {
_posts.clear();
_getPosts();
}
});
}
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(_getPosts);
_getPosts();
}
@override
@@ -68,47 +81,48 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
),
],
),
body: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: PagedListView<int, Post>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (context, item, index) {
return PostOwnedListEntry(
item: item,
isFullContent: true,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
onTap: () async {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(
item: item,
noReact: true,
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Expanded(
child: RefreshIndicator(
onRefresh: () {
_posts.clear();
return _getPosts();
},
child: InfiniteList(
itemCount: _posts.length,
hasReachedMax: _totalPosts == _posts.length,
isLoading: _isBusy,
onFetchData: () => _getPosts(),
itemBuilder: (context, index) {
final item = _posts[index];
return Card(
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PostItem(
key: Key('p${item.id}'),
item: item,
isShowEmbed: false,
isClickable: false,
isShowReply: false,
isReactable: false,
onTapMore: () => _openActions(item),
).paddingSymmetric(vertical: 8),
],
),
onTap: () => _openActions(item),
),
).then((value) {
if (value is Future) {
value.then((_) {
_pagingController.refresh();
});
} else if (value != null) {
_pagingController.refresh();
}
});
).paddingOnly(left: 12, right: 12, bottom: 4);
},
).paddingOnly(left: 12, right: 12, bottom: 4);
},
),
),
),
),
],
),
),
);
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}

View File

@@ -6,6 +6,7 @@ import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/last_read.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/posts/post_replies.dart';
@@ -40,7 +41,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
Get.find<LastReadProvider>().feedLastReadAt = _item?.id;
setState(() => _isBusy = false);
if (mounted) setState(() => _isBusy = false);
}
@override
@@ -67,6 +68,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
),
SliverToBoxAdapter(
child: PostItem(
key: ValueKey(_item),
item: _item!,
isClickable: false,
isOverrideEmbedClickable: true,
@@ -79,6 +81,24 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
vertical: 8,
)
: EdgeInsets.zero,
onTapMore: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(
item: _item!,
noReact: true,
),
).then((value) {
if (value is Future) {
value.then((_) {
_getDetail();
});
} else if (value != null) {
_getDetail();
}
});
},
),
),
SliverToBoxAdapter(

View File

@@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
@@ -34,6 +35,24 @@ class ChatEventList extends StatelessWidget {
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
}
void _openActions(BuildContext context, Event item) {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => ChatEventAction(
channel: channel,
realm: channel.realm,
item: item,
onEdit: () {
onEdit(item);
},
onReply: () {
onReply(item);
},
),
);
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
@@ -65,50 +84,45 @@ class ChatEventList extends StatelessWidget {
final item = chatController.currentEvents[index].data;
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Builder(builder: (context) {
final widget = ChatEvent(
key: Key('m${item!.uuid}'),
item: item,
isMerged: isMerged,
chatController: chatController,
).paddingOnly(
top: !isMerged ? 8 : 0,
bottom: !hasMerged ? 8 : 0,
);
return TapRegion(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Builder(builder: (context) {
final widget = ChatEvent(
key: Key('m${item!.uuid}'),
item: item,
isMerged: isMerged,
chatController: chatController,
).paddingOnly(
top: !isMerged ? 8 : 0,
bottom: !hasMerged ? 8 : 0,
);
if (noAnimated) {
return widget;
} else {
return widget
.animate(
key: Key('animated-m${item.uuid}'),
)
.slideY(
curve: Curves.fastLinearToSlowEaseIn,
duration: 250.ms,
begin: 0.5,
end: 0,
);
if (noAnimated) {
return widget;
} else {
return widget
.animate(
key: Key('animated-m${item.uuid}'),
)
.slideY(
curve: Curves.fastLinearToSlowEaseIn,
duration: 250.ms,
begin: 0.5,
end: 0,
);
}
}),
onLongPress: () {
_openActions(context, item!);
},
),
onTapInside: (event) {
if (event.buttons == kSecondaryMouseButton) {
_openActions(context, item!);
} else if (event.buttons == kMiddleMouseButton) {
onReply(item!);
}
}),
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => ChatEventAction(
channel: channel,
realm: channel.realm,
item: item!,
onEdit: () {
onEdit(item);
},
onReply: () {
onReply(item);
},
),
);
},
);
},

View File

@@ -4,8 +4,13 @@ import 'package:gap/gap.dart';
class LoadingIndicator extends StatefulWidget {
final bool isActive;
final Color? backgroundColor;
const LoadingIndicator({super.key, this.isActive = true});
const LoadingIndicator({
super.key,
this.isActive = true,
this.backgroundColor,
});
@override
State<LoadingIndicator> createState() => _LoadingIndicatorState();
@@ -63,7 +68,7 @@ class _LoadingIndicatorState extends State<LoadingIndicator>
axisAlignment: -1, // Align animation from the top
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
color:
color: widget.backgroundColor ??
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -11,6 +11,7 @@ import 'package:solian/exts.dart';
import 'package:solian/models/post.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/widgets/loading_indicator.dart';
@@ -28,20 +29,14 @@ class PostAction extends StatefulWidget {
}
class _PostActionState extends State<PostAction> {
bool _isBusy = true;
bool _isBusy = false;
bool _canModifyContent = false;
void _checkAbleToModifyContent() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
setState(() => _isBusy = true);
setState(() {
_canModifyContent =
auth.userProfile.value!['id'] == widget.item.author.id;
_isBusy = false;
});
_canModifyContent = auth.userProfile.value!['id'] == widget.item.author.id;
}
Future<void> _doShare({bool noUri = false}) async {
@@ -94,17 +89,23 @@ class _PostActionState extends State<PostAction> {
: List.empty();
final hasMultipleAttachment = attachments.length > 1;
setState(() => _isBusy = true);
final double width = hasMultipleAttachment ? 640 : 480;
final screenshot = ScreenshotController();
final image = await screenshot.captureFromLongWidget(
MediaQuery(
data: MediaQuery.of(context),
data: MediaQuery.of(context).copyWith(
size: Size(width, double.infinity),
),
child: PostShareImage(item: widget.item),
),
context: context,
pixelRatio: 2,
constraints: BoxConstraints(
minWidth: 480,
maxWidth: hasMultipleAttachment ? 640 : 480,
maxWidth: width,
minHeight: 640,
maxHeight: double.infinity,
),
@@ -137,6 +138,21 @@ class _PostActionState extends State<PostAction> {
);
context.showSnackbar('fileSavedAt'.trParams({'path': filepath}));
}
setState(() => _isBusy = false);
}
Future<Post> _getFullPost() async {
final PostProvider posts = Get.find();
try {
final resp = await posts.getPost(widget.item.id.toString());
return Post.fromJson(resp.body);
} catch (e) {
context.showErrorDialog(e).then((_) => Navigator.pop(context));
}
return widget.item;
}
@override
@@ -182,7 +198,13 @@ class _PostActionState extends State<PostAction> {
),
],
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
LoadingIndicator(isActive: _isBusy),
LoadingIndicator(
isActive: _isBusy,
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerHigh
.withOpacity(0.5),
),
Expanded(
child: ListView(
children: [
@@ -205,10 +227,12 @@ class _PostActionState extends State<PostAction> {
IconButton(
icon: const Icon(Icons.image),
tooltip: 'shareImage'.tr,
onPressed: () async {
await _shareImage();
Navigator.pop(context);
},
onPressed: _isBusy
? null
: () async {
await _shareImage();
Navigator.pop(context);
},
),
],
),
@@ -288,15 +312,23 @@ class _PostActionState extends State<PostAction> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.edit),
title: Text('edit'.tr),
onTap: () async {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
extra: PostPublishArguments(edit: widget.item),
),
);
},
onTap: _isBusy
? null
: () async {
setState(() => _isBusy = true);
var item = widget.item;
if (item.body?['content_truncated'] == true) {
item = await _getFullPost();
}
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
extra: PostPublishArguments(edit: item),
),
);
if (mounted) setState(() => _isBusy = false);
},
),
if (_canModifyContent)
ListTile(

View File

@@ -38,6 +38,7 @@ class PostItem extends StatefulWidget {
final EdgeInsets? padding;
final Function? onComment;
final Function? onTapMore;
const PostItem({
super.key,
@@ -55,6 +56,7 @@ class PostItem extends StatefulWidget {
this.attachmentParent,
this.padding,
this.onComment,
this.onTapMore,
});
@override
@@ -99,6 +101,7 @@ class _PostItemState extends State<PostItem> {
_PostHeaderWidget(
isCompact: widget.isCompact,
isFullDate: widget.isFullDate,
onTapMore: widget.onTapMore,
item: item,
).paddingSymmetric(horizontal: 12),
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
@@ -161,6 +164,7 @@ class _PostItemState extends State<PostItem> {
_PostHeaderWidget(
isCompact: widget.isCompact,
isFullDate: widget.isFullDate,
onTapMore: widget.onTapMore,
item: item,
),
_PostHeaderDividerWidget(item: item),
@@ -588,10 +592,13 @@ class _PostHeaderWidget extends StatelessWidget {
final bool isFullDate;
final Post item;
final Function? onTapMore;
const _PostHeaderWidget({
required this.isCompact,
required this.isFullDate,
required this.item,
required this.onTapMore,
});
@override
@@ -649,10 +656,12 @@ class _PostHeaderWidget extends StatelessWidget {
],
),
),
if (item.type == 'article')
Badge(
label: Text('article'.tr),
).paddingOnly(top: 3),
if (onTapMore != null)
IconButton(
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.more_vert),
onPressed: () => onTapMore!(),
),
],
),
const Gap(8),

View File

@@ -105,6 +105,7 @@ class PostListEntryWidget extends StatelessWidget {
isClickable: isNestedClickable,
showFeaturedReply: showFeaturedReply,
padding: padding,
onTapMore: () => _openActions(context),
onComment: () {
AppRouter.instance
.pushNamed(

View File

@@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/post.dart';
import 'package:solian/widgets/posts/post_item.dart';
class PostOwnedListEntry extends StatelessWidget {
final Post item;
final Function onTap;
final bool isFullContent;
final Color? backgroundColor;
const PostOwnedListEntry({
super.key,
required this.item,
required this.onTap,
this.isFullContent = false,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return Card(
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PostItem(
key: Key('p${item.id}'),
item: item,
isShowEmbed: false,
isClickable: false,
isShowReply: false,
isReactable: false,
).paddingSymmetric(vertical: 8),
],
),
onTap: () => onTap(),
),
);
}
}

View File

@@ -74,10 +74,10 @@ packages:
dependency: transitive
description:
name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.6.0"
async:
dependency: "direct main"
description:

View File

@@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App"
publish_to: "none"
version: 1.3.8+13
version: 1.4.0+14
environment:
sdk: ">=3.3.4 <4.0.0"