From 3270e4f69e1dbc33d4553d15335c0a9a818b5739 Mon Sep 17 00:00:00 2001 From: lrorpilla Date: Thu, 6 Apr 2023 12:53:55 +1000 Subject: [PATCH] Bug fixes for expandable and marquee visual bug --- yuuna/lib/src/creator/anki_mapping.dart | 7 + .../lib/src/dictionary/dictionary_utils.dart | 10 +- yuuna/lib/src/models/app_model.dart | 9 +- .../pages/implementations/creator_page.dart | 39 ++-- .../dictionary_dialog_page.dart | 193 ++++++++++-------- .../dictionary_entry_page.dart | 36 ++-- .../dictionary_history_page.dart | 8 +- .../dictionary_result_page.dart | 8 +- .../implementations/dictionary_term_page.dart | 4 +- .../mokuro_catalog_manage_dialog_page.dart | 97 +++++---- .../implementations/profiles_dialog_page.dart | 106 ++++++---- yuuna/lib/src/utils/misc/mokuro_catalog.dart | 6 + yuuna/pubspec.lock | 8 + yuuna/pubspec.yaml | 1 + 14 files changed, 332 insertions(+), 200 deletions(-) diff --git a/yuuna/lib/src/creator/anki_mapping.dart b/yuuna/lib/src/creator/anki_mapping.dart index ea5ca3a79..0028acc17 100644 --- a/yuuna/lib/src/creator/anki_mapping.dart +++ b/yuuna/lib/src/creator/anki_mapping.dart @@ -401,4 +401,11 @@ class AnkiMapping { return actions; } + + @override + bool operator ==(Object other) => + other is AnkiMapping && label == other.label; + + @override + int get hashCode => label.hashCode; } diff --git a/yuuna/lib/src/dictionary/dictionary_utils.dart b/yuuna/lib/src/dictionary/dictionary_utils.dart index d223c153b..80adb723f 100644 --- a/yuuna/lib/src/dictionary/dictionary_utils.dart +++ b/yuuna/lib/src/dictionary/dictionary_utils.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:yuuna/dictionary.dart'; import 'package:yuuna/i18n/strings.g.dart'; @@ -345,9 +346,12 @@ Future depositDictionaryDataHelper(PrepareDictionaryParams params) async { // debugPrint('Collisions Found: $collisionsFound'); }); } - } catch (e, stackTrace) { - params.send(stackTrace); - params.send(e); + } catch (e, stack) { + debugPrint('$e'); + debugPrint('$stack'); + + params.send('$e'); + rethrow; } } diff --git a/yuuna/lib/src/models/app_model.dart b/yuuna/lib/src/models/app_model.dart index 2e53fca59..50b10d042 100644 --- a/yuuna/lib/src/models/app_model.dart +++ b/yuuna/lib/src/models/app_model.dart @@ -1174,9 +1174,12 @@ class AppModel with ChangeNotifier { /// Persist a new last selected model name. This is called when the user /// changes the selected model to map in the profiles menu. - Future setLastSelectedMapping(AnkiMapping mapping) async { + Future setLastSelectedMapping(AnkiMapping mapping, + {bool notify = true}) async { await _preferences.put('last_selected_mapping', mapping.label); - notifyListeners(); + if (notify) { + notifyListeners(); + } } /// Get the current home tab index. The order of the tab indexes are based on @@ -1228,6 +1231,8 @@ class AppModel with ChangeNotifier { initialModel: initialModel, ), ); + + notifyListeners(); } /// Start the process of importing a dictionary. This is called from the diff --git a/yuuna/lib/src/pages/implementations/creator_page.dart b/yuuna/lib/src/pages/implementations/creator_page.dart index 9cf872321..388d8f230 100644 --- a/yuuna/lib/src/pages/implementations/creator_page.dart +++ b/yuuna/lib/src/pages/implementations/creator_page.dart @@ -1,8 +1,8 @@ import 'dart:ui'; -import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_expanded_tile/flutter_expanded_tile.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:spaces/spaces.dart'; import 'package:subtitle/subtitle.dart'; @@ -72,7 +72,7 @@ class _CreatorPageState extends BasePageState { Color get inactiveTextColor => Theme.of(context).unselectedWidgetColor; /// For controlling collapsed fields. - late final ExpandableController expandableController; + late final ExpandedTileController expandableController; bool _creatorInitialised = false; @@ -80,8 +80,7 @@ class _CreatorPageState extends BasePageState { void initState() { super.initState(); - expandableController = - ExpandableController(initialExpanded: !isCardEditing); + expandableController = ExpandedTileController(isExpanded: !isCardEditing); } Future initialiseCreator() async { @@ -263,19 +262,29 @@ class _CreatorPageState extends BasePageState { return const SizedBox.shrink(); } - return ExpandablePanel( - theme: ExpandableThemeData( - iconPadding: Spacing.of(context).insets.onlyRight.small, - iconSize: Theme.of(context).textTheme.titleLarge?.fontSize, - expandIcon: Icons.arrow_drop_down, - collapseIcon: Icons.arrow_drop_down, - iconColor: Theme.of(context).unselectedWidgetColor, - headerAlignment: ExpandablePanelHeaderAlignment.center, + return ExpandedTile( + theme: ExpandedTileThemeData( + headerRadius: 0, + contentRadius: 0, + headerColor: Colors.transparent, + headerSplashColor: + Theme.of(context).unselectedWidgetColor.withOpacity(0.2), + contentBackgroundColor: Colors.transparent, + titlePadding: EdgeInsets.zero, + headerPadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, + leadingPadding: EdgeInsets.zero, + trailingPadding: EdgeInsets.zero, ), + trailing: Icon( + Icons.arrow_drop_down, + size: Theme.of(context).textTheme.titleLarge?.fontSize, + color: Theme.of(context).unselectedWidgetColor, + ), + trailingRotation: 0, controller: expandableController, - header: buildCollapsableHeader(), - collapsed: const SizedBox.shrink(), - expanded: buildCollapsedTextFields(), + title: buildCollapsableHeader(), + content: buildCollapsedTextFields(), ); } diff --git a/yuuna/lib/src/pages/implementations/dictionary_dialog_page.dart b/yuuna/lib/src/pages/implementations/dictionary_dialog_page.dart index a24da9a25..a268ee67f 100644 --- a/yuuna/lib/src/pages/implementations/dictionary_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/dictionary_dialog_page.dart @@ -21,7 +21,7 @@ class DictionaryDialogPage extends BasePage { class _DictionaryDialogPageState extends BasePageState { final ScrollController _scrollController = ScrollController(); - int _selectedOrder = 0; + int? _selectedOrder; @override Widget build(BuildContext context) { @@ -191,7 +191,7 @@ class _DictionaryDialogPageState extends BasePageState { progressNotifier: progressNotifier, file: file, onImportSuccess: () { - _selectedOrder = appModel.dictionaries.length - 1; + _selectedOrder = appModel.dictionaries.last.order; setState(() {}); }, ); @@ -225,13 +225,6 @@ class _DictionaryDialogPageState extends BasePageState { ); } - void updateSelectedOrder(int? newIndex) { - if (newIndex != null) { - _selectedOrder = newIndex; - setState(() {}); - } - } - Widget buildContent() { List dictionaries = appModel.dictionaries; ScrollController contentController = ScrollController(); @@ -275,17 +268,30 @@ class _DictionaryDialogPageState extends BasePageState { ); } + Map> _notifiersByDictionary = {}; + Widget buildDictionaryList(List dictionaries) { + _notifiersByDictionary = {}; + _selectedOrder ??= dictionaries.firstOrNull?.order; + return RawScrollbar( thickness: 3, thumbVisibility: true, controller: _scrollController, child: ReorderableColumn( scrollController: _scrollController, - children: List.generate( - dictionaries.length, - (index) => buildDictionaryTile(dictionaries[index]), - ), + children: List.generate(dictionaries.length, (index) { + Dictionary dictionary = dictionaries[index]; + + _notifiersByDictionary.putIfAbsent( + dictionaries[index], + () => ValueNotifier(dictionary.order == _selectedOrder), + ); + return buildDictionaryTile( + dictionaries[index], + _notifiersByDictionary[dictionary]!, + ); + }), onReorder: (oldIndex, newIndex) { List cloneDictionaries = []; cloneDictionaries.addAll(dictionaries); @@ -298,7 +304,7 @@ class _DictionaryDialogPageState extends BasePageState { dictionary.order = index; }); - updateSelectedOrder(newIndex); + _selectedOrder = newIndex; appModel.updateDictionaryOrder(cloneDictionaries); setState(() {}); @@ -331,54 +337,67 @@ class _DictionaryDialogPageState extends BasePageState { } } - Widget buildDictionaryTile(Dictionary dictionary) { + Widget buildDictionaryTile( + Dictionary dictionary, + ValueNotifier notifier, + ) { DictionaryFormat dictionaryFormat = appModel.dictionaryFormats[dictionary.formatKey]!; - return Material( - type: MaterialType.transparency, + return ValueListenableBuilder( key: ValueKey(dictionary.name), - child: ListTile( - selected: _selectedOrder == dictionary.order, - leading: getIcon( - dictionary: dictionary, - dictionaryFormat: dictionaryFormat, - ), - title: Row( - children: [ - Expanded( - child: Column( - children: [ - JidoujishoMarquee( - text: dictionary.name, - style: TextStyle( - fontSize: textTheme.bodyMedium?.fontSize, - color: dictionary.isHidden(appModel.targetLanguage) - ? theme.unselectedWidgetColor - : null, - ), - ), - JidoujishoMarquee( - text: dictionaryFormat.name, - style: TextStyle( - fontSize: textTheme.bodySmall?.fontSize, - color: dictionary.isHidden(appModel.targetLanguage) - ? theme.unselectedWidgetColor - : null, - ), + valueListenable: notifier, + builder: (context, value, _) { + return Material( + type: MaterialType.transparency, + child: ListTile( + selected: _selectedOrder == dictionary.order, + leading: getIcon( + dictionary: dictionary, + dictionaryFormat: dictionaryFormat, + ), + title: Row( + children: [ + Expanded( + child: Column( + children: [ + JidoujishoMarquee( + text: dictionary.name, + style: TextStyle( + fontSize: textTheme.bodyMedium?.fontSize, + color: dictionary.isHidden(appModel.targetLanguage) + ? theme.unselectedWidgetColor + : null, + ), + ), + JidoujishoMarquee( + text: dictionaryFormat.name, + style: TextStyle( + fontSize: textTheme.bodySmall?.fontSize, + color: dictionary.isHidden(appModel.targetLanguage) + ? theme.unselectedWidgetColor + : null, + ), + ), + ], ), - ], - ), + ), + if (_selectedOrder == dictionary.order) const Space.normal(), + if (_selectedOrder == dictionary.order) + buildDictionaryTileTrailing(dictionary) + ], ), - if (_selectedOrder == dictionary.order) const Space.normal(), - if (_selectedOrder == dictionary.order) - buildDictionaryTileTrailing(dictionary) - ], - ), - onTap: () { - updateSelectedOrder(dictionary.order); - }, - ), + onTap: () { + _selectedOrder = dictionary.order; + + for (int i = 0; i < _notifiersByDictionary.length; i++) { + _notifiersByDictionary.entries.elementAt(i).value.value = false; + } + notifier.value = true; + }, + ), + ); + }, ); } @@ -393,6 +412,32 @@ class _DictionaryDialogPageState extends BasePageState { ); } + PopupMenuItem buildPopupItem({ + required String label, + required Function() action, + IconData? icon, + Color? color, + }) { + return PopupMenuItem( + child: Row( + children: [ + if (icon != null) + Icon( + icon, + size: textTheme.bodyMedium?.fontSize, + color: color, + ), + if (icon != null) const Space.normal(), + Text( + label, + style: TextStyle(color: color), + ), + ], + ), + value: action, + ); + } + void openDictionaryOptionsMenu( {required TapDownDetails details, required Dictionary dictionary}) async { RelativeRect position = RelativeRect.fromLTRB( @@ -417,7 +462,10 @@ class _DictionaryDialogPageState extends BasePageState { : Icons.close_fullscreen, action: () { appModel.toggleDictionaryCollapsed(dictionary); - setState(() {}); + _notifiersByDictionary[dictionary]!.value = + !_notifiersByDictionary[dictionary]!.value; + _notifiersByDictionary[dictionary]!.value = + !_notifiersByDictionary[dictionary]!.value; }, ), buildPopupItem( @@ -429,7 +477,10 @@ class _DictionaryDialogPageState extends BasePageState { : Icons.visibility_off, action: () { appModel.toggleDictionaryHidden(dictionary); - setState(() {}); + _notifiersByDictionary[dictionary]!.value = + !_notifiersByDictionary[dictionary]!.value; + _notifiersByDictionary[dictionary]!.value = + !_notifiersByDictionary[dictionary]!.value; }, ), buildPopupItem( @@ -443,32 +494,6 @@ class _DictionaryDialogPageState extends BasePageState { ]; } - PopupMenuItem buildPopupItem({ - required String label, - required Function() action, - IconData? icon, - Color? color, - }) { - return PopupMenuItem( - child: Row( - children: [ - if (icon != null) - Icon( - icon, - size: textTheme.bodyMedium?.fontSize, - color: color, - ), - if (icon != null) const Space.normal(), - Text( - label, - style: TextStyle(color: color), - ), - ], - ), - value: action, - ); - } - Widget buildImportDropdown() { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/yuuna/lib/src/pages/implementations/dictionary_entry_page.dart b/yuuna/lib/src/pages/implementations/dictionary_entry_page.dart index 0505b721a..cbc853a3d 100644 --- a/yuuna/lib/src/pages/implementations/dictionary_entry_page.dart +++ b/yuuna/lib/src/pages/implementations/dictionary_entry_page.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_expanded_tile/flutter_expanded_tile.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spaces/spaces.dart'; import 'package:yuuna/creator.dart'; @@ -31,7 +31,7 @@ class DictionaryEntryPage extends ConsumerStatefulWidget { final Function(String) onStash; /// Controller specific to a dictionary name. - final ExpandableController expandableController; + final ExpandedTileController expandableController; @override ConsumerState createState() => @@ -53,19 +53,29 @@ class _DictionaryEntryPageState extends ConsumerState { top: Spacing.of(context).spaces.extraSmall, bottom: Spacing.of(context).spaces.normal, ), - child: ExpandablePanel( - theme: ExpandableThemeData( - iconPadding: EdgeInsets.zero, - iconSize: Theme.of(context).textTheme.titleLarge?.fontSize, - expandIcon: Icons.arrow_drop_down, - collapseIcon: Icons.arrow_drop_down, - iconColor: Theme.of(context).unselectedWidgetColor, - headerAlignment: ExpandablePanelHeaderAlignment.center, + child: ExpandedTile( + theme: ExpandedTileThemeData( + headerRadius: 0, + contentRadius: 0, + headerColor: Colors.transparent, + headerSplashColor: + Theme.of(context).unselectedWidgetColor.withOpacity(0.2), + contentBackgroundColor: Colors.transparent, + titlePadding: EdgeInsets.zero, + headerPadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, + leadingPadding: EdgeInsets.zero, + trailingPadding: EdgeInsets.zero, + ), + trailingRotation: 180, + trailing: Icon( + Icons.arrow_drop_down, + size: Theme.of(context).textTheme.titleLarge?.fontSize, + color: Theme.of(context).unselectedWidgetColor, ), controller: widget.expandableController, - header: _DictionaryEntryTagsWrap(entry: widget.entry), - collapsed: const SizedBox.shrink(), - expanded: Padding( + title: _DictionaryEntryTagsWrap(entry: widget.entry), + content: Padding( padding: EdgeInsets.only( top: Spacing.of(context).spaces.small, left: Spacing.of(context).spaces.normal, diff --git a/yuuna/lib/src/pages/implementations/dictionary_history_page.dart b/yuuna/lib/src/pages/implementations/dictionary_history_page.dart index ca0cac93b..f43f3eaee 100644 --- a/yuuna/lib/src/pages/implementations/dictionary_history_page.dart +++ b/yuuna/lib/src/pages/implementations/dictionary_history_page.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; -import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_expanded_tile/flutter_expanded_tile.dart'; import 'package:spaces/spaces.dart'; import 'package:yuuna/creator.dart'; import 'package:yuuna/dictionary.dart'; @@ -115,7 +115,7 @@ class _DictionaryHistoryScrollableItemState Map dictionaryNamesByOrder = Map.fromEntries( dictionaries.map((e) => MapEntry(e.name, e.order))); - final Map> + final Map> expandableControllersByHeading = {}; for (DictionaryHeading heading in headings) { expandableControllersByHeading.putIfAbsent(heading, () => {}); @@ -123,8 +123,8 @@ class _DictionaryHistoryScrollableItemState Dictionary dictionary = entry.dictionary.value!; expandableControllersByHeading[heading]?.putIfAbsent( dictionary, - () => ExpandableController( - initialExpanded: !dictionaryNamesByCollapsed[dictionary.name]!, + () => ExpandedTileController( + isExpanded: !dictionaryNamesByCollapsed[dictionary.name]!, ), ); } diff --git a/yuuna/lib/src/pages/implementations/dictionary_result_page.dart b/yuuna/lib/src/pages/implementations/dictionary_result_page.dart index 5bd94c0db..088e7513b 100644 --- a/yuuna/lib/src/pages/implementations/dictionary_result_page.dart +++ b/yuuna/lib/src/pages/implementations/dictionary_result_page.dart @@ -1,5 +1,5 @@ -import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_expanded_tile/flutter_expanded_tile.dart'; import 'package:spaces/spaces.dart'; import 'package:yuuna/creator.dart'; import 'package:yuuna/dictionary.dart'; @@ -59,7 +59,7 @@ class _DictionaryResultPageState extends BasePageState { late ScrollController _scrollController; - Map> + Map> expandableControllersByHeading = {}; @override @@ -91,8 +91,8 @@ class _DictionaryResultPageState extends BasePageState { Dictionary dictionary = entry.dictionary.value!; expandableControllersByHeading[heading]?.putIfAbsent( dictionary, - () => ExpandableController( - initialExpanded: !dictionaryNamesByCollapsed[dictionary.name]!, + () => ExpandedTileController( + isExpanded: !dictionaryNamesByCollapsed[dictionary.name]!, ), ); } diff --git a/yuuna/lib/src/pages/implementations/dictionary_term_page.dart b/yuuna/lib/src/pages/implementations/dictionary_term_page.dart index c73737ff0..f97988da8 100644 --- a/yuuna/lib/src/pages/implementations/dictionary_term_page.dart +++ b/yuuna/lib/src/pages/implementations/dictionary_term_page.dart @@ -1,6 +1,6 @@ -import 'package:expandable/expandable.dart'; import 'package:float_column/float_column.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_expanded_tile/flutter_expanded_tile.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:spaces/spaces.dart'; @@ -39,7 +39,7 @@ class DictionaryTermPage extends ConsumerWidget { final Function(String) onStash; /// Controls expandables by dictionary name. - final Map expandableControllers; + final Map expandableControllers; /// Lists whether a dictionary is hidden. final Map dictionaryNamesByHidden; diff --git a/yuuna/lib/src/pages/implementations/mokuro_catalog_manage_dialog_page.dart b/yuuna/lib/src/pages/implementations/mokuro_catalog_manage_dialog_page.dart index 975ef5b85..b745719a0 100644 --- a/yuuna/lib/src/pages/implementations/mokuro_catalog_manage_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/mokuro_catalog_manage_dialog_page.dart @@ -19,7 +19,7 @@ class MokuroCatalogManageDialogPage extends BasePage { class _MokuroCatalogManageDialogPageState extends BasePageState { final ScrollController _scrollController = ScrollController(); - int _selectedOrder = 0; + int? _selectedOrder; @override Widget build(BuildContext context) { @@ -85,8 +85,12 @@ class _MokuroCatalogManageDialogPageState ); } + Map> _notifiersByCatalog = {}; + Widget buildCatalogList() { List catalogs = appModel.mokuroCatalogs; + _notifiersByCatalog = {}; + _selectedOrder ??= catalogs.firstOrNull?.order; if (catalogs.isEmpty) { return buildEmptyMessage(); @@ -98,10 +102,18 @@ class _MokuroCatalogManageDialogPageState controller: _scrollController, child: ReorderableColumn( scrollController: _scrollController, - children: List.generate( - catalogs.length, - (index) => buildCatalogTile(catalogs[index]), - ), + children: List.generate(catalogs.length, (index) { + MokuroCatalog catalog = catalogs[index]; + + _notifiersByCatalog.putIfAbsent( + catalogs[index], + () => ValueNotifier(catalog.order == _selectedOrder), + ); + return buildCatalogTile( + catalogs[index], + _notifiersByCatalog[catalog]!, + ); + }), onReorder: (oldIndex, newIndex) async { List cloneCatalogs = []; cloneCatalogs.addAll(catalogs); @@ -123,38 +135,53 @@ class _MokuroCatalogManageDialogPageState ); } - Widget buildCatalogTile(MokuroCatalog catalog) { - return Material( - type: MaterialType.transparency, - key: ValueKey(catalog.id), - child: ListTile( - selected: _selectedOrder == catalog.order, - leading: const Icon(Icons.bookmark), - title: Row( - children: [ - Expanded( - child: Column( - children: [ - JidoujishoMarquee( - text: catalog.name, - style: TextStyle(fontSize: textTheme.bodyMedium?.fontSize), - ), - JidoujishoMarquee( - text: catalog.url, - style: TextStyle(fontSize: textTheme.bodySmall?.fontSize), + Widget buildCatalogTile( + MokuroCatalog catalog, + ValueNotifier notifier, + ) { + return ValueListenableBuilder( + key: ValueKey(catalog.name), + valueListenable: notifier, + builder: (context, value, _) { + return Material( + type: MaterialType.transparency, + child: ListTile( + selected: _selectedOrder == catalog.order, + leading: const Icon(Icons.bookmark), + title: Row( + children: [ + Expanded( + child: Column( + children: [ + JidoujishoMarquee( + text: catalog.name, + style: + TextStyle(fontSize: textTheme.bodyMedium?.fontSize), + ), + JidoujishoMarquee( + text: catalog.url, + style: + TextStyle(fontSize: textTheme.bodySmall?.fontSize), + ), + ], ), - ], - ), + ), + if (_selectedOrder == catalog.order) const Space.normal(), + if (_selectedOrder == catalog.order) + buildCatalogTileTrailing(catalog) + ], ), - if (_selectedOrder == catalog.order) const Space.normal(), - if (_selectedOrder == catalog.order) - buildCatalogTileTrailing(catalog) - ], - ), - onTap: () async { - updateSelectedOrder(catalog.order); - }, - ), + onTap: () async { + _selectedOrder = catalog.order; + + for (int i = 0; i < _notifiersByCatalog.length; i++) { + _notifiersByCatalog.entries.elementAt(i).value.value = false; + } + notifier.value = true; + }, + ), + ); + }, ); } diff --git a/yuuna/lib/src/pages/implementations/profiles_dialog_page.dart b/yuuna/lib/src/pages/implementations/profiles_dialog_page.dart index 4f5af3794..d31569ba2 100644 --- a/yuuna/lib/src/pages/implementations/profiles_dialog_page.dart +++ b/yuuna/lib/src/pages/implementations/profiles_dialog_page.dart @@ -29,7 +29,7 @@ class ProfilesDialogPage extends BasePage { class _ProfilesDialogPageState extends BasePageState { final ScrollController _scrollController = ScrollController(); - int _selectedOrder = 0; + int? _selectedOrder; @override void initState() { @@ -121,8 +121,12 @@ class _ProfilesDialogPageState extends BasePageState { ); } + Map> _notifiersByMapping = {}; Widget buildMappingList() { List mappings = appModel.mappings; + _selectedOrder = appModel.lastSelectedMapping.order; + + _notifiersByMapping = {}; return RawScrollbar( thickness: 3, @@ -132,7 +136,19 @@ class _ProfilesDialogPageState extends BasePageState { scrollController: _scrollController, children: List.generate( mappings.length, - (index) => buildMappingTile(mappings[index]), + (index) { + AnkiMapping mapping = mappings[index]; + + _notifiersByMapping.putIfAbsent( + mapping, + () => ValueNotifier(mapping.order == _selectedOrder), + ); + + return buildMappingTile( + mapping, + _notifiersByMapping[mapping]!, + ); + }, ), onReorder: (oldIndex, newIndex) async { List cloneMappings = []; @@ -161,44 +177,59 @@ class _ProfilesDialogPageState extends BasePageState { ); } - Widget buildMappingTile(AnkiMapping mapping) { - return Material( - type: MaterialType.transparency, + Widget buildMappingTile( + AnkiMapping mapping, + ValueNotifier notifier, + ) { + return ValueListenableBuilder( key: ValueKey(mapping.label), - child: ListTile( - selected: appModel.lastSelectedMapping.label == mapping.label, - leading: const Icon(Icons.account_box), - title: Row( - children: [ - Expanded( - child: Column( - children: [ - JidoujishoMarquee( - text: mapping.label, - style: TextStyle(fontSize: textTheme.bodyMedium?.fontSize), - ), - JidoujishoMarquee( - text: mapping.model, - style: TextStyle(fontSize: textTheme.bodySmall?.fontSize), + valueListenable: notifier, + builder: (context, value, _) { + return Material( + type: MaterialType.transparency, + child: ListTile( + selected: appModel.lastSelectedMapping.label == mapping.label, + leading: const Icon(Icons.account_box), + title: Row( + children: [ + Expanded( + child: Column( + children: [ + JidoujishoMarquee( + text: mapping.label, + style: + TextStyle(fontSize: textTheme.bodyMedium?.fontSize), + ), + JidoujishoMarquee( + text: mapping.model, + style: + TextStyle(fontSize: textTheme.bodySmall?.fontSize), + ), + ], ), - ], - ), + ), + if (_selectedOrder == mapping.order) const Space.normal(), + if (_selectedOrder == mapping.order) + buildMappingTileTrailing(mapping.label) + ], ), - if (_selectedOrder == mapping.order) const Space.normal(), - if (_selectedOrder == mapping.order) - buildMappingTileTrailing(mapping.label) - ], - ), - onTap: () async { - appModel.setLastSelectedMapping(mapping); - updateSelectedOrder(mapping.order); - - await appModel.validateSelectedMapping( - context: context, - mapping: mapping, - ); - }, - ), + onTap: () async { + _selectedOrder = mapping.order; + appModel.setLastSelectedMapping(mapping, notify: false); + + for (int i = 0; i < _notifiersByMapping.length; i++) { + _notifiersByMapping.entries.elementAt(i).value.value = false; + } + notifier.value = true; + + await appModel.validateSelectedMapping( + context: context, + mapping: mapping, + ); + }, + ), + ); + }, ); } @@ -635,7 +666,6 @@ class _ProfilesDialogPageState extends BasePageState { appModel.addMapping(newMapping); Navigator.pop(context); - _selectedOrder = mapping.order; setState(() {}); } } diff --git a/yuuna/lib/src/utils/misc/mokuro_catalog.dart b/yuuna/lib/src/utils/misc/mokuro_catalog.dart index 2634414a1..c099452ac 100644 --- a/yuuna/lib/src/utils/misc/mokuro_catalog.dart +++ b/yuuna/lib/src/utils/misc/mokuro_catalog.dart @@ -27,4 +27,10 @@ class MokuroCatalog { /// dictionaries. @Index(unique: true) int order; + + @override + bool operator ==(Object other) => other is MokuroCatalog && id == other.id; + + @override + int get hashCode => id.hashCode; } diff --git a/yuuna/pubspec.lock b/yuuna/pubspec.lock index 43174bbb9..249b44d53 100644 --- a/yuuna/pubspec.lock +++ b/yuuna/pubspec.lock @@ -657,6 +657,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + flutter_expanded_tile: + dependency: "direct main" + description: + name: flutter_expanded_tile + sha256: "106e992ca20e168adc18ffeac8fbbc7ee84db62b670cc288d67132153114e035" + url: "https://pub.dev" + source: hosted + version: "0.3.7" flutter_fadein: dependency: "direct main" description: diff --git a/yuuna/pubspec.yaml b/yuuna/pubspec.yaml index cbe094aa3..9ba352c79 100644 --- a/yuuna/pubspec.yaml +++ b/yuuna/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: ref: jidoujisho flutter_colorpicker: ^1.0.3 flutter_exit_app: ^1.0.5 + flutter_expanded_tile: ^0.3.7 flutter_fadein: ^2.0.0 flutter_ffmpeg: ^0.4.2 flutter_inappwebview: