Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
df787f02a1 | |||
db43b7dca5 | |||
59c4d667f6 | |||
063c087089 | |||
48e3b510cf | |||
77288713e1 | |||
1abc65f8fa | |||
a6b17f2c05 | |||
d8dd4060c0 | |||
c8e131c1ab | |||
f4621dd2b4 | |||
6e442c144e | |||
8bbd964026 | |||
0b8a5a3303 |
@ -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",
|
||||
@ -367,7 +367,7 @@
|
||||
"bsRegisteringPushNotify": "Enabling Push Notifications",
|
||||
"bsDismissibleErrorHint": "Click anywhere to ignore this error",
|
||||
"postShareContent": "@content\n\n@username on the Solar Network\nCheck it out: @link",
|
||||
"postShareSubject": "@username posted a post on the Solar Network",
|
||||
"postShareSubject": "@title by @username on Solar Network",
|
||||
"themeColor": "Global Theme Color",
|
||||
"themeColorRed": "Modern Red",
|
||||
"themeColorBlue": "Classic Blue",
|
||||
@ -484,5 +484,7 @@
|
||||
"authMaximumAuthStepsDesc": "The maximum number of authentication steps when logging in, higher value is more secure, lower value is more convenient; default is 2",
|
||||
"auditLog": "Audit log",
|
||||
"shareImage": "Share as image",
|
||||
"shareImageFooter": "See more interesting posts on Solar Network"
|
||||
"shareImageFooter": "Only on the Solar Network",
|
||||
"fileSavedAt": "File saved at @path",
|
||||
"showIp": "Show IP Address"
|
||||
}
|
||||
|
@ -363,7 +363,7 @@
|
||||
"bsRegisteringPushNotify": "正在启用推送通知",
|
||||
"bsDismissibleErrorHint": "点击任意地方忽略此错误",
|
||||
"postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址:@link",
|
||||
"postShareSubject": "@username 在 Solar Network 上发布了一篇帖子",
|
||||
"postShareSubject": "@username 在 Solar Network 发表的 @title",
|
||||
"themeColor": "全局主题色",
|
||||
"themeColorRed": "现代红",
|
||||
"themeColorBlue": "经典蓝",
|
||||
@ -480,5 +480,7 @@
|
||||
"authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2",
|
||||
"auditLog": "活动日志",
|
||||
"shareImage": "分享图片",
|
||||
"shareImageFooter": "上 Solar Network 看更多有趣帖子"
|
||||
"shareImageFooter": "上 Solar Network 看更多有趣帖子",
|
||||
"fileSavedAt": "文件保存于 @path",
|
||||
"showIp": "显示 IP 地址"
|
||||
}
|
||||
|
@ -38,6 +38,8 @@ PODS:
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.2.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.2.0):
|
||||
@ -308,6 +310,7 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
||||
@ -383,6 +386,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
file_saver:
|
||||
:path: ".symlinks/plugins/file_saver/ios"
|
||||
firebase_analytics:
|
||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||
firebase_core:
|
||||
@ -462,6 +467,7 @@ SPEC CHECKSUMS:
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||
firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad
|
||||
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
|
||||
|
@ -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();
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/audit_log.dart';
|
||||
@ -29,7 +30,7 @@ class _AuditLogScreenState extends State<AuditLogScreen> {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('id');
|
||||
final resp =
|
||||
await client.get('/users/me/events?take=10&offset=${_events.length}');
|
||||
await client.get('/users/me/events?take=15&offset=${_events.length}');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
}
|
||||
@ -45,6 +46,22 @@ class _AuditLogScreenState extends State<AuditLogScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
bool _showIp = false;
|
||||
|
||||
String _censorIpAddress(String ip) {
|
||||
List<String> parts = ip.split('.');
|
||||
|
||||
if (parts.length == 4) {
|
||||
String censoredPart1 = '*' * parts[1].length;
|
||||
String censoredPart2 = '*' * parts[2].length;
|
||||
String censoredPart3 = '*' * parts[3].length;
|
||||
|
||||
return '${parts[0]}.$censoredPart1.$censoredPart2.$censoredPart3';
|
||||
} else {
|
||||
return '***.***.***.***';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -53,7 +70,27 @@ class _AuditLogScreenState extends State<AuditLogScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InfiniteList(
|
||||
return Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
value: _showIp,
|
||||
title: Text('showIp'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
secondary: const Icon(Icons.alternate_email),
|
||||
tileColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.5),
|
||||
onChanged: (val) {
|
||||
setState(() => _showIp = val ?? false);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_events.clear();
|
||||
return _getEvents();
|
||||
},
|
||||
child: InfiniteList(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
itemCount: _events.length,
|
||||
isLoading: _isBusy,
|
||||
onFetchData: () {
|
||||
@ -65,6 +102,7 @@ class _AuditLogScreenState extends State<AuditLogScreen> {
|
||||
isFirst: idx == 0,
|
||||
isLast: _events.length - 1 == idx,
|
||||
alignment: TimelineAlign.start,
|
||||
indicatorStyle: IndicatorStyle(width: 15),
|
||||
endChild: Container(
|
||||
child: Card(
|
||||
child: Column(
|
||||
@ -74,6 +112,24 @@ class _AuditLogScreenState extends State<AuditLogScreen> {
|
||||
element.type,
|
||||
style: GoogleFonts.robotoMono(fontSize: 15),
|
||||
),
|
||||
Text(
|
||||
_showIp
|
||||
? element.ipAddress
|
||||
: _censorIpAddress(element.ipAddress),
|
||||
style: GoogleFonts.sourceCodePro(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: double.maxFinite,
|
||||
child: Marquee(
|
||||
text: element.userAgent,
|
||||
velocity: 25,
|
||||
startAfter: Duration(milliseconds: 500),
|
||||
pauseAfterRound: Duration(milliseconds: 3000),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
RelativeDate(element.createdAt),
|
||||
@ -89,6 +145,10 @@ class _AuditLogScreenState extends State<AuditLogScreen> {
|
||||
),
|
||||
).paddingSymmetric(horizontal: 18);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class NotificationPreferencesScreen extends StatefulWidget {
|
||||
const NotificationPreferencesScreen({super.key});
|
||||
@ -76,7 +76,7 @@ class _NotificationPreferencesScreenState
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
|
@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class AuthPreferencesScreen extends StatefulWidget {
|
||||
const AuthPreferencesScreen({super.key});
|
||||
@ -67,7 +67,7 @@ class _AuthPreferencesScreenState extends State<AuthPreferencesScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
@ -12,6 +11,7 @@ import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class PersonalizeScreen extends StatefulWidget {
|
||||
const PersonalizeScreen({super.key});
|
||||
@ -188,7 +188,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
children: [
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
@ -9,6 +8,7 @@ import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@ -132,7 +132,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (widget.edit != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_resizable_container/flutter_resizable_container.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -42,14 +43,20 @@ class ChatListShell extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RootContainer(
|
||||
child: Row(
|
||||
child: ResizableContainer(
|
||||
direction: Axis.horizontal,
|
||||
divider: ResizableDivider(
|
||||
thickness: 0.3,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 360,
|
||||
const ResizableChild(
|
||||
minSize: 280,
|
||||
maxSize: 520,
|
||||
size: ResizableSize.pixels(320),
|
||||
child: ChatList(),
|
||||
),
|
||||
const VerticalDivider(thickness: 0.3, width: 0.3),
|
||||
Expanded(child: child ?? const EmptyPagePlaceholder()),
|
||||
ResizableChild(child: child ?? const EmptyPagePlaceholder()),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -281,7 +288,7 @@ class _ChatListState extends State<ChatList> {
|
||||
return Column(
|
||||
children: [
|
||||
const ChatCallCurrentIndicator(),
|
||||
if (_isBusy) const LoadingIndicator(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
|
@ -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
|
||||
@ -62,12 +63,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
SliverToBoxAdapter(
|
||||
child: LoadingIndicator(),
|
||||
child: LoadingIndicator(isActive: _isBusy),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
key: ValueKey(_item),
|
||||
item: _item!,
|
||||
isClickable: false,
|
||||
isOverrideEmbedClickable: true,
|
||||
@ -80,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(
|
||||
|
@ -16,6 +16,7 @@ import 'package:solian/router.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/markdown_text_content.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
@ -274,7 +275,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
|
@ -135,7 +135,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_isBusy) const LoadingIndicator(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
@ -15,6 +14,7 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/auto_cache_image.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
|
||||
@ -93,7 +93,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: CenteredContainer(
|
||||
child: RefreshIndicator(
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@ -13,6 +12,7 @@ import 'package:solian/router.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/root_container.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@ -208,7 +208,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (widget.edit != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
|
@ -21,6 +21,7 @@ import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_attr_editor.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_editor_thumbnail.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class AttachmentEditorPopup extends StatefulWidget {
|
||||
final String pool;
|
||||
@ -660,7 +661,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
|
@ -49,6 +49,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
bool _isLoading = true;
|
||||
bool _showMature = false;
|
||||
|
||||
// ignore: unused_field
|
||||
double _aspectRatio = 1;
|
||||
|
||||
List<Attachment?> _attachments = List.empty();
|
||||
|
@ -38,12 +38,14 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
|
||||
Future<void> _loadLastMessages() async {
|
||||
final messages = await _eventController.src.getLastInAllChannels();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_lastMessages = messages
|
||||
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||
.cast<int, LocalMessageEventTableData>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
@ -8,6 +7,7 @@ import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||
import 'package:solian/widgets/account/relative_select.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class ChannelMemberListPopup extends StatefulWidget {
|
||||
final Channel channel;
|
||||
@ -131,7 +131,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
'channelMembers'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
@ -7,6 +6,7 @@ import 'package:solian/models/event.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/chat/chat_event_deletion.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class ChatEventAction extends StatefulWidget {
|
||||
final Channel channel;
|
||||
@ -73,7 +73,7 @@ class _ChatEventActionState extends State<ChatEventAction> {
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
@ -1,15 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
|
||||
class LoadingIndicator extends StatelessWidget {
|
||||
const LoadingIndicator({super.key});
|
||||
class LoadingIndicator extends StatefulWidget {
|
||||
final bool isActive;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const LoadingIndicator({
|
||||
super.key,
|
||||
this.isActive = true,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LoadingIndicator> createState() => _LoadingIndicatorState();
|
||||
}
|
||||
|
||||
class _LoadingIndicatorState extends State<LoadingIndicator>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
_animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
if (widget.isActive) {
|
||||
_controller.forward();
|
||||
} else {
|
||||
_controller.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LoadingIndicator oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.isActive != oldWidget.isActive) {
|
||||
if (widget.isActive) {
|
||||
_controller.forward();
|
||||
} else {
|
||||
_controller.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return SizeTransition(
|
||||
sizeFactor: _animation,
|
||||
axisAlignment: -1, // Align animation from the top
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),
|
||||
color: widget.backgroundColor ??
|
||||
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@ -20,9 +80,10 @@ class LoadingIndicator extends StatelessWidget {
|
||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||
),
|
||||
const Gap(8),
|
||||
Text('loading'.tr)
|
||||
Text('loading'.tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
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';
|
||||
import 'package:solian/widgets/posts/post_share.dart';
|
||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||
|
||||
@ -29,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 {
|
||||
@ -73,7 +67,8 @@ class _PostActionState extends State<PostAction> {
|
||||
'link': 'https://solsynth.dev/posts/$id',
|
||||
}),
|
||||
subject: 'postShareSubject'.trParams({
|
||||
'username': widget.item.author.nick,
|
||||
'username': '@${widget.item.author.name}',
|
||||
'title': widget.item.body['title'] ?? '#${widget.item.id}',
|
||||
}),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
);
|
||||
@ -89,21 +84,71 @@ class _PostActionState extends State<PostAction> {
|
||||
}
|
||||
|
||||
Future<void> _shareImage() async {
|
||||
final List<String> attachments = widget.item.body['attachments'] is List
|
||||
? List.from(widget.item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
final hasMultipleAttachment = attachments.length > 1;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final screenshot = ScreenshotController();
|
||||
final image = await screenshot.captureFromWidget(
|
||||
PostShareImage(item: widget.item),
|
||||
final image = await screenshot.captureFromLongWidget(
|
||||
MediaQuery(
|
||||
data: MediaQuery.of(context),
|
||||
child: PostShareImage(item: widget.item),
|
||||
),
|
||||
context: context,
|
||||
pixelRatio: 2,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 480,
|
||||
maxWidth: hasMultipleAttachment ? 640 : 480,
|
||||
minHeight: 640,
|
||||
maxHeight: double.infinity,
|
||||
),
|
||||
);
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final imageFile = await File(
|
||||
'${directory.path}/temporary_share_image.png',
|
||||
).create();
|
||||
await imageFile.writeAsBytes(image);
|
||||
|
||||
final file = XFile(imageFile.path);
|
||||
await Share.shareXFiles([file]);
|
||||
final filename = 'share_post#${widget.item.id}';
|
||||
|
||||
await imageFile.delete();
|
||||
if (PlatformInfo.isAndroid || PlatformInfo.isIOS) {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
|
||||
final file = XFile.fromData(
|
||||
image,
|
||||
mimeType: 'image/png',
|
||||
name: filename,
|
||||
);
|
||||
await Share.shareXFiles(
|
||||
[file],
|
||||
subject: 'postShareSubject'.trParams({
|
||||
'username': '@${widget.item.author.name}',
|
||||
'title': widget.item.body['title'] ?? '#${widget.item.id}',
|
||||
}),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
);
|
||||
} else {
|
||||
final filepath = await FileSaver.instance.saveFile(
|
||||
name: filename,
|
||||
ext: 'png',
|
||||
mimeType: MimeType.png,
|
||||
bytes: image,
|
||||
);
|
||||
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
|
||||
@ -149,7 +194,13 @@ class _PostActionState extends State<PostAction> {
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(
|
||||
isActive: _isBusy,
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
@ -169,11 +220,12 @@ class _PostActionState extends State<PostAction> {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isAndroid)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.image),
|
||||
tooltip: 'shareImage'.tr,
|
||||
onPressed: () async {
|
||||
onPressed: _isBusy
|
||||
? null
|
||||
: () async {
|
||||
await _shareImage();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
@ -256,14 +308,22 @@ class _PostActionState extends State<PostAction> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.edit),
|
||||
title: Text('edit'.tr),
|
||||
onTap: () async {
|
||||
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: widget.item),
|
||||
extra: PostPublishArguments(edit: item),
|
||||
),
|
||||
);
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
},
|
||||
),
|
||||
if (_canModifyContent)
|
||||
|
@ -31,12 +31,14 @@ class PostItem extends StatefulWidget {
|
||||
final bool isOverrideEmbedClickable;
|
||||
final bool isFullDate;
|
||||
final bool isContentSelectable;
|
||||
final bool isNonScrollAttachment;
|
||||
final bool showFeaturedReply;
|
||||
final String? attachmentParent;
|
||||
|
||||
final EdgeInsets? padding;
|
||||
|
||||
final Function? onComment;
|
||||
final Function? onTapMore;
|
||||
|
||||
const PostItem({
|
||||
super.key,
|
||||
@ -49,10 +51,12 @@ class PostItem extends StatefulWidget {
|
||||
this.isOverrideEmbedClickable = false,
|
||||
this.isFullDate = false,
|
||||
this.isContentSelectable = false,
|
||||
this.isNonScrollAttachment = false,
|
||||
this.showFeaturedReply = false,
|
||||
this.attachmentParent,
|
||||
this.padding,
|
||||
this.onComment,
|
||||
this.onTapMore,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -97,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),
|
||||
@ -159,6 +164,7 @@ class _PostItemState extends State<PostItem> {
|
||||
_PostHeaderWidget(
|
||||
isCompact: widget.isCompact,
|
||||
isFullDate: widget.isFullDate,
|
||||
onTapMore: widget.onTapMore,
|
||||
item: item,
|
||||
),
|
||||
_PostHeaderDividerWidget(item: item),
|
||||
@ -214,6 +220,7 @@ class _PostItemState extends State<PostItem> {
|
||||
_PostAttachmentWidget(
|
||||
item: item,
|
||||
padding: widget.padding,
|
||||
isNonScrollAttachment: widget.isNonScrollAttachment,
|
||||
),
|
||||
if (widget.showFeaturedReply)
|
||||
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
|
||||
@ -380,8 +387,13 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
class _PostAttachmentWidget extends StatelessWidget {
|
||||
final Post item;
|
||||
final EdgeInsets? padding;
|
||||
final bool isNonScrollAttachment;
|
||||
|
||||
const _PostAttachmentWidget({required this.item, required this.padding});
|
||||
const _PostAttachmentWidget({
|
||||
required this.item,
|
||||
required this.padding,
|
||||
required this.isNonScrollAttachment,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -401,14 +413,6 @@ class _PostAttachmentWidget extends StatelessWidget {
|
||||
autoload: false,
|
||||
isFullWidth: true,
|
||||
);
|
||||
} else if (attachments.length == 1) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isColumn: true,
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
} else if (attachments.length > 1 &&
|
||||
attachments.length % 3 == 0 &&
|
||||
!isLargeScreen) {
|
||||
@ -419,6 +423,14 @@ class _PostAttachmentWidget extends StatelessWidget {
|
||||
autoload: false,
|
||||
isGrid: true,
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
} else if (attachments.length == 1 || isNonScrollAttachment) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isColumn: true,
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
} else {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
@ -580,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
|
||||
@ -641,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),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
@ -73,9 +74,30 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
void _openActions(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => PostAction(item: item),
|
||||
).then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
onUpdate();
|
||||
});
|
||||
} else if (value != null) {
|
||||
onUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
return TapRegion(
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _openActions(context),
|
||||
child: PostItem(
|
||||
key: Key('p${item.id}'),
|
||||
item: item,
|
||||
@ -83,6 +105,7 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
isClickable: isNestedClickable,
|
||||
showFeaturedReply: showFeaturedReply,
|
||||
padding: padding,
|
||||
onTapMore: () => _openActions(context),
|
||||
onComment: () {
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
@ -100,23 +123,11 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
});
|
||||
},
|
||||
).paddingSymmetric(vertical: 8),
|
||||
onLongPress: () {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => PostAction(item: item),
|
||||
).then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
onUpdate();
|
||||
});
|
||||
} else if (value != null) {
|
||||
onUpdate();
|
||||
),
|
||||
onTapInside: (event) {
|
||||
if (event.buttons == kSecondaryMouseButton) {
|
||||
_openActions(context);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -15,14 +15,15 @@ class PostShareImage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.3);
|
||||
return RootContainer(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
runAlignment: WrapAlignment.center,
|
||||
children: [
|
||||
const Gap(24),
|
||||
const SizedBox(height: 40),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: PostItem(
|
||||
item: item,
|
||||
isShowEmbed: true,
|
||||
@ -30,11 +31,15 @@ class PostShareImage extends StatelessWidget {
|
||||
showFeaturedReply: false,
|
||||
isReactable: false,
|
||||
isShowReply: false,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
isNonScrollAttachment: true,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 16,
|
||||
),
|
||||
onComment: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
).paddingOnly(bottom: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@ -80,6 +85,12 @@ class PostShareImage extends StatelessWidget {
|
||||
version: QrVersions.auto,
|
||||
padding: const EdgeInsets.all(4),
|
||||
size: 48,
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
eyeStyle: QrEyeStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
@ -8,6 +7,7 @@ import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||
import 'package:solian/widgets/account/relative_select.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class RealmMemberListPopup extends StatefulWidget {
|
||||
final Realm realm;
|
||||
@ -128,7 +128,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
||||
'realmMembers'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
@ -22,6 +23,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
|
||||
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
|
||||
g_autoptr(FlPluginRegistrar) file_saver_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
|
||||
file_saver_plugin_register_with_registrar(file_saver_registrar);
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
file_saver
|
||||
file_selector_linux
|
||||
flutter_acrylic
|
||||
flutter_secure_storage_linux
|
||||
|
@ -8,6 +8,7 @@ import Foundation
|
||||
import connectivity_plus
|
||||
import desktop_drop
|
||||
import device_info_plus
|
||||
import file_saver
|
||||
import file_selector_macos
|
||||
import firebase_analytics
|
||||
import firebase_core
|
||||
@ -39,6 +40,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
|
@ -6,6 +6,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_saver (0.0.1):
|
||||
- FlutterMacOS
|
||||
- file_selector_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Firebase/Analytics (11.2.0):
|
||||
@ -230,6 +232,7 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
- firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
|
||||
- firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
|
||||
@ -288,6 +291,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
|
||||
device_info_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
|
||||
file_saver:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
|
||||
file_selector_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
|
||||
firebase_analytics:
|
||||
@ -349,6 +354,7 @@ SPEC CHECKSUMS:
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
|
||||
device_info_plus: f1aae8670672f75c4c8850ecbe0b2ddef62b0a22
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
|
||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||
firebase_analytics: 30ff72f6d4847ff0b479d8edd92fc8582e719072
|
||||
|
@ -14,6 +14,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
|
@ -62,5 +62,9 @@
|
||||
<string>Allow you record audio for your message or post</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow you add photo to your message or post</string>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -12,6 +12,8 @@
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.downloads.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
|
36
pubspec.lock
36
pubspec.lock
@ -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:
|
||||
@ -414,6 +414,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
fading_edge_scrollview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fading_edge_scrollview
|
||||
sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -454,6 +462,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.2"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_saver
|
||||
sha256: "017a127de686af2d2fbbd64afea97052d95f2a0f87d19d25b87e097407bf9c1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.14"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -803,6 +819,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.23"
|
||||
flutter_resizable_container:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_resizable_container
|
||||
sha256: "5b15c79c6cc338ed79640c706bb5176baa3333d92fd3627ad279aa3e25d2f0e7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1261,6 +1285,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
marquee:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: marquee
|
||||
sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.3.7+10
|
||||
version: 1.3.8+13
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@ -87,6 +87,9 @@ dependencies:
|
||||
timeline_tile: ^2.0.0
|
||||
screenshot: ^3.0.0
|
||||
qr_flutter: ^4.1.0
|
||||
flutter_resizable_container: ^3.0.0
|
||||
file_saver: ^0.2.14
|
||||
marquee: ^2.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -111,14 +111,13 @@
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body oncontextmenu="return false;">
|
||||
<picture id="splash">
|
||||
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)">
|
||||
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
||||
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
||||
</picture>
|
||||
|
||||
|
||||
<script src="flutter_bootstrap.js" async=""></script>
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <file_saver/file_saver_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <flutter_acrylic/flutter_acrylic_plugin.h>
|
||||
@ -31,6 +32,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
DesktopDropPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||
FileSaverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSaverPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
connectivity_plus
|
||||
desktop_drop
|
||||
file_saver
|
||||
file_selector_windows
|
||||
firebase_core
|
||||
flutter_acrylic
|
||||
|
Reference in New Issue
Block a user