feat: home cards list view

This commit is contained in:
2025-04-22 01:52:17 +03:00
parent 4ee262d17e
commit f8df1ecd16
11 changed files with 723 additions and 497 deletions

View File

@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
enum RacePosition { start, end }
enum FavoritesView { cards, list }
@immutable
class CharacterSettings {
const CharacterSettings({
@@ -17,6 +19,7 @@ class CharacterSettings {
this.racePosition = RacePosition.start,
this.lightTheme,
this.darkTheme,
this.favoritesView = FavoritesView.cards,
});
final NoteCategoryList noteCategories;
@@ -29,6 +32,7 @@ class CharacterSettings {
final RacePosition racePosition;
final int? lightTheme;
final int? darkTheme;
final FavoritesView favoritesView;
CharacterSettings copyWith({
int? sortOrder,
@@ -40,18 +44,19 @@ class CharacterSettings {
RacePosition? racePosition,
int? lightTheme,
int? darkTheme,
}) =>
CharacterSettings(
sortOrder: sortOrder ?? this.sortOrder,
category: category ?? this.category,
rollButtons: rollButtons ?? this.rollButtons,
actionCategories: actionCategories ?? this.actionCategories,
noteCategories: noteCategories ?? this.noteCategories,
racePosition: racePosition ?? this.racePosition,
quickCategories: quickCategories ?? this.quickCategories,
lightTheme: lightTheme ?? this.lightTheme,
darkTheme: darkTheme ?? this.darkTheme,
);
FavoritesView? favoritesView,
}) => CharacterSettings(
sortOrder: sortOrder ?? this.sortOrder,
category: category ?? this.category,
rollButtons: rollButtons ?? this.rollButtons,
actionCategories: actionCategories ?? this.actionCategories,
noteCategories: noteCategories ?? this.noteCategories,
racePosition: racePosition ?? this.racePosition,
quickCategories: quickCategories ?? this.quickCategories,
lightTheme: lightTheme ?? this.lightTheme,
darkTheme: darkTheme ?? this.darkTheme,
favoritesView: favoritesView ?? this.favoritesView,
);
factory CharacterSettings.fromRawJson(String str) =>
CharacterSettings.fromJson(json.decode(str));
@@ -60,57 +65,56 @@ class CharacterSettings {
factory CharacterSettings.fromJson(Map<String, dynamic> json) =>
CharacterSettings(
noteCategories: json['noteCategories'] != null
? NoteCategoryList.fromJson(json['noteCategories'])
: const NoteCategoryList(sortOrder: {}),
actionCategories: json['actionCategories'] != null
? ActionCategoryList.fromJson(json['actionCategories'])
: const ActionCategoryList(
sortOrder: {},
hidden: {},
),
quickCategories: json['actionCategories'] != null
? ActionCategoryList.fromJson(json['actionCategories'])
: const ActionCategoryList(
sortOrder: {},
hidden: {},
),
noteCategories:
json['noteCategories'] != null
? NoteCategoryList.fromJson(json['noteCategories'])
: const NoteCategoryList(sortOrder: {}),
actionCategories:
json['actionCategories'] != null
? ActionCategoryList.fromJson(json['actionCategories'])
: const ActionCategoryList(sortOrder: {}, hidden: {}),
quickCategories:
json['actionCategories'] != null
? ActionCategoryList.fromJson(json['actionCategories'])
: const ActionCategoryList(sortOrder: {}, hidden: {}),
sortOrder: json['sortOrder'],
category: json['category'],
rollButtons: List<RollButton?>.from((json['rollButtons'] ?? [])
.map((x) => x != null ? RollButton.fromJson(x) : null)),
rollButtons: List<RollButton?>.from(
(json['rollButtons'] ?? []).map(
(x) => x != null ? RollButton.fromJson(x) : null,
),
),
racePosition: RacePosition.values.firstWhere(
(element) => element.name == json['racePosition'],
orElse: () => RacePosition.start,
),
lightTheme: json['lightTheme'],
darkTheme: json['darkTheme'],
favoritesView: FavoritesView.values.firstWhere(
(element) => element.name == json['favoritesView'],
orElse: () => FavoritesView.cards,
),
);
factory CharacterSettings.empty() => const CharacterSettings(
rollButtons: [],
noteCategories: NoteCategoryList(sortOrder: {}),
actionCategories: ActionCategoryList(
sortOrder: {},
hidden: {},
),
quickCategories: ActionCategoryList(
sortOrder: {},
hidden: {},
),
);
rollButtons: [],
noteCategories: NoteCategoryList(sortOrder: {}),
actionCategories: ActionCategoryList(sortOrder: {}, hidden: {}),
quickCategories: ActionCategoryList(sortOrder: {}, hidden: {}),
);
Map<String, dynamic> toJson() => {
'sortOrder': sortOrder,
'category': category,
'rollButtons': List<dynamic>.from(rollButtons.map((x) => x?.toJson())),
'noteCategories': noteCategories.toJson(),
'actionCategories': actionCategories.toJson(),
'quickCategories': quickCategories.toJson(),
'racePosition': racePosition.name,
'lightTheme': lightTheme,
'darkTheme': darkTheme,
};
'sortOrder': sortOrder,
'category': category,
'rollButtons': List<dynamic>.from(rollButtons.map((x) => x?.toJson())),
'noteCategories': noteCategories.toJson(),
'actionCategories': actionCategories.toJson(),
'quickCategories': quickCategories.toJson(),
'racePosition': racePosition.name,
'lightTheme': lightTheme,
'darkTheme': darkTheme,
'favoritesView': favoritesView.name,
};
CharacterSettings copyWithThemes({int? lightTheme, int? darkTheme}) =>
CharacterSettings(
@@ -123,10 +127,11 @@ class CharacterSettings {
noteCategories: noteCategories,
racePosition: racePosition,
quickCategories: quickCategories,
favoritesView: favoritesView,
);
String get debugProperties =>
'sortOrder: $sortOrder, category: $category, rollButtons: $rollButtons, noteCategories: $noteCategories, actionCategories: $actionCategories, quickCategories: $quickCategories, racePosition: $racePosition, lightTheme: $lightTheme, darkTheme: $darkTheme';
'sortOrder: $sortOrder, category: $category, rollButtons: $rollButtons, noteCategories: $noteCategories, actionCategories: $actionCategories, quickCategories: $quickCategories, racePosition: $racePosition, lightTheme: $lightTheme, darkTheme: $darkTheme, favoritesView: $favoritesView';
@override
String toString() => 'CharacterSettings($debugProperties)';
@@ -144,20 +149,22 @@ class CharacterSettings {
quickCategories == other.quickCategories &&
racePosition == other.racePosition &&
lightTheme == other.lightTheme &&
darkTheme == other.darkTheme;
darkTheme == other.darkTheme &&
favoritesView == other.favoritesView;
@override
int get hashCode => Object.hashAll([
sortOrder,
category,
rollButtons,
noteCategories,
actionCategories,
quickCategories,
racePosition,
lightTheme,
darkTheme,
]);
sortOrder,
category,
rollButtons,
noteCategories,
actionCategories,
quickCategories,
racePosition,
lightTheme,
darkTheme,
favoritesView,
]);
}
@immutable
@@ -175,11 +182,11 @@ class OrderedCategoryList<T> {
final bool canHide;
Map<String, dynamic> toJson() => {
// 'categories': List<dynamic>.from(categories),
'hidden': List<dynamic>.from(hidden),
'sortOrder': List<dynamic>.from(sortOrder),
'canHide': canHide,
};
// 'categories': List<dynamic>.from(categories),
'hidden': List<dynamic>.from(hidden),
'sortOrder': List<dynamic>.from(sortOrder),
'canHide': canHide,
};
factory OrderedCategoryList.fromRawJson(String str) =>
OrderedCategoryList.fromJson(json.decode(str));
@@ -196,23 +203,18 @@ class OrderedCategoryList<T> {
Set<String>? hidden,
Set<String>? sortOrder,
bool? canHide,
}) =>
OrderedCategoryList(
hidden: hidden ?? this.hidden,
sortOrder: sortOrder ?? this.sortOrder,
canHide: canHide ?? this.canHide,
);
}) => OrderedCategoryList(
hidden: hidden ?? this.hidden,
sortOrder: sortOrder ?? this.sortOrder,
canHide: canHide ?? this.canHide,
);
Set<T> getSorted([Set<T> all = const {}]) => <T>{
Set<T> getSorted([Set<T> all = const {}]) =>
<T>{
...sortOrder,
...all,
// .toList()..sort(stringSorter(order: SortOrder.asc))
}
.where(
(cat) => !hidden.contains(cat),
)
.toSet()
.cast<T>();
}.where((cat) => !hidden.contains(cat)).toSet().cast<T>();
@override
bool operator ==(Object other) =>
@@ -224,11 +226,7 @@ class OrderedCategoryList<T> {
canHide == other.canHide;
@override
int get hashCode => Object.hashAll([
hidden,
sortOrder,
canHide,
]);
int get hashCode => Object.hashAll([hidden, sortOrder, canHide]);
String get debugProperties =>
'hidden: $hidden, sortOrder: $sortOrder, canHide: $canHide';
@@ -248,17 +246,12 @@ class NoteCategoryList extends OrderedCategoryList<String> {
NoteCategoryList.fromJson(json.decode(str));
factory NoteCategoryList.fromJson(Map<String, dynamic> json) =>
NoteCategoryList(
sortOrder: Set<String>.from(json['sortOrder']),
);
NoteCategoryList(sortOrder: Set<String>.from(json['sortOrder']));
NoteCategoryList copyWithInherited({
// Set<String>? categories,
Set<String>? sortOrder,
}) =>
NoteCategoryList(
sortOrder: sortOrder ?? this.sortOrder,
);
}) => NoteCategoryList(sortOrder: sortOrder ?? this.sortOrder);
@override
String get debugProperties => 'sortOrder: $sortOrder';
@@ -288,11 +281,10 @@ class ActionCategoryList extends OrderedCategoryList<String> {
// Set<String>? categories,
Set<String>? sortOrder,
Set<String>? hidden,
}) =>
ActionCategoryList(
sortOrder: sortOrder ?? this.sortOrder,
hidden: hidden ?? this.hidden,
);
}) => ActionCategoryList(
sortOrder: sortOrder ?? this.sortOrder,
hidden: hidden ?? this.hidden,
);
@override
Map<String, dynamic> toJson() {

View File

@@ -1,3 +1,4 @@
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';
@@ -25,6 +26,7 @@ import 'package:dungeon_paper/app/widgets/cards/spell_card.dart';
import 'package:dungeon_paper/app/widgets/cards/spell_card_mini.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/core/utils/list_utils.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
@@ -58,14 +60,14 @@ class HomeCharacterDynamicCards extends StatelessWidget
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...{...controller.current.actionCategories, 'Note'}
.map((cat) => _buildList(context, controller, cat))
.map((cat) => _buildListByType(context, controller, cat))
.reduce((value, element) => value + element),
],
),
);
}
List<Widget> _buildList(
List<Widget> _buildListByType(
BuildContext context,
CharacterProvider controller,
String cat,
@@ -169,8 +171,9 @@ class HomeCharacterDynamicCards extends StatelessWidget
List<Widget> classActionCards(
BuildContext context,
CharacterProvider charProvider,
) {
CharacterProvider charProvider, {
required bool expandable,
}) {
final raceCard = RaceCard(
race: char.race,
onSave:
@@ -194,6 +197,8 @@ class HomeCharacterDynamicCards extends StatelessWidget
);
final alignmentCard = AlignmentValueCard(
alignment: char.bio.alignment,
expandable: expandable,
initiallyExpanded: !expandable,
actions: [
EntityEditMenu(
onDelete: null,
@@ -225,376 +230,373 @@ class HomeCharacterDynamicCards extends StatelessWidget
}, T);
}
List<Widget> _notesList(BuildContext context, CharacterProvider controller) {
List<Widget> _listViewBuilder<T extends WithMeta>(
BuildContext context,
CharacterProvider controller, {
required List<T> list,
required Widget Function(BuildContext, LibraryProvider, T) cardBuilder,
required Widget Function(BuildContext, LibraryProvider, T, VoidCallback)
cardMiniBuilder,
required Widget Function(BuildContext, LibraryProvider, T)
expandedCardBuilder,
required String title,
}) {
if (list.isEmpty) {
return [];
}
return [
if (notes.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(tr.home.categories.notes),
),
],
HorizontalCardListView<Note>(
cardSize: cardSize,
items: notes,
cardBuilder:
(context, note, index, onTap) => NoteCardMini(
note: notes[index],
onTap: onTap,
onSave:
(note) => controller.updateCharacter(
CharacterUtils.updateNotes(controller.current, [note]),
),
),
expandedCardBuilder:
(context, note, index) =>
notes.isNotEmpty && index < notes.length
? NoteCard(
maxContentHeight: maxContentHeight(context),
expandable: false,
initiallyExpanded: true,
note: notes[index],
actions: [
EntityEditMenu(
onEdit:
() => ModelPages.openNotePage(
context,
note: notes[index],
onSave:
(note) => controller.updateCharacter(
CharacterUtils.updateNotes(
controller.current,
[note],
),
),
),
onDelete: _delete(
context,
note,
note.title,
tn(Note),
() => controller.updateCharacter(
CharacterUtils.removeNotes(controller.current, [
note,
]),
),
),
),
],
onSave: (note) {
controller.updateCharacter(
CharacterUtils.updateNotes(controller.current, [
note,
]),
);
if (!note.favorite) {
Navigator.of(context).pop();
}
},
)
: const SizedBox.shrink(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).copyWith(top: 10),
child: Text(title),
),
];
}
List<Widget> _movesList(BuildContext context, CharacterProvider controller) {
return [
if (moves.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(tr.home.categories.moves),
),
Builder(
builder: (context) {
return HorizontalCardListView<Move>(
if (char.settings.favoritesView == FavoritesView.list) {
return Column(
children: [
for (final item in list)
Padding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
child: LibraryProvider.consumer(
(context, library, _) =>
cardBuilder(context, library, item),
),
),
],
);
}
return HorizontalCardListView<T>(
cardSize: cardSize,
items: moves,
items: list,
cardBuilder:
(context, move, index, onTap) => MoveCardMini(
move: moves[index],
onTap: onTap,
onSave:
(move) => controller.updateCharacter(
CharacterUtils.updateMoves(controller.current, [move]),
),
abilityScores: controller.current.abilityScores,
(context, item, index, onTap) => LibraryProvider.consumer(
(context, library, _) =>
cardMiniBuilder(context, library, item, onTap),
),
expandedCardBuilder:
(context, move, index) =>
LibraryProvider.consumer((context, library, _) {
return moves.isNotEmpty && index < moves.length
? MoveCard(
maxContentHeight: maxContentHeight(context),
expandable: false,
initiallyExpanded: true,
move: moves[index],
abilityScores: controller.current.abilityScores,
actions: [
EntityEditMenu(
onEdit:
() => ModelPages.openMovePage(
context,
abilityScores:
controller.current.abilityScores,
move: moves[index],
onSave:
(move) => library.upsertToCharacter(
[move],
forkBehavior:
ForkBehavior.increaseVersion,
),
),
onDelete: _delete(
context,
move,
move.name,
tn(Move),
() => controller.updateCharacter(
CharacterUtils.removeMoves(
controller.current,
[move],
),
),
),
),
],
onSave: (move) {
controller.updateCharacter(
CharacterUtils.updateMoves(controller.current, [
move,
]),
);
if (!move.favorite) {
Navigator.of(context).pop();
}
},
)
: const SizedBox.shrink();
}),
// leading: raceCardMini != null &&
// controller.current.settings.racePosition ==
// RacePosition.start
// ? [raceCardMini]
// : [],
// trailing: raceCardMini != null &&
// controller.current.settings.racePosition ==
// RacePosition.end
// ? [raceCardMini]
// : [],
(context, item, index) => LibraryProvider.consumer(
(context, library, _) =>
list.isNotEmpty && index < list.length
? expandedCardBuilder(context, library, item)
: SizedBox.shrink(),
),
);
},
),
];
}
List<Widget> _spellsList(BuildContext context, CharacterProvider controller) {
return [
if (spells.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(tr.home.categories.spells),
List<Widget> _notesList(BuildContext context, CharacterProvider controller) {
onSave(Note note) => controller.updateCharacter(
CharacterUtils.updateNotes(controller.current, [note]),
);
actions(LibraryProvider library, Note note) => [
EntityEditMenu(
onEdit:
() => ModelPages.openNotePage(context, note: note, onSave: onSave),
onDelete: _delete(
context,
note,
note.title,
tn(Note),
() => controller.updateCharacter(
CharacterUtils.removeNotes(controller.current, [note]),
),
),
],
HorizontalCardListView<Spell>(
cardSize: cardSize,
items: spells,
cardBuilder:
(context, spell, index, onTap) => SpellCardMini(
spell: spells[index],
onTap: onTap,
onSave:
(spell) => controller.updateCharacter(
CharacterUtils.updateSpells(controller.current, [spell]),
),
abilityScores: controller.current.abilityScores,
),
expandedCardBuilder:
(context, spell, index) =>
spells.isNotEmpty && index < spells.length
? SpellCard(
maxContentHeight: maxContentHeight(context),
expandable: false,
initiallyExpanded: true,
spell: spells[index],
abilityScores: controller.current.abilityScores,
actions: [
EntityEditMenu(
onEdit:
() => ModelPages.openSpellPage(
context,
abilityScores: controller.current.abilityScores,
classKeys: spells[index].classKeys,
spell: spells[index],
onSave:
(spell) => controller.updateCharacter(
CharacterUtils.updateSpells(
controller.current,
[spell],
),
),
),
onDelete: _delete(
context,
spell,
spell.name,
tn(Spell),
() => controller.updateCharacter(
CharacterUtils.removeSpells(controller.current, [
spell,
]),
),
),
),
],
onSave: (spell) {
controller.updateCharacter(
CharacterUtils.updateSpells(controller.current, [
spell,
]),
);
if (!spell.prepared) {
Navigator.of(context).pop();
}
},
)
: const SizedBox.shrink(),
),
];
return _listViewBuilder<Note>(
context,
controller,
title: tr.home.categories.notes,
list: notes,
cardMiniBuilder:
(context, library, note, onTap) =>
NoteCardMini(note: note, onTap: onTap, onSave: onSave),
cardBuilder:
(context, library, note) => NoteCard(
note: note,
onSave: onSave,
actions: actions(library, note),
),
expandedCardBuilder:
(context, library, note) => NoteCard(
maxContentHeight: maxContentHeight(context),
expandable: false,
initiallyExpanded: true,
note: note,
actions: actions(library, note),
onSave: (note) {
onSave(note);
if (!note.favorite) {
Navigator.of(context).pop();
}
},
),
);
}
List<Widget> _movesList(BuildContext context, CharacterProvider controller) {
onSave(Move move) => controller.updateCharacter(
CharacterUtils.updateMoves(controller.current, [move]),
);
actions(LibraryProvider library, Move move) => [
EntityEditMenu(
onEdit:
() => ModelPages.openMovePage(
context,
abilityScores: controller.current.abilityScores,
move: move,
onSave: onSave,
),
onDelete: _delete(
context,
move,
move.name,
tn(Move),
() => controller.updateCharacter(
CharacterUtils.removeMoves(controller.current, [move]),
),
),
),
];
return _listViewBuilder<Move>(
context,
controller,
title: tr.home.categories.moves,
list: moves,
cardMiniBuilder:
(context, library, move, onTap) => MoveCardMini(
move: move,
onTap: onTap,
onSave: onSave,
abilityScores: controller.current.abilityScores,
),
cardBuilder:
(context, library, move) => MoveCard(
move: move,
onSave: onSave,
abilityScores: controller.current.abilityScores,
actions: actions(library, move),
),
expandedCardBuilder:
(context, library, move) => MoveCard(
maxContentHeight: maxContentHeight(context),
expandable: false,
initiallyExpanded: true,
move: move,
abilityScores: controller.current.abilityScores,
actions: actions(library, move),
onSave: (move) {
controller.updateCharacter(
CharacterUtils.updateMoves(controller.current, [move]),
);
if (!move.favorite) {
Navigator.of(context).pop();
}
},
),
);
}
List<Widget> _spellsList(BuildContext context, CharacterProvider controller) {
onSave(Spell spell) => controller.updateCharacter(
CharacterUtils.updateSpells(controller.current, [spell]),
);
actions(LibraryProvider library, Spell spell) => [
EntityEditMenu(
onEdit:
() => ModelPages.openSpellPage(
context,
abilityScores: controller.current.abilityScores,
classKeys: spell.classKeys,
spell: spell,
onSave: onSave,
),
onDelete: _delete(
context,
spell,
spell.name,
tn(Spell),
() => controller.updateCharacter(
CharacterUtils.removeSpells(controller.current, [spell]),
),
),
),
];
return _listViewBuilder<Spell>(
context,
controller,
title: tr.home.categories.spells,
list: spells,
cardMiniBuilder:
(context, library, spell, onTap) => SpellCardMini(
spell: spell,
onTap: onTap,
onSave: onSave,
abilityScores: controller.current.abilityScores,
),
cardBuilder:
(context, library, spell) => SpellCard(
spell: spell,
onSave: onSave,
abilityScores: controller.current.abilityScores,
actions: actions(library, spell),
),
expandedCardBuilder:
(context, library, spell) => SpellCard(
maxContentHeight: maxContentHeight(context),
expandable: false,
initiallyExpanded: true,
spell: spell,
abilityScores: controller.current.abilityScores,
actions: actions(library, spell),
onSave: (spell) {
onSave(spell);
if (!spell.prepared) {
Navigator.of(context).pop();
}
},
),
);
}
List<Widget> _itemsList(BuildContext context, CharacterProvider controller) {
return [
if (items.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(tr.home.categories.items),
),
],
HorizontalCardListView<Item>(
cardSize: cardSize,
items: items,
cardBuilder:
(context, item, index, onTap) => ItemCardMini(
item: items[index],
onTap: onTap,
onSave(Item item) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [item]),
);
actions(LibraryProvider library, Item item) => [
EntityEditMenu(
onEdit:
() => ModelPages.openItemPage(
context,
item: item,
onSave:
(item) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [item]),
),
(item) => library.upsertToCharacter([
item,
], forkBehavior: ForkBehavior.increaseVersion),
),
expandedCardBuilder:
(context, item, index) =>
items.isNotEmpty && index < items.length
? ItemCard(
maxContentHeight: maxContentHeight(context),
expandable: false,
initiallyExpanded: true,
item: items[index],
actions: [
EntityEditMenu(
onEdit:
() => ModelPages.openItemPage(
context,
item: items[index],
onSave:
(item) => controller.updateCharacter(
CharacterUtils.updateItems(
controller.current,
[item],
),
),
),
onDelete: _delete(
context,
item,
item.name,
tn(Item),
() => controller.updateCharacter(
CharacterUtils.removeItems(controller.current, [
item,
]),
),
),
leading: [
ChecklistMenuEntry(
value: 'countArmor',
checked: item.settings.countArmor,
label: Text(tr.items.settings.countArmor),
onChanged:
(value) => controller.updateCharacter(
CharacterUtils.updateItems(
controller.current,
[
item.copyWithInherited(
settings: item.settings.copyWith(
countArmor: value!,
),
),
],
),
),
),
ChecklistMenuEntry(
value: 'countDamage',
checked: item.settings.countDamage,
label: Text(tr.items.settings.countDamage),
onChanged:
(value) => controller.updateCharacter(
CharacterUtils.updateItems(
controller.current,
[
item.copyWithInherited(
settings: item.settings.copyWith(
countDamage: value!,
),
),
],
),
),
),
ChecklistMenuEntry(
value: 'countWeight',
checked: item.settings.countWeight,
label: Text(tr.items.settings.countWeight),
onChanged:
(value) => controller.updateCharacter(
CharacterUtils.updateItems(
controller.current,
[
item.copyWithInherited(
settings: item.settings.copyWith(
countWeight: value!,
),
),
],
),
),
),
],
),
],
onSave: (item) {
controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [
item,
]),
);
if (!item.equipped) {
Navigator.of(context).pop();
}
},
)
: const SizedBox.shrink(),
onDelete: _delete(
context,
item,
item.name,
tn(Item),
() => controller.updateCharacter(
CharacterUtils.removeItems(controller.current, [item]),
),
),
leading: [
ChecklistMenuEntry(
value: 'countArmor',
checked: item.settings.countArmor,
label: Text(tr.items.settings.countArmor),
onChanged:
(value) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [
item.copyWithInherited(
settings: item.settings.copyWith(countArmor: value!),
),
]),
),
),
ChecklistMenuEntry(
value: 'countDamage',
checked: item.settings.countDamage,
label: Text(tr.items.settings.countDamage),
onChanged:
(value) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [
item.copyWithInherited(
settings: item.settings.copyWith(countDamage: value!),
),
]),
),
),
ChecklistMenuEntry(
value: 'countWeight',
checked: item.settings.countWeight,
label: Text(tr.items.settings.countWeight),
onChanged:
(value) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [
item.copyWithInherited(
settings: item.settings.copyWith(countWeight: value!),
),
]),
),
),
],
),
];
return _listViewBuilder<Item>(
context,
controller,
title: tr.home.categories.items,
list: items,
cardMiniBuilder:
(context, library, item, onTap) =>
ItemCardMini(item: item, onTap: onTap, onSave: onSave),
cardBuilder:
(context, library, item) => ItemCard(
item: item,
onSave: onSave,
actions: actions(library, item),
),
expandedCardBuilder:
(context, library, item) => ItemCard(
maxContentHeight: maxContentHeight(context),
expandable: false,
initiallyExpanded: true,
item: item,
actions: actions(library, item),
onSave: (item) {
controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [item]),
);
if (!item.equipped) {
Navigator.of(context).pop();
}
},
),
);
}
List<Widget> _classActionsList(
BuildContext context,
CharacterProvider controller,
) {
if (controller.current.classActions.isEmpty) {
return [];
}
if (controller.current.settings.favoritesView == FavoritesView.list) {
return [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(tr.home.categories.classActions),
),
Column(
children: [
for (final item in enumerate(classActions))
Padding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
child: LibraryProvider.consumer(
(context, library, _) =>
classActionCards(
context,
controller,
expandable: true,
)[item.index],
),
),
],
),
];
}
return [
const SizedBox(height: 10),
Padding(
@@ -609,7 +611,7 @@ class HomeCharacterDynamicCards extends StatelessWidget
classActionCardsMini(context, controller)[index],
expandedCardBuilder:
(context, item, index) =>
classActionCards(context, controller)[index],
classActionCards(context, controller, expandable: false)[index],
),
];
}

