Compare commits

..

5 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
9 changed files with 153 additions and 160 deletions

View File

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

View File

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

View File

@@ -47,16 +47,19 @@ class ChatListShell extends StatelessWidget {
direction: Axis.horizontal, direction: Axis.horizontal,
divider: ResizableDivider( divider: ResizableDivider(
thickness: 0.3, thickness: 0.3,
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor.withOpacity(0.3),
), ),
children: [ children: [
const ResizableChild( const ResizableChild(
minSize: 280, minSize: 280,
maxSize: 520, maxSize: 520,
size: ResizableSize.pixels(320), size: ResizableSize.pixels(360),
child: ChatList(), 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:flutter/material.dart';
import 'package:get/get.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/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.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_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:solian/widgets/root_container.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class DraftBoxScreen extends StatefulWidget { class DraftBoxScreen extends StatefulWidget {
const DraftBoxScreen({super.key}); const DraftBoxScreen({super.key});
@@ -19,38 +20,50 @@ class DraftBoxScreen extends StatefulWidget {
} }
class _DraftBoxScreenState extends State<DraftBoxScreen> { class _DraftBoxScreenState extends State<DraftBoxScreen> {
final PagingController<int, Post> _pagingController = bool _isBusy = true;
PagingController(firstPageKey: 0); int? _totalPosts;
final List<Post> _posts = List.empty(growable: true);
_getPosts(int pageKey) async { _getPosts() async {
final PostProvider provider = Get.find(); setState(() => _isBusy = true);
Response resp; final PostProvider posts = Get.find();
try { final resp = await posts.listDraft(_posts.length);
resp = await provider.listDraft(pageKey);
} catch (e) {
_pagingController.error = e;
return;
}
final PaginationResult result = PaginationResult.fromJson(resp.body); final PaginationResult result = PaginationResult.fromJson(resp.body);
if (result.count == 0) {
_pagingController.appendLastPage([]);
return;
}
final parsed = result.data?.map((e) => Post.fromJson(e)).toList(); final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) { _totalPosts = result.count;
_pagingController.appendPage(parsed, pageKey + parsed.length); _posts.addAll(parsed ?? List.empty());
} else if (parsed != null) {
_pagingController.appendLastPage(parsed); 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 @override
void initState() { void initState() {
super.initState(); super.initState();
_pagingController.addPageRequestListener(_getPosts); _getPosts();
} }
@override @override
@@ -68,47 +81,48 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
), ),
], ],
), ),
body: RefreshIndicator( body: Column(
onRefresh: () => Future.sync(() => _pagingController.refresh()), children: [
child: PagedListView<int, Post>( LoadingIndicator(isActive: _isBusy),
pagingController: _pagingController, Expanded(
builderDelegate: PagedChildBuilderDelegate( child: RefreshIndicator(
itemBuilder: (context, item, index) { onRefresh: () {
return PostOwnedListEntry( _posts.clear();
item: item, return _getPosts();
isFullContent: true, },
backgroundColor: child: InfiniteList(
Theme.of(context).colorScheme.surfaceContainerLow, itemCount: _posts.length,
onTap: () async { hasReachedMax: _totalPosts == _posts.length,
showModalBottomSheet( isLoading: _isBusy,
useRootNavigator: true, onFetchData: () => _getPosts(),
context: context, itemBuilder: (context, index) {
builder: (context) => PostAction( final item = _posts[index];
item: item, return Card(
noReact: true, 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) { ).paddingOnly(left: 12, right: 12, bottom: 4);
if (value is Future) {
value.then((_) {
_pagingController.refresh();
});
} else if (value != null) {
_pagingController.refresh();
}
});
}, },
).paddingOnly(left: 12, right: 12, bottom: 4); ),
}, ),
), ),
), ],
), ),
), ),
); );
} }
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
} }

View File

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

@@ -91,17 +91,21 @@ class _PostActionState extends State<PostAction> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final double width = hasMultipleAttachment ? 640 : 480;
final screenshot = ScreenshotController(); final screenshot = ScreenshotController();
final image = await screenshot.captureFromLongWidget( final image = await screenshot.captureFromLongWidget(
MediaQuery( MediaQuery(
data: MediaQuery.of(context), data: MediaQuery.of(context).copyWith(
size: Size(width, double.infinity),
),
child: PostShareImage(item: widget.item), child: PostShareImage(item: widget.item),
), ),
context: context, context: context,
pixelRatio: 2, pixelRatio: 2,
constraints: BoxConstraints( constraints: BoxConstraints(
minWidth: 480, minWidth: 480,
maxWidth: hasMultipleAttachment ? 640 : 480, maxWidth: width,
minHeight: 640, minHeight: 640,
maxHeight: double.infinity, maxHeight: double.infinity,
), ),

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 dependency: transitive
description: description:
name: args name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.0" version: "2.6.0"
async: async:
dependency: "direct main" dependency: "direct main"
description: description:

View File

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