Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc1071d86e | |||
e334b862df | |||
32c33a963a | |||
a04bfe4cf9 | |||
7b7988e6cb |
@ -477,5 +477,9 @@
|
||||
"agedTheme": "Old school style theme",
|
||||
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
|
||||
"appBackgroundImage": "Global background image",
|
||||
"appBackgroundImageDesc": "The global background image will be displayed on all pages"
|
||||
"appBackgroundImageDesc": "The global background image will be displayed on all pages",
|
||||
"authPreferences": "Auth preferences",
|
||||
"authPreferencesDesc": "Set the security behavior of your account",
|
||||
"authMaximumAuthSteps": "Maximum authentication steps",
|
||||
"authMaximumAuthStepsDesc": "The maximum number of authentication steps when logging in, higher value is more secure, lower value is more convenient; default is 2"
|
||||
}
|
||||
|
@ -473,5 +473,9 @@
|
||||
"agedTheme": "过时主题",
|
||||
"agedThemeDesc": "将全局主题降级为 Material Design 2,可能发生意料之外的问题,仅供实验使用",
|
||||
"appBackgroundImage": "全局背景图片",
|
||||
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示"
|
||||
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示",
|
||||
"authPreferences": "安全偏好设置",
|
||||
"authPreferencesDesc": "调整账号的安全行为模式",
|
||||
"authMaximumAuthSteps": "最大认证步数",
|
||||
"authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2"
|
||||
}
|
||||
|
@ -269,7 +269,7 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- "sqlite3 (3.46.1+1)":
|
||||
@ -334,7 +334,7 @@ DEPENDENCIES:
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
@ -437,8 +437,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
sqlite3_flutter_libs:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
||||
url_launcher_ios:
|
||||
@ -505,7 +505,7 @@ SPEC CHECKSUMS:
|
||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
|
@ -83,7 +83,6 @@
|
||||
</array>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>zh_CN</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
@ -165,11 +166,13 @@ class WebSocketProvider extends GetxController {
|
||||
|
||||
late final String? token;
|
||||
late final String provider;
|
||||
final deviceUuid = await _getDeviceUuid();
|
||||
var deviceUuid = await _getDeviceUuid();
|
||||
|
||||
if (deviceUuid == null || deviceUuid.isEmpty) {
|
||||
log("Unable to active push notifications, couldn't get device uuid");
|
||||
return;
|
||||
} else {
|
||||
deviceUuid = md5.convert(utf8.encode(deviceUuid)).toString();
|
||||
log('Device UUID is $deviceUuid');
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import 'package:solian/screens/about.dart';
|
||||
import 'package:solian/screens/account.dart';
|
||||
import 'package:solian/screens/account/friend.dart';
|
||||
import 'package:solian/screens/account/preferences/notifications.dart';
|
||||
import 'package:solian/screens/account/preferences/security.dart';
|
||||
import 'package:solian/screens/account/profile_edit.dart';
|
||||
import 'package:solian/screens/account/profile_page.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
@ -264,6 +265,14 @@ abstract class AppRouter {
|
||||
child: const NotificationPreferencesScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/preferences/auth',
|
||||
name: 'authPreferences',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const AuthPreferencesScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/view/:name',
|
||||
name: 'accountProfilePage',
|
||||
|
@ -129,6 +129,15 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
},
|
||||
),
|
||||
if (auth.isAuthorized.value)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.lock),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
title: Text('authPreferences'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('authPreferences');
|
||||
},
|
||||
),
|
||||
if (auth.isAuthorized.value)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
|
@ -59,9 +59,9 @@ class _NotificationPreferencesScreenState
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
}
|
||||
|
||||
} else {
|
||||
context.showSnackbar('preferencesApplied'.tr);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
118
lib/screens/account/preferences/security.dart
Normal file
118
lib/screens/account/preferences/security.dart
Normal file
@ -0,0 +1,118 @@
|
||||
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';
|
||||
|
||||
class AuthPreferencesScreen extends StatefulWidget {
|
||||
const AuthPreferencesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AuthPreferencesScreen> createState() => _AuthPreferencesScreenState();
|
||||
}
|
||||
|
||||
class _AuthPreferencesScreenState extends State<AuthPreferencesScreen> {
|
||||
bool _isBusy = true;
|
||||
|
||||
Map<String, dynamic> _config = {
|
||||
'maximum_auth_steps': 2,
|
||||
};
|
||||
|
||||
Future<void> _getPreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('id');
|
||||
final resp = await client.get('/preferences/auth');
|
||||
if (resp.statusCode != 200 && resp.statusCode != 404) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
}
|
||||
|
||||
if (resp.statusCode == 200) {
|
||||
_config = resp.body;
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('id');
|
||||
final resp = await client.put('/preferences/auth', _config);
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
} else {
|
||||
context.showSnackbar('preferencesApplied'.tr);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getPreferences();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text('save'.tr),
|
||||
enabled: !_isBusy,
|
||||
onTap: () {
|
||||
_savePreferences();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('authMaximumAuthSteps'.tr),
|
||||
subtitle: Text('authMaximumAuthStepsDesc'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: SizedBox(
|
||||
width: 60,
|
||||
child: _isBusy
|
||||
? null
|
||||
: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
initialValue:
|
||||
_config['maximum_auth_steps']?.toString() ?? '2',
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
],
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (value) {
|
||||
_config['maximum_auth_steps'] =
|
||||
int.tryParse(value) ?? 2;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -192,7 +192,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
const Gap(24),
|
||||
Stack(
|
||||
children: [
|
||||
AccountAvatar(content: _avatar, radius: 40),
|
||||
AttachedCircleAvatar(content: _avatar, radius: 40),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 40,
|
||||
|
@ -260,7 +260,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
const Gap(8),
|
||||
const Gap(8),
|
||||
if (_userinfo != null)
|
||||
AccountAvatar(content: _userinfo!.avatar, radius: 16),
|
||||
AttachedCircleAvatar(
|
||||
content: _userinfo!.avatar, radius: 16),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
@ -252,7 +252,7 @@ class _ChatListState extends State<ChatList> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AccountAvatar(
|
||||
AttachedCircleAvatar(
|
||||
content: x.avatar,
|
||||
radius: 14,
|
||||
fallbackWidget: const Icon(
|
||||
|
@ -389,6 +389,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
onUpdate: (_) {
|
||||
_pullPosts();
|
||||
},
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@ -6,13 +7,13 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/post_list_controller.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/navigation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/navigation/realm_switcher.dart';
|
||||
import 'package:solian/widgets/posts/post_creation.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
@ -87,40 +88,28 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
|
||||
final scrollProgress =
|
||||
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
|
||||
final backgroundColor = Color.lerp(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow
|
||||
.withOpacity(0),
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow
|
||||
.withOpacity(0.9),
|
||||
scrollProgress,
|
||||
);
|
||||
final blurSigma = lerpDouble(0, 10, scrollProgress) ?? 0;
|
||||
|
||||
return SliverAppBar(
|
||||
backgroundColor: backgroundColor,
|
||||
flexibleSpace: SizedBox(
|
||||
flexibleSpace: ClipRRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: blurSigma,
|
||||
sigmaY: blurSigma,
|
||||
),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: const Row(
|
||||
children: [
|
||||
RealmSwitcher(),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||
snap: true,
|
||||
floating: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
dividerHeight: 0.3,
|
||||
tabAlignment: TabAlignment.fill,
|
||||
@ -149,7 +138,8 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.shuffle_on_outlined, size: 20),
|
||||
const Icon(Icons.shuffle_on_outlined,
|
||||
size: 20),
|
||||
const Gap(8),
|
||||
Text('postListShuffle'.tr),
|
||||
],
|
||||
@ -157,6 +147,22 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||
),
|
||||
),
|
||||
expandedHeight: 96,
|
||||
snap: true,
|
||||
floating: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
@ -180,6 +186,12 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
ControlledPostListWidget(
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
),
|
||||
@ -191,6 +203,9 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
ControlledPostListWidget(
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(horizontal: 16)
|
||||
: EdgeInsets.zero,
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
),
|
||||
@ -225,106 +240,3 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class PostCreatePopup extends StatelessWidget {
|
||||
final bool hideDraftBox;
|
||||
|
||||
const PostCreatePopup({
|
||||
super.key,
|
||||
this.hideDraftBox = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<dynamic> actionList = [
|
||||
(
|
||||
icon: const Icon(Icons.post_add),
|
||||
label: 'postEditorModeStory'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 0.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.description),
|
||||
label: 'postEditorModeArticle'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 1.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.drafts),
|
||||
label: 'draftBoxOpen'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed('draftBox'),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.38,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'postNew'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
children: actionList
|
||||
.map((x) => Card(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: x.onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
x.icon,
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
x.label,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingAll(18),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
).paddingSymmetric(horizontal: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/providers/last_read.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:solian/widgets/posts/post_replies.dart';
|
||||
|
||||
@ -24,11 +25,11 @@ class PostDetailScreen extends StatefulWidget {
|
||||
class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
Post? item;
|
||||
|
||||
Future<Post?> getDetail() async {
|
||||
Future<Post?> _getDetail() async {
|
||||
if (widget.post != null) {
|
||||
setState(() {
|
||||
item = widget.post;
|
||||
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
|
||||
return widget.post;
|
||||
});
|
||||
}
|
||||
|
||||
final PostProvider provider = Get.find();
|
||||
@ -48,7 +49,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: getDetail(),
|
||||
future: _getDetail(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == null) {
|
||||
return const Center(
|
||||
@ -67,11 +68,18 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
isFullContent: true,
|
||||
isShowReply: false,
|
||||
isContentSelectable: true,
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4),
|
||||
child: const Divider(thickness: 0.3, height: 1).paddingOnly(
|
||||
top: 8,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Align(
|
||||
@ -82,7 +90,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
).paddingOnly(left: 24, right: 24, top: 16),
|
||||
),
|
||||
),
|
||||
PostReplyList(item: item!),
|
||||
PostReplyList(
|
||||
item: item!,
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
|
@ -156,7 +156,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
: AccountAvatar(
|
||||
: AttachedCircleAvatar(
|
||||
content: element.avatar!,
|
||||
bgColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
|
@ -1,15 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||
import 'package:solian/widgets/auto_cache_image.dart';
|
||||
|
||||
class AccountAvatar extends StatelessWidget {
|
||||
class AttachedCircleAvatar extends StatelessWidget {
|
||||
final dynamic content;
|
||||
final Color? bgColor;
|
||||
final Color? feColor;
|
||||
final double? radius;
|
||||
final Widget? fallbackWidget;
|
||||
|
||||
const AccountAvatar({
|
||||
const AttachedCircleAvatar({
|
||||
super.key,
|
||||
required this.content,
|
||||
this.bgColor,
|
||||
@ -39,7 +40,7 @@ class AccountAvatar extends StatelessWidget {
|
||||
child: isEmpty
|
||||
? (fallbackWidget ??
|
||||
Icon(
|
||||
Icons.account_circle,
|
||||
Icons.image,
|
||||
size: radius != null ? radius! * 1.2 : 24,
|
||||
color: feColor,
|
||||
))
|
||||
@ -48,6 +49,54 @@ class AccountAvatar extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class AccountAvatar extends StatelessWidget {
|
||||
final dynamic content;
|
||||
final String username;
|
||||
final Color? bgColor;
|
||||
final Color? feColor;
|
||||
final double? radius;
|
||||
final Widget? fallbackWidget;
|
||||
|
||||
const AccountAvatar({
|
||||
super.key,
|
||||
required this.content,
|
||||
required this.username,
|
||||
this.bgColor,
|
||||
this.feColor,
|
||||
this.radius,
|
||||
this.fallbackWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: AttachedCircleAvatar(
|
||||
content: content,
|
||||
bgColor: bgColor,
|
||||
feColor: feColor,
|
||||
radius: radius,
|
||||
fallbackWidget: (fallbackWidget ??
|
||||
Icon(
|
||||
Icons.account_circle,
|
||||
size: radius != null ? radius! * 1.2 : 24,
|
||||
color: feColor,
|
||||
)),
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
name: username,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountProfileImage extends StatelessWidget {
|
||||
final dynamic content;
|
||||
final BoxFit fit;
|
||||
|
@ -84,7 +84,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
||||
Positioned(
|
||||
bottom: -30,
|
||||
left: 32,
|
||||
child: AccountAvatar(content: avatar, radius: 40),
|
||||
child: AttachedCircleAvatar(content: avatar, radius: 40),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -89,8 +89,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: ListView(
|
||||
children: [
|
||||
AccountHeadingWidget(
|
||||
avatar: _userinfo!.avatar,
|
||||
|
@ -138,7 +138,7 @@ class _AccountSelectorState extends State<AccountSelector> {
|
||||
return ListTile(
|
||||
title: Text(element.nick),
|
||||
subtitle: Text(element.name),
|
||||
leading: AccountAvatar(content: element.avatar),
|
||||
leading: AttachedCircleAvatar(content: element.avatar),
|
||||
trailing: widget.trailingBuilder != null
|
||||
? widget.trailingBuilder!(element)
|
||||
: _checkSelected(element)
|
||||
|
@ -23,7 +23,7 @@ class SilverRelativeList extends StatelessWidget {
|
||||
title: Text(element.related.nick),
|
||||
subtitle: Text(element.related.name),
|
||||
leading: GestureDetector(
|
||||
child: AccountAvatar(content: element.related.avatar),
|
||||
child: AttachedCircleAvatar(content: element.related.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
|
@ -56,7 +56,7 @@ class _RelativeSelectorState extends State<RelativeSelector> {
|
||||
return ListTile(
|
||||
title: Text(element.nick),
|
||||
subtitle: Text(element.name),
|
||||
leading: AccountAvatar(content: element.avatar),
|
||||
leading: AttachedCircleAvatar(content: element.avatar),
|
||||
trailing: widget.trailingBuilder != null
|
||||
? widget.trailingBuilder!(element)
|
||||
: null,
|
||||
|
@ -175,7 +175,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
||||
Row(
|
||||
children: [
|
||||
IgnorePointer(
|
||||
child: AccountAvatar(
|
||||
child: AttachedCircleAvatar(
|
||||
content: widget.item.account!.avatar,
|
||||
radius: 19,
|
||||
),
|
||||
|
@ -155,11 +155,18 @@ class _AttachmentItemImage extends StatelessWidget {
|
||||
),
|
||||
if (showBadge && badge != null)
|
||||
Positioned(
|
||||
right: 12,
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
bottom: 4,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Chip(label: Text(badge!)),
|
||||
child: Chip(
|
||||
label: Text(badge!),
|
||||
labelStyle: GoogleFonts.robotoMono(),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showHideButton && item.isMature)
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:dismissible_page/dismissible_page.dart';
|
||||
import 'package:flutter/material.dart' hide CarouselController;
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
@ -19,11 +18,11 @@ class AttachmentList extends StatefulWidget {
|
||||
final List<Attachment>? attachments;
|
||||
final bool isGrid;
|
||||
final bool isColumn;
|
||||
final bool isForceGrid;
|
||||
final bool isFullWidth;
|
||||
final bool autoload;
|
||||
final double flatMaxHeight;
|
||||
final double columnMaxWidth;
|
||||
|
||||
final EdgeInsets? padding;
|
||||
final double? width;
|
||||
final double? viewport;
|
||||
|
||||
@ -34,10 +33,10 @@ class AttachmentList extends StatefulWidget {
|
||||
this.attachments,
|
||||
this.isGrid = false,
|
||||
this.isColumn = false,
|
||||
this.isForceGrid = false,
|
||||
this.isFullWidth = false,
|
||||
this.autoload = false,
|
||||
this.flatMaxHeight = 720,
|
||||
this.columnMaxWidth = 480,
|
||||
this.padding,
|
||||
this.width,
|
||||
this.viewport,
|
||||
});
|
||||
@ -163,9 +162,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
color: _unFocusColor,
|
||||
).paddingOnly(right: 5),
|
||||
Text(
|
||||
'attachmentHint'.trParams(
|
||||
{'count': _attachments.toString()},
|
||||
),
|
||||
'attachmentHint'.trParams({'count': _attachments.toString()}),
|
||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
@ -175,9 +172,70 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
.fadeIn(duration: 1250.ms);
|
||||
}
|
||||
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
|
||||
if (widget.isFullWidth && _attachments.length == 1) {
|
||||
final element = _attachments.first;
|
||||
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 640,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildEntry(element, 0),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final isNotPureImage = _attachments.any(
|
||||
(x) => x?.mimetype.split('/').firstOrNull != 'image',
|
||||
);
|
||||
if (widget.isGrid && !isNotPureImage) {
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
primary: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: math.min(3, _attachments.length),
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
),
|
||||
itemCount: _attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final element = _attachments[idx];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: radius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: radius,
|
||||
child: _buildEntry(element, idx),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.isColumn) {
|
||||
var idx = 0;
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
@ -212,27 +270,31 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
final isNotPureImage = _attachments.any(
|
||||
(x) => x?.mimetype.split('/').firstOrNull != 'image',
|
||||
);
|
||||
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) {
|
||||
const radius = BorderRadius.all(Radius.circular(8));
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
primary: false,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: math.min(3, _attachments.length),
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 320,
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
itemCount: _attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final element = _attachments[idx];
|
||||
if (element == null) const SizedBox.shrink();
|
||||
final ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: math.min(
|
||||
widget.columnMaxWidth,
|
||||
MediaQuery.of(context).size.width -
|
||||
(widget.padding?.horizontal ?? 0),
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
@ -243,38 +305,11 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
borderRadius: radius,
|
||||
child: _buildEntry(element, idx),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
).paddingSymmetric(horizontal: 24);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: widget.flatMaxHeight,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
border: Border.symmetric(
|
||||
horizontal: BorderSide(
|
||||
width: 0.3,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CarouselSlider.builder(
|
||||
options: CarouselOptions(
|
||||
animateToClosest: true,
|
||||
aspectRatio: _aspectRatio,
|
||||
viewportFraction:
|
||||
widget.viewport ?? (_attachments.length > 1 ? 0.95 : 1),
|
||||
enableInfiniteScroll: false,
|
||||
),
|
||||
itemCount: _attachments.length,
|
||||
itemBuilder: (context, idx, _) {
|
||||
final element = _attachments[idx];
|
||||
return _buildEntry(element, idx);
|
||||
},
|
||||
separatorBuilder: (context, _) => const Gap(8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
|
||||
|
||||
if (item.type == 1 && otherside != null) {
|
||||
final avatar = AccountAvatar(
|
||||
final avatar = AttachedCircleAvatar(
|
||||
content: otherside.account.avatar,
|
||||
radius: 20,
|
||||
bgColor: Theme.of(context).colorScheme.primary,
|
||||
@ -241,7 +241,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
padding: const EdgeInsets.all(2),
|
||||
elevation: 8,
|
||||
),
|
||||
badgeContent: AccountAvatar(
|
||||
badgeContent: AttachedCircleAvatar(
|
||||
content: item.realm?.avatar,
|
||||
radius: 10,
|
||||
fallbackWidget: const Icon(
|
||||
|
@ -152,7 +152,8 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
title: Text(element.account.nick),
|
||||
subtitle: Text(element.account.name),
|
||||
leading: GestureDetector(
|
||||
child: AccountAvatar(content: element.account.avatar),
|
||||
child:
|
||||
AttachedCircleAvatar(content: element.account.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
|
@ -74,7 +74,7 @@ class _NoContentWidgetState extends State<NoContentWidget>
|
||||
),
|
||||
)
|
||||
],
|
||||
child: AccountAvatar(
|
||||
child: AttachedCircleAvatar(
|
||||
content: widget.userinfo!.avatar,
|
||||
bgColor: Colors.transparent,
|
||||
radius: radius,
|
||||
|
@ -220,7 +220,7 @@ class ChatEvent extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
AccountAvatar(
|
||||
AttachedCircleAvatar(
|
||||
content: item.sender.account.avatar,
|
||||
radius: 9,
|
||||
),
|
||||
@ -250,7 +250,8 @@ class ChatEvent extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: AccountAvatar(content: item.sender.account.avatar),
|
||||
child:
|
||||
AttachedCircleAvatar(content: item.sender.account.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
|
@ -443,7 +443,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
.map(
|
||||
(x) => ChatMessageSuggestion(
|
||||
type: 'users',
|
||||
leading: AccountAvatar(content: x.avatar),
|
||||
leading: AttachedCircleAvatar(content: x.avatar),
|
||||
display: x.nick,
|
||||
content: '@${x.name}',
|
||||
),
|
||||
|
@ -2,15 +2,21 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/link.dart';
|
||||
import 'package:solian/providers/link_expander.dart';
|
||||
import 'package:solian/widgets/auto_cache_image.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class LinkExpansion extends StatelessWidget {
|
||||
class LinkExpansion extends StatefulWidget {
|
||||
final String content;
|
||||
|
||||
const LinkExpansion({super.key, required this.content});
|
||||
|
||||
@override
|
||||
State<LinkExpansion> createState() => _LinkExpansionState();
|
||||
}
|
||||
|
||||
class _LinkExpansionState extends State<LinkExpansion> {
|
||||
Widget _buildImage(String url, {double? width, double? height}) {
|
||||
if (url.endsWith('svg')) {
|
||||
return SvgPicture.network(url, width: width, height: height);
|
||||
@ -22,61 +28,74 @@ class LinkExpansion extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<LinkMeta>? _meta;
|
||||
|
||||
Future<void> _doExpand() async {
|
||||
final linkRegex = RegExp(
|
||||
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
||||
);
|
||||
final matches = linkRegex.allMatches(content);
|
||||
if (matches.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final matches = linkRegex.allMatches(widget.content);
|
||||
if (matches.isEmpty) return;
|
||||
|
||||
final LinkExpandProvider expandController = Get.find();
|
||||
|
||||
return Wrap(
|
||||
children: matches.map((x) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: matches.length == 1 ? 480 : 340,
|
||||
),
|
||||
child: FutureBuilder(
|
||||
future: expandController.expandLink(x.group(0)!),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
if (matches.isEmpty) return;
|
||||
|
||||
List<LinkMeta> out = List.empty(growable: true);
|
||||
for (final x in matches) {
|
||||
final result = await expandController.expandLink(x.group(0)!);
|
||||
if (result != null) out.add(result);
|
||||
}
|
||||
|
||||
setState(() => _meta = out);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_doExpand();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_meta?.isEmpty ?? true) return const SizedBox.shrink();
|
||||
|
||||
return Wrap(
|
||||
children: _meta!.map((x) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _meta!.length == 1 ? 480 : 340,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final isRichDescription = [
|
||||
'solsynth.dev',
|
||||
].contains(Uri.parse(snapshot.data!.url).host);
|
||||
].contains(Uri.parse(x.url).host);
|
||||
|
||||
return GestureDetector(
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if ([
|
||||
(snapshot.data!.icon?.isNotEmpty ?? false),
|
||||
snapshot.data!.siteName != null
|
||||
].any((x) => x))
|
||||
if ([(x.icon?.isNotEmpty ?? false), x.siteName != null]
|
||||
.any((x) => x))
|
||||
Row(
|
||||
children: [
|
||||
if (snapshot.data!.icon?.isNotEmpty ?? false)
|
||||
if (x.icon?.isNotEmpty ?? false)
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: _buildImage(
|
||||
snapshot.data!.icon!,
|
||||
x.icon!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
).paddingOnly(right: 8),
|
||||
if (snapshot.data!.siteName != null)
|
||||
if (x.siteName != null)
|
||||
Expanded(
|
||||
child: Text(
|
||||
snapshot.data!.siteName!,
|
||||
x.siteName!,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -84,32 +103,27 @@ class LinkExpansion extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
).paddingOnly(
|
||||
bottom: (snapshot.data!.icon?.isNotEmpty ?? false)
|
||||
? 8
|
||||
: 4,
|
||||
bottom: (x.icon?.isNotEmpty ?? false) ? 8 : 4,
|
||||
),
|
||||
if (snapshot.data!.image != null &&
|
||||
(snapshot.data!.image?.startsWith('http') ?? false))
|
||||
if (x.image != null &&
|
||||
(x.image?.startsWith('http') ?? false))
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: _buildImage(
|
||||
snapshot.data!.image!,
|
||||
),
|
||||
child: _buildImage(x.image!),
|
||||
).paddingOnly(bottom: 8),
|
||||
Text(
|
||||
snapshot.data!.title ?? 'No Title',
|
||||
x.title ?? 'No Title',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (snapshot.data!.description != null &&
|
||||
isRichDescription)
|
||||
MarkdownBody(data: snapshot.data!.description!)
|
||||
else if (snapshot.data!.description != null)
|
||||
if (x.description != null && isRichDescription)
|
||||
MarkdownBody(data: x.description!)
|
||||
else if (x.description != null)
|
||||
Text(
|
||||
snapshot.data!.description!,
|
||||
x.description!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@ -117,7 +131,7 @@ class LinkExpansion extends StatelessWidget {
|
||||
).paddingAll(12),
|
||||
),
|
||||
onTap: () {
|
||||
launchUrlString(x.group(0)!);
|
||||
launchUrlString(x.url);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -69,7 +69,7 @@ class _AppAccountWidgetState extends State<AppAccountWidget> {
|
||||
bottom: 0,
|
||||
end: -2,
|
||||
),
|
||||
child: AccountAvatar(
|
||||
child: AttachedCircleAvatar(
|
||||
radius: 14,
|
||||
content: auth.userProfile.value!['avatar'],
|
||||
),
|
||||
|
@ -36,7 +36,7 @@ class RealmSwitcher extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (item != null)
|
||||
AccountAvatar(
|
||||
AttachedCircleAvatar(
|
||||
content: item.avatar,
|
||||
radius: 14,
|
||||
fallbackWidget: const Icon(
|
||||
|
108
lib/widgets/posts/post_creation.dart
Normal file
108
lib/widgets/posts/post_creation.dart
Normal file
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
|
||||
class PostCreatePopup extends StatelessWidget {
|
||||
final bool hideDraftBox;
|
||||
|
||||
const PostCreatePopup({
|
||||
super.key,
|
||||
this.hideDraftBox = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<dynamic> actionList = [
|
||||
(
|
||||
icon: const Icon(Icons.post_add),
|
||||
label: 'postEditorModeStory'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 0.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.description),
|
||||
label: 'postEditorModeArticle'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 1.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.drafts),
|
||||
label: 'draftBoxOpen'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed('draftBox'),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.38,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'postNew'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
children: actionList
|
||||
.map((x) => Card(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: x.onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
x.icon,
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
x.label,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingAll(18),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
).paddingSymmetric(horizontal: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -8,11 +8,11 @@ import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/posts/post_detail.dart';
|
||||
import 'package:solian/shells/title_shell.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||
import 'package:solian/widgets/link_expansion.dart';
|
||||
import 'package:solian/widgets/markdown_text_content.dart';
|
||||
@ -35,8 +35,12 @@ class PostItem extends StatefulWidget {
|
||||
final bool isContentSelectable;
|
||||
final bool showFeaturedReply;
|
||||
final String? attachmentParent;
|
||||
|
||||
final EdgeInsets? padding;
|
||||
final Color? backgroundColor;
|
||||
|
||||
final Function? onComment;
|
||||
|
||||
const PostItem({
|
||||
super.key,
|
||||
required this.item,
|
||||
@ -51,7 +55,9 @@ class PostItem extends StatefulWidget {
|
||||
this.isContentSelectable = false,
|
||||
this.showFeaturedReply = false,
|
||||
this.attachmentParent,
|
||||
this.padding,
|
||||
this.backgroundColor,
|
||||
this.onComment,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -92,8 +98,6 @@ class _PostItemState extends State<PostItem> {
|
||||
item: item,
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
|
||||
Stack(
|
||||
children: [
|
||||
SizedContainer(
|
||||
maxWidth: 640,
|
||||
maxHeight: widget.isFullContent ? double.infinity : 80,
|
||||
@ -110,15 +114,12 @@ class _PostItemState extends State<PostItem> {
|
||||
isSelectable: widget.isContentSelectable,
|
||||
),
|
||||
).paddingOnly(
|
||||
left: 16,
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 2,
|
||||
bottom: hasAttachment ? 4 : 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_contentHeight >= 80 && !widget.isFullContent)
|
||||
Opacity(
|
||||
opacity: 0.8,
|
||||
@ -130,9 +131,7 @@ class _PostItemState extends State<PostItem> {
|
||||
LinkExpansion(content: item.body['content']).paddingOnly(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
),
|
||||
_PostFooterWidget(item: item).paddingOnly(left: 16),
|
||||
if (attachments.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
@ -148,39 +147,20 @@ class _PostItemState extends State<PostItem> {
|
||||
style: TextStyle(color: _unFocusColor),
|
||||
)
|
||||
],
|
||||
).paddingOnly(left: 16, top: 4),
|
||||
).paddingOnly(left: 14, top: 4),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return OpenContainer(
|
||||
tappable: widget.isClickable,
|
||||
closedBuilder: (_, openContainer) => Column(
|
||||
return GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostThumbnail(
|
||||
rid: item.body['thumbnail'],
|
||||
parentId: widget.item.id.toString(),
|
||||
).paddingOnly(bottom: 4),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: AccountAvatar(content: item.author.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
context: context,
|
||||
builder: (context) => AccountProfilePopup(
|
||||
name: item.author.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_PostHeaderWidget(
|
||||
@ -188,12 +168,9 @@ class _PostItemState extends State<PostItem> {
|
||||
item: item,
|
||||
),
|
||||
_PostHeaderDividerWidget(item: item),
|
||||
Stack(
|
||||
children: [
|
||||
SizedContainer(
|
||||
maxWidth: 640,
|
||||
maxHeight:
|
||||
widget.isFullContent ? double.infinity : 320,
|
||||
maxHeight: widget.isFullContent ? double.infinity : 320,
|
||||
child: _MeasureSize(
|
||||
onChange: (size) {
|
||||
setState(() => _contentHeight = size.height);
|
||||
@ -205,30 +182,24 @@ class _PostItemState extends State<PostItem> {
|
||||
content: item.body['content'],
|
||||
isAutoWarp: item.type == 'story',
|
||||
isSelectable: widget.isContentSelectable,
|
||||
isLargeText: item.type == 'article' &&
|
||||
widget.isFullContent,
|
||||
).paddingOnly(left: 12, right: 8),
|
||||
isLargeText:
|
||||
item.type == 'article' && widget.isFullContent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_contentHeight >= 320 && !widget.isFullContent)
|
||||
Opacity(
|
||||
opacity: 0.8,
|
||||
child: InkWell(child: Text('readMore'.tr)),
|
||||
).paddingOnly(
|
||||
left: 12,
|
||||
top: 4,
|
||||
),
|
||||
).paddingOnly(top: 4),
|
||||
if (widget.item.replyTo != null && widget.isShowEmbed)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: _PostEmbedWidget(
|
||||
isClickable: widget.isClickable,
|
||||
isOverrideEmbedClickable:
|
||||
widget.isOverrideEmbedClickable,
|
||||
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
|
||||
item: widget.item.replyTo!,
|
||||
username: widget.item.replyTo!.author.name,
|
||||
hintText: 'postRepliedNotify',
|
||||
@ -239,11 +210,10 @@ class _PostItemState extends State<PostItem> {
|
||||
if (widget.item.repostTo != null && widget.isShowEmbed)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: _PostEmbedWidget(
|
||||
isClickable: widget.isClickable,
|
||||
isOverrideEmbedClickable:
|
||||
widget.isOverrideEmbedClickable,
|
||||
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
|
||||
item: widget.item.repostTo!,
|
||||
username: widget.item.repostTo!.author.name,
|
||||
hintText: 'postRepostedNotify',
|
||||
@ -251,24 +221,21 @@ class _PostItemState extends State<PostItem> {
|
||||
id: widget.item.repostTo!.id.toString(),
|
||||
),
|
||||
),
|
||||
_PostFooterWidget(item: item).paddingOnly(left: 12),
|
||||
LinkExpansion(content: item.body['content'])
|
||||
.paddingOnly(top: 4),
|
||||
_PostFooterWidget(item: item),
|
||||
LinkExpansion(content: item.body['content']),
|
||||
],
|
||||
).paddingSymmetric(
|
||||
horizontal: (widget.padding?.horizontal ?? 0) + 16,
|
||||
),
|
||||
if (hasAttachment) const Gap(8),
|
||||
_PostAttachmentWidget(
|
||||
item: item,
|
||||
padding: widget.padding,
|
||||
),
|
||||
],
|
||||
).paddingOnly(
|
||||
top: 10,
|
||||
bottom:
|
||||
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
||||
? 10
|
||||
: 0,
|
||||
right: 16,
|
||||
left: 16,
|
||||
if (widget.showFeaturedReply)
|
||||
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
|
||||
horizontal: (widget.padding?.horizontal ?? 0) + 12,
|
||||
),
|
||||
_PostAttachmentWidget(item: item),
|
||||
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item),
|
||||
if (widget.isShowReply || widget.isReactable)
|
||||
PostQuickAction(
|
||||
isShowReply: widget.isShowReply,
|
||||
@ -280,32 +247,29 @@ class _PostItemState extends State<PostItem> {
|
||||
(item.metric!.reactionList[symbol] ?? 0) + changes;
|
||||
});
|
||||
},
|
||||
onComment: () {
|
||||
if (widget.onComment != null) {
|
||||
widget.onComment!();
|
||||
}
|
||||
},
|
||||
).paddingOnly(
|
||||
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
||||
? 10
|
||||
: 6,
|
||||
left:
|
||||
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
||||
? 24
|
||||
: 60,
|
||||
right: 16,
|
||||
bottom: 10,
|
||||
top: 8,
|
||||
left: (widget.padding?.left ?? 0) + 14,
|
||||
right: (widget.padding?.right ?? 0) + 14,
|
||||
)
|
||||
else
|
||||
const Gap(10),
|
||||
],
|
||||
).paddingOnly(
|
||||
top: widget.padding?.top ?? 0,
|
||||
bottom: widget.padding?.bottom ?? 0,
|
||||
),
|
||||
openBuilder: (_, __) => TitleShell(
|
||||
title: 'postDetail'.tr,
|
||||
child: PostDetailScreen(
|
||||
id: item.id.toString(),
|
||||
post: item,
|
||||
),
|
||||
),
|
||||
closedElevation: 0,
|
||||
openElevation: 0,
|
||||
closedColor: Colors.transparent,
|
||||
openColor: Theme.of(context).colorScheme.surface,
|
||||
onTap: () {
|
||||
if (widget.isClickable) {
|
||||
AppRouter.instance.pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'id': item.id.toString()},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -317,7 +281,6 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLargeScreen = AppTheme.isLargeScreen(context);
|
||||
final unFocusColor =
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
@ -325,19 +288,17 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<String> attachments = item.body['attachments'] is List
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
|
||||
return FutureBuilder(
|
||||
future:
|
||||
Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()),
|
||||
future: Get.find<PostProvider>().listPostFeaturedReply(
|
||||
item.id.toString(),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
@ -351,7 +312,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountAvatar(
|
||||
AttachedCircleAvatar(
|
||||
content: reply.author.avatar,
|
||||
radius: 10,
|
||||
),
|
||||
@ -423,16 +384,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
).animate().fadeIn(
|
||||
duration: 300.ms,
|
||||
curve: Curves.easeIn,
|
||||
)
|
||||
.paddingOnly(
|
||||
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
|
||||
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
|
||||
right: 16,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -441,8 +395,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
||||
|
||||
class _PostAttachmentWidget extends StatelessWidget {
|
||||
final Post item;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const _PostAttachmentWidget({required this.item});
|
||||
const _PostAttachmentWidget({required this.item, required this.padding});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -452,29 +407,43 @@ class _PostAttachmentWidget extends StatelessWidget {
|
||||
? List.from(item.body['attachments']?.whereType<String>())
|
||||
: List.empty();
|
||||
|
||||
if (attachments.length > 3) {
|
||||
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
if (attachments.length == 1 && !isLargeScreen) {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isGrid: true,
|
||||
).paddingOnly(left: 36, top: 4, bottom: 4);
|
||||
} else if (attachments.length > 1 || isLargeScreen) {
|
||||
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,
|
||||
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
|
||||
} else {
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
} else if (attachments.length > 1 &&
|
||||
attachments.length % 3 == 0 &&
|
||||
!isLargeScreen) {
|
||||
return AttachmentList(
|
||||
flatMaxHeight: MediaQuery.of(context).size.width,
|
||||
parentId: item.id.toString(),
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
autoload: false,
|
||||
isGrid: true,
|
||||
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||
} else {
|
||||
return AttachmentList(
|
||||
parentId: item.id.toString(),
|
||||
attachmentIds: item.preload == null ? attachments : null,
|
||||
attachments: item.preload?.attachments,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: (padding?.horizontal ?? 0) + 14,
|
||||
),
|
||||
autoload: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -515,16 +484,17 @@ class _PostEmbedWidget extends StatelessWidget {
|
||||
size: 16,
|
||||
color: unFocusColor,
|
||||
),
|
||||
const Gap(6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
hintText.trParams(
|
||||
{'username': '@$username'},
|
||||
),
|
||||
style: TextStyle(color: unFocusColor),
|
||||
).paddingOnly(left: 6),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 12),
|
||||
).paddingOnly(left: 2),
|
||||
Card(
|
||||
elevation: 1,
|
||||
child: PostItem(
|
||||
@ -560,9 +530,7 @@ class _PostHeaderDividerWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (item.body['description'] != null || item.body['title'] != null) {
|
||||
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
||||
vertical: 8,
|
||||
);
|
||||
return const Gap(8);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@ -618,7 +586,7 @@ class _PostFooterWidget extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widgets,
|
||||
).paddingOnly(top: 4);
|
||||
).paddingSymmetric(vertical: 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -634,49 +602,59 @@ class _PostHeaderWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isCompact)
|
||||
AccountAvatar(
|
||||
content: item.author.avatar,
|
||||
radius: 10,
|
||||
).paddingOnly(left: 2, top: 1),
|
||||
username: item.author.name,
|
||||
radius: isCompact ? 10 : null,
|
||||
),
|
||||
Gap(isCompact ? 6 : 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
item.author.nick,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now())
|
||||
.paddingOnly(left: 4),
|
||||
if (isCompact) const Gap(4),
|
||||
if (isCompact)
|
||||
RelativeDate(
|
||||
item.publishedAt?.toLocal() ?? DateTime.now(),
|
||||
).paddingOnly(top: 1),
|
||||
],
|
||||
),
|
||||
if (item.body['title'] != null)
|
||||
Text(
|
||||
item.body['title'],
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontSize: 15),
|
||||
),
|
||||
if (item.body['description'] != null)
|
||||
Text(
|
||||
item.body['description'],
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
if (!isCompact)
|
||||
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now()),
|
||||
],
|
||||
).paddingOnly(left: isCompact ? 6 : 12),
|
||||
),
|
||||
),
|
||||
if (item.type == 'article')
|
||||
Badge(
|
||||
label: Text('article'.tr),
|
||||
).paddingOnly(top: 3),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
if (item.body['title'] != null)
|
||||
Text(
|
||||
item.body['title'],
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
if (item.body['description'] != null)
|
||||
Text(
|
||||
item.body['description'],
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/posts/post_editor.dart';
|
||||
import 'package:solian/widgets/posts/post_action.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
|
||||
@ -12,6 +14,7 @@ class PostListWidget extends StatelessWidget {
|
||||
final bool isNestedClickable;
|
||||
final PagingController<int, Post> controller;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const PostListWidget({
|
||||
super.key,
|
||||
@ -20,6 +23,7 @@ class PostListWidget extends StatelessWidget {
|
||||
this.isClickable = true,
|
||||
this.isNestedClickable = true,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -29,7 +33,9 @@ class PostListWidget extends StatelessWidget {
|
||||
pagingController: controller,
|
||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return PostListEntryWidget(
|
||||
return Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: PostListEntryWidget(
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
@ -39,6 +45,7 @@ class PostListWidget extends StatelessWidget {
|
||||
onUpdate: () {
|
||||
controller.refresh();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -53,8 +60,9 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
final bool isClickable;
|
||||
final bool showFeaturedReply;
|
||||
final Post item;
|
||||
final Function onUpdate;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets? padding;
|
||||
final Function onUpdate;
|
||||
|
||||
const PostListEntryWidget({
|
||||
super.key,
|
||||
@ -63,8 +71,9 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
required this.isClickable,
|
||||
required this.showFeaturedReply,
|
||||
required this.item,
|
||||
required this.onUpdate,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -76,7 +85,24 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
isShowEmbed: isShowEmbed,
|
||||
isClickable: isNestedClickable,
|
||||
showFeaturedReply: showFeaturedReply,
|
||||
padding: padding,
|
||||
backgroundColor: backgroundColor,
|
||||
onComment: () {
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
'postEditor',
|
||||
extra: PostPublishArguments(reply: item),
|
||||
)
|
||||
.then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
onUpdate();
|
||||
});
|
||||
} else if (value != null) {
|
||||
onUpdate();
|
||||
}
|
||||
});
|
||||
},
|
||||
).paddingSymmetric(vertical: 8),
|
||||
onLongPress: () {
|
||||
final AuthProvider auth = Get.find();
|
||||
@ -106,6 +132,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
||||
final bool isNestedClickable;
|
||||
final bool isPinned;
|
||||
final PagingController<int, Post> controller;
|
||||
final EdgeInsets? padding;
|
||||
final Function? onUpdate;
|
||||
|
||||
const ControlledPostListWidget({
|
||||
@ -115,6 +142,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
||||
this.isClickable = true,
|
||||
this.isNestedClickable = true,
|
||||
this.isPinned = true,
|
||||
this.padding,
|
||||
this.onUpdate,
|
||||
});
|
||||
|
||||
@ -133,6 +161,7 @@ class ControlledPostListWidget extends StatelessWidget {
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
showFeaturedReply: true,
|
||||
padding: padding,
|
||||
item: item,
|
||||
onUpdate: onUpdate ?? () {},
|
||||
);
|
||||
|
@ -11,6 +11,7 @@ class PostQuickAction extends StatefulWidget {
|
||||
final Post item;
|
||||
final bool isReactable;
|
||||
final bool isShowReply;
|
||||
final Function onComment;
|
||||
final void Function(String symbol, int num) onReact;
|
||||
|
||||
const PostQuickAction({
|
||||
@ -18,6 +19,7 @@ class PostQuickAction extends StatefulWidget {
|
||||
required this.item,
|
||||
this.isShowReply = true,
|
||||
this.isReactable = true,
|
||||
required this.onComment,
|
||||
required this.onReact,
|
||||
});
|
||||
|
||||
@ -106,7 +108,11 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
||||
builder: (context) {
|
||||
return PostReplyListPopup(item: widget.item);
|
||||
},
|
||||
);
|
||||
).then((signal) {
|
||||
if (signal == true) {
|
||||
widget.onComment();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -8,11 +8,13 @@ import 'package:solian/widgets/posts/post_list.dart';
|
||||
|
||||
class PostReplyList extends StatefulWidget {
|
||||
final Post item;
|
||||
final EdgeInsets? padding;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const PostReplyList({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.padding,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@ -53,6 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PostListWidget(
|
||||
padding: widget.padding,
|
||||
isShowEmbed: false,
|
||||
controller: _pagingController,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
@ -70,16 +73,31 @@ class PostReplyListPopup extends StatelessWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'postReplies'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_comment),
|
||||
visualDensity: const VisualDensity(horizontal: -4),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 24, right: 24, top: 24, bottom: 8),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PostReplyList(
|
||||
item: item,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -149,7 +149,8 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
||||
title: Text(element.account.nick),
|
||||
subtitle: Text(element.account.name),
|
||||
leading: GestureDetector(
|
||||
child: AccountAvatar(content: element.account.avatar),
|
||||
child:
|
||||
AttachedCircleAvatar(content: element.account.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
|
@ -29,7 +29,7 @@ import protocol_handler_macos
|
||||
import screen_brightness_macos
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sqflite
|
||||
import sqflite_darwin
|
||||
import sqlite3_flutter_libs
|
||||
import url_launcher_macos
|
||||
import wakelock_plus
|
||||
|
@ -195,7 +195,7 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- "sqlite3 (3.46.1+1)":
|
||||
@ -249,7 +249,7 @@ DEPENDENCIES:
|
||||
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||
@ -328,8 +328,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sqflite:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
sqflite_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||
sqlite3_flutter_libs:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
|
||||
url_launcher_macos:
|
||||
@ -371,16 +371,16 @@ SPEC CHECKSUMS:
|
||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
|
||||
package_info_plus: d2f71247aab4b6521434f887276093acc70d214c
|
||||
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
|
||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
|
||||
share_plus: a182a58e04e51647c0481aadabbc4de44b3a2bce
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
|
@ -49,7 +49,6 @@
|
||||
<string>NSApplication</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>zh_CN</string>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>NSUserActivityTypes</key>
|
||||
|
72
pubspec.lock
72
pubspec.lock
@ -198,14 +198,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
carousel_slider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: carousel_slider
|
||||
sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -450,10 +442,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -695,10 +687,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_card_swiper
|
||||
sha256: "880ad669017154d6d1f8c3abd861db08af97b3b7b0f7d7d5cbde690a9253811d"
|
||||
sha256: "1eacbfab31b572223042e03409726553aec431abe48af48c8d591d376d070d3d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "7.0.2"
|
||||
flutter_keyboard_visibility:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1401,10 +1393,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918
|
||||
sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.2"
|
||||
version: "8.0.3"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1449,10 +1441,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc
|
||||
sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.11"
|
||||
version: "2.2.12"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1769,18 +1761,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52"
|
||||
sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.2"
|
||||
version: "10.0.3"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5"
|
||||
sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "5.0.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1902,18 +1894,42 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788
|
||||
sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3+2"
|
||||
version: "2.4.0"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4"
|
||||
sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4+4"
|
||||
version: "2.5.4+5"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1-1"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2062,10 +2078,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2254,10 +2270,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec"
|
||||
sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.5"
|
||||
version: "5.6.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -2,7 +2,7 @@ name: solian
|
||||
description: "The Solar Network App"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.3.7+7
|
||||
version: 1.3.7+9
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.4 <4.0.0"
|
||||
@ -18,7 +18,6 @@ dependencies:
|
||||
flutter_markdown: ^0.7.1
|
||||
flutter_animate: ^4.5.0
|
||||
flutter_secure_storage: ^9.2.1
|
||||
carousel_slider: ^5.0.0
|
||||
url_launcher: ^6.2.6
|
||||
infinite_scroll_pagination: ^4.0.0
|
||||
image_picker: ^1.1.1
|
||||
|
Reference in New Issue
Block a user