From f50461a7f77a97eafd5fea4fda5aecf1526cec80 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 5 Oct 2024 23:12:23 +0800 Subject: [PATCH] :lipstick: Better chat list --- assets/locales/en_us.json | 4 +- assets/locales/zh_cn.json | 4 +- lib/providers/content/channel.dart | 13 +- lib/screens/chat.dart | 351 +++++++++++++----- lib/screens/explore.dart | 1 + lib/screens/realms/realm_view.dart | 8 +- lib/widgets/channel/channel_list.dart | 246 ++++++------ .../navigation/app_navigation_region.dart | 230 ------------ .../posts/editor/post_editor_thumbnail.dart | 5 +- 9 files changed, 381 insertions(+), 481 deletions(-) delete mode 100644 lib/widgets/navigation/app_navigation_region.dart diff --git a/assets/locales/en_us.json b/assets/locales/en_us.json index f53c97c..64e85ae 100644 --- a/assets/locales/en_us.json +++ b/assets/locales/en_us.json @@ -464,5 +464,7 @@ "blockUser": "Block user", "unblockUser": "Unblock user", "learnMoreAboutPerson": "Learn more about that person", - "global": "Global" + "global": "Global", + "all": "All", + "unablePreview": "Unable to preview" } diff --git a/assets/locales/zh_cn.json b/assets/locales/zh_cn.json index 1fee706..aed4a84 100644 --- a/assets/locales/zh_cn.json +++ b/assets/locales/zh_cn.json @@ -460,5 +460,7 @@ "blockUser": "屏蔽用户", "unblockUser": "解除屏蔽用户", "learnMoreAboutPerson": "了解关于 TA 的更多", - "global": "全局" + "global": "全局", + "all": "全部", + "unablePreview": "无法预览" } diff --git a/lib/providers/content/channel.dart b/lib/providers/content/channel.dart index 9606bc2..1bd88df 100644 --- a/lib/providers/content/channel.dart +++ b/lib/providers/content/channel.dart @@ -24,8 +24,7 @@ class ChannelProvider extends GetxController { final resp = await listAvailableChannel(); isLoading.value = false; - availableChannels.value = - resp.body.map((x) => Channel.fromJson(x)).toList().cast(); + availableChannels.value = await listAvailableChannel(); availableChannels.refresh(); } @@ -89,18 +88,22 @@ class ChannelProvider extends GetxController { return resp; } - Future listAvailableChannel({String scope = 'global'}) async { + Future> listAvailableChannel({ + String scope = 'global', + bool isDirect = false, + }) async { final AuthProvider auth = Get.find(); if (auth.isAuthorized.isFalse) throw const UnauthorizedException(); final client = await auth.configureClient('messaging'); - final resp = await client.get('/channels/$scope/me/available'); + final resp = + await client.get('/channels/$scope/me/available?direct=$isDirect'); if (resp.statusCode != 200) { throw RequestException(resp); } - return resp; + return List.from(resp.body.map((x) => Channel.fromJson(x))); } Future createChannel(String scope, dynamic payload) async { diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index bce0502..e051dc9 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -1,19 +1,24 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:gap/gap.dart'; import 'package:get/get.dart'; +import 'package:solian/controllers/chat_events_controller.dart'; import 'package:solian/exts.dart'; +import 'package:solian/models/channel.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/channel.dart'; +import 'package:solian/providers/content/realm.dart'; +import 'package:solian/providers/database/database.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/account/notification.dart'; import 'package:solian/theme.dart'; +import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/chat/call/chat_call_indicator.dart'; import 'package:solian/widgets/current_state_action.dart'; -import 'package:solian/widgets/sized_container.dart'; class ChatScreen extends StatefulWidget { const ChatScreen({super.key}); @@ -23,125 +28,265 @@ class ChatScreen extends StatefulWidget { } class _ChatScreenState extends State { - late final ChannelProvider _channels; + List _normalChannels = List.empty(); + List _directChannels = List.empty(); + final Map> _realmChannels = {}; + + late final ChannelProvider _channels = Get.find(); + + List _sortChannels(List channels) { + channels.sort( + (a, b) => + _lastMessages?[b.id]?.createdAt.compareTo( + _lastMessages?[a.id]?.createdAt ?? + DateTime.fromMillisecondsSinceEpoch(0), + ) ?? + 0, + ); + return channels; + } + + Future _loadNormalChannels() async { + final resp = await _channels.listAvailableChannel(isDirect: false); + setState(() { + _normalChannels = _sortChannels(resp); + }); + } + + Future _loadDirectChannels() async { + final resp = await _channels.listAvailableChannel(isDirect: true); + setState(() { + _directChannels = _sortChannels(resp); + }); + } + + Future _loadRealmChannels(String realm) async { + final resp = await _channels.listAvailableChannel(scope: realm); + setState(() { + _realmChannels[realm] = _sortChannels(List.from(resp)); + }); + } + + Future _loadAllChannels() async { + final RealmProvider realms = Get.find(); + Future.wait([ + _loadNormalChannels(), + _loadDirectChannels(), + ...realms.availableRealms.map((x) => _loadRealmChannels(x.alias)), + ]); + } + + Map? _lastMessages; + + Future _loadLastMessages() async { + final ctrl = ChatEventController(); + await ctrl.initialize(); + final messages = await ctrl.src.getLastInAllChannels(); + setState(() { + _lastMessages = messages + .map((k, v) => MapEntry(k, v.firstOrNull)) + .cast(); + }); + } @override void initState() { super.initState(); - try { - _channels = Get.find(); - _channels.refreshAvailableChannel(); - } catch (e) { - context.showErrorDialog(e); - } + _loadLastMessages().then((_) { + _loadAllChannels(); + }); } @override Widget build(BuildContext context) { final AuthProvider auth = Get.find(); + final RealmProvider realms = Get.find(); - return Material( - color: Theme.of(context).colorScheme.surface, - child: Scaffold( - appBar: AppBar( - leading: Obx(() { - final adaptive = AppBarLeadingButton.adaptive(context); - if (adaptive != null) return adaptive; - if (_channels.isLoading.value) { - return const CircularProgressIndicator( - strokeWidth: 3, - ).paddingAll(18); - } - return const SizedBox.shrink(); - }), - title: AppBarTitle('chat'.tr), - centerTitle: true, - toolbarHeight: AppTheme.toolbarHeight(context), - actions: [ - const BackgroundStateWidget(), - const NotificationButton(), - PopupMenuButton( - icon: const Icon(Icons.add_circle), - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - child: ListTile( - title: Text('channelOrganizeCommon'.tr), - leading: const Icon(Icons.tag), - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - ), - onTap: () { - AppRouter.instance.pushNamed('channelOrganizing').then( - (value) { - if (value != null) { - _channels.refreshAvailableChannel(); - } + return Obx( + () => DefaultTabController( + length: 2 + realms.availableRealms.length, + child: Material( + color: Theme.of(context).colorScheme.surface, + child: Scaffold( + appBar: AppBar( + leading: Obx(() { + final adaptive = AppBarLeadingButton.adaptive(context); + if (adaptive != null) return adaptive; + if (_channels.isLoading.value) { + return const CircularProgressIndicator( + strokeWidth: 3, + ).paddingAll(18); + } + return const SizedBox.shrink(); + }), + title: AppBarTitle('chat'.tr), + centerTitle: true, + toolbarHeight: AppTheme.toolbarHeight(context), + actions: [ + const BackgroundStateWidget(), + const NotificationButton(), + PopupMenuButton( + icon: const Icon(Icons.add_circle), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + child: ListTile( + title: Text('channelOrganizeCommon'.tr), + leading: const Icon(Icons.tag), + contentPadding: + const EdgeInsets.symmetric(horizontal: 8), + ), + onTap: () { + AppRouter.instance.pushNamed('channelOrganizing').then( + (value) { + if (value != null) { + _channels.refreshAvailableChannel(); + } + }, + ); }, - ); - }, - ), - PopupMenuItem( - child: ListTile( - title: Text('channelOrganizeDirect'.tr), - leading: const FaIcon( - FontAwesomeIcons.userGroup, - size: 16, ), - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - ), - onTap: () { - final ChannelProvider channels = Get.find(); - channels - .createDirectChannel(context, 'global') - .then((resp) { - if (resp != null) { - _channels.refreshAvailableChannel(); - } - }).catchError((e) { - context.showErrorDialog(e); - }); - }, + PopupMenuItem( + child: ListTile( + title: Text('channelOrganizeDirect'.tr), + leading: const FaIcon( + FontAwesomeIcons.userGroup, + size: 16, + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 8), + ), + onTap: () { + final ChannelProvider channels = Get.find(); + channels + .createDirectChannel(context, 'global') + .then((resp) { + if (resp != null) { + _channels.refreshAvailableChannel(); + } + }).catchError((e) { + context.showErrorDialog(e); + }); + }, + ), + ], + ), + SizedBox( + width: AppTheme.isLargeScreen(context) ? 8 : 16, ), ], - ), - SizedBox( - width: AppTheme.isLargeScreen(context) ? 8 : 16, - ), - ], - ), - body: Obx(() { - if (auth.isAuthorized.isFalse) { - return SigninRequiredOverlay( - onDone: () => _channels.refreshAvailableChannel(), - ); - } - - final selfId = auth.userProfile.value!['id']; - - return Column( - children: [ - const ChatCallCurrentIndicator(), - Expanded( - child: CenteredContainer( - child: RefreshIndicator( - onRefresh: _channels.refreshAvailableChannel, - child: Obx( - () => ChannelListWidget( - noCategory: true, - channels: List.from([ - ..._channels.groupChannels - .where((x) => x.realmId == null), - ..._channels.directChannels - ]), - selfId: selfId, - useReplace: false, - ), + bottom: TabBar( + isScrollable: true, + dividerColor: Theme.of(context).dividerColor.withOpacity(0.1), + tabAlignment: TabAlignment.startOffset, + tabs: [ + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 14, + backgroundColor: + Theme.of(context).colorScheme.primary, + child: const Icon( + Icons.forum, + size: 16, + color: Colors.white, + ), + ), + const Gap(8), + Text('all'.tr), + ], ), ), - ), + Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const CircleAvatar( + radius: 14, + child: Icon( + Icons.chat_bubble, + size: 16, + ), + ), + const Gap(8), + Text('channelTypeDirect'.tr), + ], + ), + ), + ...realms.availableRealms.map((x) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AccountAvatar( + content: x.avatar, + radius: 14, + fallbackWidget: const Icon( + Icons.workspaces, + size: 16, + ), + ), + const Gap(8), + Text(x.name), + ], + ), + )), + ], ), - ], - ); - }), + ), + body: Obx(() { + if (auth.isAuthorized.isFalse) { + return SigninRequiredOverlay( + onDone: () => _loadAllChannels(), + ); + } + + final selfId = auth.userProfile.value!['id']; + + return Column( + children: [ + const ChatCallCurrentIndicator(), + Expanded( + child: TabBarView( + children: [ + RefreshIndicator( + onRefresh: _loadNormalChannels, + child: ChannelListWidget( + channels: _sortChannels([ + ..._normalChannels, + ..._directChannels, + ..._realmChannels.values.expand((x) => x), + ]), + selfId: selfId, + useReplace: false, + ), + ), + RefreshIndicator( + onRefresh: _loadDirectChannels, + child: ChannelListWidget( + channels: _directChannels, + selfId: selfId, + useReplace: false, + ), + ), + ...realms.availableRealms.map( + (x) => RefreshIndicator( + onRefresh: () => _loadRealmChannels(x.alias), + child: ChannelListWidget( + channels: _realmChannels[x.alias] ?? [], + selfId: selfId, + useReplace: false, + ), + ), + ), + ], + ), + ), + ], + ); + }), + ), + ), ), ); } diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 492eeb2..b79a9a6 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -101,6 +101,7 @@ class _ExploreScreenState extends State ], bottom: TabBar( controller: _tabController, + dividerColor: Theme.of(context).dividerColor.withOpacity(0.1), tabs: [ Tab(text: 'postListNews'.tr), Tab(text: 'postListFriends'.tr), diff --git a/lib/screens/realms/realm_view.dart b/lib/screens/realms/realm_view.dart index 031ed8d..f18ad93 100644 --- a/lib/screens/realms/realm_view.dart +++ b/lib/screens/realms/realm_view.dart @@ -68,12 +68,7 @@ class _RealmViewScreenState extends State { _channels.addAll( resp.body.map((e) => Channel.fromJson(e)).toList().cast(), ); - _channels.addAll( - availableResp.body - .map((e) => Channel.fromJson(e)) - .toList() - .cast(), - ); + _channels.addAll(availableResp); _channels.retainWhere((x) => channelIdx.add(x.id)); }); @@ -260,7 +255,6 @@ class RealmChannelListWidget extends StatelessWidget { child: ChannelListWidget( channels: channels, selfId: auth.userProfile.value!['id'], - noCategory: true, ), ) ], diff --git a/lib/widgets/channel/channel_list.dart b/lib/widgets/channel/channel_list.dart index c72e69a..398f1ff 100644 --- a/lib/widgets/channel/channel_list.dart +++ b/lib/widgets/channel/channel_list.dart @@ -4,19 +4,18 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:gap/gap.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:solian/controllers/chat_events_controller.dart'; import 'package:solian/models/channel.dart'; import 'package:solian/platform.dart'; import 'package:solian/providers/database/database.dart'; import 'package:solian/router.dart'; import 'package:solian/widgets/account/account_avatar.dart'; +import 'package:badges/badges.dart' as badges; class ChannelListWidget extends StatefulWidget { final List channels; final int selfId; - final bool isDense; - final bool isCollapsed; - final bool noCategory; final bool useReplace; final Function(Channel)? onSelected; @@ -24,9 +23,6 @@ class ChannelListWidget extends StatefulWidget { super.key, required this.channels, required this.selfId, - this.isDense = false, - this.isCollapsed = false, - this.noCategory = false, this.useReplace = false, this.onSelected, }); @@ -36,9 +32,6 @@ class ChannelListWidget extends StatefulWidget { } class _ChannelListWidgetState extends State { - final List _globalChannels = List.empty(growable: true); - final Map> _inRealms = {}; - Map? _lastMessages; final ChatEventController _eventController = ChatEventController(); @@ -52,37 +45,9 @@ class _ChannelListWidgetState extends State { }); } - void _mapChannels() { - _inRealms.clear(); - _globalChannels.clear(); - - if (widget.noCategory) { - _globalChannels.addAll(widget.channels); - return; - } - - for (final channel in widget.channels) { - if (channel.realmId != null) { - if (_inRealms[channel.realm!.alias] == null) { - _inRealms[channel.realm!.alias] = List.empty(growable: true); - } - _inRealms[channel.realm!.alias]!.add(channel); - } else { - _globalChannels.add(channel); - } - } - } - - @override - void didUpdateWidget(covariant ChannelListWidget oldWidget) { - super.didUpdateWidget(oldWidget); - setState(() => _mapChannels()); - } - @override void initState() { super.initState(); - _mapChannels(); _eventController.initialize().then((_) { _loadLastMessages(); }); @@ -112,7 +77,45 @@ class _ChannelListWidgetState extends State { } } - Widget _buildChannelDescription(Channel item, ChannelMember? otherside) { + Widget _buildTitle(Channel item, ChannelMember? otherside) { + if (otherside != null) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: Text(otherside.account.nick)), + if (_lastMessages != null && _lastMessages![item.id] != null) + Text( + DateFormat('MM/dd').format( + _lastMessages![item.id]!.createdAt.toLocal(), + ), + style: TextStyle( + fontSize: 12, + color: + Theme.of(context).colorScheme.onSurface.withOpacity(0.75), + ), + ), + ], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: Text(item.name)), + if (_lastMessages != null && _lastMessages![item.id] != null) + Text( + DateFormat('MM/dd').format( + _lastMessages![item.id]!.createdAt.toLocal(), + ), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), + ), + ), + ], + ); + } + + Widget _buildSubtitle(Channel item, ChannelMember? otherside) { if (PlatformInfo.isWeb) { return otherside != null ? Text( @@ -137,28 +140,49 @@ class _ChannelListWidgetState extends State { }, duration: const Duration(milliseconds: 300), child: (_lastMessages == null || _lastMessages![item.id] == null) - ? Builder(builder: (context) { - return otherside != null - ? Text( - 'channelDirectDescription'.trParams( - {'username': '@${otherside.account.name}'}, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - : Text( - item.description, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - }) + ? Builder( + builder: (context) { + return otherside != null + ? Text( + 'channelDirectDescription'.trParams( + {'username': '@${otherside.account.name}'}, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : Text( + item.description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }, + ) : Builder( builder: (context) { final data = _lastMessages![item.id]!.data!; - return Text( - '${data.sender.account.nick}: ${data.body['text'] ?? 'Unsupported message to preview'}', - maxLines: 1, - overflow: TextOverflow.ellipsis, + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (item.type == 0) + Badge( + label: Text(data.sender.account.nick), + backgroundColor: + Theme.of(context).colorScheme.secondaryContainer, + textColor: + Theme.of(context).colorScheme.onSecondaryContainer, + ), + if (item.type == 0) const Gap(6), + if (data.body['text'] != null) + Expanded( + child: Text( + data.body['text'], + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ) + else + Badge(label: Text('unablePreview'.tr)), + ], ); }, ), @@ -175,9 +199,7 @@ class _ChannelListWidgetState extends State { } Widget _buildEntry(Channel item) { - final padding = widget.isDense - ? const EdgeInsets.symmetric(horizontal: 20) - : const EdgeInsets.symmetric(horizontal: 16); + const padding = EdgeInsets.symmetric(horizontal: 20); final otherside = item.members!.where((e) => e.account.id != widget.selfId).firstOrNull; @@ -185,60 +207,53 @@ class _ChannelListWidgetState extends State { if (item.type == 1 && otherside != null) { final avatar = AccountAvatar( content: otherside.account.avatar, - radius: widget.isDense ? 12 : 20, + radius: 20, bgColor: Theme.of(context).colorScheme.primary, feColor: Theme.of(context).colorScheme.onPrimary, ); - if (widget.isCollapsed) { - return Tooltip( - message: otherside.account.nick, - child: InkWell( - child: avatar.paddingSymmetric(vertical: 12), - onTap: () => _gotoChannel(item), - ), - ); - } - return ListTile( leading: avatar, contentPadding: padding, - title: Text(otherside.account.nick), - subtitle: - !widget.isDense ? _buildChannelDescription(item, otherside) : null, + title: _buildTitle(item, otherside), + subtitle: _buildSubtitle(item, otherside), onTap: () => _gotoChannel(item), ); } else { final avatar = CircleAvatar( - backgroundColor: item.realmId == null - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - radius: widget.isDense ? 12 : 20, + backgroundColor: Theme.of(context).colorScheme.primary, + radius: 20, child: FaIcon( FontAwesomeIcons.hashtag, - color: item.realmId == null - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.primary, - size: widget.isDense ? 12 : 16, + color: Theme.of(context).colorScheme.onPrimary, + size: 16, ), ); - if (widget.isCollapsed) { - return Tooltip( - message: item.name, - child: InkWell( - child: avatar.paddingSymmetric(vertical: 12), - onTap: () => _gotoChannel(item), - ), - ); - } - return ListTile( - minTileHeight: widget.isDense ? 48 : null, - leading: avatar, + minTileHeight: null, + leading: item.realmId == null + ? avatar + : badges.Badge( + position: badges.BadgePosition.bottomEnd(bottom: -4, end: -6), + badgeStyle: badges.BadgeStyle( + badgeColor: Theme.of(context).colorScheme.secondaryContainer, + padding: const EdgeInsets.all(2), + elevation: 8, + ), + badgeContent: AccountAvatar( + content: item.realm?.avatar, + radius: 10, + fallbackWidget: const Icon( + Icons.workspaces, + size: 16, + ), + ), + child: avatar, + ), contentPadding: padding, - title: Text(item.name), - subtitle: !widget.isDense ? _buildChannelDescription(item, null) : null, + title: _buildTitle(item, null), + subtitle: _buildSubtitle(item, null), onTap: () => _gotoChannel(item), ); } @@ -246,49 +261,16 @@ class _ChannelListWidgetState extends State { @override Widget build(BuildContext context) { - if (widget.noCategory) { - return CustomScrollView( - slivers: [ - SliverList.builder( - itemCount: _globalChannels.length, - itemBuilder: (context, index) { - final element = _globalChannels[index]; - return _buildEntry(element); - }, - ), - SliverGap(max(16, MediaQuery.of(context).padding.bottom)), - ], - ); - } - return CustomScrollView( slivers: [ SliverList.builder( - itemCount: _globalChannels.length, + itemCount: widget.channels.length, itemBuilder: (context, index) { - final element = _globalChannels[index]; + final element = widget.channels[index]; return _buildEntry(element); }, ), - SliverList.list( - children: _inRealms.entries.map((element) { - return ExpansionTile( - tilePadding: const EdgeInsets.only(left: 20, right: 24), - minTileHeight: 48, - title: Text(element.value.first.realm!.name), - leading: CircleAvatar( - backgroundColor: Colors.teal, - radius: widget.isDense ? 12 : 24, - child: Icon( - Icons.workspaces, - color: Colors.white, - size: widget.isDense ? 12 : 16, - ), - ), - children: element.value.map((x) => _buildEntry(x)).toList(), - ); - }).toList(), - ), + SliverGap(max(16, MediaQuery.of(context).padding.bottom)), ], ); } diff --git a/lib/widgets/navigation/app_navigation_region.dart b/lib/widgets/navigation/app_navigation_region.dart deleted file mode 100644 index ef88360..0000000 --- a/lib/widgets/navigation/app_navigation_region.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'package:animations/animations.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:solian/models/realm.dart'; -import 'package:solian/providers/auth.dart'; -import 'package:solian/providers/content/channel.dart'; -import 'package:solian/providers/content/realm.dart'; -import 'package:solian/providers/navigation.dart'; -import 'package:solian/services.dart'; -import 'package:solian/widgets/account/account_avatar.dart'; -import 'package:solian/widgets/auto_cache_image.dart'; -import 'package:solian/widgets/channel/channel_list.dart'; - -class AppNavigationRegion extends StatefulWidget { - final bool isCollapsed; - final Function onSelected; - - const AppNavigationRegion({ - super.key, - this.isCollapsed = false, - required this.onSelected, - }); - - @override - State createState() => _AppNavigationRegionState(); -} - -class _AppNavigationRegionState extends State { - bool _isTryingExit = false; - - void _focusRealm(Realm item) { - setState( - () => Get.find().focusedRealm.value = item, - ); - } - - void _unFocusRealm() { - setState( - () => Get.find().focusedRealm.value = null, - ); - } - - @override - void dispose() { - super.dispose(); - } - - Widget _buildRealmFocusAvatar() { - final focusedRealm = Get.find().focusedRealm.value; - return GestureDetector( - child: MouseRegion( - child: AnimatedSwitcher( - switchInCurve: Curves.fastOutSlowIn, - switchOutCurve: Curves.fastOutSlowIn, - duration: const Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - return ScaleTransition( - scale: animation, - child: child, - ); - }, - child: _isTryingExit - ? CircleAvatar( - backgroundColor: Theme.of(context).colorScheme.primary, - child: const Icon( - Icons.arrow_back, - color: Colors.white, - size: 16, - ), - ).paddingSymmetric( - vertical: 8, - ) - : _buildEntryAvatar(focusedRealm!), - ), - onEnter: (_) => setState(() => _isTryingExit = true), - onExit: (_) => setState(() => _isTryingExit = false), - ), - onTap: () => _unFocusRealm(), - ); - } - - Widget _buildEntryAvatar(Realm item) { - return Hero( - tag: Key('region-realm-avatar-${item.id}'), - child: (item.avatar?.isNotEmpty ?? false) - ? AccountAvatar(content: item.avatar) - : CircleAvatar( - backgroundColor: Theme.of(context).colorScheme.primary, - child: const Icon( - Icons.workspaces, - color: Colors.white, - size: 16, - ), - ).paddingSymmetric( - vertical: 8, - ), - ); - } - - Widget _buildEntry(BuildContext context, Realm item) { - const padding = EdgeInsets.symmetric(horizontal: 20, vertical: 8); - - if (widget.isCollapsed) { - return InkWell( - child: _buildEntryAvatar(item).paddingSymmetric(vertical: 8), - onTap: () => _focusRealm(item), - ); - } - - return ListTile( - minTileHeight: 0, - leading: _buildEntryAvatar(item), - contentPadding: padding, - title: Text(item.name), - subtitle: Text( - item.description, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - onTap: () => _focusRealm(item), - ); - } - - @override - Widget build(BuildContext context) { - final RealmProvider realms = Get.find(); - final ChannelProvider channels = Get.find(); - final AuthProvider auth = Get.find(); - final NavigationStateProvider navState = Get.find(); - - return Obx( - () => PageTransitionSwitcher( - transitionBuilder: (child, animation, secondaryAnimation) { - return SharedAxisTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - child: Material( - color: Theme.of(context).colorScheme.surface, - child: child, - ), - ); - }, - child: navState.focusedRealm.value == null - ? widget.isCollapsed - ? CustomScrollView( - slivers: [ - const SliverPadding(padding: EdgeInsets.only(top: 16)), - SliverList.builder( - itemCount: realms.availableRealms.length, - itemBuilder: (context, index) { - final element = realms.availableRealms[index]; - return Tooltip( - message: element.name, - child: _buildEntry(context, element), - ); - }, - ), - ], - ) - : CustomScrollView( - slivers: [ - SliverList.builder( - itemCount: realms.availableRealms.length, - itemBuilder: (context, index) { - final element = realms.availableRealms[index]; - return _buildEntry(context, element); - }, - ), - ], - ) - : Column( - children: [ - if (!widget.isCollapsed && - (navState.focusedRealm.value!.banner?.isNotEmpty ?? - false)) - AspectRatio( - aspectRatio: 16 / 7, - child: AutoCacheImage( - ServiceFinder.buildUrl( - 'uc', - '/attachments/${navState.focusedRealm.value!.banner}', - ), - fit: BoxFit.cover, - ), - ), - if (widget.isCollapsed) - Tooltip( - message: navState.focusedRealm.value!.name, - child: _buildRealmFocusAvatar().paddingOnly( - top: 24, - bottom: 8, - ), - ) - else - ListTile( - minTileHeight: 0, - tileColor: - Theme.of(context).colorScheme.surfaceContainerLow, - leading: _buildRealmFocusAvatar(), - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 8), - title: Text(navState.focusedRealm.value!.name), - subtitle: Text( - navState.focusedRealm.value!.description, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - Expanded( - child: Obx( - () => ChannelListWidget( - useReplace: true, - channels: channels.availableChannels - .where((x) => - x.realm?.id == navState.focusedRealm.value?.id) - .toList(), - isCollapsed: widget.isCollapsed, - selfId: auth.userProfile.value!['id'], - noCategory: true, - onSelected: (_) => widget.onSelected(), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/widgets/posts/editor/post_editor_thumbnail.dart b/lib/widgets/posts/editor/post_editor_thumbnail.dart index 783543e..ed16360 100644 --- a/lib/widgets/posts/editor/post_editor_thumbnail.dart +++ b/lib/widgets/posts/editor/post_editor_thumbnail.dart @@ -29,7 +29,7 @@ class _PostEditorThumbnailDialogState extends State { _attachmentController.text = value.toString(); }); - widget.controller.thumbnail.value = value; + widget.controller.thumbnail.value = value.isEmpty ? null : value; }, initialAttachments: const [], onRemove: (_) {}, @@ -91,7 +91,8 @@ class _PostEditorThumbnailDialogState extends State { actions: [ TextButton( onPressed: () { - widget.controller.thumbnail.value = _attachmentController.text; + final text = _attachmentController.text; + widget.controller.thumbnail.value = text.isEmpty ? null : text; Navigator.pop(context); }, child: Text('confirm'.tr),