View File

@@ -10,6 +10,7 @@ import 'package:dungeon_paper/app/routes/app_pages.dart';
import 'package:dungeon_paper/app/widgets/atoms/menu_button.dart';
import 'package:dungeon_paper/app/widgets/dialogs/character_bio_dialog.dart';
import 'package:dungeon_paper/app/widgets/dialogs/character_bonds_flags_dialog.dart';
import 'package:dungeon_paper/app/widgets/dialogs/character_favorites_view_select_dialog.dart';
import 'package:dungeon_paper/app/widgets/dialogs/custom_roll_buttons_dialog.dart';
import 'package:dungeon_paper/app/widgets/dialogs/debilities_dialog.dart';
import 'package:dungeon_paper/core/dw_icons.dart';
@@ -43,8 +44,9 @@ class HomeCharacterExtras extends StatelessWidget with CharacterProviderMixin {
MenuEntry(
value: 'class',
icon: Icon(CharacterClass.genericIcon),
label:
Text(tr.generic.changeEntity(tr.entity(tn(CharacterClass)))),
label: Text(
tr.generic.changeEntity(tr.entity(tn(CharacterClass))),
),
onSelect: () => _openCharClass(context),
),
MenuEntry(
@@ -53,6 +55,35 @@ class HomeCharacterExtras extends StatelessWidget with CharacterProviderMixin {
label: Text(tr.generic.changeEntity(tr.entity(tn(Race)))),
onSelect: () => _openRace(context),
),
],
),
IconButton(
icon: const Icon(Icons.text_snippet),
tooltip: tr.home.menu.bio,
onPressed: () => _openBio(context),
),
CharacterProvider.consumer(
(context, controller, _) => IconButton(
onPressed: () => _openBondsFlags(context),
icon: Transform.scale(
scaleX: -1,
child: const Icon(Icons.handshake),
),
tooltip: SessionMark.categoryTitle(
bonds: controller.maybeCurrent?.bonds ?? [],
flags: controller.maybeCurrent?.flags ?? [],
),
),
),
IconButton(
onPressed: () => _openDebilities(context),
icon: const Icon(Icons.personal_injury),
tooltip: tr.home.menu.debilities,
),
MenuButton<String>(
icon: const Icon(Icons.settings),
tooltip: tr.home.menu.character.settings,
items: [
MenuEntry(
value: 'roll_buttons',
icon: const Icon(DwIcons.dice_d6),
@@ -65,34 +96,20 @@ class HomeCharacterExtras extends StatelessWidget with CharacterProviderMixin {
label: Text(tr.home.menu.character.theme),
onSelect: () => _openThemeSelect(context),
),
MenuEntry(
value: 'favorites_view',
icon: const Icon(Icons.grid_view),
label: Text(tr.home.menu.character.favoritesView),
onSelect: () => _openFavoritesViewSelect(context),
),
],
),
IconButton(
icon: const Icon(Icons.text_snippet),
tooltip: tr.home.menu.bio,
onPressed: () => _openBio(context),
),
CharacterProvider.consumer(
(context, controller, _) => IconButton(
onPressed: () => _openBondsFlags(context),
icon:
Transform.scale(scaleX: -1, child: const Icon(Icons.handshake)),
tooltip: SessionMark.categoryTitle(
bonds: controller.maybeCurrent?.bonds ?? [],
flags: controller.maybeCurrent?.flags ?? [],
),
),
),
IconButton(
onPressed: () => _openDebilities(context),
icon: const Icon(Icons.personal_injury),
tooltip: tr.home.menu.debilities,
),
IconButton(
onPressed: null,
icon: const Icon(Icons.groups),
tooltip: tr.entityPlural(tn(Campaign)),
),
// IconButton(
// onPressed: null,
// icon: const Icon(Icons.groups),
// tooltip: tr.entityPlural(tn(Campaign)),
// ),
],
);
}
@@ -102,8 +119,10 @@ class HomeCharacterExtras extends StatelessWidget with CharacterProviderMixin {
Routes.abilityScores,
arguments: AbilityScoresFormArguments(
abilityScores: charProvider.current.abilityScores,
onChanged: (abilityScores) => charProvider.updateCharacter(
charProvider.current.copyWith(abilityScores: abilityScores)),
onChanged:
(abilityScores) => charProvider.updateCharacter(
charProvider.current.copyWith(abilityScores: abilityScores),
),
),
);
}
@@ -112,9 +131,13 @@ class HomeCharacterExtras extends StatelessWidget with CharacterProviderMixin {
Navigator.of(context).pushNamed(
Routes.basicInfo,
arguments: BasicInfoFormArguments(
onChanged: (name, avatar) => charProvider.updateCharacter(
charProvider.current.copyWith(displayName: name, avatarUrl: avatar),
),
onChanged:
(name, avatar) => charProvider.updateCharacter(
charProvider.current.copyWith(
displayName: name,
avatarUrl: avatar,
),
),
name: charProvider.current.displayName,
avatarUrl: charProvider.current.avatarUrl,
),
@@ -130,12 +153,14 @@ class HomeCharacterExtras extends StatelessWidget with CharacterProviderMixin {
context,
character: charProvider.current,
preSelection: charProvider.current.race,
onSelected: (race) => charProvider.updateCharacter(
charProvider.current.copyWithInherited(
race: race.copyWithInherited(
favorite: charProvider.current.race.favorite),
),
),
onSelected:
(race) => charProvider.updateCharacter(
charProvider.current.copyWithInherited(
race: race.copyWithInherited(
favorite: charProvider.current.race.favorite,
),
),
),
);
}
@@ -143,37 +168,50 @@ class HomeCharacterExtras extends StatelessWidget with CharacterProviderMixin {
ModelPages.openCharacterClassesList(
context,
character: charProvider.current,
onSelected: (cls) => charProvider.updateCharacter(
// TODO add a reset dialog to confirm + ask what to reset: moves, spells, alignment, rac
charProvider.current.copyWithInherited(
characterClass: cls,
),
),
onSelected:
(cls) => charProvider.updateCharacter(
// TODO add a reset dialog to confirm + ask what to reset: moves, spells, alignment, rac
charProvider.current.copyWithInherited(characterClass: cls),
),
);
}
void _openBondsFlags(BuildContext context) {
showDialog(
context: context, builder: (_) => const CharacterBondsFlagsDialog());
context: context,
builder: (_) => const CharacterBondsFlagsDialog(),
);
}
void _openDebilities(BuildContext context) {
showDialog(
context: context, builder: (_) => const CharacterDebilitiesDialog());
context: context,
builder: (_) => const CharacterDebilitiesDialog(),
);
}
void _openRollButtons(BuildContext context) {
showDialog(
context: context,
builder: (_) => CustomRollButtonsDialog(
character: charProvider.current,
onChanged: (rollButtons) => charProvider.updateCharacter(
charProvider.current.copyWith(
settings: charProvider.current.settings
.copyWith(rollButtons: rollButtons),
builder:
(_) => CustomRollButtonsDialog(
character: charProvider.current,
onChanged:
(rollButtons) => charProvider.updateCharacter(
charProvider.current.copyWith(
settings: charProvider.current.settings.copyWith(
rollButtons: rollButtons,
),
),
),
),
),
),
);
}
void _openFavoritesViewSelect(BuildContext context) {
showDialog(
context: context,
builder: (_) => const CharacterFavoritesViewSelectDialog(),
);
}

View File

@@ -0,0 +1,81 @@
import 'package:dungeon_paper/app/data/models/character_settings.dart';
import 'package:dungeon_paper/app/data/services/character_provider.dart';
import 'package:dungeon_paper/app/widgets/molecules/dialog_controls.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
class CharacterFavoritesViewSelectDialog extends StatefulWidget {
const CharacterFavoritesViewSelectDialog({super.key});
@override
State<CharacterFavoritesViewSelectDialog> createState() =>
_CharacterFavoritesViewSelectDialogState();
}
class _CharacterFavoritesViewSelectDialogState
extends State<CharacterFavoritesViewSelectDialog>
with CharacterProviderMixin {
late FavoritesView value;
@override
void initState() {
super.initState();
value = char.settings.favoritesView;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Select View'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile.adaptive(
value: FavoritesView.cards,
groupValue: value,
onChanged: _onSelect,
title: Row(
children: [
Icon(Icons.grid_view),
const SizedBox(width: 8),
Expanded(child: Text(tr.character.favoritesView.cards)),
],
),
),
RadioListTile.adaptive(
value: FavoritesView.list,
groupValue: value,
onChanged: _onSelect,
title: Row(
children: [
Icon(Icons.list),
const SizedBox(width: 8),
Expanded(child: Text(tr.character.favoritesView.list)),
],
),
),
],
),
),
actions: DialogControls.save(
context,
onSave: () {
charProvider.updateCharacter(
char.copyWith(
settings: char.settings.copyWith(favoritesView: value),
),
);
Navigator.of(context).pop();
},
onCancel: () => Navigator.of(context).pop(),
),
);
}
void _onSelect(FavoritesView? value) {
setState(() {
this.value = value!;
});
}
}

