mirror of
https://github.com/DungeonPaper/dungeon-paper-app.git
synced 2026-05-17 17:58:11 +00:00
514 lines
17 KiB
Dart
514 lines
17 KiB
Dart
import 'package:dungeon_paper/app/data/models/character.dart';
|
|
import 'package:dungeon_paper/app/data/models/character_settings.dart';
|
|
import 'package:dungeon_paper/app/data/models/item.dart';
|
|
import 'package:dungeon_paper/app/data/models/meta.dart';
|
|
import 'package:dungeon_paper/app/data/models/move.dart';
|
|
import 'package:dungeon_paper/app/data/models/race.dart';
|
|
import 'package:dungeon_paper/app/data/models/spell.dart';
|
|
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
|
import 'package:dungeon_paper/app/data/services/library_provider.dart';
|
|
import 'package:dungeon_paper/app/model_utils/character_utils.dart';
|
|
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
|
import 'package:dungeon_paper/app/modules/BioForm/controllers/bio_form_controller.dart';
|
|
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
|
import 'package:dungeon_paper/app/modules/LibraryList/views/items_library_list_view.dart';
|
|
import 'package:dungeon_paper/app/modules/LibraryList/views/moves_library_list_view.dart';
|
|
import 'package:dungeon_paper/app/modules/LibraryList/views/spells_library_list_view.dart';
|
|
import 'package:dungeon_paper/app/routes/app_pages.dart';
|
|
import 'package:dungeon_paper/app/widgets/atoms/checklist_menu_entry.dart';
|
|
import 'package:dungeon_paper/app/widgets/atoms/menu_button.dart';
|
|
import 'package:dungeon_paper/app/widgets/cards/alignment_value_card.dart';
|
|
import 'package:dungeon_paper/app/widgets/cards/item_card.dart';
|
|
import 'package:dungeon_paper/app/widgets/cards/move_card.dart';
|
|
import 'package:dungeon_paper/app/widgets/cards/race_card.dart';
|
|
import 'package:dungeon_paper/app/widgets/cards/spell_card.dart';
|
|
import 'package:dungeon_paper/app/widgets/chips/move_category_chip.dart';
|
|
import 'package:dungeon_paper/app/widgets/dialogs/confirm_delete_dialog.dart';
|
|
import 'package:dungeon_paper/app/widgets/menus/entity_edit_menu.dart';
|
|
import 'package:dungeon_paper/app/widgets/menus/group_sort_menu.dart';
|
|
import 'package:dungeon_paper/app/widgets/molecules/categorized_list.dart';
|
|
import 'package:dungeon_paper/core/utils/builder_utils.dart';
|
|
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
|
import 'package:dungeon_paper/i18n.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'local_widgets/home_character_actions_summary.dart';
|
|
|
|
class HomeCharacterActionsView extends StatelessWidget
|
|
with CharacterProviderMixin {
|
|
const HomeCharacterActionsView({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PageStorage(
|
|
bucket: PageStorageBucket(),
|
|
child: CharacterProvider.consumer(
|
|
(context, controller, _) {
|
|
if (controller.maybeCurrent == null) {
|
|
return Container();
|
|
}
|
|
final builder = _getBuilder(controller);
|
|
return SizedBox(
|
|
child: builder.asListView(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
ItemBuilder _getBuilder(CharacterProvider ctrl) {
|
|
final char = ctrl.current;
|
|
return ItemBuilder.builder(
|
|
leadingBuilder: (context, index) => const HomeCharacterActionsSummary(),
|
|
leadingCount: 1,
|
|
itemCount: char.actionCategories.length,
|
|
itemBuilder: (context, index) {
|
|
return Center(
|
|
child: SizedBox(
|
|
width: 800,
|
|
child: Builder(
|
|
builder: (context) {
|
|
switch (char.actionCategories.elementAt(index)) {
|
|
case 'ClassAction':
|
|
return classActionsList(context, ctrl) ??
|
|
const SizedBox.shrink();
|
|
case 'Move':
|
|
return movesList(context, ctrl) ?? const SizedBox.shrink();
|
|
case 'Spell':
|
|
return spellsList(context, ctrl) ?? const SizedBox.shrink();
|
|
case 'Item':
|
|
return itemsList(context, ctrl) ?? const SizedBox.shrink();
|
|
}
|
|
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget? classActionsList(BuildContext context, CharacterProvider controller) {
|
|
if (char.settings.actionCategories.hidden.contains('ClassAction')) {
|
|
return null;
|
|
}
|
|
final raceCard = RaceCard(
|
|
race: char.race,
|
|
onSave: (race) => controller.updateCharacter(
|
|
char.copyWithInherited(race: race),
|
|
),
|
|
actions: [
|
|
EntityEditMenu(
|
|
onDelete: null,
|
|
onEdit: () => ModelPages.openRacePage(
|
|
context,
|
|
race: char.race,
|
|
abilityScores: char.abilityScores,
|
|
onSave: (race) => controller.updateCharacter(
|
|
char.copyWithInherited(race: race),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
final alignmentCard = AlignmentValueCard(
|
|
alignment: char.bio.alignment,
|
|
actions: [
|
|
EntityEditMenu(
|
|
onDelete: null,
|
|
onEdit: () => Navigator.of(context).pushNamed(
|
|
Routes.bio,
|
|
arguments: BioFormArguments(character: char),
|
|
),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => CharacterUtils.addXP(context, char, 1),
|
|
child: Text(tr.actions.classActions.markXP.button),
|
|
),
|
|
],
|
|
);
|
|
final list = [
|
|
raceCard,
|
|
alignmentCard,
|
|
];
|
|
final index = char.actionCategories.toList().indexOf('ClassAction');
|
|
return CategorizedList(
|
|
initiallyExpanded: true,
|
|
title: Text(tr.actions.classActions.title),
|
|
titleTrailing: [
|
|
GroupSortMenu(
|
|
index: index,
|
|
totalItemCount: Character.allActionCategories.length,
|
|
onReorder: _onReorder,
|
|
)
|
|
],
|
|
itemPadding: const EdgeInsets.only(bottom: 8),
|
|
children: [
|
|
...list.map(
|
|
(obj) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
key: PageStorageKey('type-ClassAction-${obj.key}'),
|
|
child: obj,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget? movesList(BuildContext context, CharacterProvider controller) {
|
|
if (char.settings.actionCategories.hidden.contains('Move')) {
|
|
return null;
|
|
}
|
|
return ActionsCardList<Move>(
|
|
index: char.actionCategories.toList().indexOf('Move'),
|
|
typeName: tn(Move),
|
|
onReorder: _onReorder,
|
|
list: char.moves,
|
|
route: Routes.moves,
|
|
leading: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: () => _openBasicMoves(context),
|
|
child: Text(
|
|
tr.actions.moves.basic,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: () => _openSpecialMoves(context),
|
|
child: Text(
|
|
tr.actions.moves.special,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 0),
|
|
const Divider(height: 1),
|
|
const SizedBox(height: 4),
|
|
],
|
|
menuTrailing: [
|
|
if (char.settings.racePosition != RacePosition.start)
|
|
// Move to start of list
|
|
MenuEntry(
|
|
value: 'move_to_start',
|
|
label: Text(tr.sort.moveEntityToTop(tr.entity(tn(Race)))),
|
|
onSelect: () => controller.updateCharacter(
|
|
char.copyWith(
|
|
settings:
|
|
char.settings.copyWith(racePosition: RacePosition.start),
|
|
),
|
|
),
|
|
),
|
|
if (char.settings.racePosition != RacePosition.end)
|
|
// Move to end of list
|
|
MenuEntry(
|
|
value: 'move_to_end',
|
|
label: Text(tr.sort.moveEntityToBottom(tr.entity(tn(Race)))),
|
|
onSelect: () => controller.updateCharacter(
|
|
char.copyWith(
|
|
settings:
|
|
char.settings.copyWith(racePosition: RacePosition.end),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
addPageArguments: ({required onSelected}) => MoveLibraryListArguments(
|
|
character: char,
|
|
onSelected: onSelected,
|
|
preSelections: char.moves,
|
|
),
|
|
cardBuilder: (move, {required onSave, required onDelete}) => MoveCard(
|
|
reorderablePadding: true,
|
|
move: move,
|
|
advancedLevelDisplay: AdvancedLevelDisplay.none,
|
|
abilityScores: char.abilityScores,
|
|
actions: [
|
|
EntityEditMenu(
|
|
onDelete: onDelete,
|
|
onEdit: () => ModelPages.openMovePage(
|
|
context,
|
|
move: move,
|
|
abilityScores: char.abilityScores,
|
|
onSave: onSave(true),
|
|
),
|
|
),
|
|
],
|
|
onSave: onSave(false),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget? spellsList(BuildContext context, CharacterProvider controller) {
|
|
if (char.settings.actionCategories.hidden.contains('Spell')) {
|
|
return null;
|
|
}
|
|
return ActionsCardList<Spell>(
|
|
index: char.actionCategories.toList().indexOf('Spell'),
|
|
typeName: tn(Spell),
|
|
onReorder: _onReorder,
|
|
list: char.spells,
|
|
route: Routes.spells,
|
|
addPageArguments: ({required onSelected}) => SpellLibraryListArguments(
|
|
character: char,
|
|
onSelected: onSelected,
|
|
preSelections: char.spells,
|
|
),
|
|
cardBuilder: (spell, {required onSave, required onDelete}) => SpellCard(
|
|
reorderablePadding: true,
|
|
spell: spell,
|
|
abilityScores: char.abilityScores,
|
|
actions: [
|
|
EntityEditMenu(
|
|
onDelete: onDelete,
|
|
onEdit: () => ModelPages.openSpellPage(
|
|
context,
|
|
spell: spell,
|
|
classKeys: spell.classKeys,
|
|
abilityScores: char.abilityScores,
|
|
onSave: onSave(true),
|
|
),
|
|
),
|
|
],
|
|
onSave: onSave(false),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget? itemsList(BuildContext context, CharacterProvider controller) {
|
|
if (char.settings.actionCategories.hidden.contains('Item')) {
|
|
return null;
|
|
}
|
|
return ActionsCardList<Item>(
|
|
index: char.actionCategories.toList().indexOf('Item'),
|
|
typeName: tn(Item),
|
|
onReorder: _onReorder,
|
|
list: char.items,
|
|
route: Routes.items,
|
|
addPageArguments: ({required onSelected}) => ItemLibraryListArguments(
|
|
onSelected: (items) => onSelected(
|
|
items
|
|
.map(
|
|
(x) =>
|
|
x.copyWithInherited(amount: x.amount == 0 ? 1 : x.amount),
|
|
)
|
|
.toList(),
|
|
),
|
|
preSelections: char.items,
|
|
),
|
|
cardBuilder: (item, {required onSave, required onDelete}) => ItemCard(
|
|
reorderablePadding: true,
|
|
item: item,
|
|
actions: [
|
|
EntityEditMenu(
|
|
onDelete: onDelete,
|
|
onEdit: () => ModelPages.openItemPage(
|
|
context,
|
|
item: item,
|
|
onSave: onSave(true),
|
|
),
|
|
leading: [
|
|
ChecklistMenuEntry(
|
|
value: 'countArmor',
|
|
checked: item.settings.countArmor,
|
|
label: Text(tr.items.settings.countArmor),
|
|
onChanged: (value) => onSave(false)(
|
|
item.copyWithInherited(
|
|
settings: item.settings.copyWith(countArmor: value!),
|
|
),
|
|
),
|
|
),
|
|
ChecklistMenuEntry(
|
|
value: 'countDamage',
|
|
checked: item.settings.countDamage,
|
|
label: Text(tr.items.settings.countDamage),
|
|
onChanged: (value) => onSave(false)(
|
|
item.copyWithInherited(
|
|
settings: item.settings.copyWith(countDamage: value!),
|
|
),
|
|
),
|
|
),
|
|
ChecklistMenuEntry(
|
|
value: 'countWeight',
|
|
checked: item.settings.countWeight,
|
|
label: Text(tr.items.settings.countWeight),
|
|
onChanged: (value) => onSave(false)(
|
|
item.copyWithInherited(
|
|
settings: item.settings.copyWith(countWeight: value!),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
onSave: onSave(false),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onReorder(BuildContext context, int oldIndex, int newIndex) {
|
|
charProvider.updateCharacter(
|
|
char.copyWith(
|
|
settings: char.settings.copyWith(
|
|
actionCategories: char.settings.actionCategories.copyWithInherited(
|
|
sortOrder: Set.from(
|
|
reorder(
|
|
char.actionCategories.toList(),
|
|
oldIndex,
|
|
newIndex,
|
|
useReorderableOffset: false,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _openBasicMoves(BuildContext context) {
|
|
ModelPages.openMovesList(
|
|
context,
|
|
category: MoveCategory.basic,
|
|
initialTab: FiltersGroup.playbook,
|
|
abilityScores: char.abilityScores,
|
|
);
|
|
}
|
|
|
|
void _openSpecialMoves(BuildContext context) {
|
|
ModelPages.openMovesList(
|
|
context,
|
|
category: MoveCategory.special,
|
|
initialTab: FiltersGroup.playbook,
|
|
abilityScores: char.abilityScores,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ActionsCardList<T extends WithMeta> extends StatelessWidget
|
|
with CharacterProviderMixin {
|
|
const ActionsCardList({
|
|
super.key,
|
|
required this.route,
|
|
required this.typeName,
|
|
required this.addPageArguments,
|
|
required this.cardBuilder,
|
|
required this.list,
|
|
required this.index,
|
|
required this.onReorder,
|
|
this.menuLeading = const [],
|
|
this.menuTrailing = const [],
|
|
this.leading = const [],
|
|
this.trailing = const [],
|
|
});
|
|
|
|
final String route;
|
|
final String typeName;
|
|
final List<Widget> leading;
|
|
final List<Widget> trailing;
|
|
final LibraryListArguments<T, EntityFilters<T>> Function({
|
|
required void Function(Iterable<T> obj) onSelected,
|
|
}) addPageArguments;
|
|
final Widget Function(
|
|
T object, {
|
|
required void Function() onDelete,
|
|
required void Function(T object) Function(bool fork) onSave,
|
|
// required void Function() onEdit,
|
|
}) cardBuilder;
|
|
final List<T> list;
|
|
final int index;
|
|
final void Function(BuildContext context, int oldIndex, int newIndex)
|
|
onReorder;
|
|
final List<MenuEntry<String>> menuLeading;
|
|
final List<MenuEntry<String>> menuTrailing;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return CharacterProvider.consumer((context, controller, _) {
|
|
debugPrint('ActionsCardList rebuild');
|
|
debugPrint(
|
|
'Character items: \n${controller.current.items.map((e) => '- ${e.displayName}').join('\n')}');
|
|
return CategorizedList(
|
|
initiallyExpanded: true,
|
|
title: Text(tr.entityPlural(typeName)),
|
|
itemPadding: const EdgeInsets.only(bottom: 8),
|
|
titleTrailing: [
|
|
LibraryProvider.consumer(
|
|
(context, library, _) => TextButton.icon(
|
|
onPressed: () => Navigator.pushNamed(
|
|
context,
|
|
route,
|
|
arguments: addPageArguments(
|
|
onSelected: (items) => library.upsertToCharacter(items,
|
|
forkBehavior: ForkBehavior.fork),
|
|
),
|
|
),
|
|
label: Text(tr.generic.addEntity(tr.entityPlural(typeName))),
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
),
|
|
GroupSortMenu(
|
|
index: index,
|
|
totalItemCount: Character.allActionCategories.length,
|
|
onReorder: onReorder,
|
|
leading: menuLeading,
|
|
trailing: menuTrailing,
|
|
)
|
|
],
|
|
leading: leading.map((obj) => _wrapChild(child: obj)).toList(),
|
|
trailing: trailing.map((obj) => _wrapChild(child: obj)).toList(),
|
|
children: [
|
|
...list.map(
|
|
(obj) {
|
|
return _wrapChild(
|
|
key: PageStorageKey('type-$T-${obj.key}'),
|
|
child: cardBuilder(
|
|
obj,
|
|
onDelete: _confirmDeleteDlg(context, obj, obj.displayName),
|
|
onSave: (fork) => (obj) {
|
|
final library = LibraryProvider.of(context);
|
|
library.upsertToCharacter([obj],
|
|
forkBehavior: ForkBehavior.none);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
onReorder: (oldIndex, newIndex) => controller.updateCharacter(
|
|
CharacterUtils.reorderByType<T>(
|
|
controller.current, oldIndex, newIndex)),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _wrapChild({Key? key, required Widget child}) => Padding(
|
|
key: key,
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: child,
|
|
);
|
|
|
|
void Function() _confirmDeleteDlg(
|
|
BuildContext context,
|
|
T object,
|
|
String name,
|
|
) {
|
|
return () {
|
|
awaitDeleteConfirmation<T>(
|
|
context,
|
|
name,
|
|
() => charProvider
|
|
.updateCharacter(CharacterUtils.removeByType<T>(char, [object])),
|
|
);
|
|
};
|
|
}
|
|
}
|