diff --git a/android/app/build.gradle b/android/app/build.gradle index f360f667e5..227e9e4f99 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -21,7 +21,7 @@ try { android { namespace "com.zulip.flutter" - + ndkVersion = "26.3.11579264" compileSdkVersion flutter.compileSdkVersion compileOptions { diff --git a/assets/icons/ZulipIcons.ttf b/assets/icons/ZulipIcons.ttf index 0be670d5cb..1ee5f37bb5 100644 Binary files a/assets/icons/ZulipIcons.ttf and b/assets/icons/ZulipIcons.ttf differ diff --git a/assets/icons/no_dm.svg b/assets/icons/no_dm.svg new file mode 100644 index 0000000000..0bbef6272b --- /dev/null +++ b/assets/icons/no_dm.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/no_dm_down_arrow.svg b/assets/icons/no_dm_down_arrow.svg new file mode 100644 index 0000000000..614b367c62 --- /dev/null +++ b/assets/icons/no_dm_down_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/widgets/icons.dart b/lib/widgets/icons.dart index 82cb83704b..0e3bbd0101 100644 --- a/lib/widgets/icons.dart +++ b/lib/widgets/icons.dart @@ -105,38 +105,44 @@ abstract final class ZulipIcons { /// The Zulip custom icon "mute". static const IconData mute = IconData(0xf11b, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "no_dm". + static const IconData no_dm = IconData(0xf11c, fontFamily: "Zulip Icons"); + + /// The Zulip custom icon "no_dm_down_arrow". + static const IconData no_dm_down_arrow = IconData(0xf11d, fontFamily: "Zulip Icons"); + /// The Zulip custom icon "read_receipts". - static const IconData read_receipts = IconData(0xf11c, fontFamily: "Zulip Icons"); + static const IconData read_receipts = IconData(0xf11e, fontFamily: "Zulip Icons"); /// The Zulip custom icon "send". - static const IconData send = IconData(0xf11d, fontFamily: "Zulip Icons"); + static const IconData send = IconData(0xf11f, fontFamily: "Zulip Icons"); /// The Zulip custom icon "share". - static const IconData share = IconData(0xf11e, fontFamily: "Zulip Icons"); + static const IconData share = IconData(0xf120, fontFamily: "Zulip Icons"); /// The Zulip custom icon "share_ios". - static const IconData share_ios = IconData(0xf11f, fontFamily: "Zulip Icons"); + static const IconData share_ios = IconData(0xf121, fontFamily: "Zulip Icons"); /// The Zulip custom icon "smile". - static const IconData smile = IconData(0xf120, fontFamily: "Zulip Icons"); + static const IconData smile = IconData(0xf122, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star". - static const IconData star = IconData(0xf121, fontFamily: "Zulip Icons"); + static const IconData star = IconData(0xf123, fontFamily: "Zulip Icons"); /// The Zulip custom icon "star_filled". - static const IconData star_filled = IconData(0xf122, fontFamily: "Zulip Icons"); + static const IconData star_filled = IconData(0xf124, fontFamily: "Zulip Icons"); /// The Zulip custom icon "three_person". - static const IconData three_person = IconData(0xf123, fontFamily: "Zulip Icons"); + static const IconData three_person = IconData(0xf125, fontFamily: "Zulip Icons"); /// The Zulip custom icon "topic". - static const IconData topic = IconData(0xf124, fontFamily: "Zulip Icons"); + static const IconData topic = IconData(0xf126, fontFamily: "Zulip Icons"); /// The Zulip custom icon "unmute". - static const IconData unmute = IconData(0xf125, fontFamily: "Zulip Icons"); + static const IconData unmute = IconData(0xf127, fontFamily: "Zulip Icons"); /// The Zulip custom icon "user". - static const IconData user = IconData(0xf126, fontFamily: "Zulip Icons"); + static const IconData user = IconData(0xf128, fontFamily: "Zulip Icons"); // END GENERATED ICON DATA } diff --git a/lib/widgets/new_dm.dart b/lib/widgets/new_dm.dart new file mode 100644 index 0000000000..fe5f548539 --- /dev/null +++ b/lib/widgets/new_dm.dart @@ -0,0 +1,253 @@ +import 'package:flutter/material.dart'; +import '../api/model/model.dart'; +import '../model/narrow.dart'; +import 'content.dart'; +import 'message_list.dart'; +import 'page.dart'; +import 'store.dart'; +import 'theme.dart'; + +class NewDmScreen extends StatefulWidget { + const NewDmScreen({super.key}); + + static Route buildRoute({int? accountId, BuildContext? context}) { + return MaterialAccountWidgetRoute( + accountId: accountId, + context: context, + page: const NewDmScreen() + ); + } + + @override + State createState() => _NewDmScreenState(); +} + +class _NewDmScreenState extends State { + final List _selectedUsers = []; + final TextEditingController _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + + List _allUsers = []; + bool _isLoading = true; + bool _isDataFetched = false; // To ensure `_fetchUsers` is called only once + + @override + void initState() { + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + if (!_isDataFetched) { + _isDataFetched = true; // Avoid calling `_fetchUsers` multiple times + _fetchUsers(); + } + } + + Future _fetchUsers() async { + setState(() { + _isLoading = true; + }); + + try { + final store = PerAccountStoreWidget.of(context); + final usersMap = store.users; + setState(() { + _allUsers = usersMap.values.toList(); + _allUsers.removeWhere((user) => user.userId == store.selfUserId); + _isLoading = false; + }); + } catch (error) { + setState(() { + _isLoading = false; + }); + } + } + + List get _filteredUsers { + final query = _searchController.text.toLowerCase(); + return _allUsers.where((user) => + user.fullName.toLowerCase().contains(query) + ).toList(); + } + + void _handleUserSelect(User user) { + setState(() { + if (_selectedUsers.contains(user)) { + _selectedUsers.remove(user); + } else { + _selectedUsers.add(user); + } + _searchController.clear(); + }); + Future.delayed(const Duration(milliseconds: 10), () { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + }); + } + + + void _handleDone() { + if (_selectedUsers.isNotEmpty) { + final store = PerAccountStoreWidget.of(context); + final narrow = DmNarrow.withOtherUsers( + _selectedUsers.map((u) => u.userId), + selfUserId: store.selfUserId, + ); + Navigator.pushReplacement(context, + MessageListPage.buildRoute(context: context, narrow: narrow)); + } + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + DesignVariables designVariables = DesignVariables.of(context); + + return Scaffold( + backgroundColor: designVariables.bgContextMenu, + appBar: AppBar( + automaticallyImplyLeading: false, + title: Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Row( + children: [ + Icon(Icons.chevron_left, color: designVariables.icon, size: 24), + Text('Back', style: TextStyle(color: designVariables.icon, fontSize: 20, fontWeight: FontWeight.w600)), + ], + ), + ), + const Spacer(), // Pushes the title to the center + const Text('New DM', textAlign: TextAlign.center), + const Spacer(), // Ensures title stays centered + ], + ), + centerTitle: false, // Prevents default centering when using custom layout + actions: [ + TextButton( + onPressed: _selectedUsers.isEmpty ? null : _handleDone, + child: Row( + children: [ + Text( + 'Next', + style: TextStyle( + color: _selectedUsers.isEmpty ? designVariables.icon.withValues(alpha: 0.5) : designVariables.icon, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + Icon(Icons.chevron_right, color: _selectedUsers.isEmpty ? designVariables.icon.withValues(alpha: 0.5) : designVariables.icon, size: 24), + ], + ), + ), + ], + ), + + body: _isLoading? const Center(child: CircularProgressIndicator()) : Column( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: designVariables.bgSearchInput, + constraints: BoxConstraints( + minWidth: double.infinity, + maxHeight: screenHeight * 0.2, // Limit height to 20% of screen + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(14,11,14,0), + child: SingleChildScrollView( + controller: _scrollController, + scrollDirection: Axis.vertical, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 16, + children: [ + ..._selectedUsers.map((user) => Chip( + avatar: Avatar(userId: user.userId, size: 22, borderRadius: 3), + label: Text(user.fullName, style: TextStyle(fontSize: 16, color: designVariables.labelMenuButton)), + deleteIcon: null, + backgroundColor: designVariables.bgMenuButtonSelected, + padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 1), // Adjust padding to control height + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap + )), + SizedBox( + width: 150, + child: TextField( + controller: _searchController, + decoration: const InputDecoration( + hintText: 'Add person', + border: InputBorder.none, + ), + onChanged: (_) => setState(() {}), + ), + ), + ], + ), + ), + ), + ), + ], + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: _filteredUsers.length, + itemBuilder: (context, index) { + final user = _filteredUsers[index]; + final isSelected = _selectedUsers.contains(user); // Check if user is selected + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: isSelected ? designVariables.bgMenuButtonSelected : designVariables.bgContextMenu, + borderRadius: BorderRadius.circular(10) + ), + child: ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSelected ? Icons.check_circle : Icons.circle_outlined, + color: isSelected ? designVariables.radioFillSelected : Colors.grey, + ), + const SizedBox(width: 8), + Stack( + clipBehavior: Clip.none, + children: [ + Avatar(userId: user.userId, size: 32, borderRadius: 3), + if (user.isActive) + Positioned( + bottom: -2, + right: -2, + child: Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: designVariables.statusOnline, + shape: BoxShape.circle, + border: Border.all(color: isSelected ? designVariables.bgMenuButtonSelected: designVariables.bgContextMenu, width: 1.5), + ), + ), + ), + ], + ), + ], + ), + title: Text(user.fullName, style: TextStyle(color: designVariables.textMessage, fontSize: 17, fontWeight: FontWeight.w500)), + onTap: () => _handleUserSelect(user), + ), + ); + }, + ), + ), + + ], + ), + ); + } +} diff --git a/lib/widgets/recent_dm_conversations.dart b/lib/widgets/recent_dm_conversations.dart index ddc32861a3..fa166ab039 100644 --- a/lib/widgets/recent_dm_conversations.dart +++ b/lib/widgets/recent_dm_conversations.dart @@ -7,6 +7,7 @@ import '../model/unreads.dart'; import 'content.dart'; import 'icons.dart'; import 'message_list.dart'; +import 'new_dm.dart'; import 'store.dart'; import 'theme.dart'; import 'unread_count_badge.dart'; @@ -18,10 +19,15 @@ class RecentDmConversationsPageBody extends StatefulWidget { State createState() => _RecentDmConversationsPageBodyState(); } -class _RecentDmConversationsPageBodyState extends State with PerAccountStoreAwareStateMixin { +class _RecentDmConversationsPageBodyState extends State with PerAccountStoreAwareStateMixin{ RecentDmConversationsView? model; Unreads? unreadsModel; + final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocusNode = FocusNode(); + List _filteredConversations = []; + bool _isSearching = false; + @override void onNewStore() { model?.removeListener(_modelChanged); @@ -31,37 +37,348 @@ class _RecentDmConversationsPageBodyState extends State( + onNotification: (scrollNotification) { + if (scrollNotification is ScrollStartNotification) { + _searchFocusNode.unfocus(); // Unfocus when scrolling starts + } + return false; + }, + child: ListView.builder( + itemCount: _filteredConversations.length + (_isSearching ? 1 : 0), + itemBuilder: (context, index) { + if(index < _filteredConversations.length) { + final narrow = _filteredConversations[index]; + return RecentDmConversationsItem( + narrow: narrow, + unreadCount: unreadsModel!.countInDmNarrow(narrow), + searchQuery: _searchController.text, + focusNode: _searchFocusNode + ); + } + else{ + return NewDirectMessageButton(focusNode: _searchFocusNode); + } + })), + ) + ], + ), + ), + floatingActionButton: Visibility( + visible: !_isSearching, + child: const NewDmButton(), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + ), + ); + } +} + +class EmptyDmState extends StatelessWidget { + const EmptyDmState({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: SafeArea( + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(0, 48, 0, 16), + child: SizedBox( + width: 124, + height: 112, + child: FittedBox( + fit: BoxFit.contain, + child: Opacity( + opacity: 0.3, + child: Icon(ZulipIcons.no_dm), + ), + ), + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 24), + child: const Opacity( + opacity: 0.5, + child: Text( + 'There are no Direct Messages yet.\nStart a conversation with another person\nor a group of people.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 17), + ), + ), + ), + const SizedBox(height: 16), + const SizedBox( + height: 157, + child: FittedBox( + fit: BoxFit.contain, + child: Opacity( + opacity: 0.3, + child: Icon(ZulipIcons.no_dm_down_arrow), + ), + ), + ), + ], + ), + ), + ), + floatingActionButton: NewDmButton(), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + ); + } +} + +class NewDirectMessageButton extends StatelessWidget { + const NewDirectMessageButton({super.key, this.focusNode}); + final FocusNode? focusNode; + + @override + Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); + + return Container( + margin: const EdgeInsets.fromLTRB(24, 8.0, 24, 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), // Match the button's shape + color: designVariables.contextMenuItemBg.withAlpha(30) //12% opacity + ), + child: FilledButton.icon( + style: FilledButton.styleFrom( + minimumSize: const Size(137, 44), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + backgroundColor: Colors.transparent, + ), + onPressed: (){ + focusNode?.unfocus(); + Navigator.of(context).push( + NewDmScreen.buildRoute(context: context) + ); + }, + icon: Icon(Icons.add, color: designVariables.contextMenuItemIcon, size: 24), + label: Text( + 'New Direct Message', + style: TextStyle(color: designVariables.contextMenuItemText, fontSize: 20, fontWeight: FontWeight.w600), + ), + ), + ); + } +} + +class NewDmButton extends StatelessWidget { + const NewDmButton({super.key}); @override Widget build(BuildContext context) { - final sorted = model!.sorted; - return SafeArea( - // Don't pad the bottom here; we want the list content to do that. - bottom: false, - child: ListView.builder( - itemCount: sorted.length, - itemBuilder: (context, index) { - final narrow = sorted[index]; - return RecentDmConversationsItem( - narrow: narrow, - unreadCount: unreadsModel!.countInDmNarrow(narrow), + final designVariables = DesignVariables.of(context); + + return Container( + padding: const EdgeInsets.fromLTRB(12, 8.0, 16.0, 16), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Color(0x662B0E8A), // 40% opacity for #2B0E8A + offset: Offset(0, 4), // X: 0, Y: 4 + blurRadius: 16, // Blur: 16 + spreadRadius: 0, // Spread: 0 + ), + ], + borderRadius: BorderRadius.circular(28), // Match the button's shape + ), + child: FilledButton.icon( + style: FilledButton.styleFrom( + minimumSize: const Size(137, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + backgroundColor: designVariables.fabBg, + ), + onPressed: (){ + Navigator.of(context).push( + NewDmScreen.buildRoute(context: context) ); - })); + }, + icon: const Icon(Icons.add, color: Colors.white, size: 24), + label: Text( + 'New DM', + style: TextStyle(color: designVariables.fabLabel, fontSize: 20, fontWeight: FontWeight.w500), + ), + ), + ); + } +} + +class SearchRow extends StatefulWidget { + SearchRow({super.key, required this.controller, required this.focusNode}); // Accept focusNode + + final TextEditingController controller; + final FocusNode focusNode; + + @override + State createState() => _SearchRowState(); +} + +class _SearchRowState extends State { + bool _showCancelButton = false; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_onTextChanged); + widget.focusNode.addListener(_onFocusChanged); + } + + @override + void dispose() { + widget.controller.removeListener(_onTextChanged); + widget.focusNode.removeListener(_onFocusChanged); + widget.focusNode.dispose(); + super.dispose(); + } + + void _onTextChanged() { + _updateSearchState(); + } + void _onFocusChanged() { + _updateSearchState(); + } + + void _updateSearchState() { + setState(() { + _showCancelButton = widget.controller.text.isNotEmpty; + // Notify parent widget about the search state change + (context as Element).markNeedsBuild(); + }); + } + + @override + Widget build(BuildContext context) { + final designVariables = DesignVariables.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14.0), + color: DesignVariables.of(context).bgSearchInput, + child: Row( + children: [ + const Icon( + Icons.search, + size: 24.0, + color: Colors.grey, + ), + const SizedBox(width: 14.0), // Add space between the icon and the text field + // Text Field + Expanded( + child: TextField( + controller: widget.controller, + focusNode: widget.focusNode, + decoration: InputDecoration( + hintText: 'Filter conversations', + hintStyle: TextStyle(color: designVariables.labelSearchPrompt), + border: InputBorder.none, // Remove the border + ), + style: const TextStyle(fontSize: 17.0, fontWeight: FontWeight.w400), + ), + ), + if (_showCancelButton) ...[ + const SizedBox(width: 8.0), + GestureDetector( + onTap: () { + widget.controller.clear(); + widget.focusNode.unfocus(); + }, + child: const Icon( + Icons.cancel, + size: 20.0, + color: Colors.grey, + ), + ), + ], + ], + ), + ); } } @@ -70,10 +387,14 @@ class RecentDmConversationsItem extends StatelessWidget { super.key, required this.narrow, required this.unreadCount, + required this.searchQuery, + required this.focusNode, }); final DmNarrow narrow; final int unreadCount; + final String searchQuery; + final FocusNode focusNode; static const double _avatarSize = 32; @@ -112,9 +433,10 @@ class RecentDmConversationsItem extends StatelessWidget { } return Material( - color: designVariables.background, // TODO(design) check if this is the right variable + color: designVariables.mainBackground, // TODO(design) check if this is the right variable child: InkWell( onTap: () { + focusNode.unfocus(); Navigator.push(context, MessageListPage.buildRoute(context: context, narrow: narrow)); }, @@ -125,16 +447,7 @@ class RecentDmConversationsItem extends StatelessWidget { const SizedBox(width: 8), Expanded(child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - style: TextStyle( - fontSize: 17, - height: (20 / 17), - // TODO(design) check if this is the right variable - color: designVariables.labelMenuButton, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - title))), + child: _buildHighlightedText(title, searchQuery, designVariables))), const SizedBox(width: 12), unreadCount > 0 ? Padding(padding: const EdgeInsetsDirectional.only(end: 16), @@ -143,4 +456,56 @@ class RecentDmConversationsItem extends StatelessWidget { : const SizedBox(), ])))); } + + Widget _buildHighlightedText(String text, String query, DesignVariables designVariables) { + if (query.isEmpty || !text.toLowerCase().contains(query.toLowerCase())) { + // If there's no query or it doesn't match, show normal text + return Text( + text, + style: TextStyle( + fontSize: 17, + height: (20 / 17), + color: designVariables.labelMenuButton, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + + final startIndex = text.toLowerCase().indexOf(query.toLowerCase()); + final endIndex = startIndex + query.length; + + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: text.substring(0, startIndex), + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: designVariables.textMessage, + ), + ), + TextSpan( + text: text.substring(startIndex, endIndex), + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, // Bold for the matching text + color: designVariables.textMessage, + ), + ), + TextSpan( + text: text.substring(endIndex), + style: TextStyle( + fontSize: 17, + color: designVariables.labelMenuButton, + ), + ), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + } diff --git a/lib/widgets/theme.dart b/lib/widgets/theme.dart index ec8ad8aecc..bb0edea2dc 100644 --- a/lib/widgets/theme.dart +++ b/lib/widgets/theme.dart @@ -121,110 +121,123 @@ const kZulipBrandColor = Color.fromRGBO(0x64, 0x92, 0xfe, 1); /// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2945-49492&t=MEb4vtp7S26nntxm-0 class DesignVariables extends ThemeExtension { static final light = DesignVariables._( - background: const Color(0xffffffff), - bannerBgIntDanger: const Color(0xfff2e4e4), - bgBotBar: const Color(0xfff6f6f6), - bgContextMenu: const Color(0xfff2f2f2), - bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.15), - bgMenuButtonActive: Colors.black.withValues(alpha: 0.05), - bgMenuButtonSelected: Colors.white, - bgTopBar: const Color(0xfff5f5f5), - borderBar: Colors.black.withValues(alpha: 0.2), - borderMenuButtonSelected: Colors.black.withValues(alpha: 0.2), - btnLabelAttLowIntDanger: const Color(0xffc0070a), - btnLabelAttMediumIntDanger: const Color(0xffac0508), - composeBoxBg: const Color(0xffffffff), - contextMenuCancelText: const Color(0xff222222), - contextMenuItemBg: const Color(0xff6159e1), - contextMenuItemText: const Color(0xff381da7), - editorButtonPressedBg: Colors.black.withValues(alpha: 0.06), - foreground: const Color(0xff000000), - icon: const Color(0xff6159e1), - iconSelected: const Color(0xff222222), - labelCounterUnread: const Color(0xff222222), - labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(), - labelMenuButton: const Color(0xff222222), - mainBackground: const Color(0xfff0f0f0), - textInput: const Color(0xff000000), - title: const Color(0xff1a1a1a), - bgSearchInput: const Color(0xffe3e3e3), - textMessage: const Color(0xff262626), - channelColorSwatches: ChannelColorSwatches.light, - colorMessageHeaderIconInteractive: Colors.black.withValues(alpha: 0.2), - contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15), - contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20), - dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(), - groupDmConversationIcon: Colors.black.withValues(alpha: 0.5), - groupDmConversationIconBg: const Color(0x33808080), - inboxItemIconMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(), - loginOrDivider: const Color(0xffdedede), - loginOrDividerText: const Color(0xff575757), - modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.3), - mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.8).toColor(), - navigationButtonBg: Colors.black.withValues(alpha: 0.05), - sectionCollapseIcon: const Color(0x7f1e2e48), - star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), - subscriptionListHeaderLine: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor(), - subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(), - unreadCountBadgeTextForChannel: Colors.black.withValues(alpha: 0.9), - ); + background: const Color(0xffffffff), + bannerBgIntDanger: const Color(0xfff2e4e4), + bgBotBar: const Color(0xfff6f6f6), + bgContextMenu: const Color(0xfff2f2f2), + bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.15), + bgMenuButtonActive: Colors.black.withValues(alpha: 0.05), + bgMenuButtonSelected: Colors.white, + bgTopBar: const Color(0xfff5f5f5), + borderBar: Colors.black.withValues(alpha: 0.2), + borderMenuButtonSelected: Colors.black.withValues(alpha: 0.2), + btnLabelAttLowIntDanger: const Color(0xffc0070a), + btnLabelAttMediumIntDanger: const Color(0xffac0508), + composeBoxBg: const Color(0xffffffff), + contextMenuCancelText: const Color(0xff222222), + contextMenuItemBg: const Color(0xff6159e1), + contextMenuItemText: const Color(0xff381da7), + contextMenuItemIcon: const Color(0xff4F42C9), + editorButtonPressedBg: Colors.black.withValues(alpha: 0.06), + fabBg: const Color(0xFF6E69F3), + fabLabel: const Color(0xffECEEFC), + foreground: const Color(0xff000000), + icon: const Color(0xff6159e1), + iconSelected: const Color(0xff222222), + labelCounterUnread: const Color(0xff222222), + labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 0).toColor(), + labelMenuButton: const Color(0xff222222), + labelSearchPrompt: const Color(0xff000000).withValues(alpha: 0.5), + mainBackground: const Color(0xfff0f0f0), + textInput: const Color(0xff000000), + title: const Color(0xff1a1a1a), + bgSearchInput: const Color(0xffe3e3e3), + textMessage: const Color(0xff262626), + channelColorSwatches: ChannelColorSwatches.light, + colorMessageHeaderIconInteractive: Colors.black.withValues(alpha: 0.2), + contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15), + contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20), + dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(), + groupDmConversationIcon: Colors.black.withValues(alpha: 0.5), + groupDmConversationIconBg: const Color(0x33808080), + inboxItemIconMarker: const HSLColor.fromAHSL(0.5, 0, 0, 0.2).toColor(), + loginOrDivider: const Color(0xffdedede), + loginOrDividerText: const Color(0xff575757), + modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.3), + mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.8).toColor(), + navigationButtonBg: Colors.black.withValues(alpha: 0.05), + radioFillSelected: const Color(0xff4370F0), + sectionCollapseIcon: const Color(0x7f1e2e48), + star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), + subscriptionListHeaderLine: const HSLColor.fromAHSL(0.2, 240, 0.1, 0.5).toColor(), + subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.5).toColor(), + statusOnline: const Color(0xff46AA62), + unreadCountBadgeTextForChannel: Colors.black.withValues(alpha: 0.9), + ); static final dark = DesignVariables._( - background: const Color(0xff000000), - bannerBgIntDanger: const Color(0xff461616), - bgBotBar: const Color(0xff222222), - bgContextMenu: const Color(0xff262626), - bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37), - bgMenuButtonActive: Colors.black.withValues(alpha: 0.2), - bgMenuButtonSelected: Colors.black.withValues(alpha: 0.25), - bgTopBar: const Color(0xff242424), - borderBar: const Color(0xffffffff).withValues(alpha: 0.1), - borderMenuButtonSelected: Colors.white.withValues(alpha: 0.1), - btnLabelAttLowIntDanger: const Color(0xffff8b7c), - btnLabelAttMediumIntDanger: const Color(0xffff8b7c), - composeBoxBg: const Color(0xff0f0f0f), - contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75), - contextMenuItemBg: const Color(0xff7977fe), - contextMenuItemText: const Color(0xff9398fd), - editorButtonPressedBg: Colors.white.withValues(alpha: 0.06), - foreground: const Color(0xffffffff), - icon: const Color(0xff7977fe), - iconSelected: Colors.white.withValues(alpha: 0.8), - labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7), - labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(), - labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85), - mainBackground: const Color(0xff1d1d1d), - textInput: const Color(0xffffffff).withValues(alpha: 0.9), - title: const Color(0xffffffff), - bgSearchInput: const Color(0xff313131), - textMessage: const Color(0xffffffff).withValues(alpha: 0.8), - channelColorSwatches: ChannelColorSwatches.dark, - contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15), // the same as the light mode in Figma - contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20), // the same as the light mode in Figma - // TODO(design-dark) need proper dark-theme color (this is ad hoc) - colorMessageHeaderIconInteractive: Colors.white.withValues(alpha: 0.2), - dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.15, 0.2).toColor(), - // TODO(design-dark) need proper dark-theme color (this is ad hoc) - groupDmConversationIcon: Colors.white.withValues(alpha: 0.5), - // TODO(design-dark) need proper dark-theme color (this is ad hoc) - groupDmConversationIconBg: const Color(0x33cccccc), - inboxItemIconMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(), - loginOrDivider: const Color(0xff424242), - loginOrDividerText: const Color(0xffa8a8a8), - modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.5), - // TODO(design-dark) need proper dark-theme color (this is ad hoc) - mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.6).toColor(), - navigationButtonBg: Colors.white.withValues(alpha: 0.05), - // TODO(design-dark) need proper dark-theme color (this is ad hoc) - sectionCollapseIcon: const Color(0x7fb6c8e2), - // TODO(design-dark) unchanged in dark theme? - star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), - // TODO(design-dark) need proper dark-theme color (this is ad hoc) - subscriptionListHeaderLine: const HSLColor.fromAHSL(0.4, 240, 0.1, 0.75).toColor(), - // TODO(design-dark) need proper dark-theme color (this is ad hoc) - subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.75).toColor(), - unreadCountBadgeTextForChannel: Colors.white.withValues(alpha: 0.9), - ); + background: const Color(0xff000000), + bannerBgIntDanger: const Color(0xff461616), + bgBotBar: const Color(0xff222222), + bgContextMenu: const Color(0xff262626), + bgCounterUnread: const Color(0xff666699).withValues(alpha: 0.37), + bgMenuButtonActive: Colors.black.withValues(alpha: 0.2), + bgMenuButtonSelected: Colors.black.withValues(alpha: 0.25), + bgTopBar: const Color(0xff242424), + borderBar: Colors.black.withValues(alpha: 0.1), + borderMenuButtonSelected: Colors.white.withValues(alpha: 0.1), + btnLabelAttLowIntDanger: const Color(0xffff8b7c), + btnLabelAttMediumIntDanger: const Color(0xffff8b7c), + composeBoxBg: const Color(0xff0f0f0f), + contextMenuCancelText: const Color(0xffffffff).withValues(alpha: 0.75), + contextMenuItemBg: const Color(0xff7977fe), + contextMenuItemText: const Color(0xff9398fd), + contextMenuItemIcon: const Color(0xff9398FD), + editorButtonPressedBg: Colors.white.withValues(alpha: 0.06), + fabBg: const Color(0xFF4F42C9), + fabLabel: const Color(0xffECEEFC), + foreground: const Color(0xffffffff), + icon: const Color(0xff7977fe), + iconSelected: Colors.white.withValues(alpha: 0.8), + labelCounterUnread: const Color(0xffffffff).withValues(alpha: 0.7), + labelEdited: const HSLColor.fromAHSL(0.35, 0, 0, 1).toColor(), + labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85), + labelSearchPrompt: const Color(0xffffffff).withValues(alpha: 0.5), + mainBackground: const Color(0xff1d1d1d), + textInput: const Color(0xffffffff).withValues(alpha: 0.9), + title: const Color(0xffffffff), + bgSearchInput: const Color(0xff313131), + textMessage: const Color(0xffffffff).withValues(alpha: 0.8), + channelColorSwatches: ChannelColorSwatches.dark, + contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15), // the same as the light mode in Figma + contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20), // the same as the light mode in Figma + // TODO(design-dark) need proper dark-theme color (this is ad hoc) + colorMessageHeaderIconInteractive: Colors.white.withValues(alpha: 0.2), + dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.15, 0.2).toColor(), + // TODO(design-dark) need proper dark-theme color (this is ad hoc) + groupDmConversationIcon: Colors.white.withValues(alpha: 0.5), + // TODO(design-dark) need proper dark-theme color (this is ad hoc) + groupDmConversationIconBg: const Color(0x33cccccc), + inboxItemIconMarker: const HSLColor.fromAHSL(0.4, 0, 0, 1).toColor(), + loginOrDivider: const Color(0xff424242), + loginOrDividerText: const Color(0xffa8a8a8), + modalBarrierColor: const Color(0xff000000).withValues(alpha: 0.5), + // TODO(design-dark) need proper dark-theme color (this is ad hoc) + mutedUnreadBadge: const HSLColor.fromAHSL(0.5, 0, 0, 0.6).toColor(), + navigationButtonBg: Colors.white.withValues(alpha: 0.05), + radioFillSelected: const Color(0xff4E7CFA), + // TODO(design-dark) need proper dark-theme color (this is ad hoc) + sectionCollapseIcon: const Color(0x7fb6c8e2), + // TODO(design-dark) unchanged in dark theme? + star: const HSLColor.fromAHSL(0.5, 47, 1, 0.41).toColor(), + statusOnline: const Color(0xff46AA62), + // TODO(design-dark) need proper dark-theme color (this is ad hoc) + subscriptionListHeaderLine: const HSLColor.fromAHSL(0.4, 240, 0.1, 0.75).toColor(), + // TODO(design-dark) need proper dark-theme color (this is ad hoc) + subscriptionListHeaderText: const HSLColor.fromAHSL(1.0, 240, 0.1, 0.75).toColor(), + unreadCountBadgeTextForChannel: Colors.white.withValues(alpha: 0.9), + ); + DesignVariables._({ required this.background, @@ -243,7 +256,10 @@ class DesignVariables extends ThemeExtension { required this.contextMenuCancelText, required this.contextMenuItemBg, required this.contextMenuItemText, + required this.contextMenuItemIcon, required this.editorButtonPressedBg, + required this.fabBg, + required this.fabLabel, required this.foreground, required this.icon, required this.iconSelected, @@ -273,6 +289,9 @@ class DesignVariables extends ThemeExtension { required this.subscriptionListHeaderLine, required this.subscriptionListHeaderText, required this.unreadCountBadgeTextForChannel, + required this.labelSearchPrompt, + required this.radioFillSelected, + required this.statusOnline, }); /// The [DesignVariables] from the context's active theme. @@ -301,7 +320,10 @@ class DesignVariables extends ThemeExtension { final Color contextMenuCancelText; final Color contextMenuItemBg; final Color contextMenuItemText; + final Color contextMenuItemIcon; final Color editorButtonPressedBg; + final Color fabBg; + final Color fabLabel; final Color foreground; final Color icon; final Color iconSelected; @@ -335,6 +357,9 @@ class DesignVariables extends ThemeExtension { final Color subscriptionListHeaderLine; final Color subscriptionListHeaderText; final Color unreadCountBadgeTextForChannel; + final Color labelSearchPrompt; + final Color radioFillSelected; + final Color statusOnline; @override DesignVariables copyWith({ @@ -354,6 +379,7 @@ class DesignVariables extends ThemeExtension { Color? contextMenuCancelText, Color? contextMenuItemBg, Color? contextMenuItemText, + Color? contextMenuItemIcon, Color? editorButtonPressedBg, Color? foreground, Color? icon, @@ -361,6 +387,7 @@ class DesignVariables extends ThemeExtension { Color? labelCounterUnread, Color? labelEdited, Color? labelMenuButton, + Color? labelSearchPrompt, Color? mainBackground, Color? textInput, Color? title, @@ -379,8 +406,12 @@ class DesignVariables extends ThemeExtension { Color? modalBarrierColor, Color? mutedUnreadBadge, Color? navigationButtonBg, + Color? fabBg, + Color? fabLabel, + Color? radioFillSelected, Color? sectionCollapseIcon, Color? star, + Color? statusOnline, Color? subscriptionListHeaderLine, Color? subscriptionListHeaderText, Color? unreadCountBadgeTextForChannel, @@ -402,6 +433,7 @@ class DesignVariables extends ThemeExtension { contextMenuCancelText: contextMenuCancelText ?? this.contextMenuCancelText, contextMenuItemBg: contextMenuItemBg ?? this.contextMenuItemBg, contextMenuItemText: contextMenuItemText ?? this.contextMenuItemBg, + contextMenuItemIcon: contextMenuItemIcon ?? this.contextMenuItemIcon, editorButtonPressedBg: editorButtonPressedBg ?? this.editorButtonPressedBg, foreground: foreground ?? this.foreground, icon: icon ?? this.icon, @@ -427,11 +459,16 @@ class DesignVariables extends ThemeExtension { modalBarrierColor: modalBarrierColor ?? this.modalBarrierColor, mutedUnreadBadge: mutedUnreadBadge ?? this.mutedUnreadBadge, navigationButtonBg: navigationButtonBg ?? this.navigationButtonBg, + fabBg: fabBg ?? this.fabBg, + fabLabel: fabLabel ?? this.fabLabel, sectionCollapseIcon: sectionCollapseIcon ?? this.sectionCollapseIcon, star: star ?? this.star, subscriptionListHeaderLine: subscriptionListHeaderLine ?? this.subscriptionListHeaderLine, subscriptionListHeaderText: subscriptionListHeaderText ?? this.subscriptionListHeaderText, unreadCountBadgeTextForChannel: unreadCountBadgeTextForChannel ?? this.unreadCountBadgeTextForChannel, + labelSearchPrompt: labelSearchPrompt ?? this.labelSearchPrompt, + radioFillSelected: radioFillSelected ?? this.radioFillSelected, + statusOnline: statusOnline ?? this.statusOnline, ); } @@ -482,11 +519,17 @@ class DesignVariables extends ThemeExtension { modalBarrierColor: Color.lerp(modalBarrierColor, other.modalBarrierColor, t)!, mutedUnreadBadge: Color.lerp(mutedUnreadBadge, other.mutedUnreadBadge, t)!, navigationButtonBg: Color.lerp(navigationButtonBg, other.navigationButtonBg, t)!, + fabBg: Color.lerp(fabBg, other.fabBg, t)!, + fabLabel: Color.lerp(fabLabel, other.fabLabel, t)!, sectionCollapseIcon: Color.lerp(sectionCollapseIcon, other.sectionCollapseIcon, t)!, star: Color.lerp(star, other.star, t)!, subscriptionListHeaderLine: Color.lerp(subscriptionListHeaderLine, other.subscriptionListHeaderLine, t)!, subscriptionListHeaderText: Color.lerp(subscriptionListHeaderText, other.subscriptionListHeaderText, t)!, unreadCountBadgeTextForChannel: Color.lerp(unreadCountBadgeTextForChannel, other.unreadCountBadgeTextForChannel, t)!, + contextMenuItemIcon: Color.lerp(contextMenuItemIcon, other.contextMenuItemIcon, t)!, + labelSearchPrompt: Color.lerp(labelSearchPrompt, other.labelSearchPrompt, t)!, + radioFillSelected: Color.lerp(radioFillSelected, other.radioFillSelected, t)!, + statusOnline: Color.lerp(statusOnline, other.statusOnline, t)!, ); } }