View File

@@ -1239,6 +1239,16 @@ class CharacterMenuHomeMessages {
/// "Character Theme"
/// ```
String get theme => """Character Theme""";
/// ```dart
/// "View Settings"
/// ```
String get settings => """View Settings""";
/// ```dart
/// "Change Favorites View"
/// ```
String get favoritesView => """Change Favorites View""";
}
class EmptyStateHomeMessages {
@@ -1432,6 +1442,8 @@ class CharacterMessages {
/// ```
String get noCategory => """No Category""";
ThemeCharacterMessages get theme => ThemeCharacterMessages(this);
FavoritesViewCharacterMessages get favoritesView =>
FavoritesViewCharacterMessages(this);
}
class DataCharacterMessages {
@@ -1536,6 +1548,21 @@ class ThemeCharacterMessages {
String get defaultDark => """${_defaultTheme('dark')}""";
}
class FavoritesViewCharacterMessages {
final CharacterMessages _parent;
const FavoritesViewCharacterMessages(this._parent);
/// ```dart
/// "Cards View"
/// ```
String get cards => """Cards View""";
/// ```dart
/// "List View"
/// ```
String get list => """List View""";
}
class CharacterClassMessages {
final Messages _parent;
const CharacterClassMessages(this._parent);
@@ -4145,6 +4172,8 @@ Map<String, String> get messagesMap => {
"""home.menu.character.abilityScores""": """Ability Scores""",
"""home.menu.character.customRolls""": """Quick-Roll Buttons""",
"""home.menu.character.theme""": """Character Theme""",
"""home.menu.character.settings""": """View Settings""",
"""home.menu.character.favoritesView""": """Change Favorites View""",
"""home.menu.bio""": """Character Biography""",
"""home.menu.debilities""": """Debilities""",
"""home.emptyState.guest.title""": """Sign in to get more features""",
@@ -4188,6 +4217,8 @@ Map<String, String> get messagesMap => {
"""character.header.separator""": """""",
"""character.noCategory""": """No Category""",
"""character.theme.title""": """Character Theme""",
"""character.favoritesView.cards""": """Cards View""",
"""character.favoritesView.list""": """List View""",
"""characterClass.baseLoad""": """Base Load""",
"""characterClass.baseHp""": """Base HP""",
"""characterClass.damageDice""": """Damage Dice""",

View File

@@ -247,6 +247,8 @@ home:
abilityScores: Ability Scores
customRolls: Quick-Roll Buttons
theme: Character Theme
settings: View Settings
favoritesView: Change Favorites View
bio: Character Biography
debilities: Debilities
emptyState:
@@ -307,6 +309,9 @@ character:
_defaultTheme(String type): Default $type theme
defaultLight: "${_defaultTheme('light')}"
defaultDark: "${_defaultTheme('dark')}"
favoritesView:
cards: Cards View
list: List View
characterClass:
baseLoad: Base Load

View File

@@ -1251,6 +1251,16 @@ class CharacterMenuHomeMessagesPlPL extends CharacterMenuHomeMessages {
/// "Character Theme"
/// ```
String get theme => """Character Theme""";
/// ```dart
/// "View Settings"
/// ```
String get settings => """View Settings""";
/// ```dart
/// "Change Favorites View"
/// ```
String get favoritesView => """Change Favorites View""";
}
class EmptyStateHomeMessagesPlPL extends EmptyStateHomeMessages {
@@ -1445,6 +1455,8 @@ class CharacterMessagesPlPL extends CharacterMessages {
/// ```
String get noCategory => """No Category""";
ThemeCharacterMessagesPlPL get theme => ThemeCharacterMessagesPlPL(this);
FavoritesViewCharacterMessagesPlPL get favoritesView =>
FavoritesViewCharacterMessagesPlPL(this);
}
class DataCharacterMessagesPlPL extends DataCharacterMessages {
@@ -1549,6 +1561,22 @@ class ThemeCharacterMessagesPlPL extends ThemeCharacterMessages {
String get defaultDark => """${_defaultTheme('dark')}""";
}
class FavoritesViewCharacterMessagesPlPL
extends FavoritesViewCharacterMessages {
final CharacterMessagesPlPL _parent;
const FavoritesViewCharacterMessagesPlPL(this._parent) : super(_parent);
/// ```dart
/// "Cards View"
/// ```
String get cards => """Cards View""";
/// ```dart
/// "List View"
/// ```
String get list => """List View""";
}
class CharacterClassMessagesPlPL extends CharacterClassMessages {
final MessagesPlPL _parent;
const CharacterClassMessagesPlPL(this._parent) : super(_parent);
@@ -4259,6 +4287,8 @@ Map<String, String> get messagesPlPLMap => {
"""home.menu.character.abilityScores""": """Ability Scores""",
"""home.menu.character.customRolls""": """Quick-Roll Buttons""",
"""home.menu.character.theme""": """Character Theme""",
"""home.menu.character.settings""": """View Settings""",
"""home.menu.character.favoritesView""": """Change Favorites View""",
"""home.menu.bio""": """Character Biography""",
"""home.menu.debilities""": """Debilities""",
"""home.emptyState.guest.title""": """Sign in to get more features""",
@@ -4302,6 +4332,8 @@ Map<String, String> get messagesPlPLMap => {
"""character.header.separator""": """""",
"""character.noCategory""": """No Category""",
"""character.theme.title""": """Character Theme""",
"""character.favoritesView.cards""": """Cards View""",
"""character.favoritesView.list""": """List View""",
"""characterClass.baseLoad""": """Base Load""",
"""characterClass.baseHp""": """Base HP""",
"""characterClass.damageDice""": """Damage Dice""",

View File

@@ -247,6 +247,8 @@ home:
abilityScores: Ability Scores
customRolls: Quick-Roll Buttons
theme: Character Theme
settings: View Settings
favoritesView: Change Favorites View
bio: Character Biography
debilities: Debilities
emptyState:
@@ -307,6 +309,9 @@ character:
_defaultTheme(String type): Default $type theme
defaultLight: "${_defaultTheme('light')}"
defaultDark: "${_defaultTheme('dark')}"
favoritesView:
cards: Cards View
list: List View
characterClass:
baseLoad: Base Load

View File

@@ -1254,6 +1254,11 @@ class MenuHomeMessagesPtBR extends MenuHomeMessages {
CharacterMenuHomeMessagesPtBR get character =>
CharacterMenuHomeMessagesPtBR(this);
/// ```dart
/// "Change Favorites View"
/// ```
String get favoritesView => """Change Favorites View""";
/// ```dart
/// "Biografia do Personagem"
/// ```
@@ -1293,6 +1298,11 @@ class CharacterMenuHomeMessagesPtBR extends CharacterMenuHomeMessages {
/// "Tema do Personagem"
/// ```
String get theme => """Tema do Personagem""";
/// ```dart
/// "View Settings"
/// ```
String get settings => """View Settings""";
}
class EmptyStateHomeMessagesPtBR extends EmptyStateHomeMessages {
@@ -1488,6 +1498,8 @@ class CharacterMessagesPtBR extends CharacterMessages {
/// ```
String get noCategory => """Sem Categoria""";
ThemeCharacterMessagesPtBR get theme => ThemeCharacterMessagesPtBR(this);
FavoritesViewCharacterMessagesPtBR get favoritesView =>
FavoritesViewCharacterMessagesPtBR(this);
}
class DataCharacterMessagesPtBR extends DataCharacterMessages {
@@ -1592,6 +1604,22 @@ class ThemeCharacterMessagesPtBR extends ThemeCharacterMessages {
String get defaultDark => """${_defaultTheme('escuro')}""";
}
class FavoritesViewCharacterMessagesPtBR
extends FavoritesViewCharacterMessages {
final CharacterMessagesPtBR _parent;
const FavoritesViewCharacterMessagesPtBR(this._parent) : super(_parent);
/// ```dart
/// "Cards View"
/// ```
String get cards => """Cards View""";
/// ```dart
/// "List View"
/// ```
String get list => """List View""";
}
class CharacterClassMessagesPtBR extends CharacterClassMessages {
final MessagesPtBR _parent;
const CharacterClassMessagesPtBR(this._parent) : super(_parent);
@@ -4302,6 +4330,8 @@ Map<String, String> get messagesPtBRMap => {
"""home.menu.character.abilityScores""": """Pontuações de Habilidade""",
"""home.menu.character.customRolls""": """Botões de Rolagem Rápida""",
"""home.menu.character.theme""": """Tema do Personagem""",
"""home.menu.character.settings""": """View Settings""",
"""home.menu.favoritesView""": """Change Favorites View""",
"""home.menu.bio""": """Biografia do Personagem""",
"""home.menu.debilities""": """Debilidades""",
"""home.emptyState.guest.title""": """Entre para obter mais recursos""",
@@ -4345,6 +4375,8 @@ Map<String, String> get messagesPtBRMap => {
"""character.header.separator""": """""",
"""character.noCategory""": """Sem Categoria""",
"""character.theme.title""": """Tema do Personagem""",
"""character.favoritesView.cards""": """Cards View""",
"""character.favoritesView.list""": """List View""",
"""characterClass.baseLoad""": """Carga Base""",
"""characterClass.baseHp""": """HP Base""",
"""characterClass.damageDice""": """Dados de Dano""",

View File

@@ -267,6 +267,9 @@ home:
abilityScores: Pontuações de Habilidade
customRolls: Botões de Rolagem Rápida
theme: Tema do Personagem
# TODO translate
settings: View Settings
favoritesView: Change Favorites View
bio: Biografia do Personagem
debilities: Debilidades
emptyState:
@@ -327,6 +330,10 @@ character:
_defaultTheme(String type): Tema padrão $type
defaultLight: "${_defaultTheme('claro')}"
defaultDark: "${_defaultTheme('escuro')}"
# TODO translate
favoritesView:
cards: Cards View
list: List View
characterClass:
baseLoad: Carga Base

View File

@@ -37,6 +37,7 @@ void main() async {
options.dsn = secrets.sentryDsn;
options.tracesSampleRate = kDebugMode ? 1.0 : 0.0;
options.environment = kDebugMode ? 'development' : 'release';
options.debug = false;
},
appRunner: _init,
);