refactor: i18n (#30)

* feat: intl poc

* refactor: more translation migrations

* refactor: move `tr` export to nicer path

* refactor: more translation updates

* feat: intl on many more files

* feat: more i18n replacements

* feat: more translation updates

* feat: replace all left usages of S.current

* chore: last forgotten cleanups

* refactor: remove old intl files

* chore: format all files

* docs: update docs

chore: use git version of i18n
This commit is contained in:
Chen Asraf
2023-12-09 01:09:30 +02:00
committed by GitHub
parent c00b0522f7
commit 905ed49245
231 changed files with 7420 additions and 11564 deletions

View File

@@ -82,13 +82,29 @@ don't hesitate to open an appropriate issue and I will do my best to reply promp
}
```
Sentry DSN can remain empty to skip error reporting
Sentry DSN can remain empty to skip error reporting.
1. To run build scripts, install [script_runner](https://pub.dev/packages/script_runner) and use
`scr -h` to see all available commands
As mentioned above, Firebase secret keys must be your own, and so are the databases and services
related to them. This project uses Firebase auth, Firestore, and Crashlytics.
related to them. This project uses Firebase auth and Cloud Firestore.
### Translations
This app is currently only available in English. However, it's possible to contribute translations
if you wish to help localize the app to your language. The app should be fully-localizable easily by
just updating the translation files.
- The current main translations file is at `lib/i18n/messages.i18n.dart`
- To add a new localization file, copy this file to `lib/i18n/messages_<lang code>.i18n.dart` (for
example, for Hebrew you would use `messages_he.i18n.dart`)
- For help using the translation syntax, see the [i18n docs](https://github.com/MohiuddinM/i18n)
- To translate the playbook data (classes, items, moves, spells, etc) we must localize a separate
package containing all the Dungeon World base data. This package is
[available here](https://github.com/DungeonPaper/dungeon_world_data), see the localization docs
there for more help.
- The app and data may be translated separately and do not depend on each other.
### Help by bug reporting or requesting features

View File

@@ -2,7 +2,7 @@ import 'dart:convert';
import 'package:dungeon_paper/app/data/models/meta.dart';
import 'package:dungeon_paper/core/utils/icon_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import '../../../core/dw_icons.dart';
@@ -12,7 +12,8 @@ class AbilityScores {
required Iterable<AbilityScore> stats,
}) : stats = stats.toList();
factory AbilityScores.dungeonWorldAll(int value) => AbilityScores.dungeonWorld(
factory AbilityScores.dungeonWorldAll(int value) =>
AbilityScores.dungeonWorld(
dex: value,
str: value,
wis: value,
@@ -32,57 +33,58 @@ class AbilityScores {
AbilityScores(stats: [
AbilityScore(
key: 'STR',
name: S.current.abilityScoreStrName,
description: S.current.abilityScoreStrDescription,
debilityName: S.current.abilityScoreStrDebilityName,
debilityDescription: S.current.abilityScoreStrDebilityDescription,
name: tr.abilityScores.stats.str.name,
description: tr.abilityScores.stats.str.description,
debilityName: tr.abilityScores.stats.str.debility.name,
debilityDescription: tr.abilityScores.stats.str.debility.description,
value: str,
),
AbilityScore(
key: 'DEX',
name: S.current.abilityScoreDexName,
description: S.current.abilityScoreDexDescription,
debilityName: S.current.abilityScoreDexDebilityName,
debilityDescription: S.current.abilityScoreDexDebilityDescription,
name: tr.abilityScores.stats.dex.name,
description: tr.abilityScores.stats.dex.description,
debilityName: tr.abilityScores.stats.dex.debility.name,
debilityDescription: tr.abilityScores.stats.dex.debility.description,
value: dex,
),
AbilityScore(
key: 'CON',
name: S.current.abilityScoreConName,
description: S.current.abilityScoreConDescription,
debilityName: S.current.abilityScoreConDebilityName,
debilityDescription: S.current.abilityScoreConDebilityDescription,
name: tr.abilityScores.stats.con.name,
description: tr.abilityScores.stats.con.description,
debilityName: tr.abilityScores.stats.con.debility.name,
debilityDescription: tr.abilityScores.stats.con.debility.description,
value: con,
),
AbilityScore(
key: 'INT',
name: S.current.abilityScoreIntName,
description: S.current.abilityScoreIntDescription,
debilityName: S.current.abilityScoreIntDebilityName,
debilityDescription: S.current.abilityScoreIntDebilityDescription,
name: tr.abilityScores.stats.intl.name,
description: tr.abilityScores.stats.intl.description,
debilityName: tr.abilityScores.stats.intl.debility.name,
debilityDescription: tr.abilityScores.stats.intl.debility.description,
value: intl,
),
AbilityScore(
key: 'WIS',
name: S.current.abilityScoreWisName,
description: S.current.abilityScoreWisDescription,
debilityName: S.current.abilityScoreWisDebilityName,
debilityDescription: S.current.abilityScoreWisDebilityDescription,
name: tr.abilityScores.stats.wis.name,
description: tr.abilityScores.stats.wis.description,
debilityName: tr.abilityScores.stats.wis.debility.name,
debilityDescription: tr.abilityScores.stats.wis.debility.description,
value: wis,
),
AbilityScore(
key: 'CHA',
name: S.current.abilityScoreChaName,
description: S.current.abilityScoreChaDescription,
debilityName: S.current.abilityScoreChaDebilityName,
debilityDescription: S.current.abilityScoreChaDebilityDescription,
name: tr.abilityScores.stats.cha.name,
description: tr.abilityScores.stats.cha.description,
debilityName: tr.abilityScores.stats.cha.debility.name,
debilityDescription: tr.abilityScores.stats.cha.debility.description,
value: cha,
),
]);
final List<AbilityScore> stats;
Map<String, AbilityScore> get statsMap => Map.fromIterable(stats, key: (s) => s.key);
Map<String, AbilityScore> get statsMap =>
Map.fromIterable(stats, key: (s) => s.key);
AbilityScores copyWith({
Iterable<AbilityScore>? stats,
@@ -91,15 +93,22 @@ class AbilityScores {
AbilityScores copyWithStatValues(Map<String, int> map) => copyWith(
stats: stats.map(
(stat) => map.containsKey(stat.key) ? stat.copyWith(value: map[stat.key]) : stat,
(stat) => map.containsKey(stat.key)
? stat.copyWith(value: map[stat.key])
: stat,
),
);
AbilityScores copyWithDebilities(Iterable<String> keys, {required bool isDebilitated}) => copyWith(
stats: stats.map((e) => keys.contains(e.key) ? e.copyWith(isDebilitated: isDebilitated) : e),
AbilityScores copyWithDebilities(Iterable<String> keys,
{required bool isDebilitated}) =>
copyWith(
stats: stats.map((e) => keys.contains(e.key)
? e.copyWith(isDebilitated: isDebilitated)
: e),
);
factory AbilityScores.fromRawJson(String str) => AbilityScores.fromJson(json.decode(str));
factory AbilityScores.fromRawJson(String str) =>
AbilityScores.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
@@ -108,12 +117,12 @@ class AbilityScores {
if (statKey == 'BOND') {
return AbilityScore(
key: 'BOND',
name: S.current.abilityScoreBondName,
name: tr.abilityScores.stats.bond.name,
value: 10,
isDebilitated: false,
description: S.current.abilityScoreBondDescription,
debilityName: S.current.abilityScoreBondDebilityName,
debilityDescription: S.current.abilityScoreBondDebilityDescription,
description: tr.abilityScores.stats.bond.description,
debilityName: tr.abilityScores.stats.bond.debility.name,
debilityDescription: tr.abilityScores.stats.bond.debility.description,
);
}
if (!statsMap.containsKey(statKey)) {
@@ -147,7 +156,8 @@ class AbilityScores {
int get loadBaseValue => str?.modifier ?? 0;
factory AbilityScores.fromJson(Map<String, dynamic> json) => AbilityScores(
stats: List<AbilityScore>.from(json['stats'].map((x) => AbilityScore.fromJson(x))),
stats: List<AbilityScore>.from(
json['stats'].map((x) => AbilityScore.fromJson(x))),
);
Map<String, dynamic> toJson() => {
@@ -156,12 +166,16 @@ class AbilityScores {
@override
bool operator ==(Object? other) =>
identical(this, other) || other is AbilityScores && runtimeType == other.runtimeType && stats == other.stats;
identical(this, other) ||
other is AbilityScores &&
runtimeType == other.runtimeType &&
stats == other.stats;
@override
int get hashCode => Object.hashAll(stats);
String get debugProperties => stats.map((s) => '${s.key}: ${s.value}').join(', ');
String get debugProperties =>
stats.map((s) => '${s.key}: ${s.value}').join(', ');
@override
String toString() => 'AbilityScores($debugProperties)';
@@ -244,12 +258,14 @@ class AbilityScore with WithIcon, WithKey {
icon: icon ?? customIcon,
);
int get modifier => isDebilitated ? modifierForValue(value) - 1 : modifierForValue(value);
int get modifier =>
isDebilitated ? modifierForValue(value) - 1 : modifierForValue(value);
@override
IconData get icon => customIcon ?? iconFor(key);
static IconData iconFor(String key) => _icons[key.toLowerCase()] ?? _icons['_other']!;
static IconData iconFor(String key) =>
_icons[key.toLowerCase()] ?? _icons['_other']!;
static int modifierForValue(int value) {
var modifiers = {1: -3, 4: -2, 6: -1, 9: 0, 13: 1, 16: 2, 18: 3};
@@ -291,7 +307,15 @@ class AbilityScore with WithIcon, WithKey {
debilityDescription == other.debilityDescription;
@override
int get hashCode => Object.hashAll([key, name, description, value, isDebilitated, debilityName, debilityDescription]);
int get hashCode => Object.hashAll([
key,
name,
description,
value,
isDebilitated,
debilityName,
debilityDescription
]);
String get debugProperties =>
'key: $key, value: $value, name: $name, description: $description, isDebilitated: $isDebilitated, debilityName: $debilityName, debilityDescription: $debilityDescription';

View File

@@ -17,16 +17,27 @@ class AlignmentValue extends dw.Alignment with WithIcon implements WithMeta {
Meta get meta => _meta;
final Meta _meta;
static final allKeys = <String>['good', 'lawful', 'neutral', 'chaotic', 'evil'];
static final allKeys = <String>[
'good',
'lawful',
'neutral',
'chaotic',
'evil'
];
factory AlignmentValue.fromRawJson(String str) => AlignmentValue.fromJson(json.decode(str));
factory AlignmentValue.fromRawJson(String str) =>
AlignmentValue.fromJson(json.decode(str));
factory AlignmentValue.fromDwAlignmentValue(dw.Alignment original) =>
AlignmentValue(meta: Meta.empty(createdBy: '__repo__'), type: original.type, description: original.description);
AlignmentValue(
meta: Meta.empty(createdBy: '__repo__'),
type: original.type,
description: original.description);
factory AlignmentValue.fromJson(Map<String, dynamic> json) => AlignmentValue(
meta: Meta.tryParse(json['_meta']),
type: dw.AlignmentType.values.firstWhere((element) => element.name == json['type']),
type: dw.AlignmentType.values
.firstWhere((element) => element.name == json['type']),
description: json['description'],
);
@@ -149,9 +160,11 @@ class AlignmentValues extends dw.AlignmentValues {
chaotic: '',
);
factory AlignmentValues.fromRawJson(String str) => AlignmentValues.fromJson(json.decode(str));
factory AlignmentValues.fromRawJson(String str) =>
AlignmentValues.fromJson(json.decode(str));
factory AlignmentValues.fromJson(Map<String, dynamic> json) => AlignmentValues(
factory AlignmentValues.fromJson(Map<String, dynamic> json) =>
AlignmentValues(
meta: Meta.tryParse(json['_meta']),
good: json['good'],
evil: json['evil'],
@@ -177,7 +190,8 @@ class AlignmentValues extends dw.AlignmentValues {
chaotic: chaotic ?? this.chaotic,
);
factory AlignmentValues.fromDwAlignmentValues(dw.AlignmentValues original) => AlignmentValues(
factory AlignmentValues.fromDwAlignmentValues(dw.AlignmentValues original) =>
AlignmentValues(
meta: Meta.empty(createdBy: '__repo__'),
good: original.good,
evil: original.evil,
@@ -205,7 +219,8 @@ class AlignmentValues extends dw.AlignmentValues {
chaotic == other.chaotic;
@override
int get hashCode => Object.hashAll([meta, good, evil, lawful, neutral, chaotic]);
int get hashCode =>
Object.hashAll([meta, good, evil, lawful, neutral, chaotic]);
@override
String get debugProperties =>

View File

@@ -52,7 +52,8 @@ class Bio {
'alignment': alignment.toJson(),
};
String get debugProperties => 'looks: $looks, description: $description, alignment: $alignment';
String get debugProperties =>
'looks: $looks, description: $description, alignment: $alignment';
@override
String toString() => 'Bio($debugProperties)';

View File

@@ -41,9 +41,12 @@ class Campaign with WithIcon implements WithMeta {
key: json['key'],
name: json['name'],
description: json['description'],
owners: List<dw.EntityReference>.from(json['owners'].map((x) => dw.EntityReference.fromJson(x))),
moderators: List<dw.EntityReference>.from(json['moderators'].map((x) => dw.EntityReference.fromJson(x))),
participants: List<dw.EntityReference>.from(json['participants'].map((x) => dw.EntityReference.fromJson(x))),
owners: List<dw.EntityReference>.from(
json['owners'].map((x) => dw.EntityReference.fromJson(x))),
moderators: List<dw.EntityReference>.from(
json['moderators'].map((x) => dw.EntityReference.fromJson(x))),
participants: List<dw.EntityReference>.from(
json['participants'].map((x) => dw.EntityReference.fromJson(x))),
);
@override

View File

@@ -11,21 +11,21 @@ import 'package:dungeon_paper/core/utils/icon_utils.dart';
import 'package:dungeon_paper/core/utils/list_utils.dart';
import 'package:dungeon_paper/core/utils/math_utils.dart';
import 'package:dungeon_paper/core/utils/uuid.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
import 'package:flutter/material.dart';
import 'ability_scores.dart';
import 'bio.dart';
import 'session_marks.dart';
import 'character_class.dart';
import 'character_settings.dart';
import 'item.dart';
import 'character_stats.dart';
import 'item.dart';
import 'meta.dart';
import 'move.dart';
import 'note.dart';
import 'race.dart';
import 'ability_scores.dart';
import 'session_marks.dart';
import 'spell.dart';
class Character with WithIcon implements WithMeta<Character, CharacterMeta> {
@@ -85,34 +85,38 @@ class Character with WithIcon implements WithMeta<Character, CharacterMeta> {
int get defaultArmor => items.fold(0, (armor, item) => armor + item.armor);
int get damageModifier => items.fold(0, (mod, item) => mod + item.damage);
int getLightTheme(User user) => settings.lightTheme ?? user.settings.defaultLightTheme;
int getDarkTheme(User user) => settings.darkTheme ?? user.settings.defaultDarkTheme;
int getLightTheme(User user) =>
settings.lightTheme ?? user.settings.defaultLightTheme;
int getDarkTheme(User user) =>
settings.darkTheme ?? user.settings.defaultDarkTheme;
int getCurrentTheme(User user) => getThemeForUserBrightness(user);
int getThemeForUserBrightness(User user) =>
user.brightness == Brightness.light ? getLightTheme(user) : getDarkTheme(user);
user.brightness == Brightness.light
? getLightTheme(user)
: getDarkTheme(user);
static RollButton get basicActionRollButton => RollButton(
label: S.current.rollBasicActionButton,
label: tr.customRolls.presets.basicAction,
dice: [dw.Dice.d6 * 2],
specialDice: [],
);
static RollButton get hackAndSlashRollButton => RollButton(
label: S.current.rollAttackDamageButton,
label: tr.customRolls.presets.hackAndSlash,
dice: [dw.Dice.fromJson('2d6+STR')],
specialDice: [SpecialDice.damage],
);
static RollButton get volleyRollButton => RollButton(
label: S.current.rollVolleyButton,
label: tr.customRolls.presets.volley,
dice: [dw.Dice.fromJson('2d6+DEX')],
specialDice: [SpecialDice.damage],
);
static RollButton get discernRealitiesRollButton => RollButton(
label: S.current.rollDiscernRealitiesButton,
label: tr.customRolls.presets.discernRealities,
dice: [dw.Dice.fromJson('2d6+WIS')],
specialDice: [],
);
@@ -132,18 +136,22 @@ class Character with WithIcon implements WithMeta<Character, CharacterMeta> {
rawRollButtons[1] ?? hackAndSlashRollButton,
];
Set<String> get noteCategories =>
settings.noteCategories.getSorted(notes.map((note) => note.localizedCategory).toSet());
Set<String> get noteCategories => settings.noteCategories
.getSorted(notes.map((note) => note.localizedCategory).toSet());
Set<Type> get actionCategories => settings.actionCategories.getSorted(allActionCategories);
Set<Type> get actionCategories =>
settings.actionCategories.getSorted(allActionCategories);
dw.Dice get damageDice => stats.damageDice ?? defaultDamageDice;
dw.Dice get defaultDamageDice => characterClass.damageDice.copyWithModifierValue(damageModifier);
dw.Dice get defaultDamageDice =>
characterClass.damageDice.copyWithModifierValue(damageModifier);
List<SessionMark> get bonds => sessionMarks.where((e) => e.type == dw.SessionMarkType.bond).toList();
List<SessionMark> get bonds =>
sessionMarks.where((e) => e.type == dw.SessionMarkType.bond).toList();
List<SessionMark> get flags => sessionMarks.where((e) => e.type == dw.SessionMarkType.flag).toList();
List<SessionMark> get flags =>
sessionMarks.where((e) => e.type == dw.SessionMarkType.flag).toList();
List<SessionMark> get endOfSessionMarks => sessionMarks
.where((e) => e.type == dw.SessionMarkType.endOfSession)
@@ -154,19 +162,19 @@ class Character with WithIcon implements WithMeta<Character, CharacterMeta> {
SessionMark(
key: uuid(),
type: dw.SessionMarkType.endOfSession,
description: S.current.endOfSessionQ1,
description: tr.sessionMarks.endOfSession.q1,
completed: false,
),
SessionMark(
key: uuid(),
type: dw.SessionMarkType.endOfSession,
description: S.current.endOfSessionQ2,
description: tr.sessionMarks.endOfSession.q2,
completed: false,
),
SessionMark(
key: uuid(),
type: dw.SessionMarkType.endOfSession,
description: S.current.endOfSessionQ3,
description: tr.sessionMarks.endOfSession.q3,
completed: false,
),
];
@@ -262,7 +270,8 @@ class Character with WithIcon implements WithMeta<Character, CharacterMeta> {
],
);
factory Character.fromRawJson(String str) => Character.fromJson(json.decode(str));
factory Character.fromRawJson(String str) =>
Character.fromJson(json.decode(str));
factory Character.empty() {
final rand = Random();
@@ -298,7 +307,8 @@ class Character with WithIcon implements WithMeta<Character, CharacterMeta> {
);
}
factory Character.withClass({required CharacterClass characterClass, Race? race}) {
factory Character.withClass(
{required CharacterClass characterClass, Race? race}) {
return Character.empty().copyWith(
characterClass: characterClass,
race: race ??
@@ -318,11 +328,14 @@ class Character with WithIcon implements WithMeta<Character, CharacterMeta> {
String toRawJson() => json.encode(toJson());
factory Character.fromJson(Map<String, dynamic> json) => Character(
meta: Meta.tryParse(json['_meta'], parseData: (data) => CharacterMeta.fromJson(data)),
meta: Meta.tryParse(json['_meta'],
parseData: (data) => CharacterMeta.fromJson(data)),
key: json['key'],
displayName: json['displayName'],
avatarUrl: json['avatarURL'],
settings: json['settings'] != null ? CharacterSettings.fromJson(json['settings']) : CharacterSettings.empty(),
settings: json['settings'] != null
? CharacterSettings.fromJson(json['settings'])
: CharacterSettings.empty(),
characterClass: CharacterClass.fromJson(json['class']),
moves: List<Move>.from(json['moves'].map((x) => Move.fromJson(x))),
spells: List<Spell>.from(json['spells'].map((x) => Spell.fromJson(x))),
@@ -331,7 +344,8 @@ class Character with WithIcon implements WithMeta<Character, CharacterMeta> {
notes: List<Note>.from(json['notes'].map((x) => Note.fromJson(x))),
stats: CharacterStats.fromJson(json['stats']),
abilityScores: AbilityScores.fromJson(json['abilityScores']),
sessionMarks: List<SessionMark>.from(json['sessionMarks'].map((x) => SessionMark.fromJson(x))),
sessionMarks: List<SessionMark>.from(
json['sessionMarks'].map((x) => SessionMark.fromJson(x))),
bio: Bio.fromJson(json['bio']),
race: Race.fromJson(json['race']),
);
@@ -421,7 +435,8 @@ class CharacterMeta {
CharacterMeta({this.lastUsed});
factory CharacterMeta.fromJson(Map<String, dynamic> json) => CharacterMeta(
lastUsed: json['lastUsed'] != null ? DateTime.parse(json['lastUsed']) : null,
lastUsed:
json['lastUsed'] != null ? DateTime.parse(json['lastUsed']) : null,
);
CharacterMeta copyWith({
@@ -438,7 +453,9 @@ class CharacterMeta {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CharacterMeta && runtimeType == other.runtimeType && lastUsed == other.lastUsed;
other is CharacterMeta &&
runtimeType == other.runtimeType &&
lastUsed == other.lastUsed;
@override
int get hashCode => Object.hashAll([lastUsed]);

View File

@@ -10,7 +10,9 @@ import 'gear_choice.dart';
import 'meta.dart';
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
class CharacterClass extends dw.CharacterClass with WithIcon implements WithMeta {
class CharacterClass extends dw.CharacterClass
with WithIcon
implements WithMeta {
get isApp => true;
CharacterClass({
@@ -86,7 +88,8 @@ class CharacterClass extends dw.CharacterClass with WithIcon implements WithMeta
isSpellcaster: isSpellcaster ?? this.isSpellcaster,
);
factory CharacterClass.fromRawJson(String str) => CharacterClass.fromJson(json.decode(str));
factory CharacterClass.fromRawJson(String str) =>
CharacterClass.fromJson(json.decode(str));
factory CharacterClass.empty() => CharacterClass(
meta: Meta.empty(),
@@ -103,7 +106,8 @@ class CharacterClass extends dw.CharacterClass with WithIcon implements WithMeta
isSpellcaster: false,
);
factory CharacterClass.fromDwCharacterClass(dw.CharacterClass cls) => CharacterClass(
factory CharacterClass.fromDwCharacterClass(dw.CharacterClass cls) =>
CharacterClass(
meta: Meta.tryParse(cls.meta),
name: cls.name,
key: cls.key,
@@ -114,7 +118,8 @@ class CharacterClass extends dw.CharacterClass with WithIcon implements WithMeta
alignments: AlignmentValues.fromDwAlignmentValues(cls.alignments),
bonds: cls.bonds,
flags: cls.flags,
gearChoices: cls.gearChoices.map((c) => GearChoice.fromDwGearChoice(c)).toList(),
gearChoices:
cls.gearChoices.map((c) => GearChoice.fromDwGearChoice(c)).toList(),
isSpellcaster: cls.isSpellcaster,
);
@@ -131,7 +136,8 @@ class CharacterClass extends dw.CharacterClass with WithIcon implements WithMeta
IconData get icon => genericIcon;
static IconData get genericIcon => Icons.person_outline;
static int Function(CharacterClass a, CharacterClass b) sorter(CharacterClassFilters filters) =>
static int Function(CharacterClass a, CharacterClass b) sorter(
CharacterClassFilters filters) =>
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase());
@override
@@ -159,8 +165,19 @@ class CharacterClass extends dw.CharacterClass with WithIcon implements WithMeta
isSpellcaster == other.isSpellcaster;
@override
int get hashCode =>
Object.hashAll([meta, name, key, description, damageDice, load, hp, alignments, bonds, flags, gearChoices]);
int get hashCode => Object.hashAll([
meta,
name,
key,
description,
damageDice,
load,
hp,
alignments,
bonds,
flags,
gearChoices
]);
@override
String get debugProperties =>

View File

@@ -56,11 +56,13 @@ class CharacterSettings {
darkTheme: darkTheme ?? this.darkTheme,
);
factory CharacterSettings.fromRawJson(String str) => CharacterSettings.fromJson(json.decode(str));
factory CharacterSettings.fromRawJson(String str) =>
CharacterSettings.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory CharacterSettings.fromJson(Map<String, dynamic> json) => CharacterSettings(
factory CharacterSettings.fromJson(Map<String, dynamic> json) =>
CharacterSettings(
noteCategories: json['noteCategories'] != null
? NoteCategoryList.fromJson(json['noteCategories'])
: const NoteCategoryList(sortOrder: {}),
@@ -78,8 +80,8 @@ class CharacterSettings {
),
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,
@@ -113,7 +115,8 @@ class CharacterSettings {
'darkTheme': darkTheme,
};
CharacterSettings copyWithThemes({int? lightTheme, int? darkTheme}) => CharacterSettings(
CharacterSettings copyWithThemes({int? lightTheme, int? darkTheme}) =>
CharacterSettings(
lightTheme: lightTheme,
darkTheme: darkTheme,
sortOrder: sortOrder,
@@ -181,9 +184,11 @@ class OrderedCategoryList<T> {
'canHide': canHide,
};
factory OrderedCategoryList.fromRawJson(String str) => OrderedCategoryList.fromJson(json.decode(str));
factory OrderedCategoryList.fromRawJson(String str) =>
OrderedCategoryList.fromJson(json.decode(str));
factory OrderedCategoryList.fromJson(Map<String, dynamic> json) => OrderedCategoryList(
factory OrderedCategoryList.fromJson(Map<String, dynamic> json) =>
OrderedCategoryList(
hidden: Set<T>.from(json['hidden']),
sortOrder: Set<T>.from(json['sortOrder']),
canHide: json['canHide'],
@@ -228,7 +233,8 @@ class OrderedCategoryList<T> {
canHide,
]);
String get debugProperties => 'hidden: $hidden, sortOrder: $sortOrder, canHide: $canHide';
String get debugProperties =>
'hidden: $hidden, sortOrder: $sortOrder, canHide: $canHide';
@override
String toString() => 'OrderedCategoryList($debugProperties)';
@@ -241,9 +247,11 @@ class NoteCategoryList extends OrderedCategoryList<String> {
required super.sortOrder,
}) : super(canHide: false, hidden: const {});
factory NoteCategoryList.fromRawJson(String str) => NoteCategoryList.fromJson(json.decode(str));
factory NoteCategoryList.fromRawJson(String str) =>
NoteCategoryList.fromJson(json.decode(str));
factory NoteCategoryList.fromJson(Map<String, dynamic> json) => NoteCategoryList(
factory NoteCategoryList.fromJson(Map<String, dynamic> json) =>
NoteCategoryList(
sortOrder: Set<String>.from(json['sortOrder']),
);
@@ -270,10 +278,13 @@ class ActionCategoryList extends OrderedCategoryList<Type> {
required super.hidden,
}) : super(canHide: true);
factory ActionCategoryList.fromRawJson(String str) => ActionCategoryList.fromJson(json.decode(str));
factory ActionCategoryList.fromRawJson(String str) =>
ActionCategoryList.fromJson(json.decode(str));
factory ActionCategoryList.fromJson(Map<String, dynamic> json) => ActionCategoryList(
sortOrder: Set<Type>.from((json['sortOrder'] ?? []).map((x) => _toType(x))),
factory ActionCategoryList.fromJson(Map<String, dynamic> json) =>
ActionCategoryList(
sortOrder:
Set<Type>.from((json['sortOrder'] ?? []).map((x) => _toType(x))),
hidden: Set<Type>.from((json['hidden'] ?? []).map((x) => _toType(x))),
);
@@ -297,7 +308,8 @@ class ActionCategoryList extends OrderedCategoryList<Type> {
}
@override
Set<Type> getSorted([Set<Type> all = const {}]) => super.getSorted(all).map((el) => _toType(el.toString())).toSet();
Set<Type> getSorted([Set<Type> all = const {}]) =>
super.getSorted(all).map((el) => _toType(el.toString())).toSet();
@override
String get debugProperties => 'sortOrder: $sortOrder, hidden: $hidden';

View File

@@ -26,7 +26,8 @@ class CharacterStats {
static int maxExpForLevel(int level) => level + 7;
int get totalMaxXp => totalMaxExpForLevel(level);
static int totalMaxExpForLevel(int level) => range(1, level).fold<int>(8, (acc, l) => acc + maxExpForLevel(l + 1));
static int totalMaxExpForLevel(int level) =>
range(1, level).fold<int>(8, (acc, l) => acc + maxExpForLevel(l + 1));
factory CharacterStats.empty() => CharacterStats(
level: 1,
@@ -66,7 +67,8 @@ class CharacterStats {
load: load,
);
factory CharacterStats.fromRawJson(String str) => CharacterStats.fromJson(json.decode(str));
factory CharacterStats.fromRawJson(String str) =>
CharacterStats.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
@@ -76,7 +78,9 @@ class CharacterStats {
currentHp: json['currentHP'],
currentXp: json['currentXP'],
armor: json['armor'],
damageDice: json['damageDice'] != null ? Dice.fromJson(json['damageDice']) : null,
damageDice: json['damageDice'] != null
? Dice.fromJson(json['damageDice'])
: null,
load: json['load'],
);
@@ -134,7 +138,8 @@ class CharacterStats {
load == other.load;
@override
int get hashCode => Object.hashAll([level, maxHp, currentHp, currentXp, armor, damageDice, load]);
int get hashCode => Object.hashAll(
[level, maxHp, currentHp, currentXp, armor, damageDice, load]);
String get debugProperties =>
'level: $level, maxHp: $maxHp, currentHp: $currentHp, currentXp: $currentXp, armor: $armor, damageDice: $damageDice, load: $load';

View File

@@ -38,21 +38,27 @@ class GearChoice extends dw.GearChoice {
maxSelections: maxSelections ?? this.maxSelections,
);
factory GearChoice.fromRawJson(String str) => GearChoice.fromJson(json.decode(str));
factory GearChoice.fromRawJson(String str) =>
GearChoice.fromJson(json.decode(str));
factory GearChoice.fromDwGearChoice(dw.GearChoice gearChoice) => GearChoice(
key: gearChoice.key,
description: gearChoice.description,
selections: gearChoice.selections.map((s) => GearSelection.fromDwGearSelection(s)).toList(),
selections: gearChoice.selections
.map((s) => GearSelection.fromDwGearSelection(s))
.toList(),
preselect: gearChoice.preselect,
maxSelections: gearChoice.maxSelections,
);
factory GearChoice.fromJson(Map<String, dynamic> json) => GearChoice.fromDwGearChoice(dw.GearChoice.fromJson(json));
factory GearChoice.fromJson(Map<String, dynamic> json) =>
GearChoice.fromDwGearChoice(dw.GearChoice.fromJson(json));
@override
List<GearSelection> get preselectedGearSelections =>
super.preselectedGearSelections.map((e) => GearSelection.fromDwGearSelection(e)).toList();
List<GearSelection> get preselectedGearSelections => super
.preselectedGearSelections
.map((e) => GearSelection.fromDwGearSelection(e))
.toList();
@override
Map<String, dynamic> toJson() => {
@@ -60,17 +66,20 @@ class GearChoice extends dw.GearChoice {
'selections': List<dynamic>.from(selections.map((x) => x.toJson())),
};
static List<Item> selectionToItems(List<GearSelection> selections, {bool equipped = false}) =>
static List<Item> selectionToItems(List<GearSelection> selections,
{bool equipped = false}) =>
selections.fold<List<Item>>([], (acc, sel) {
return Item.unifyItems([
...acc,
...sel.options.map(
(e) => Item.fromDwItem(e.item, amount: e.amount, equipped: equipped),
(e) =>
Item.fromDwItem(e.item, amount: e.amount, equipped: equipped),
)
]);
});
static double selectionToCoins(List<GearSelection> selections) => selections.fold(0.0, (acc, sel) => acc + sel.coins);
static double selectionToCoins(List<GearSelection> selections) =>
selections.fold(0.0, (acc, sel) => acc + sel.coins);
@override
bool operator ==(Object? other) =>

View File

@@ -33,7 +33,8 @@ class GearSelection extends dw.GearSelection {
coins: coins ?? this.coins,
);
factory GearSelection.fromRawJson(String str) => GearSelection.fromJson(json.decode(str));
factory GearSelection.fromRawJson(String str) =>
GearSelection.fromJson(json.decode(str));
factory GearSelection.fromDwGearSelection(dw.GearSelection gearSelection) =>
GearSelection.fromJson(gearSelection.toJson());
@@ -41,7 +42,8 @@ class GearSelection extends dw.GearSelection {
factory GearSelection.fromJson(Map<String, dynamic> json) => GearSelection(
key: json['key'],
description: json['description'] ?? '',
options: List<GearOption>.from((json['options'] ?? []).map((x) => GearOption.fromJson(x))),
options: List<GearOption>.from(
(json['options'] ?? []).map((x) => GearOption.fromJson(x))),
coins: json['coins'] ?? 0,
);
@@ -59,7 +61,8 @@ class GearSelection extends dw.GearSelection {
int get hashCode => Object.hashAll([key, description, options, coins]);
@override
String get debugProperties => 'key: $key, description: $description, options: $options, coins: $coins';
String get debugProperties =>
'key: $key, description: $description, options: $options, coins: $coins';
@override
String toString() => 'GearSelection($debugProperties)';

View File

@@ -37,12 +37,16 @@ class Item extends dw.Item with WithIcon implements WithMeta {
final double amount;
final bool equipped;
dw.Tag? findTag(String name) => tags.cast<dw.Tag?>().firstWhereOrNull((tag) => cleanStr(tag?.name ?? '') == name);
dw.Tag? findTag(String name) => tags
.cast<dw.Tag?>()
.firstWhereOrNull((tag) => cleanStr(tag?.name ?? '') == name);
bool get isWorn => findTag('worn') != null;
int get weight => settings.countWeight ? tagIntValue('weight') ?? 0 : 0;
int get armor => settings.countArmor && isWorn && equipped ? tagIntValue('armor') ?? 0 : 0;
int get damage => settings.countDamage && equipped ? tagIntValue('damage') ?? 0 : 0;
int get armor =>
settings.countArmor && isWorn && equipped ? tagIntValue('armor') ?? 0 : 0;
int get damage =>
settings.countDamage && equipped ? tagIntValue('damage') ?? 0 : 0;
int? tagIntValue(String name) {
final tag = findTag(name);
@@ -100,7 +104,8 @@ class Item extends dw.Item with WithIcon implements WithMeta {
description: item.description,
tags: item.tags,
equipped: equipped ?? false,
settings: settings != null ? ItemSettings.fromJson(settings) : ItemSettings(),
settings:
settings != null ? ItemSettings.fromJson(settings) : ItemSettings(),
);
factory Item.fromJson(Map<String, dynamic> json) => Item.fromDwItem(
@@ -140,7 +145,8 @@ class Item extends dw.Item with WithIcon implements WithMeta {
final map = <String, Item>{};
for (final item in items) {
if (map[item.key] != null) {
map[item.key] = map[item.key]!.copyWithInherited(amount: map[item.key]!.amount + 1);
map[item.key] =
map[item.key]!.copyWithInherited(amount: map[item.key]!.amount + 1);
} else {
map[item.key] = item;
}
@@ -168,7 +174,8 @@ class Item extends dw.Item with WithIcon implements WithMeta {
equipped == other.equipped;
@override
int get hashCode => Object.hashAll([key, name, description, tags, settings, amount, equipped]);
int get hashCode => Object.hashAll(
[key, name, description, tags, settings, amount, equipped]);
@override
String get debugProperties =>

View File

@@ -22,7 +22,8 @@ class ItemSettings {
countWeight: countWeight ?? this.countWeight,
);
factory ItemSettings.fromRawJson(String str) => ItemSettings.fromJson(json.decode(str));
factory ItemSettings.fromRawJson(String str) =>
ItemSettings.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
@@ -50,7 +51,8 @@ class ItemSettings {
@override
int get hashCode => Object.hashAll([countArmor, countDamage, countWeight]);
String get debugProperties => 'countArmor: $countArmor, countDamage: $countDamage, countWeight: $countWeight';
String get debugProperties =>
'countArmor: $countArmor, countDamage: $countDamage, countWeight: $countWeight';
@override
String toString() => 'ItemSettings($debugProperties)';

View File

@@ -44,8 +44,12 @@ class Meta<DataType> with RepositoryServiceMixin {
final MetaSharing? sharing;
final DateTime? updated;
M? getLibraryCopy<M extends WithMeta>() =>
repo.my.listByType<M>().entries.toList().firstWhereOrNull((e) => e.value.key == sharing?.sourceKey)?.value;
M? getLibraryCopy<M extends WithMeta>() => repo.my
.listByType<M>()
.entries
.toList()
.firstWhereOrNull((e) => e.value.key == sharing?.sourceKey)
?.value;
bool get isFork => sharing != null;
bool get isSource => !isFork;
@@ -53,7 +57,8 @@ class Meta<DataType> with RepositoryServiceMixin {
bool isForkOf(WithMeta parent) => isFork && sharing!.sourceKey == parent.key;
bool isOwnedBy(User user) => createdBy == user.username;
bool isSourceOf(WithMeta parent) => !isForkOf(parent);
bool isOutOfSyncWith(WithMeta parent) => isForkOf(parent) && sharing!.sourceVersion != version;
bool isOutOfSyncWith(WithMeta parent) =>
isForkOf(parent) && sharing!.sourceVersion != version;
factory Meta.empty({
String? version,
@@ -65,7 +70,8 @@ class Meta<DataType> with RepositoryServiceMixin {
String? language,
}) =>
Meta._(
createdBy: createdBy ?? '', // ?? Get.find<UserService>().current.displayName,
createdBy:
createdBy ?? '', // ?? Get.find<UserService>().current.displayName,
version: version ?? uuid(),
created: created ?? DateTime.now(),
updated: updated,
@@ -113,8 +119,12 @@ class Meta<DataType> with RepositoryServiceMixin {
String toRawJson() => json.encode(toJson());
factory Meta.fromJson(Map<String, dynamic> json, [DataType Function(dynamic json)? parseData]) => Meta._(
created: json['created'] != null ? parseDate(json['created']) : DateTime.now(),
factory Meta.fromJson(Map<String, dynamic> json,
[DataType Function(dynamic json)? parseData]) =>
Meta._(
created: json['created'] != null
? parseDate(json['created'])
: DateTime.now(),
createdBy: json['createdBy'],
data: json['data'] != null
? parseData != null
@@ -123,15 +133,19 @@ class Meta<DataType> with RepositoryServiceMixin {
: null,
language: json['language'],
version: json['version']?.toString() ?? uuid(),
sharing: json['sharing'] != null ? MetaSharing.fromJson(json['sharing']) : null,
sharing: json['sharing'] != null
? MetaSharing.fromJson(json['sharing'])
: null,
updated: json['updated'] != null ? parseDate(json['updated']) : null,
);
factory Meta.tryParse(dynamic meta, {String? owner, DataType Function(dynamic json)? parseData}) => meta != null
? meta is Meta<DataType>
? meta
: Meta.fromJson(meta, parseData)
: Meta.empty(createdBy: owner);
factory Meta.tryParse(dynamic meta,
{String? owner, DataType Function(dynamic json)? parseData}) =>
meta != null
? meta is Meta<DataType>
? meta
: Meta.fromJson(meta, parseData)
: Meta.empty(createdBy: owner);
Map<String, dynamic> toJson([dynamic Function(DataType? data)? dumpData]) => {
'created': created.toString(),
@@ -155,7 +169,8 @@ class Meta<DataType> with RepositoryServiceMixin {
}
/// Returns an item with forked meta, or the same meta if its by the same user
static T forkMeta<T extends WithMeta>(T object, User user, {Meta? meta, String? version}) {
static T forkMeta<T extends WithMeta>(T object, User user,
{Meta? meta, String? version}) {
final Meta _m = (meta ?? object.meta);
// final _o =
// force || _m.createdBy != user.username ? object.copyWithInherited(key: uuid()) : object;
@@ -252,7 +267,17 @@ class Meta<DataType> with RepositoryServiceMixin {
}
static final allStorageKeys = <Type, String>{
for (final t in [CharacterClass, Character, Item, Monster, Move, Spell, Race, Note, dw.Tag])
for (final t in [
CharacterClass,
Character,
Item,
Monster,
Move,
Spell,
Race,
Note,
dw.Tag
])
t: Meta.storageKeyFor(t),
};
@@ -297,7 +322,8 @@ class Meta<DataType> with RepositoryServiceMixin {
language == other.language;
@override
int get hashCode => Object.hashAll([created, createdBy, updated, version, sharing, data, language]);
int get hashCode => Object.hashAll(
[created, createdBy, updated, version, sharing, data, language]);
String get debugProperties =>
'created: $created, createdBy: $createdBy, updated: $updated, version: $version, sharing: $sharing, data: $data, language: $language';
@@ -352,7 +378,8 @@ class MetaSharing {
sourceVersion: sourceVersion ?? this.sourceVersion,
);
factory MetaSharing.fromRawJson(String str) => MetaSharing.fromJson(json.decode(str));
factory MetaSharing.fromRawJson(String str) =>
MetaSharing.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
@@ -401,7 +428,8 @@ class MetaSharing {
sourceVersion == other.sourceVersion;
@override
int get hashCode => Object.hashAll([shared, dirty, sourceKey, sourceOwner, sourceVersion]);
int get hashCode =>
Object.hashAll([shared, dirty, sourceKey, sourceOwner, sourceVersion]);
String get debugProperties =>
'shared: $shared, dirty: $dirty, sourceKey: $sourceKey, sourceOwner: $sourceOwner, sourceVersion: $sourceVersion';
@@ -420,7 +448,8 @@ abstract class MetaInterface<T, M> {
dynamic toJson();
}
mixin WithMeta<T, MetaDataType> implements WithKey, MetaInterface<T, MetaDataType> {
mixin WithMeta<T, MetaDataType>
implements WithKey, MetaInterface<T, MetaDataType> {
abstract final Meta<MetaDataType> meta;
String get displayName;
String get storageKey;

View File

@@ -48,7 +48,8 @@ class Monster extends dw.Monster implements WithMeta {
);
factory Monster.fromRawJson(String str) => Monster.fromJson(json.decode(str));
factory Monster.fromDwMonster(dw.Monster monster) => Monster.fromJson(monster.toJson());
factory Monster.fromDwMonster(dw.Monster monster) =>
Monster.fromJson(monster.toJson());
factory Monster.fromJson(Map<String, dynamic> json) => Monster(
meta: Meta.tryParse(json['_meta']),
@@ -80,7 +81,8 @@ class Monster extends dw.Monster implements WithMeta {
moves == other.moves;
@override
int get hashCode => Object.hashAll([meta, key, name, description, instinct, tags, moves]);
int get hashCode =>
Object.hashAll([meta, key, name, description, instinct, tags, moves]);
@override
String get debugProperties =>

View File

@@ -141,7 +141,17 @@ class Move extends dw.Move with WithIcon implements WithMeta {
category == other.category;
@override
int get hashCode => Object.hashAll([meta, key, name, description, explanation, dice, classKeys, tags, category]);
int get hashCode => Object.hashAll([
meta,
key,
name,
description,
explanation,
dice,
classKeys,
tags,
category
]);
@override
String get debugProperties =>

View File

@@ -24,19 +24,22 @@ class MoveTemplateList {
MoveTemplate(
shortLabel: 'Multi. choice',
longLabel: 'Multiple choice success & trouble',
help: 'Template with multiple options for success,\nand multiple options for trouble.',
help:
'Template with multiple options for success,\nand multiple options for trouble.',
text: _multiBoth,
),
MoveTemplate(
shortLabel: 'Multi. success',
longLabel: 'Multiple choice success',
help: 'Template with multiple options for success,\nbut only one outcome for trouble.',
help:
'Template with multiple options for success,\nbut only one outcome for trouble.',
text: _multiSuccess,
),
MoveTemplate(
shortLabel: 'Multi. trouble',
longLabel: 'Multiple choice trouble',
help: 'Template with multiple options for trouble,\nbut only one outcome for success.',
help:
'Template with multiple options for trouble,\nbut only one outcome for success.',
text: _multiFail,
),
];
@@ -44,21 +47,25 @@ class MoveTemplateList {
static const _blank = '_____'; // or '…'?
static const _bullet = '-';
static const _singleBoth = 'When you $_blank, roll+STAT. On a 10+, you succeed $_blank. '
static const _singleBoth =
'When you $_blank, roll+STAT. On a 10+, you succeed $_blank. '
'On a 7-9, you fail $_blank.';
static const _multiSuccess = 'When you $_blank, roll+STAT. On a 10+, you succeed and choose:\n'
static const _multiSuccess =
'When you $_blank, roll+STAT. On a 10+, you succeed and choose:\n'
' $_bullet $_blank\n'
' $_bullet $_blank\n'
' $_bullet $_blank\n'
'On a 7-9, you fail $_blank.';
static const _multiFail = 'When you $_blank, roll+STAT. On a 10+, you succeed $_blank. On a 7-9, choose one:\n'
static const _multiFail =
'When you $_blank, roll+STAT. On a 10+, you succeed $_blank. On a 7-9, choose one:\n'
' $_bullet $_blank\n'
' $_bullet $_blank\n'
' $_bullet $_blank\n';
static const _multiBoth = 'When you $_blank, roll+STAT. On a 10+, you succeed and choose:\n'
static const _multiBoth =
'When you $_blank, roll+STAT. On a 10+, you succeed and choose:\n'
' $_bullet $_blank\n'
' $_bullet $_blank\n'
' $_bullet $_blank\n'

View File

@@ -1,7 +1,8 @@
import 'dart:convert';
import 'package:dungeon_paper/core/utils/icon_utils.dart';
import 'package:dungeon_paper/core/utils/uuid.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
import 'package:flutter/material.dart';
@@ -28,7 +29,8 @@ class Note with WithIcon implements WithMeta {
final List<dw.Tag> tags;
final bool favorite;
String get localizedCategory => category.isEmpty ? S.current.noteNoCategory : category;
String get localizedCategory =>
category.isEmpty ? tr.notes.noCategory : category;
@override
Note copyWith({
@@ -128,7 +130,8 @@ class Note with WithIcon implements WithMeta {
favorite == other.favorite;
@override
int get hashCode => Object.hashAll([meta, key, title, description, category, tags, favorite]);
int get hashCode =>
Object.hashAll([meta, key, title, description, category, tags, favorite]);
String get debugProperties =>
'meta: $meta, key: $key, title: $title, description: $description, category: $category, tags: $tags, favorite: $favorite';

View File

@@ -63,7 +63,8 @@ class Race extends dw.Race with WithIcon implements WithMeta {
factory Race.fromRawJson(String str) => Race.fromJson(json.decode(str));
factory Race.fromDwRace(dw.Race race, {Meta? meta, bool favorite = false}) => Race(
factory Race.fromDwRace(dw.Race race, {Meta? meta, bool favorite = false}) =>
Race(
meta: race.meta != null ? Meta.fromJson(race.meta) : Meta.empty(),
key: race.key,
name: race.name,
@@ -81,7 +82,8 @@ class Race extends dw.Race with WithIcon implements WithMeta {
name: json['name'],
description: json['description'],
explanation: json['explanation'],
classKeys: List<dw.EntityReference>.from(json['classKeys'].map((x) => dw.EntityReference.fromJson(x))),
classKeys: List<dw.EntityReference>.from(
json['classKeys'].map((x) => dw.EntityReference.fromJson(x))),
tags: List<dw.Tag>.from(json['tags'].map((x) => dw.Tag.fromJson(x))),
favorite: json['favorite'] ?? false,
dice: List<dw.Dice>.from(json['dice'].map((x) => dw.Dice.fromJson(x))),
@@ -149,7 +151,8 @@ class Race extends dw.Race with WithIcon implements WithMeta {
dice == other.dice;
@override
int get hashCode => Object.hashAll([meta, key, name, description, explanation, classKeys, tags, dice]);
int get hashCode => Object.hashAll(
[meta, key, name, description, explanation, classKeys, tags, dice]);
@override
String get debugProperties =>

View File

@@ -23,9 +23,11 @@ class RollButton {
factory RollButton.fromJson(Map<String, dynamic> json) => RollButton(
label: json['label'],
dice: List<dw.Dice>.from((json['dice'] ?? []).map((x) => dw.Dice.fromJson(x))),
dice: List<dw.Dice>.from(
(json['dice'] ?? []).map((x) => dw.Dice.fromJson(x))),
specialDice: List<SpecialDice>.from(
(json['specialDice'] ?? []).map((x) => getEnumByName(SpecialDice.values, x)),
(json['specialDice'] ?? [])
.map((x) => getEnumByName(SpecialDice.values, x)),
),
);
@@ -35,17 +37,19 @@ class RollButton {
'specialDice': specialDice.map((d) => d.name).toList(),
};
List<dw.Dice> specialDiceFor(Character character, List<SpecialDice> specialDice) => specialDice
.map((d) {
switch (d) {
case SpecialDice.damage:
return character.damageDice;
default:
return null;
}
})
.whereType<dw.Dice>()
.toList();
List<dw.Dice> specialDiceFor(
Character character, List<SpecialDice> specialDice) =>
specialDice
.map((d) {
switch (d) {
case SpecialDice.damage:
return character.damageDice;
default:
return null;
}
})
.whereType<dw.Dice>()
.toList();
@override
bool operator ==(Object other) =>
@@ -59,7 +63,8 @@ class RollButton {
@override
int get hashCode => Object.hashAll([label, dice, specialDice]);
String get debugProperties => 'label: $label, dice: $dice, specialDice: $specialDice';
String get debugProperties =>
'label: $label, dice: $dice, specialDice: $specialDice';
@override
String toString() => 'RollButton($debugProperties)';

View File

@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
import 'meta.dart';
@@ -30,13 +31,15 @@ class SessionMark extends dw.SessionMark implements WithKey {
required super.completed,
}) : super(type: dw.SessionMarkType.endOfSession);
factory SessionMark.fromRawJson(String str) => SessionMark.fromJson(json.decode(str));
factory SessionMark.fromRawJson(String str) =>
SessionMark.fromJson(json.decode(str));
factory SessionMark.fromJson(Map<String, dynamic> json) => SessionMark(
key: json['key'],
completed: json['completed'],
description: json['description'],
type: dw.SessionMarkType.values.firstWhere((e) => e.name == json['type']),
type:
dw.SessionMarkType.values.firstWhere((e) => e.name == json['type']),
);
SessionMark copyWithInherited({
@@ -57,12 +60,12 @@ class SessionMark extends dw.SessionMark implements WithKey {
required List<SessionMark> flags,
}) =>
bonds.isNotEmpty && flags.isNotEmpty
? S.current.characterBondsFlagsDialogTitle
? tr.sessionMarks.title
: flags.isNotEmpty
? S.current.characterBondsFlagsDialogFlags
? tr.sessionMarks.flags
: bonds.isNotEmpty
? S.current.characterBondsFlagsDialogBonds
: S.current.characterBondsFlagsDialogTitle;
? tr.sessionMarks.bonds
: tr.sessionMarks.title;
@override
bool operator ==(Object other) =>
@@ -78,7 +81,8 @@ class SessionMark extends dw.SessionMark implements WithKey {
int get hashCode => Object.hashAll([key, description, completed, type]);
@override
String get debugProperties => 'key: $key, description: $description, completed: $completed, type: $type';
String get debugProperties =>
'key: $key, description: $description, completed: $completed, type: $type';
@override
String toString() => 'SessionMark($debugProperties)';

View File

@@ -109,8 +109,13 @@ class Spell extends dw.Spell with WithIcon implements WithMeta {
IconData get icon => DwIcons.book_cover;
static IconData get genericIcon => DwIcons.book_cover;
static int Function(Spell a, Spell b) sorter(SpellFilters filters) => (a, b) {
final levelOrder = ['cantrip', 'rote', ...List.generate(9, (i) => '${i + 1}')];
final level = levelOrder.indexOf(a.level).compareTo(levelOrder.indexOf(b.level));
final levelOrder = [
'cantrip',
'rote',
...List.generate(9, (i) => '${i + 1}')
];
final level =
levelOrder.indexOf(a.level).compareTo(levelOrder.indexOf(b.level));
if (level != 0) {
return level;
}
@@ -141,7 +146,17 @@ class Spell extends dw.Spell with WithIcon implements WithMeta {
tags == other.tags;
@override
int get hashCode => Object.hashAll([meta, key, name, description, explanation, level, classKeys, dice, tags]);
int get hashCode => Object.hashAll([
meta,
key,
name,
description,
explanation,
level,
classKeys,
dice,
tags
]);
@override
String get debugProperties =>

View File

@@ -50,7 +50,9 @@ class User {
displayName: json['displayName'],
email: json['email'],
photoUrl: json['photoURL'],
settings: json['settings'] != null ? UserSettings.fromJson(json['settings']) : UserSettings(),
settings: json['settings'] != null
? UserSettings.fromJson(json['settings'])
: UserSettings(),
flags: json['flags'] ?? {},
);
@@ -77,7 +79,8 @@ class User {
bool get isSu => flags['su'] == true;
bool get isDm => flags['dm_tools_preview'] == true;
Brightness get brightness => settings.brightnessOverride ?? getCurrentPlatformBrightness();
Brightness get brightness =>
settings.brightnessOverride ?? getCurrentPlatformBrightness();
void applySettings() => settings.apply();
@@ -85,7 +88,9 @@ class User {
AppThemes.setTheme(getTheme());
}
int getTheme() => brightness == Brightness.light ? settings.defaultLightTheme : settings.defaultDarkTheme;
int getTheme() => brightness == Brightness.light
? settings.defaultLightTheme
: settings.defaultDarkTheme;
@override
bool operator ==(Object other) =>
@@ -100,7 +105,8 @@ class User {
flags == other.flags;
@override
int get hashCode => Object.hashAll([username, displayName, email, photoUrl, settings, flags]);
int get hashCode =>
Object.hashAll([username, displayName, email, photoUrl, settings, flags]);
String get debugProperties =>
'username: $username, displayName: $displayName, email: $email, photoUrl: $photoUrl, settings: $settings';

View File

@@ -32,15 +32,17 @@ class UserSettings with CharacterServiceMixin {
brightnessOverride: brightnessOverride ?? this.brightnessOverride,
);
factory UserSettings.fromRawJson(String str) => UserSettings.fromJson(json.decode(str));
factory UserSettings.fromRawJson(String str) =>
UserSettings.fromJson(json.decode(str));
factory UserSettings.fromJson(Map<String, dynamic> json) => UserSettings(
keepScreenAwake: json['keepScreenAwake'],
defaultLightTheme: json['defaultLightTheme'],
defaultDarkTheme: json['defaultDarkTheme'],
brightnessOverride: Brightness.values.cast<Brightness?>().firstWhereOrNull(
(element) => element!.name == json['brightnessOverride'],
),
brightnessOverride:
Brightness.values.cast<Brightness?>().firstWhereOrNull(
(element) => element!.name == json['brightnessOverride'],
),
);
Map<String, dynamic> toJson() => {
@@ -61,7 +63,12 @@ class UserSettings with CharacterServiceMixin {
brightnessOverride == other.brightnessOverride;
@override
int get hashCode => Object.hashAll([keepScreenAwake, defaultLightTheme, defaultDarkTheme, brightnessOverride]);
int get hashCode => Object.hashAll([
keepScreenAwake,
defaultLightTheme,
defaultDarkTheme,
brightnessOverride
]);
String get debugProperties =>
'keepScreenAwake: $keepScreenAwake, defaultLightTheme: $defaultLightTheme, defaultDarkTheme: $defaultDarkTheme, brightnessOverride: $brightnessOverride';

View File

@@ -12,7 +12,8 @@ import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import '../../model_utils/user_utils.dart';
class AuthService extends GetxService with UserServiceMixin, LoadingServiceMixin, RepositoryServiceMixin {
class AuthService extends GetxService
with UserServiceMixin, LoadingServiceMixin, RepositoryServiceMixin {
StreamSubscription<User?>? _sub;
FirebaseAuth get auth => FirebaseAuth.instance;
@@ -157,7 +158,8 @@ class AuthService extends GetxService with UserServiceMixin, LoadingServiceMixin
userService.loadGuestData();
}
Future<UserCredential> signUp({required String email, required String password}) async =>
Future<UserCredential> signUp(
{required String email, required String password}) async =>
auth.createUserWithEmailAndPassword(email: email, password: password);
void _clearAuthListener() {

View File

@@ -12,7 +12,8 @@ import 'package:get/get.dart';
import '../models/character.dart';
import 'loading_service.dart';
class CharacterService extends GetxService with LoadingServiceMixin, UserServiceMixin {
class CharacterService extends GetxService
with LoadingServiceMixin, UserServiceMixin {
static CharacterService find() => Get.find();
final all = <String, Character>{}.obs;
@@ -34,9 +35,13 @@ class CharacterService extends GetxService with LoadingServiceMixin, UserService
}
PageController get pageController => _pageController;
double get page => pageController.hasClients && pageController.positions.length == 1 ? pageController.page ?? 0 : 0;
double get page =>
pageController.hasClients && pageController.positions.length == 1
? pageController.page ?? 0
: 0;
Character? get maybeCurrent => _currentKey.value != null ? all[_currentKey.value] : null;
Character? get maybeCurrent =>
_currentKey.value != null ? all[_currentKey.value] : null;
Character get current => maybeCurrent!;
List<Character> get allAsList => all.values.toList();
@@ -48,22 +53,24 @@ class CharacterService extends GetxService with LoadingServiceMixin, UserService
out[char.settings.category ?? '']!.add(char);
}
for (final key in out.keys) {
out[key]!
.sort((a, b) => (a.settings.sortOrder ?? double.infinity).compareTo(b.settings.sortOrder ?? double.infinity));
out[key]!.sort((a, b) => (a.settings.sortOrder ?? double.infinity)
.compareTo(b.settings.sortOrder ?? double.infinity));
}
return out;
}
Iterable<Character> get charsByLastUsed {
final copy = [...all.values];
copy.sort(createSortByDate(order: SortOrder.desc, parse: (char) => char?.meta.data?.lastUsed));
copy.sort(createSortByDate(
order: SortOrder.desc, parse: (char) => char?.meta.data?.lastUsed));
return copy;
}
Future<void> registerCharacterListener() async {
_clearCharListener();
debugPrint('registering character listener');
_sub = StorageHandler.instance.collectionListener('Characters', charsListener);
_sub =
StorageHandler.instance.collectionListener('Characters', charsListener);
}
void clear() {
@@ -77,13 +84,16 @@ class CharacterService extends GetxService with LoadingServiceMixin, UserService
switchToCharacterTheme(current);
updateCharacter(
current.copyWith(
meta: current.meta.copyWith(data: (current.meta.data ?? CharacterMeta()).copyWith(lastUsed: DateTime.now())),
meta: current.meta.copyWith(
data: (current.meta.data ?? CharacterMeta())
.copyWith(lastUsed: DateTime.now())),
),
);
}
}
void switchToCharacterTheme(Character character) => switchToTheme(character.getCurrentTheme(user));
void switchToCharacterTheme(Character character) =>
switchToTheme(character.getCurrentTheme(user));
void switchToTheme(int themeId) {
final dynamicTheme = DynamicTheme.of(Get.context!)!;
@@ -125,21 +135,26 @@ class CharacterService extends GetxService with LoadingServiceMixin, UserService
}
}
Future<void> updateCharacter(Character character, {bool switchToCharacter = false}) {
Future<void> updateCharacter(Character character,
{bool switchToCharacter = false}) {
// (StorageHandler.instance.delegate as LocalStorageDelegate).storage.collection('Characters');
character = character.copyWithInherited(meta: character.meta.stampUpdate());
all[character.key] = character;
if (switchToCharacter || _currentKey.value == null || !all.containsKey(_currentKey.value)) {
if (switchToCharacter ||
_currentKey.value == null ||
!all.containsKey(_currentKey.value)) {
setCurrent(character.key);
}
debugPrint('Updated char: ${character.key} (${character.displayName})');
debugPrint(character.toRawJson());
return StorageHandler.instance.update('Characters', character.key, character.toJson());
return StorageHandler.instance
.update('Characters', character.key, character.toJson());
}
void createCharacter(Character character, {bool switchToCharacter = false}) {
all[character.key] = character;
StorageHandler.instance.create('Characters', character.key, character.toJson());
StorageHandler.instance
.create('Characters', character.key, character.toJson());
if (switchToCharacter || _currentKey.value == null) {
_currentKey.value = character.key;
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../i18n/messages.i18n.dart';
class IntlService extends GetxService {
static final Map<Locale, Messages> _m = {};
static late Locale _locale;
static Messages get m => _m[Get.locale] ?? _loadMessages(_locale);
List<Locale> get supportedLocales => _m.keys.toList();
@override
void onInit() {
super.onInit();
_loadMessages(Get.deviceLocale ?? const Locale('en'));
}
static void changeLocale(Locale locale) {
_loadMessages(locale);
}
static Messages _loadMessages(Locale locale) {
final map = {
const Locale('en'): () => const Messages(),
};
_m[locale] = map[locale]?.call() ?? const Messages();
_locale = locale;
return _m[locale]!;
}
}

View File

@@ -66,7 +66,8 @@ class LibraryService extends GetxService {
);
}
void removeFromCharacter<T extends WithMeta>(Iterable<T> items, [Character? char]) async {
void removeFromCharacter<T extends WithMeta>(Iterable<T> items,
[Character? char]) async {
chars.updateCharacter(
CharacterUtils.removeByType<T>(char ?? chars.current, items),
);

View File

@@ -100,14 +100,17 @@ abstract class RepositoryCache {
SearchResponse cacheRes;
try {
cacheRes = ignoreCache ? SearchResponse.empty() : await getCacheResponse();
cacheRes =
ignoreCache ? SearchResponse.empty() : await getCacheResponse();
} catch (e) {
cacheRes = SearchResponse.empty();
}
final shouldLoadFromRemote = ignoreCache ? true : await shouldUseRemote(cacheRes);
final shouldLoadFromRemote =
ignoreCache ? true : await shouldUseRemote(cacheRes);
if (shouldLoadFromRemote) {
debugPrint('[$id] Cache ${ignoreCache ? 'skipped' : 'invalid'}, loading from remote');
debugPrint(
'[$id] Cache ${ignoreCache ? 'skipped' : 'invalid'}, loading from remote');
SearchResponse resp;
try {
resp = await getFromRemote;
@@ -170,7 +173,8 @@ abstract class RepositoryCache {
void registerListeners() {
clearListeners();
debugPrint('[$id] registering listeners, delegate: $storage, listener prefix: "${listenerKey('')}"');
debugPrint(
'[$id] registering listeners, delegate: $storage, listener prefix: "${listenerKey('')}"');
subs.addAll([
storage.collectionListener(
@@ -307,14 +311,23 @@ abstract class RepositoryCache {
required bool saveIntoCache,
}) async {
await Future.wait([
updateList<CharacterClass>(cacheKey('CharacterClasses'), classes, resp.classes, saveIntoCache: saveIntoCache),
updateList<Item>(cacheKey('Items'), items, resp.items, saveIntoCache: saveIntoCache),
updateList<Monster>(cacheKey('Monsters'), monsters, resp.monsters, saveIntoCache: saveIntoCache),
updateList<Move>(cacheKey('Moves'), moves, resp.moves, saveIntoCache: saveIntoCache),
updateList<Race>(cacheKey('Races'), races, resp.races, saveIntoCache: saveIntoCache),
updateList<Spell>(cacheKey('Spells'), spells, resp.spells, saveIntoCache: saveIntoCache),
updateList<Note>(cacheKey('Tags'), notes, resp.notes, saveIntoCache: saveIntoCache),
updateList<dw.Tag>(cacheKey('Tags'), tags, resp.tags, saveIntoCache: saveIntoCache),
updateList<CharacterClass>(
cacheKey('CharacterClasses'), classes, resp.classes,
saveIntoCache: saveIntoCache),
updateList<Item>(cacheKey('Items'), items, resp.items,
saveIntoCache: saveIntoCache),
updateList<Monster>(cacheKey('Monsters'), monsters, resp.monsters,
saveIntoCache: saveIntoCache),
updateList<Move>(cacheKey('Moves'), moves, resp.moves,
saveIntoCache: saveIntoCache),
updateList<Race>(cacheKey('Races'), races, resp.races,
saveIntoCache: saveIntoCache),
updateList<Spell>(cacheKey('Spells'), spells, resp.spells,
saveIntoCache: saveIntoCache),
updateList<Note>(cacheKey('Tags'), notes, resp.notes,
saveIntoCache: saveIntoCache),
updateList<dw.Tag>(cacheKey('Tags'), tags, resp.tags,
saveIntoCache: saveIntoCache),
]);
}
@@ -370,7 +383,8 @@ abstract class RepositoryCache {
list.addAll(Map.fromIterable(resp, key: (x) => x.key));
if (saveIntoCache && list.isNotEmpty) {
for (final x in list.values) await cache.create(collectionName, Meta.keyFor(x), Meta.toJsonFor(x));
for (final x in list.values)
await cache.create(collectionName, Meta.keyFor(x), Meta.toJsonFor(x));
}
}
}
@@ -421,7 +435,10 @@ class PersonalRepository extends RepositoryCache {
'Notes': storage.getCollection('Notes'),
};
return Future.wait(futures.values).then((v) async {
final map = {for (final e in enumerate(v)) futures.keys.elementAt(e.index): e.value};
final map = {
for (final e in enumerate(v))
futures.keys.elementAt(e.index): e.value
};
return SearchResponse.fromJson(map);
});
},

View File

@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'character_service.dart';
import 'intl_service.dart';
import 'library_service.dart';
import 'loading_service.dart';
import 'repository_service.dart';
@@ -13,6 +14,7 @@ Future<void> initServices() async {
/// Here is where you put get_storage, hive, shared_pref initialization.
/// or moor connection, or whatever that's async.
await Get.putAsync(() => Future.value(IntlService()));
await Get.putAsync(() => Future.value(LoadingService()));
await Get.putAsync(() => Future.value(RepositoryService()));
await Get.putAsync(() => Future.value(LibraryService()));

View File

@@ -11,7 +11,7 @@ import 'package:dungeon_paper/app/routes/app_pages.dart';
import 'package:dungeon_paper/core/http/api.dart';
import 'package:dungeon_paper/core/http/api_requests/migration.dart';
import 'package:dungeon_paper/core/storage_handler/storage_handler.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:email_validator/email_validator.dart';
import 'package:firebase_auth/firebase_auth.dart' as fba;
import 'package:flutter/material.dart';
@@ -22,7 +22,11 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import '../../../core/utils/secrets_base.dart';
class UserService extends GetxService
with RepositoryServiceMixin, AuthServiceMixin, CharacterServiceMixin, LoadingServiceMixin {
with
RepositoryServiceMixin,
AuthServiceMixin,
CharacterServiceMixin,
LoadingServiceMixin {
final _current = User.guest().obs;
User get current => _current.value;
@@ -46,7 +50,8 @@ class UserService extends GetxService
StorageHandler.instance.currentDelegate = 'firestore';
StorageHandler.instance.setCollectionPrefix('Data/$email');
final shouldLoadRepo = current.email != user.email;
final dbUser = await StorageHandler.instance.firestoreGlobal.getDocument('Data', email!);
final dbUser = await StorageHandler.instance.firestoreGlobal
.getDocument('Data', email!);
await _setUserAfterMigration(user, dbUser);
_registerUserListener();
charService.registerCharacterListener();
@@ -95,7 +100,8 @@ class UserService extends GetxService
Future<User> updateUser(User user) async {
final email = user.email;
debugPrint('updating user data for $email: ${user.toJson()}');
await StorageHandler.instance.firestoreGlobal.update('Data', email, user.toJson());
await StorageHandler.instance.firestoreGlobal
.update('Data', email, user.toJson());
return user;
}
@@ -127,7 +133,8 @@ class UserService extends GetxService
void _registerUserListener() {
_clearUserListener();
debugPrint('registering user listener');
_userDataSub = StorageHandler.instance.firestoreGlobal.documentListener('Data', current.email, _updateUser);
_userDataSub = StorageHandler.instance.firestoreGlobal
.documentListener('Data', current.email, _updateUser);
}
void _updateUser(DocData? data) {
@@ -165,7 +172,7 @@ class UserService extends GetxService
if (needsMigration) {
final resp = await _migrateUser(user);
if (resp == null) {
Get.rawSnackbar(title: S.current.errorUserOperationCanceled);
Get.rawSnackbar(title: tr.errors.userOperationCanceled);
loadingService.loadingUser = false;
loadingService.afterFirstLoad = !loadingService.loadingCharacters;
return;
@@ -198,4 +205,3 @@ mixin UserServiceMixin {
UserService get userService => Get.find();
User get user => userService.current;
}

View File

@@ -7,58 +7,74 @@ import 'package:dungeon_paper/core/utils/list_utils.dart';
class CharacterUtils {
// Moves
static Character updateMoves(Character char, Iterable<Move> moves) => char.copyWith(
static Character updateMoves(Character char, Iterable<Move> moves) =>
char.copyWith(
moves: updateByKey(char.moves, moves),
);
static Character addMoves(Character char, Iterable<Move> moves) => char.copyWith(
static Character addMoves(Character char, Iterable<Move> moves) =>
char.copyWith(
moves: addByKey(char.moves, moves),
);
static Character removeMoves(Character char, Iterable<Move> move) => char.copyWith(
static Character removeMoves(Character char, Iterable<Move> move) =>
char.copyWith(
moves: removeByKey(char.moves, move),
);
static Character reorderMoves(Character char, int oldIndex, int newIndex) => char.copyWith(
static Character reorderMoves(Character char, int oldIndex, int newIndex) =>
char.copyWith(
moves: reorder(char.moves, oldIndex, newIndex),
);
// Spells
static Character updateSpells(Character char, Iterable<Spell> spells) => char.copyWith(
static Character updateSpells(Character char, Iterable<Spell> spells) =>
char.copyWith(
spells: updateByKey(char.spells, spells),
);
static Character addSpells(Character char, Iterable<Spell> spells) => char.copyWith(
static Character addSpells(Character char, Iterable<Spell> spells) =>
char.copyWith(
spells: addByKey(char.spells, spells),
);
static Character removeSpells(Character char, Iterable<Spell> spell) => char.copyWith(
static Character removeSpells(Character char, Iterable<Spell> spell) =>
char.copyWith(
spells: removeByKey(char.spells, spell),
);
static Character reorderSpells(Character char, int oldIndex, int newIndex) => char.copyWith(
static Character reorderSpells(Character char, int oldIndex, int newIndex) =>
char.copyWith(
spells: reorder(char.spells, oldIndex, newIndex),
);
// Items
static Character updateItems(Character char, Iterable<Item> items) => char.copyWith(
static Character updateItems(Character char, Iterable<Item> items) =>
char.copyWith(
items: updateByKey(char.items, items),
);
static Character addItems(Character char, Iterable<Item> items) => char.copyWith(
static Character addItems(Character char, Iterable<Item> items) =>
char.copyWith(
items: addByKey(char.items, items),
);
static Character removeItems(Character char, Iterable<Item> item) => char.copyWith(
static Character removeItems(Character char, Iterable<Item> item) =>
char.copyWith(
items: removeByKey(char.items, item),
);
static Character reorderItems(Character char, int oldIndex, int newIndex) => char.copyWith(
static Character reorderItems(Character char, int oldIndex, int newIndex) =>
char.copyWith(
items: reorder(char.items, oldIndex, newIndex),
);
// Notes
static Character updateNotes(Character char, Iterable<Note> notes) => char.copyWith(
static Character updateNotes(Character char, Iterable<Note> notes) =>
char.copyWith(
notes: updateByKey(char.notes, notes),
);
static Character addNotes(Character char, Iterable<Note> notes) => char.copyWith(
static Character addNotes(Character char, Iterable<Note> notes) =>
char.copyWith(
notes: addByKey(char.notes, notes),
);
static Character removeNotes(Character char, Iterable<Note> note) => char.copyWith(
static Character removeNotes(Character char, Iterable<Note> note) =>
char.copyWith(
notes: removeByKey(char.notes, note),
);
static Character reorderNotes(Character char, int oldIndex, int newIndex) => char.copyWith(
static Character reorderNotes(Character char, int oldIndex, int newIndex) =>
char.copyWith(
notes: reorder(char.notes, oldIndex, newIndex),
);
@@ -66,43 +82,58 @@ class CharacterUtils {
// COMBINED
static Character updateByType<T>(Character char, Iterable<T> items) => char.copyWith(
static Character updateByType<T>(Character char, Iterable<T> items) =>
char.copyWith(
moves: T == Move ? updateByKey(char.moves, items.cast<Move>()) : null,
spells: T == Spell ? updateByKey(char.spells, items.cast<Spell>()) : null,
spells:
T == Spell ? updateByKey(char.spells, items.cast<Spell>()) : null,
items: T == Item ? updateByKey(char.items, items.cast<Item>()) : null,
notes: T == Note ? updateByKey(char.notes, items.cast<Note>()) : null,
);
static Character addByType<T>(Character char, Iterable<T> items) => char.copyWith(
static Character addByType<T>(Character char, Iterable<T> items) =>
char.copyWith(
moves: T == Move ? addByKey(char.moves, items.cast<Move>()) : null,
spells: T == Spell ? addByKey(char.spells, items.cast<Spell>()) : null,
items: T == Item ? addByKey(char.items, items.cast<Item>()) : null,
notes: T == Note ? addByKey(char.notes, items.cast<Note>()) : null,
);
static Character upsertByType<T>(Character char, Iterable<T> items) => char.copyWith(
static Character upsertByType<T>(Character char, Iterable<T> items) =>
char.copyWith(
moves: T == Move ? upsertByKey(char.moves, items.cast<Move>()) : null,
spells: T == Spell ? upsertByKey(char.spells, items.cast<Spell>()) : null,
spells:
T == Spell ? upsertByKey(char.spells, items.cast<Spell>()) : null,
items: T == Item ? upsertByKey(char.items, items.cast<Item>()) : null,
notes: T == Note ? upsertByKey(char.notes, items.cast<Note>()) : null,
);
static Character removeByType<T>(Character char, Iterable<T> items) => char.copyWith(
static Character removeByType<T>(Character char, Iterable<T> items) =>
char.copyWith(
moves: T == Move ? removeByKey(char.moves, items.cast<Move>()) : null,
spells: T == Spell ? removeByKey(char.spells, items.cast<Spell>()) : null,
spells:
T == Spell ? removeByKey(char.spells, items.cast<Spell>()) : null,
items: T == Item ? removeByKey(char.items, items.cast<Item>()) : null,
notes: T == Note ? removeByKey(char.notes, items.cast<Note>()) : null,
);
static Character reorderByType<T>(Character char, int oldIndex, int newIndex, {dynamic extraData}) => char.copyWith(
static Character reorderByType<T>(Character char, int oldIndex, int newIndex,
{dynamic extraData}) =>
char.copyWith(
moves: T == Move ? reorder(char.moves, oldIndex, newIndex) : null,
spells: T == Spell ? reorder(char.spells, oldIndex, newIndex) : null,
items: T == Item ? reorder(char.items, oldIndex, newIndex) : null,
notes: T == Note ? _reorderNotes(char.notes, oldIndex, newIndex, extraData as String) : null,
notes: T == Note
? _reorderNotes(char.notes, oldIndex, newIndex, extraData as String)
: null,
);
static List<Note> _reorderNotes(List<Note> notes, int oldIndex, int newIndex, String category) {
final sortedInCat = reorder(notes.where((note) => note.localizedCategory == category).toList(), oldIndex, newIndex);
static List<Note> _reorderNotes(
List<Note> notes, int oldIndex, int newIndex, String category) {
final sortedInCat = reorder(
notes.where((note) => note.localizedCategory == category).toList(),
oldIndex,
newIndex);
final otherCats = notes.where((note) => note.localizedCategory != category);
return [...sortedInCat, ...otherCats];
@@ -119,12 +150,13 @@ class CharacterUtils {
static List<Character> Function(int oldIndex, int newIndex) reorderCharacters(
Iterable<Character> list,
) {
return (oldIndex, newIndex) => enumerate(reorder(list.toList(), oldIndex, newIndex))
.map(
(e) => e.value.copyWith(
settings: e.value.settings.copyWith(sortOrder: e.index),
),
)
.toList();
return (oldIndex, newIndex) =>
enumerate(reorder(list.toList(), oldIndex, newIndex))
.map(
(e) => e.value.copyWith(
settings: e.value.settings.copyWith(sortOrder: e.index),
),
)
.toList();
}
}

View File

@@ -68,13 +68,17 @@ class ModelPages {
),
CharacterClass: () => openCharacterClassesList(
character: character,
onSelected: onSelected != null ? (x) => onSelected.call(asList<T>(x)) : null,
onSelected: onSelected != null
? (x) => onSelected.call(asList<T>(x))
: null,
preSelection: preSelections?.first as CharacterClass?,
initialTab: initialTab,
),
Race: () => openRacesList(
character: character,
onSelected: onSelected != null ? (x) => onSelected.call(asList<T>(x)) : null,
onSelected: onSelected != null
? (x) => onSelected.call(asList<T>(x))
: null,
preSelection: preSelections?.first as Race?,
initialTab: initialTab,
),
@@ -274,7 +278,10 @@ class ModelPages {
arguments: CharacterClassLibraryListArguments(
initialTab: initialTab,
onSelected: onSelected ??
(char != null ? (cls) => controller.updateCharacter(char.copyWith(characterClass: cls)) : null),
(char != null
? (cls) => controller
.updateCharacter(char.copyWith(characterClass: cls))
: null),
preSelections: asList(preSelection ?? char?.characterClass),
),
);
@@ -289,7 +296,8 @@ class ModelPages {
arguments: CharacterClassFormArguments(
entity: characterClass,
onSave: onSave,
formContext: characterClass == null ? FormContext.create : FormContext.edit,
formContext:
characterClass == null ? FormContext.create : FormContext.edit,
),
);
}

View File

@@ -20,7 +20,8 @@ bool searchFor(Type t, dynamic object, String search) {
case Move:
return MoveFilters(classKey: null, search: search).filter(object);
case Spell:
return SpellFilters(classKey: null, search: search, level: null).filter(object);
return SpellFilters(classKey: null, search: search, level: null)
.filter(object);
case Note:
return NoteFilters(search: search).filter(object);
// case AbilityScore:
@@ -41,7 +42,8 @@ double getScoreFor(Type t, dynamic object, String search) {
case Move:
return MoveFilters(classKey: null, search: search).getScore(object);
case Spell:
return SpellFilters(classKey: null, search: search, level: null).getScore(object);
return SpellFilters(classKey: null, search: search, level: null)
.getScore(object);
case Note:
return NoteFilters(search: search).getScore(object);
// case AbilityScore:

View File

@@ -3,8 +3,10 @@ import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
import 'package:flutter/material.dart';
class TagUtils {
static List<dw.Tag> excludeMetaTags(Iterable<dw.Tag> tags) =>
tags.where((tag) => !['language', 'source'].contains(tag.name.toLowerCase().trim())).toList();
static List<dw.Tag> excludeMetaTags(Iterable<dw.Tag> tags) => tags
.where((tag) =>
!['language', 'source'].contains(tag.name.toLowerCase().trim()))
.toList();
static Widget iconOf(dw.Tag tag) => Transform.rotate(
child: const Icon(Icons.label),

View File

@@ -1,7 +1,7 @@
import 'package:dungeon_paper/app/data/models/ability_scores.dart';
import 'package:dungeon_paper/core/utils/enums.dart';
import 'package:dungeon_paper/core/utils/string_validator.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@@ -14,13 +14,16 @@ class AbilityScoreFormController extends GetxController {
late final Rx<TextEditingController> _name = TextEditingController().obs;
TextEditingController get name => _name.value;
late final Rx<TextEditingController> _description = TextEditingController().obs;
late final Rx<TextEditingController> _description =
TextEditingController().obs;
TextEditingController get description => _description.value;
late final Rx<TextEditingController> _debilityName = TextEditingController().obs;
late final Rx<TextEditingController> _debilityName =
TextEditingController().obs;
TextEditingController get debilityName => _debilityName.value;
late final Rx<TextEditingController> _debilityDescription = TextEditingController().obs;
late final Rx<TextEditingController> _debilityDescription =
TextEditingController().obs;
TextEditingController get debilityDescription => _debilityDescription.value;
late final Rx<IconData?> _icon = Rx(null);
@@ -34,16 +37,23 @@ class AbilityScoreFormController extends GetxController {
void onInit() {
super.onInit();
final AbilityScoreFormArguments args = Get.arguments;
formContext = args.abilityScore != null ? FormContext.edit : FormContext.create;
formContext =
args.abilityScore != null ? FormContext.edit : FormContext.create;
if (args.abilityScore != null) {
entity.value = args.abilityScore!;
}
onSave = args.onSave;
_key.value = TextEditingController(text: entity.value.key)..addListener(_update);
_name.value = TextEditingController(text: entity.value.name)..addListener(_update);
_description.value = TextEditingController(text: entity.value.description)..addListener(_update);
_debilityName.value = TextEditingController(text: entity.value.debilityName)..addListener(_update);
_debilityDescription.value = TextEditingController(text: entity.value.debilityDescription)..addListener(_update);
_key.value = TextEditingController(text: entity.value.key)
..addListener(_update);
_name.value = TextEditingController(text: entity.value.name)
..addListener(_update);
_description.value = TextEditingController(text: entity.value.description)
..addListener(_update);
_debilityName.value = TextEditingController(text: entity.value.debilityName)
..addListener(_update);
_debilityDescription.value =
TextEditingController(text: entity.value.debilityDescription)
..addListener(_update);
_icon.value = entity.value.customIcon;
}
@@ -73,10 +83,11 @@ class AbilityScoreFormController extends GetxController {
String? keyValidator(String? value) => StringValidator(
exactLength: 3,
notContainsPattern: RegExp(r'[^a-z]', caseSensitive: false),
patternMessage: S.current.errorOnlyLetters,
patternMessage: tr.errors.onlyLetters,
).validator(value);
String? requiredValidator(String? value) => StringValidator(minLength: 1).validator(value);
String? requiredValidator(String? value) =>
StringValidator(minLength: 1).validator(value);
void _update() {
_key.refresh();

View File

@@ -2,15 +2,14 @@ import 'package:dungeon_paper/app/data/models/ability_scores.dart';
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/app/widgets/atoms/help_text.dart';
import 'package:dungeon_paper/core/utils/enums.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/ability_score_form_controller.dart';
class AbilityScoreFormView extends GetView<AbilityScoreFormController> {
const AbilityScoreFormView({Key? key}) : super(key: key);
const AbilityScoreFormView({super.key});
@override
Widget build(BuildContext context) {
const separator = SizedBox(height: 16);
@@ -18,15 +17,15 @@ class AbilityScoreFormView extends GetView<AbilityScoreFormController> {
appBar: AppBar(
title: Text(
controller.formContext == FormContext.create
? S.current.addGeneric(S.current.entity(AbilityScore))
: S.current.editGeneric(S.current.entity(AbilityScore)),
? tr.generic.addEntity(tr.entity(AbilityScore))
: tr.generic.editEntity(tr.entity(AbilityScore)),
),
centerTitle: true,
),
floatingActionButton: Obx(
() => AdvancedFloatingActionButton.extended(
onPressed: controller.isValid ? controller.save : null,
label: Text(S.current.save),
label: Text(tr.generic.save),
icon: const Icon(Icons.save),
),
),
@@ -38,20 +37,20 @@ class AbilityScoreFormView extends GetView<AbilityScoreFormController> {
TextFormField(
controller: controller.key,
decoration: InputDecoration(
labelText: S.current.abilityScoreFormKeyLabel,
labelText: tr.abilityScores.form.key.label,
),
validator: controller.keyValidator,
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: HelpText(text: S.current.abilityScoreFormKeyDescription),
child: HelpText(text: tr.abilityScores.form.key.description),
),
separator,
TextFormField(
controller: controller.name,
decoration: InputDecoration(
labelText: S.current.abilityScoreFormNameLabel,
hintText: S.current.abilityScoreFormNameDescription,
labelText: tr.abilityScores.form.name.label,
hintText: tr.abilityScores.form.name.description,
),
validator: controller.requiredValidator,
),
@@ -61,8 +60,8 @@ class AbilityScoreFormView extends GetView<AbilityScoreFormController> {
minLines: 3,
maxLines: 3,
decoration: InputDecoration(
labelText: S.current.abilityScoreFormDescriptionLabel,
hintText: S.current.abilityScoreFormDescriptionDescription,
labelText: tr.abilityScores.form.description.label,
hintText: tr.abilityScores.form.description.description,
),
validator: controller.requiredValidator,
),
@@ -70,8 +69,8 @@ class AbilityScoreFormView extends GetView<AbilityScoreFormController> {
TextFormField(
controller: controller.debilityName,
decoration: InputDecoration(
labelText: S.current.abilityScoreFormDebilityNameLabel,
hintText: S.current.abilityScoreFormDebilityNameDescription,
labelText: tr.abilityScores.form.debilityName.label,
hintText: tr.abilityScores.form.debilityName.description,
),
validator: controller.requiredValidator,
),
@@ -81,25 +80,26 @@ class AbilityScoreFormView extends GetView<AbilityScoreFormController> {
minLines: 3,
maxLines: 3,
decoration: InputDecoration(
labelText: S.current.abilityScoreFormDebilityDescriptionLabel,
hintText: S.current.abilityScoreFormDebilityDescriptionDescription,
labelText: tr.abilityScores.form.debilityDescription.label,
hintText: tr.abilityScores.form.debilityDescription.description,
),
validator: controller.requiredValidator,
),
separator,
Text(S.current.abilityScoreFormIconLabel),
Text(tr.abilityScores.form.icon.label),
separator,
Obx(
() => Align(
alignment: Alignment.centerLeft,
child: Icon(controller.icon ?? AbilityScore.iconFor(controller.key.text)),
child: Icon(controller.icon ??
AbilityScore.iconFor(controller.key.text)),
),
),
separator,
ElevatedButton(
onPressed: () => controller.pickIcon(context),
child: Text(
S.current.abilityScoreFormPickIconLabel,
tr.abilityScores.form.icon.button,
),
),
],

View File

@@ -6,8 +6,9 @@ import 'package:get/get.dart';
class AbilityScoresFormController extends GetxController {
final dirty = false.obs;
final Rx<AbilityScores> abilityScores =
AbilityScores.dungeonWorld(dex: 10, str: 10, wis: 10, con: 10, intl: 10, cha: 10).obs;
final Rx<AbilityScores> abilityScores = AbilityScores.dungeonWorld(
dex: 10, str: 10, wis: 10, con: 10, intl: 10, cha: 10)
.obs;
final textControllers = <String, TextEditingController>{};
late final void Function(AbilityScores abilityScores) onChanged;
@@ -25,7 +26,9 @@ class AbilityScoresFormController extends GetxController {
}
textControllers.clear();
for (final stat in abilityScores.value.stats) {
textControllers[stat.key] = TextEditingController(text: stat.value.toString())..addListener(validate);
textControllers[stat.key] =
TextEditingController(text: stat.value.toString())
..addListener(validate);
}
onChanged = args.onChanged;
}
@@ -39,13 +42,17 @@ class AbilityScoresFormController extends GetxController {
}
void updateStat(AbilityScore stat) {
abilityScores.value = abilityScores.value.copyWith(stats: updateByKey(abilityScores.value.stats, [stat]));
textControllers[stat.key] ??= TextEditingController(text: stat.value.toString())..addListener(validate);
abilityScores.value = abilityScores.value
.copyWith(stats: updateByKey(abilityScores.value.stats, [stat]));
textControllers[stat.key] ??=
TextEditingController(text: stat.value.toString())
..addListener(validate);
textControllers[stat.key]!.text = stat.value.toString();
}
void removeStat(AbilityScore stat) {
abilityScores.value = abilityScores.value.copyWith(stats: removeByKey(abilityScores.value.stats, [stat]));
abilityScores.value = abilityScores.value
.copyWith(stats: removeByKey(abilityScores.value.stats, [stat]));
textControllers.remove(stat.key);
}
@@ -53,9 +60,11 @@ class AbilityScoresFormController extends GetxController {
if (textControllers.containsKey(abilityScore.key)) {
return;
}
abilityScores.value = abilityScores.value.copyWith(stats: [...abilityScores.value.stats, abilityScore]);
textControllers[abilityScore.key] = TextEditingController(text: abilityScore.value.toString())
..addListener(validate);
abilityScores.value = abilityScores.value
.copyWith(stats: [...abilityScores.value.stats, abilityScore]);
textControllers[abilityScore.key] =
TextEditingController(text: abilityScore.value.toString())
..addListener(validate);
}
}

View File

@@ -13,16 +13,15 @@ import 'package:dungeon_paper/app/widgets/atoms/round_icon_button.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/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
const AbilityScoresFormView({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@@ -31,18 +30,18 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
dirty: controller.dirty.value,
child: Scaffold(
appBar: AppBar(
title: Text(S.current.entityPlural(AbilityScore)),
title: Text(tr.entityPlural(AbilityScore)),
centerTitle: true,
),
floatingActionButton: AdvancedFloatingActionButton.extended(
onPressed: _save,
label: Text(S.current.save),
label: Text(tr.generic.save),
icon: const Icon(Icons.save),
),
body: ListView(
padding: const EdgeInsets.all(16).copyWith(bottom: 80),
children: [
HelpText(text: S.current.abilityScoreInfo),
HelpText(text: tr.abilityScores.info),
const SizedBox(height: 8),
Form(
child: ReorderableListView.builder(
@@ -50,8 +49,10 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.abilityScores.value.stats.length,
onReorder: (int oldIndex, int newIndex) {
controller.abilityScores.value = controller.abilityScores.value
.copyWith(stats: reorder(controller.abilityScores.value.stats, oldIndex, newIndex));
controller.abilityScores.value =
controller.abilityScores.value.copyWith(
stats: reorder(controller.abilityScores.value.stats,
oldIndex, newIndex));
},
itemBuilder: (context, index) => _buildCard(context, index),
),
@@ -64,8 +65,8 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
)),
icon: const Icon(Icons.add),
label: Text(
S.current.addGeneric(
S.current.entity(AbilityScore),
tr.generic.addEntity(
tr.entity(AbilityScore),
),
),
),
@@ -81,9 +82,11 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
final textTheme = theme.textTheme;
final statKey = sortByPredefined(
controller.textControllers.keys.toList(),
order: controller.abilityScores.value.stats.map((stat) => stat.key).toList(),
order:
controller.abilityScores.value.stats.map((stat) => stat.key).toList(),
).elementAt(index);
final stat = controller.abilityScores.value.stats.firstWhere((stat) => stat.key == statKey);
final stat = controller.abilityScores.value.stats
.firstWhere((stat) => stat.key == statKey);
return Padding(
key: Key('stat-$statKey'),
padding: const EdgeInsets.symmetric(vertical: 4),
@@ -131,15 +134,18 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8).copyWith(right: 16),
padding: const EdgeInsets.symmetric(horizontal: 8)
.copyWith(right: 16),
child: Text(
S.current.abilityScoreModifierValueLabel(stat.modifier),
tr.abilityScores.form
.modifierValueLabel(stat.modifier.toString()),
),
),
RoundIconButton(
icon: DiceIcon.from(dw.Dice.d6),
onPressed: () => controller.textControllers[stat.key]!.text = Random().nextInt(21).toString(),
tooltip: S.current.abilityScoreRollButtonTooltip,
onPressed: () => controller.textControllers[stat.key]!
.text = Random().nextInt(21).toString(),
tooltip: tr.abilityScores.rollButton.randStat,
),
Expanded(child: Container()),
Padding(
@@ -154,7 +160,9 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
),
onDelete: () => deleteDialog.confirm(
context,
DeleteDialogOptions(entityName: stat.name, entityKind: S.current.entity(AbilityScore)),
DeleteDialogOptions(
entityName: stat.name,
entityKind: tr.entity(AbilityScore)),
() => controller.removeStat(stat),
),
),

View File

@@ -4,7 +4,7 @@ import 'package:dungeon_paper/app/routes/app_pages.dart';
import 'package:dungeon_paper/app/widgets/atoms/rainbow_text.dart';
import 'package:dungeon_paper/core/dw_icons.dart';
import 'package:dungeon_paper/core/utils/builder_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -13,7 +13,7 @@ import '../../../model_utils/user_utils.dart';
import '../controllers/about_controller.dart';
class AboutView extends GetView<AboutController> {
const AboutView({Key? key}) : super(key: key);
const AboutView({super.key});
@override
Widget build(BuildContext context) {
@@ -22,19 +22,19 @@ class AboutView extends GetView<AboutController> {
return Scaffold(
appBar: AppBar(
title: Text(S.current.about),
title: Text(tr.about.title),
centerTitle: true,
),
body: ItemBuilder.lazyListView(
children: [
() => Text(
S.current.appName,
tr.app.name,
textAlign: TextAlign.center,
style: textTheme.headlineMedium,
),
() => Obx(
() => Text(
S.current.aboutVersion(controller.version.value?.toString() ?? '-'),
tr.about.version(controller.version.value?.toString() ?? '-'),
textAlign: TextAlign.center,
style: textTheme.bodySmall,
),
@@ -46,34 +46,38 @@ class AboutView extends GetView<AboutController> {
),
() => const SizedBox(height: 16),
() => Text(
S.current.aboutCopyright(DateTime.now().year),
tr.about.copyright(DateTime.now().year),
textAlign: TextAlign.center,
),
() => Text(
S.current.aboutAuthor,
tr.about.author,
textAlign: TextAlign.center,
),
() => const Divider(height: 48),
() => ListTile(
leading: const Icon(DwIcons.discord),
title: Text(S.current.aboutJoinDiscord),
subtitle: Text(S.current.aboutJoinDiscordSubtitle, style: textTheme.bodySmall),
onTap: () => launchUrl(Uri.parse('https://bit.ly/DungeonPaper-Discord')),
title: Text(tr.about.discord.title),
subtitle:
Text(tr.about.discord.subtitle, style: textTheme.bodySmall),
onTap: () =>
launchUrl(Uri.parse('https://bit.ly/DungeonPaper-Discord')),
isThreeLine: true,
visualDensity: VisualDensity.compact,
),
() => ListTile(
leading: const Icon(Icons.send),
title: Text(S.current.aboutSendFeedback),
subtitle: Text(S.current.aboutSendFeedbackSubtitle, style: textTheme.bodySmall),
title: Text(tr.about.feedback.title),
subtitle: Text(tr.about.feedback.subtitle,
style: textTheme.bodySmall),
onTap: () => Get.toNamed(Routes.sendFeedback),
isThreeLine: true,
visualDensity: VisualDensity.compact,
),
() => const Divider(),
() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(S.current.aboutSocialLinks, style: textTheme.bodySmall),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(tr.about.socials.title, style: textTheme.bodySmall),
),
() => Padding(
padding: const EdgeInsets.all(16.0).copyWith(top: 0),
@@ -83,37 +87,37 @@ class AboutView extends GetView<AboutController> {
children: [
_SocialButton(
icon: Icon(DwIcons.providerIcon(ProviderName.twitter)),
label: Text(S.current.socialTwitter),
label: Text(tr.about.socials.twitter),
url: 'https://bit.ly/DungeonPaper-Twitter',
color: const Color.fromARGB(255, 28, 157, 236),
),
_SocialButton(
icon: Icon(DwIcons.providerIcon(ProviderName.facebook)),
label: Text(S.current.socialFacebook),
label: Text(tr.about.socials.facebook),
url: 'https://bit.ly/DungeonPaper-Facebook',
color: const Color.fromARGB(255, 22, 116, 236),
),
_SocialButton(
icon: Icon(DwIcons.providerIcon(ProviderName.discord)),
label: Text(S.current.socialDiscord),
label: Text(tr.about.socials.discord),
url: 'https://bit.ly/DungeonPaper-Discord',
color: const Color.fromARGB(255, 111, 133, 212),
),
_SocialButton(
icon: Icon(DwIcons.providerIcon(ProviderName.github)),
label: Text(S.current.socialGitHub),
label: Text(tr.about.socials.github),
url: 'https://bit.ly/DungeonPaper-GitHub',
color: const Color.fromARGB(255, 33, 32, 32),
),
_SocialButton(
icon: Icon(DwIcons.providerIcon(ProviderName.google)),
label: Text(S.current.socialGoogle),
label: Text(tr.about.socials.google),
url: 'https://bit.ly/DungeonPaper-Android',
color: const Color.fromARGB(255, 1, 135, 95),
),
_SocialButton(
icon: Icon(DwIcons.providerIcon(ProviderName.apple)),
label: Text(S.current.socialApple),
label: Text(tr.about.socials.apple),
url: 'https://bit.ly/DungeonPaper-iOS',
color: const Color.fromARGB(255, 30, 143, 232),
),
@@ -124,7 +128,7 @@ class AboutView extends GetView<AboutController> {
() => ListTile(
minLeadingWidth: 36,
leading: const Icon(Icons.favorite),
title: Text(S.current.aboutSpecialThanks),
title: Text(tr.about.specialThanks),
subtitle: RainbowText(
[
'dekelts',
@@ -146,7 +150,7 @@ class AboutView extends GetView<AboutController> {
() => ListTile(
minLeadingWidth: 36,
leading: const Icon(Icons.code),
title: Text(S.current.aboutIconCredits),
title: Text(tr.about.icons),
subtitle: Text(iconCredits),
),
],

View File

@@ -2,24 +2,25 @@ import 'package:dungeon_paper/app/data/services/auth_service.dart';
import 'package:dungeon_paper/app/data/services/user_service.dart';
import 'package:dungeon_paper/core/utils/upload_utils.dart';
import 'package:dungeon_paper/core/utils/uuid.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
class AccountController extends GetxController with UserServiceMixin, AuthServiceMixin {
class AccountController extends GetxController
with UserServiceMixin, AuthServiceMixin {
final uploading = false.obs;
void updateEmail(String email) async {
await userService.updateEmail(email);
Get.rawSnackbar(message: S.current.accountChangeEmailSuccess);
Get.rawSnackbar(message: tr.account.details.email.success);
}
void uploadPhoto(BuildContext context) {
cropAndUploadPhoto(
context,
UploadSettings(
uploadPath: '/UserPhoto/' + uuid(),
uploadPath: '/UserPhoto/${uuid()}',
cropStyle: CropStyle.circle,
onUploadFile: (_) => uploading.value = true,
onSuccess: (url) {

View File

@@ -9,9 +9,8 @@ import 'package:dungeon_paper/core/utils/builder_utils.dart';
import 'package:dungeon_paper/core/utils/email_address_validator.dart';
import 'package:dungeon_paper/core/utils/password_validator.dart';
import 'package:dungeon_paper/core/utils/string_validator.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../core/dw_icons.dart';
@@ -20,7 +19,7 @@ import '../../../model_utils/user_utils.dart';
import '../controllers/account_controller.dart';
class AccountView extends GetView<AccountController> {
const AccountView({Key? key}) : super(key: key);
const AccountView({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@@ -39,15 +38,16 @@ class AccountView extends GetView<AccountController> {
() => const SizedBox(height: 8),
() => const Divider(),
() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).copyWith(top: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16).copyWith(top: 8),
child: Text(
S.current.accountCategoryDetails,
tr.account.details.title,
style: textTheme.bodySmall,
),
),
() => Obx(
() => ListTile(
title: Text(S.current.accountChangeDisplayNameTitle),
title: Text(tr.account.details.displayName.title),
subtitle: Text(controller.user.displayName),
leading: const Icon(Icons.abc),
onTap: _openNameDialog,
@@ -55,8 +55,8 @@ class AccountView extends GetView<AccountController> {
),
() => Obx(
() => ListTile(
title: Text(S.current.accountChangeImageTitle),
subtitle: Text(S.current.accountChangeImageSubtitle),
title: Text(tr.account.details.image.title),
subtitle: Text(tr.account.details.image.subtitle),
leading: controller.uploading.value
? const SizedBox.square(
dimension: 24,
@@ -66,28 +66,31 @@ class AccountView extends GetView<AccountController> {
)
: const Icon(Icons.image),
enabled: !controller.uploading.value,
onTap: !controller.uploading.value ? () => _uploadImage(context) : null,
onTap: !controller.uploading.value
? () => _uploadImage(context)
: null,
),
),
() => Obx(
() => ListTile(
title: Text(S.current.accountChangeEmailTitle),
title: Text(tr.account.details.email.title),
subtitle: Text(controller.user.email),
onTap: _openEmailDialog,
leading: const Icon(Icons.email),
),
),
() => ListTile(
title: Text(S.current.accountChangePasswordTitle),
subtitle: Text(S.current.accountChangePasswordSubtitle),
title: Text(tr.account.details.password.title),
subtitle: Text(tr.account.details.password.subtitle),
onTap: _openPasswordDialog,
leading: const Icon(Icons.key),
),
() => const Divider(),
() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).copyWith(top: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16).copyWith(top: 8),
child: Text(
S.current.accountCategorySocials,
tr.account.providers.title,
style: textTheme.bodySmall,
),
),
@@ -102,19 +105,17 @@ class AccountView extends GetView<AccountController> {
(provider) {
return () => Obx(
() => ListTile(
title: Text(S.current.signinProvider(provider)),
title: Text(tr.auth.providers.name(provider.name)),
// subtitle: Text(provider.),
leading: Icon(DwIcons.providerIcon(provider)),
subtitle: !PlatformHelper.canUseProvider(provider)
? Text(
S.current.signinCantUseProvider(S.current.signinProvider(provider)),
tr.auth.providers.unusable(
tr.auth.providers.name(provider.name)),
textScaleFactor: 0.8,
)
: null,
trailing: ElevatedButton(
child: Text(
isProviderLinked(provider) ? S.current.signinProviderUnlink : S.current.signinProviderLink,
),
onPressed: providerCount > 1
? isProviderLinked(provider)
? unlinkProvider(context, provider)
@@ -122,6 +123,11 @@ class AccountView extends GetView<AccountController> {
? linkProvider(provider)
: null
: null,
child: Text(
isProviderLinked(provider)
? tr.auth.providers.unlink
: tr.auth.providers.link,
),
),
),
);
@@ -129,7 +135,7 @@ class AccountView extends GetView<AccountController> {
),
// delete account
() => ListTile(
title: Text(S.current.accountDelete),
title: Text(tr.account.deleteAccount.title),
leading: const Icon(Icons.delete_forever),
onTap: () => awaitDeleteAccountConfirmation(
context,
@@ -137,12 +143,12 @@ class AccountView extends GetView<AccountController> {
api.requests.sendFeedback(
email: controller.user.email,
subject: 'Account Deletion Request',
body: 'Automated: Request Account Deletion for ${controller.user.email}',
body:
'Automated: Request Account Deletion for ${controller.user.email}',
username: controller.user.username,
);
// A deletion request for your account was sent successfully
Get.rawSnackbar(message: 'A deletion request for your account was sent successfully');
// Get.rawSnackbar(message: S.current.accountDeleteSuccess);
// A deletion request for your account was sent successfully
Get.rawSnackbar(message: tr.account.deleteAccount.success);
},
),
),
@@ -158,7 +164,7 @@ class AccountView extends GetView<AccountController> {
// Size(100, 28),
// ),
// ),
// child: Text(S.current.accountDelete),
// child: Text(tr.account.deleteAccount.title),
// onPressed: () =>
// awaitDeleteAccountConfirmation(context, () => null),
// ),
@@ -176,21 +182,23 @@ class AccountView extends GetView<AccountController> {
);
}
int get providerCount => controller.authService.fbUser?.providerData.length ?? 0;
int get providerCount =>
controller.authService.fbUser?.providerData.length ?? 0;
bool isProviderLinked(ProviderName provider) =>
controller.authService.fbUser?.providerData.any((pr) => pr.providerId == domainFromProviderName(provider)) ==
controller.authService.fbUser?.providerData
.any((pr) => pr.providerId == domainFromProviderName(provider)) ==
true;
void _openNameDialog() {
Get.dialog(
SingleTextFieldDialog(
title: S.current.accountChangeDisplayNameTitle,
inputLabel: S.current.accountChangeDisplayNameLabel,
inputHint: S.current.accountChangeDisplayNameHint,
title: tr.account.details.displayName.title,
inputLabel: tr.account.details.displayName.label,
inputHint: tr.account.details.displayName.placeholder,
value: controller.user.displayName,
onSave: (displayName) {
Get.rawSnackbar(message: S.current.accountChangeDisplayNameSuccess);
Get.rawSnackbar(message: tr.account.details.displayName.success);
controller.userService.updateUser(
controller.user.copyWith(displayName: displayName),
);
@@ -202,9 +210,9 @@ class AccountView extends GetView<AccountController> {
void _openEmailDialog() {
Get.dialog(
SingleTextFieldDialog(
title: S.current.accountChangeEmailTitle,
inputLabel: S.current.accountChangeEmailLabel,
inputHint: S.current.accountChangeEmailHint,
title: tr.account.details.email.title,
inputLabel: tr.account.details.email.label,
inputHint: tr.account.details.email.placeholder,
value: controller.user.email,
validator: EmailAddressValidator().validator,
onSave: controller.updateEmail,
@@ -216,7 +224,7 @@ class AccountView extends GetView<AccountController> {
Get.dialog(
PasswordFieldDialog(
onSave: (password) {
Get.rawSnackbar(message: S.current.accountChangePasswordSuccess);
Get.rawSnackbar(message: tr.account.details.password.success);
controller.authService.fbUser!.updatePassword(password);
},
),
@@ -227,18 +235,21 @@ class AccountView extends GetView<AccountController> {
controller.uploadPhoto(context);
}
Future<void> Function() unlinkProvider(BuildContext context, ProviderName provider) =>
Future<void> Function() unlinkProvider(
BuildContext context, ProviderName provider) =>
() => awaitUnlinkProviderConfirmation(
context,
provider,
() {
controller.authService.logoutFromProvider(provider);
controller.authService.fbUser!.unlink(domainFromProviderName(provider));
controller.authService.fbUser!
.unlink(domainFromProviderName(provider));
},
);
Future<void> Function() linkProvider(ProviderName provider) => () async {
final cred = await controller.authService.getProviderCredential(provider);
final cred =
await controller.authService.getProviderCredential(provider);
controller.authService.fbUser!.linkWithCredential(cred);
};
}
@@ -370,7 +381,7 @@ class _PasswordFieldDialogState extends State<PasswordFieldDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(S.current.accountChangePasswordTitle),
title: Text(tr.account.details.password.title),
content: SizedBox(
width: 400,
child: Form(
@@ -382,8 +393,8 @@ class _PasswordFieldDialogState extends State<PasswordFieldDialog> {
controller: password,
validator: PasswordValidator().validator,
decoration: InputDecoration(
labelText: S.current.accountChangePasswordLabel,
hintText: S.current.accountChangePasswordHint,
labelText: tr.account.details.password.label,
hintText: tr.account.details.password.placeholder,
),
),
const SizedBox(height: 16),
@@ -391,8 +402,8 @@ class _PasswordFieldDialogState extends State<PasswordFieldDialog> {
controller: passwordConfirm,
validator: _passwordValidator,
decoration: InputDecoration(
labelText: S.current.accountChangePasswordConfirmLabel,
hintText: S.current.accountChangePasswordConfirmHint,
labelText: tr.account.details.password.confirm.label,
hintText: tr.account.details.password.confirm.placeholder,
),
),
],
@@ -418,10 +429,10 @@ class _PasswordFieldDialogState extends State<PasswordFieldDialog> {
});
}
String? _passwordValidator(String? _value) {
String? _passwordValidator(String? value) {
if (password.text != passwordConfirm.text) {
return S.current.signupPasswordValidationMatch;
return tr.account.details.password.error;
}
return PasswordValidator().validator(_value);
return PasswordValidator().validator(value);
}
}

View File

@@ -2,25 +2,25 @@ import 'package:dungeon_paper/app/data/models/character.dart';
import 'package:dungeon_paper/app/data/services/user_service.dart';
import 'package:dungeon_paper/app/routes/app_pages.dart';
import 'package:dungeon_paper/app/themes/colors.dart';
import 'package:dungeon_paper/app/widgets/atoms/hyperlink.dart';
import 'package:dungeon_paper/app/widgets/atoms/labeled_divider.dart';
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/app/widgets/atoms/character_avatar.dart';
import 'package:dungeon_paper/app/widgets/atoms/confirm_exit_view.dart';
import 'package:dungeon_paper/app/widgets/atoms/hyperlink.dart';
import 'package:dungeon_paper/app/widgets/atoms/labeled_divider.dart';
import 'package:dungeon_paper/core/dw_icons.dart';
import 'package:dungeon_paper/core/platform_helper.dart';
import 'package:dungeon_paper/core/utils/content_generators/character_name_generator.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/basic_info_form_controller.dart';
class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServiceMixin {
class BasicInfoFormView extends GetView<BasicInfoFormController>
with UserServiceMixin {
const BasicInfoFormView({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@@ -30,12 +30,12 @@ class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServic
dirty: controller.dirty.value,
child: Scaffold(
appBar: AppBar(
title: Text(S.current.basicInformationTitle),
title: Text(tr.basicInfo.title),
centerTitle: true,
),
floatingActionButton: AdvancedFloatingActionButton.extended(
onPressed: _save,
label: Text(S.current.save),
label: Text(tr.generic.save),
icon: const Icon(Icons.save),
),
body: Form(
@@ -48,22 +48,24 @@ class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServic
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: controller.name.value,
textInputAction: TextInputAction.next,
validator: (val) => val == null || val.isEmpty ? 'Cannot be empty' : null,
validator: (val) =>
val == null || val.isEmpty ? 'Cannot be empty' : null,
// onChanged: (val) => updateControllers(),
textCapitalization: TextCapitalization.words,
decoration: InputDecoration(
labelText: S.current.createCharacterNameFieldLabel,
hintText: S.current.createCharacterNameFieldPlaceholder,
labelText: tr.basicInfo.form.name.label,
hintText: tr.basicInfo.form.name.placeholder,
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: IconButton(
tooltip: PlatformHelper.byInteractionType(
context,
touch: S.current.createCharRandomizeNameTooltipTouch,
mouse: S.current.createCharRandomizeNameTooltipClick,
touch: tr.basicInfo.form.name.random.tooltip.touch,
mouse: tr.basicInfo.form.name.random.tooltip.click,
),
icon: const Icon(DwIcons.dice_d6_numbered),
onPressed: () {
controller.name.value.text = CharacterNameGenerator().generate();
controller.name.value.text =
CharacterNameGenerator().generate();
},
),
),
@@ -86,17 +88,21 @@ class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServic
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.isUploading ? null : () => controller.startUploadFlow(context),
onPressed: controller.isUploading
? null
: () => controller.startUploadFlow(context),
icon: const Icon(Icons.upload_file),
label: Text(S.current.basicInfoImageChooseNew),
label: Text(tr.basicInfo.form.photo.change),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.isUploading ? null : controller.resetPhoto,
onPressed: controller.isUploading
? null
: controller.resetPhoto,
icon: const Icon(Icons.close),
label: Text(S.current.basicInfoImageRemove),
label: Text(tr.basicInfo.form.photo.remove),
),
),
],
@@ -106,11 +112,12 @@ class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServic
SizedBox(
height: 40,
child: ElevatedButton.icon(
onPressed: !controller.isUploading && userService.isLoggedIn
? () => controller.startUploadFlow(context)
: null,
onPressed:
!controller.isUploading && userService.isLoggedIn
? () => controller.startUploadFlow(context)
: null,
icon: const Icon(Icons.upload_file),
label: Text(S.current.basicInfoImageChoose),
label: Text(tr.basicInfo.form.photo.choose),
),
),
if (userService.isGuest) ...[
@@ -122,16 +129,17 @@ class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServic
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(Icons.warning, color: DwColors.warning, size: 16),
child: Icon(Icons.warning,
color: DwColors.warning, size: 16),
),
),
TextSpan(text: S.current.basicInfoImageNeedAccountPrefix + ' '),
TextSpan(text: tr.basicInfo.form.photo.guest.prefix),
Hyperlink.textSpan(
context,
S.current.basicInfoImageNeedAccountLinkLabel,
tr.basicInfo.form.photo.guest.label,
onTap: () => Get.toNamed(Routes.login),
),
TextSpan(text: S.current.basicInfoImageNeedAccountSuffix),
TextSpan(text: tr.basicInfo.form.photo.guest.suffix),
],
style: theme.textTheme.bodyMedium,
),
@@ -148,10 +156,10 @@ class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServic
child: CircularProgressIndicator(strokeWidth: 3),
),
const SizedBox(width: 8),
Text(S.current.basicInfoImageUploading),
Text(tr.basicInfo.form.photo.uploading),
],
)
: Text(S.current.separatorOr),
: Text(tr.basicInfo.form.photo.orSeparator),
),
TextFormField(
controller: controller.avatarUrl.value,
@@ -159,8 +167,8 @@ class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServic
enabled: !controller.isUploading,
// onChanged: (val) => updateControllers(),
decoration: InputDecoration(
labelText: S.current.createCharacterAvatarFieldLabel,
hintText: S.current.createCharacterAvatarFieldPlaceholder,
labelText: tr.basicInfo.form.photo.url.label,
hintText: tr.basicInfo.form.photo.url.placeholder,
floatingLabelBehavior: FloatingLabelBehavior.always,
),
),
@@ -174,7 +182,8 @@ class BasicInfoFormView extends GetView<BasicInfoFormController> with UserServic
}
_save() {
controller.onChanged(controller.name.value.text, controller.avatarUrl.value.text);
controller.onChanged(
controller.name.value.text, controller.avatarUrl.value.text);
Get.back();
}
}

View File

@@ -21,8 +21,11 @@ class BioFormController extends GetxController with CharacterServiceMixin {
bioDesc.value = TextEditingController(text: char.bio.description);
looks.value = TextEditingController(text: char.bio.looks);
alignmentName.value = char.bio.alignment.key;
alignmentValue.value = TextEditingController(text: char.bio.alignment.description);
bonds.value = char.sessionMarks.map((e) => TextEditingController(text: e.description)).toList();
alignmentValue.value =
TextEditingController(text: char.bio.alignment.description);
bonds.value = char.sessionMarks
.map((e) => TextEditingController(text: e.description))
.toList();
}
void save() {

View File

@@ -5,12 +5,13 @@ import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.
import 'package:dungeon_paper/app/widgets/atoms/confirm_exit_view.dart';
import 'package:dungeon_paper/app/widgets/atoms/rich_text_field.dart';
import 'package:dungeon_paper/app/widgets/atoms/select_box.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class BioFormView extends GetView<BioFormController> with CharacterServiceMixin {
const BioFormView({Key? key}) : super(key: key);
class BioFormView extends GetView<BioFormController>
with CharacterServiceMixin {
const BioFormView({super.key});
@override
Widget build(BuildContext context) {
@@ -19,11 +20,11 @@ class BioFormView extends GetView<BioFormController> with CharacterServiceMixin
dirty: controller.dirty.value,
child: Scaffold(
appBar: AppBar(
title: Text(S.current.characterBioDialogTitle),
title: Text(tr.bio.dialog.title),
),
floatingActionButton: AdvancedFloatingActionButton.extended(
onPressed: _save,
label: Text(S.current.save),
label: Text(tr.generic.save),
icon: const Icon(Icons.save),
),
body: ListView(
@@ -36,8 +37,8 @@ class BioFormView extends GetView<BioFormController> with CharacterServiceMixin
textCapitalization: TextCapitalization.sentences,
onChanged: controller.setDirty,
decoration: InputDecoration(
label: Text(S.current.characterBioDialogDescLabel),
hintText: S.current.characterBioDialogDescPlaceholder,
label: Text(tr.bio.dialog.description.label),
hintText: tr.bio.dialog.description.placeholder,
),
),
const SizedBox(height: 8),
@@ -48,8 +49,8 @@ class BioFormView extends GetView<BioFormController> with CharacterServiceMixin
textCapitalization: TextCapitalization.sentences,
onChanged: controller.setDirty,
decoration: InputDecoration(
label: Text(S.current.characterBioDialogLooksLabel),
hintText: S.current.characterBioDialogLooksPlaceholder,
label: Text(tr.bio.dialog.looks.label),
hintText: tr.bio.dialog.looks.placeholder,
),
),
const SizedBox(height: 24),
@@ -63,7 +64,7 @@ class BioFormView extends GetView<BioFormController> with CharacterServiceMixin
children: [
Icon(AlignmentValue.iconMap[a]!),
const SizedBox(width: 4),
Text(S.current.alignment(a)),
Text(tr.alignment.name(a)),
],
),
),
@@ -74,7 +75,7 @@ class BioFormView extends GetView<BioFormController> with CharacterServiceMixin
controller.setDirty();
},
isExpanded: true,
label: Text(S.current.characterBioDialogAlignmentNameLabel),
label: Text(tr.bio.dialog.alignment.label),
),
const SizedBox(height: 8),
RichTextField(
@@ -84,8 +85,8 @@ class BioFormView extends GetView<BioFormController> with CharacterServiceMixin
textCapitalization: TextCapitalization.sentences,
onChanged: controller.setDirty,
decoration: InputDecoration(
label: Text(S.current.characterBioDialogAlignmentDescriptionLabel),
hintText: S.current.characterBioDialogAlignmentDescriptionPlaceholder,
label: Text(tr.bio.dialog.alignmentDescription.label),
hintText: tr.bio.dialog.alignmentDescription.placeholder,
),
),
const SizedBox(height: 80),

View File

@@ -9,7 +9,8 @@ class BondsFlagsFormController extends GetxController {
final flags = <SessionMark>[].obs;
final bondsDesc = <TextEditingController>[].obs;
final flagsDesc = <TextEditingController>[].obs;
late final void Function(List<SessionMark> bonds, List<SessionMark> flags) onChanged;
late final void Function(List<SessionMark> bonds, List<SessionMark> flags)
onChanged;
final dirty = false.obs;
@override
@@ -17,11 +18,15 @@ class BondsFlagsFormController extends GetxController {
super.onReady();
final BondsFlagsFormArguments args = Get.arguments;
bonds.value = args.bonds;
bondsDesc.value =
args.bonds.map((e) => TextEditingController(text: e.description)..addListener(_setDirty)).toList();
bondsDesc.value = args.bonds
.map((e) =>
TextEditingController(text: e.description)..addListener(_setDirty))
.toList();
flags.value = args.flags;
flagsDesc.value =
args.flags.map((e) => TextEditingController(text: e.description)..addListener(_setDirty)).toList();
flagsDesc.value = args.flags
.map((e) =>
TextEditingController(text: e.description)..addListener(_setDirty))
.toList();
onChanged = args.onChanged;
}
@@ -65,11 +70,13 @@ class BondsFlagsFormController extends GetxController {
void save() {
final newBonds = enumerate(bonds)
.map((e) => e.value.copyWithInherited(description: bondsDesc[e.index].text))
.map((e) =>
e.value.copyWithInherited(description: bondsDesc[e.index].text))
.where((e) => e.description.isNotEmpty)
.toList();
final newFlags = enumerate(flags)
.map((e) => e.value.copyWithInherited(description: flagsDesc[e.index].text))
.map((e) =>
e.value.copyWithInherited(description: flagsDesc[e.index].text))
.where((e) => e.description.isNotEmpty)
.toList();
@@ -87,7 +94,8 @@ class BondsFlagsFormController extends GetxController {
class BondsFlagsFormArguments {
final List<SessionMark> bonds;
final List<SessionMark> flags;
final void Function(List<SessionMark> bonds, List<SessionMark> flags) onChanged;
final void Function(List<SessionMark> bonds, List<SessionMark> flags)
onChanged;
BondsFlagsFormArguments({
required this.bonds,

View File

@@ -1,15 +1,14 @@
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/app/widgets/atoms/confirm_exit_view.dart';
import 'package:dungeon_paper/core/utils/list_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/bonds_flags_form_controller.dart';
class BondsFlagsFormView extends GetView<BondsFlagsFormController> {
const BondsFlagsFormView({Key? key}) : super(key: key);
const BondsFlagsFormView({super.key});
@override
Widget build(BuildContext context) {
@@ -19,18 +18,18 @@ class BondsFlagsFormView extends GetView<BondsFlagsFormController> {
dirty: controller.dirty.value,
child: Scaffold(
appBar: AppBar(
title: Text(S.current.characterBondsFlagsDialogTitle),
title: Text(tr.sessionMarks.title),
centerTitle: true,
),
floatingActionButton: AdvancedFloatingActionButton.extended(
onPressed: controller.save,
icon: const Icon(Icons.save),
label: Text(S.current.save),
label: Text(tr.generic.save),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(S.current.characterBondsFlagsDialogBonds, style: textTheme.headlineSmall),
Text(tr.sessionMarks.bonds, style: textTheme.headlineSmall),
for (final bond in enumerate(controller.bondsDesc))
ListTile(
contentPadding: const EdgeInsets.all(0),
@@ -47,11 +46,11 @@ class BondsFlagsFormView extends GetView<BondsFlagsFormController> {
),
OutlinedButton.icon(
onPressed: () => controller.addBond(),
label: Text(S.current.createGeneric(S.current.characterBondsFlagsDialogBond)),
label: Text(tr.generic.createEntity(tr.sessionMarks.bond)),
icon: const Icon(Icons.add),
),
const Divider(height: 24),
Text(S.current.characterBondsFlagsDialogFlags, style: textTheme.headlineSmall),
Text(tr.sessionMarks.flags, style: textTheme.headlineSmall),
for (final flag in enumerate(controller.flagsDesc))
ListTile(
contentPadding: const EdgeInsets.all(0),
@@ -68,7 +67,7 @@ class BondsFlagsFormView extends GetView<BondsFlagsFormController> {
),
OutlinedButton.icon(
onPressed: () => controller.addFlag(),
label: Text(S.current.createGeneric(S.current.characterBondsFlagsDialogFlag)),
label: Text(tr.generic.createEntity(tr.sessionMarks.flag)),
icon: const Icon(Icons.add),
),
],

View File

@@ -14,7 +14,8 @@ class CampaignsListController extends GetxController {
@override
void onInit() {
super.onInit();
_campaignsListenerSubscription = StorageHandler.instance.collectionListener('Campaigns', _campaignsListener);
_campaignsListenerSubscription = StorageHandler.instance
.collectionListener('Campaigns', _campaignsListener);
}
@override

View File

@@ -1,24 +1,23 @@
import 'package:dungeon_paper/app/data/models/campaign.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/campaigns_list_controller.dart';
class CampaignsListView extends GetView<CampaignsListController> {
const CampaignsListView({Key? key}) : super(key: key);
const CampaignsListView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.current.myGeneric(S.current.entityPlural(Campaign))),
title: Text(tr.generic.myEntity(tr.entityPlural(Campaign))),
centerTitle: true,
),
body: Obx(
() => controller.campaigns.isEmpty
? Center(
child: Text(S.current.noGeneric(S.current.entityPlural(Campaign))),
child: Text(tr.generic.noEntity(tr.entityPlural(Campaign))),
)
: ListView.builder(
itemCount: controller.campaigns.length,

View File

@@ -11,25 +11,24 @@ import 'package:dungeon_paper/app/widgets/menus/entity_edit_menu.dart';
import 'package:dungeon_paper/app/widgets/molecules/categorized_list.dart';
import 'package:dungeon_paper/app/widgets/molecules/character_subtitle.dart';
import 'package:dungeon_paper/core/utils/builder_utils.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../generated/l10n.dart';
class CharacterListPageView extends GetView<CharacterService> with UserServiceMixin {
const CharacterListPageView({Key? key}) : super(key: key);
class CharacterListPageView extends GetView<CharacterService>
with UserServiceMixin {
const CharacterListPageView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.current.characterListTitle),
title: Text(tr.generic.allEntities(tr.entity(Character))),
centerTitle: true,
),
floatingActionButton: AdvancedFloatingActionButton.extended(
onPressed: () => Get.toNamed(Routes.createCharacter),
label: Text(S.current.createCharacterAddButton),
label: Text(tr.generic.createEntity(tr.entity(Character))),
icon: const Icon(Icons.add),
),
body: Obx(
@@ -38,18 +37,23 @@ class CharacterListPageView extends GetView<CharacterService> with UserServiceMi
children: [
for (final cat in controller.charsByCategory.keys)
() => CategorizedList(
title: Text(cat.isNotEmpty ? cat : S.current.characterNoCategory),
title:
Text(cat.isNotEmpty ? cat : tr.character.noCategory),
onReorder: (oldIndex, newIndex) => controller.updateAll(
CharacterUtils.reorderCharacters(controller.charsByCategory[cat]!).call(oldIndex, newIndex),
CharacterUtils.reorderCharacters(
controller.charsByCategory[cat]!)
.call(oldIndex, newIndex),
),
children: [
for (var char in controller.charsByCategory[cat]!)
Builder(
key: Key(char.key),
builder: (context) {
final charTheme = AppThemes.getTheme(char.getCurrentTheme(user));
final charTheme = AppThemes.getTheme(
char.getCurrentTheme(user));
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
padding:
const EdgeInsets.symmetric(vertical: 4),
child: Card(
margin: EdgeInsets.zero,
color: charTheme.scaffoldBackgroundColor,
@@ -57,17 +61,20 @@ class CharacterListPageView extends GetView<CharacterService> with UserServiceMi
minLeadingWidth: 48,
minVerticalPadding: 16,
horizontalTitleGap: 10,
textColor: charTheme.colorScheme.onBackground,
textColor:
charTheme.colorScheme.onBackground,
// textColor: ThemeData.estimateBrightnessForColor(charTheme.scaffoldBackgroundColor) == Brightness.light ? Colors.black : Colors.white,
child: InkWell(
borderRadius: borderRadius,
splashColor: Theme.of(context).splashColor,
splashColor:
Theme.of(context).splashColor,
onTap: () {
controller.setCurrent(char.key);
Get.offAllNamed(Routes.home);
},
child: ListTile(
leading: CharacterAvatar.squircle(character: char, size: 48),
leading: CharacterAvatar.squircle(
character: char, size: 48),
title: Text(char.displayName),
subtitle: CharacterSubtitle(
character: char,
@@ -79,9 +86,10 @@ class CharacterListPageView extends GetView<CharacterService> with UserServiceMi
context,
DeleteDialogOptions(
entityName: char.displayName,
entityKind: S.current.entity(Character),
entityKind: tr.entity(Character),
),
() => controller.deleteCharacter(char),
() => controller
.deleteCharacter(char),
),
),
),

View File

@@ -8,7 +8,8 @@ class ClassAlignmentsController extends GetxController {
final selected = Rx<dw.AlignmentType?>(null);
bool selectable = false;
bool editable = false;
late final void Function(AlignmentValues alignments, dw.AlignmentType? selected)? onChanged;
late final void Function(
AlignmentValues alignments, dw.AlignmentType? selected)? onChanged;
final sortedAlignmentTypes = dw.AlignmentType.values.toList();
final editing = <dw.AlignmentType, bool>{}.obs;
final textControllers = <dw.AlignmentType, TextEditingController>{}.obs;
@@ -46,7 +47,8 @@ class ClassAlignmentsController extends GetxController {
}
bool isEditing(dw.AlignmentType type) => editable && editing[type] == true;
bool isSelected(dw.AlignmentType type) => selectable && selected.value == type;
bool isSelected(dw.AlignmentType type) =>
selectable && selected.value == type;
void save() {
final updated = alignments.value.copyWithInherited(
@@ -64,7 +66,8 @@ class ClassAlignmentsController extends GetxController {
class ClassAlignmentsArguments {
final AlignmentValues? alignments;
final void Function(AlignmentValues alignments, dw.AlignmentType? selected)? onChanged;
final void Function(AlignmentValues alignments, dw.AlignmentType? selected)?
onChanged;
final bool selectable;
final dw.AlignmentType? preselected;
final bool editable;

View File

@@ -3,37 +3,38 @@ import 'package:dungeon_paper/app/themes/colors.dart';
import 'package:dungeon_paper/app/themes/themes.dart';
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/app/widgets/molecules/dialog_controls.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/class_alignments_controller.dart';
class ClassAlignmentsView extends GetView<ClassAlignmentsController> {
const ClassAlignmentsView({Key? key}) : super(key: key);
const ClassAlignmentsView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(S.current.selectGeneric(S.current.entity(AlignmentValue))),
title: Text(tr.generic.selectEntity(tr.entity(AlignmentValue))),
centerTitle: true,
),
floatingActionButton: controller.onChanged != null
? AdvancedFloatingActionButton.extended(
onPressed: controller.save,
label: Text(S.current.save),
label: Text(tr.generic.save),
icon: const Icon(Icons.save),
)
: null,
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0).copyWith(bottom: 80),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0)
.copyWith(bottom: 80),
children: [
for (final alignment in controller.sortedAlignmentTypes)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Obx(() {
final description = controller.alignments.value.byType(alignment);
final description =
controller.alignments.value.byType(alignment);
final isEditing = controller.isEditing(alignment);
final isSelected = controller.isSelected(alignment);
@@ -47,7 +48,7 @@ class ClassAlignmentsView extends GetView<ClassAlignmentsController> {
ListTile(
minLeadingWidth: 16,
leading: Icon(AlignmentValue.iconOf(alignment)),
title: Text(S.current.alignment(alignment)),
title: Text(tr.alignment.name(alignment.name)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: !isEditing
@@ -55,25 +56,37 @@ class ClassAlignmentsView extends GetView<ClassAlignmentsController> {
if (controller.editable)
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => controller.toggleEdit(alignment, true),
onPressed: () => controller.toggleEdit(
alignment, true),
iconSize: 16,
),
if (controller.selectable)
ElevatedButton.icon(
icon: const Icon(Icons.check),
label: Text(!isSelected ? S.current.select : S.current.selected),
onPressed: !isSelected ? () => controller.select(alignment) : null,
label: Text(!isSelected
? tr.generic.select
: tr.generic.selected),
onPressed: !isSelected
? () => controller.select(alignment)
: null,
),
]
: DialogControls.done(context, () => controller.toggleEdit(alignment, false)),
: DialogControls.done(
context,
() => controller.toggleEdit(
alignment, false)),
),
),
Padding(
padding: const EdgeInsets.all(8).copyWith(left: 56, top: 0),
padding: const EdgeInsets.all(8)
.copyWith(left: 56, top: 0),
child: !isEditing
? Text(description.isEmpty ? S.current.noDescription : description)
? Text(description.isEmpty
? tr.generic.noDescription
: description)
: TextField(
controller: controller.textControllers[alignment]!,
controller:
controller.textControllers[alignment]!,
),
)
],

View File

@@ -27,13 +27,14 @@ class SelectMovesSpellsController extends GetxController {
onChanged = args.onChanged;
}
Iterable<Move> get sortedMoves => [...moves]..sort((a, b) => a.category == b.category
? cleanStr(a.name).compareTo(cleanStr(b.name))
: a.category == MoveCategory.basic
? -1
: b.category == MoveCategory.basic
? 1
: 0);
Iterable<Move> get sortedMoves =>
[...moves]..sort((a, b) => a.category == b.category
? cleanStr(a.name).compareTo(cleanStr(b.name))
: a.category == MoveCategory.basic
? -1
: b.category == MoveCategory.basic
? 1
: 0);
}
class SelectMovesSpellsArguments {

View File

@@ -9,17 +9,16 @@ import 'package:dungeon_paper/app/widgets/cards/move_card.dart';
import 'package:dungeon_paper/app/widgets/cards/spell_card.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/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/select_moves_spells_controller.dart';
class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
const SelectMovesSpellsView({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@@ -28,12 +27,13 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
dirty: controller.dirty.value,
child: Scaffold(
appBar: AppBar(
title: Text(S.current.selectGeneric(S.current.createCharacterMovesSpells)),
title: Text(
tr.generic.selectEntity(tr.createCharacter.movesSpells.title)),
centerTitle: true,
),
floatingActionButton: AdvancedFloatingActionButton.extended(
onPressed: _save,
label: Text(S.current.save),
label: Text(tr.generic.save),
icon: const Icon(Icons.save),
),
body: SingleChildScrollView(
@@ -43,8 +43,11 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
children: [
// MOVES TITLE
Obx(() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(S.current.movesWithCount(controller.moves.length), style: titleStyle),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
tr.entityCountNum(Move, controller.moves.length),
style: titleStyle),
)),
// MOVES CARDS
Obx(
@@ -62,11 +65,14 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openMovePage(
abilityScores: controller.abilityScores.value,
abilityScores:
controller.abilityScores.value,
move: move,
onSave: (_move) => controller.moves.value = updateByKey(controller.moves, [_move]),
onSave: (move) => controller.moves.value =
updateByKey(controller.moves, [move]),
),
onDelete: () => controller.moves.value = removeByKey(controller.moves, [move]),
onDelete: () => controller.moves.value =
removeByKey(controller.moves, [move]),
),
],
),
@@ -95,15 +101,18 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
);
},
),
label: Text(S.current.addGeneric(S.current.entityPlural(Move))),
label: Text(tr.generic.addEntity(tr.entityPlural(Move))),
icon: const Icon(Icons.add),
),
),
),
// SPELLS TITLE
Obx(() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8).copyWith(top: 24),
child: Text(S.current.spellsWithCount(controller.spells.length), style: titleStyle),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8)
.copyWith(top: 24),
child: Text(tr.entityCount(Spell, controller.spells.length),
style: titleStyle),
)),
// SPELL CARDS
Obx(
@@ -122,9 +131,10 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
ElevatedButton.icon(
style: ButtonThemes.primaryElevated(context),
onPressed: () {
controller.spells.value = removeByKey(controller.spells, [spell]);
controller.spells.value =
removeByKey(controller.spells, [spell]);
},
label: Text(S.current.remove),
label: Text(tr.generic.remove),
icon: const Icon(Icons.remove),
)
],
@@ -149,11 +159,12 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
controller.dirty.value = true;
controller.spells.value = addByKey(
controller.spells,
spells.map((m) => m.copyWithInherited(prepared: true)),
spells
.map((m) => m.copyWithInherited(prepared: true)),
);
},
),
label: Text(S.current.addGeneric(S.current.entityPlural(Spell))),
label: Text(tr.generic.addEntity(tr.entityPlural(Spell))),
icon: const Icon(Icons.add),
),
),

View File

@@ -22,7 +22,9 @@ class CreateCharacterController extends GetxController {
final name = ''.obs;
final avatarUrl = ''.obs;
final characterClass = Rx<CharacterClass?>(null);
final abilityScores = AbilityScores.dungeonWorld(dex: 10, str: 10, wis: 10, con: 10, intl: 10, cha: 10).obs;
final abilityScores = AbilityScores.dungeonWorld(
dex: 10, str: 10, wis: 10, con: 10, intl: 10, cha: 10)
.obs;
final startingGear = <GearSelection>[].obs;
final moves = <Move>[].obs;
final spells = <Spell>[].obs;
@@ -41,7 +43,8 @@ class CreateCharacterController extends GetxController {
race.value != null,
].every((element) => element == true);
List<Item> get items => GearChoice.selectionToItems(startingGear, equipped: true);
List<Item> get items =>
GearChoice.selectionToItems(startingGear, equipped: true);
double get coins => GearChoice.selectionToCoins(startingGear);
@@ -54,7 +57,8 @@ class CreateCharacterController extends GetxController {
void setClass(CharacterClass cls) {
characterClass.value = cls;
setStartingGear(
cls.gearChoices.fold([], (all, cur) => [...all, ...cur.preselectedGearSelections]),
cls.gearChoices
.fold([], (all, cur) => [...all, ...cur.preselectedGearSelections]),
);
addStartingMoves();
setDirty();
@@ -95,7 +99,9 @@ class CreateCharacterController extends GetxController {
moves.clear();
moves.addAll(
[...repo.builtIn.moves.values, ...repo.my.moves.values]
.where((m) => (m.classKeys.contains(characterClass.value!.reference) && m.category == MoveCategory.starting))
.where((m) =>
(m.classKeys.contains(characterClass.value!.reference) &&
m.category == MoveCategory.starting))
.map(
// favorite: move.category != MoveCategory.basic
(move) => Move.fromDwMove(move, favorite: true),
@@ -121,7 +127,8 @@ class CreateCharacterController extends GetxController {
),
sessionMarks: [
...(characterClass.value?.bonds
.map((bond) => SessionMark.bond(description: bond, completed: false, key: uuid()))
.map((bond) => SessionMark.bond(
description: bond, completed: false, key: uuid()))
.toList() ??
[]),
...Character.defaultEndOfSessionMarks,

View File

@@ -20,9 +20,8 @@ import 'package:dungeon_paper/app/themes/colors.dart';
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/app/widgets/atoms/character_avatar.dart';
import 'package:dungeon_paper/app/widgets/atoms/confirm_exit_view.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
@@ -31,7 +30,7 @@ import '../../../widgets/chips/advanced_chip.dart';
import '../controllers/create_character_controller.dart';
class CreateCharacterView extends GetView<CreateCharacterController> {
const CreateCharacterView({Key? key}) : super(key: key);
const CreateCharacterView({super.key});
CharacterClass? get cls => controller.characterClass.value;
@override
@@ -62,7 +61,7 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
: null,
icon: const Icon(Icons.person_add),
label: Text(
S.current.createGeneric(Character),
tr.generic.createEntity(tr.entity(Character)),
),
),
),
@@ -82,15 +81,21 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
_Card(
leading: CharacterAvatar.squircle(
size: 48,
character: Character.empty().copyWith(avatarUrl: controller.avatarUrl.value),
character: Character.empty().copyWith(
avatarUrl: controller.avatarUrl.value,
),
),
title: controller.name.isEmpty
? Text(S.current.createCharacterTravelerBlankName)
? Text(
tr.createCharacter.basicInfo.defaultName,
)
: Text(controller.name.value),
subtitle: controller.name.isEmpty
? Text(S.current.createCharacterTravelerHelpText)
? Text(tr.createCharacter.basicInfo.helpText)
: Text(
S.current.createCharacterTravelerDescription(cls?.name ?? ''),
tr.createCharacter.basicInfo.description(
cls?.name ?? '',
),
),
valid: controller.name.isNotEmpty,
onTap: () => Get.toNamed(
@@ -106,19 +111,28 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
// Class
_Card(
title: cls == null
? Text(S.current.selectGeneric(S.current.entity(CharacterClass)))
? Text(tr.generic
.selectEntity(tr.entity(CharacterClass)))
: Text(cls!.name),
subtitle: cls == null
? Text(S.current.createCharacterClassHelpText)
? Text(tr.createCharacter.characterClass
.noSelection)
: Text(
S.current.createCharacterClassDescription(cls!.hp, cls!.load, cls!.damageDice),
tr.createCharacter.characterClass
.description(
cls!.hp,
cls!.load,
cls!.damageDice.toString(),
),
),
valid: cls != null,
onTap: () => Get.toNamed(
Routes.createCharacterSelectClass,
arguments: CharacterClassLibraryListArguments(
preSelections:
controller.characterClass.value != null ? [controller.characterClass.value!] : [],
controller.characterClass.value != null
? [controller.characterClass.value!]
: [],
onSelected: (cls) => controller.setClass(cls),
),
preventDuplicates: false,
@@ -127,10 +141,12 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
// Race
_Card(
title: controller.race.value == null
? Text(S.current.selectGeneric(S.current.entity(Race)))
? Text(
tr.generic.selectEntity(tr.entity(Race)))
: Text(controller.race.value!.name),
subtitle: controller.race.value == null
? Text(S.current.errorNoSelectionGeneric(S.current.entity(Race)))
? Text(tr.generic
.noEntitySelected(tr.entity(Race)))
: Text(
controller.race.value!.description,
overflow: TextOverflow.ellipsis,
@@ -139,27 +155,32 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
? () => ModelPages.openRacesList(
character: controller.getAsCharacter(),
preSelection: controller.race.value,
onSelected: (race) => controller.race.value = race,
onSelected: (race) =>
controller.race.value = race,
)
: null,
valid: controller.race.value != null,
),
// Ability Scores
_Card(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(S.current.selectGeneric(S.current.entityPlural(AbilityScore))),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
title: Text(tr.generic
.selectEntity(tr.entityPlural(AbilityScore))),
// subtitle: Text(
// controller.abilityScores.value.stats
// .map((stat) => '${stat.key}: ${stat.value}')
// .join(', '),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: _AbilityScoreChipList(controller: controller),
child: _AbilityScoreChipList(
controller: controller),
),
onTap: () => Get.toNamed(
Routes.createCharacterAbilityScores,
arguments: AbilityScoresFormArguments(
onChanged: (abilityScores) => controller.setAbilityScores(abilityScores),
onChanged: (abilityScores) => controller
.setAbilityScores(abilityScores),
abilityScores: controller.abilityScores.value,
),
preventDuplicates: false,
@@ -167,31 +188,43 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
),
// Alignment
_Card(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 0),
valid: controller.alignment.value != null,
title: Text(controller.alignment.value != null
? S.current.entity(AlignmentValue) +
': ' +
S.current.alignment(controller.alignment.value!.type)
: S.current.selectGeneric(S.current.entity(AlignmentValue))),
title: Text(
controller.alignment.value != null
? [
tr.entity(AlignmentValue),
tr.alignment.name(controller
.alignment.value!.type.name)
].join(': ')
: tr.generic.selectEntity(
tr.entity(AlignmentValue),
),
),
subtitle: controller.alignment.value != null
? Text(
controller.alignment.value!.description.isNotEmpty
? controller.alignment.value!.description
: S.current.noDescription,
controller.alignment.value!.description
.isNotEmpty
? controller
.alignment.value!.description
: tr.generic.noDescription,
overflow: TextOverflow.ellipsis,
maxLines: 1,
)
: Text(
S.current.errorNoSelectionGenericRequired(S.current.entity(AlignmentValue)),
tr.generic.noEntitySelectedRequired(
tr.entity(AlignmentValue)),
),
onTap: cls != null
? () => Get.toNamed(
Routes.classAlignments,
arguments: ClassAlignmentsArguments(
onChanged: controller.setAlignment,
alignments: controller.characterClass.value!.alignments,
preselected: controller.alignment.value?.type,
alignments: controller
.characterClass.value!.alignments,
preselected:
controller.alignment.value?.type,
selectable: true,
editable: true,
),
@@ -202,19 +235,27 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
// Starting Gear
_Card(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(S.current.selectGeneric(S.current.entity(GearSelection))),
subtitle: Text(controller.items.isEmpty && controller.coins == 0
? S.current.createCharacterStartingGearHelpText
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
title: Text(tr.generic
.selectEntity(tr.entity(GearSelection))),
subtitle: Text(controller.items.isEmpty &&
controller.coins == 0
? tr.createCharacter.startingGear.helpText
: [
controller.coins > 0
? S.current.createCharacterStartingGearDescriptionCoins(
NumberFormat('#0.#').format(controller.coins),
? tr.createCharacter.startingGear
.coins(
NumberFormat('#0.#')
.format(controller.coins),
)
: null,
controller.items
.map((i) => S.current.createCharacterStartingGearDescriptionItem(
NumberFormat('#0.#').format(i.amount),
.map((i) => tr
.createCharacter.startingGear
.item(
NumberFormat('#0.#')
.format(i.amount),
i.name,
))
.join(', '),
@@ -224,7 +265,8 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
Routes.createCharacterStartingGear,
arguments: StartingGearFormArguments(
onChanged: controller.setStartingGear,
selectedOptions: controller.startingGear,
selectedOptions:
controller.startingGear,
characterClass: cls!,
),
preventDuplicates: false,
@@ -234,7 +276,9 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
? false
: cls!.gearChoices.every(
(c) => c.selections.any(
(s) => controller.startingGear.map((x) => x.key).contains(s.key),
(s) => controller.startingGear
.map((x) => x.key)
.contains(s.key),
),
),
),
@@ -242,19 +286,23 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
_Card(
// contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
title: Text(
S.current.selectGeneric(
tr.generic.selectEntity(
(cls?.isSpellcaster ?? false)
? S.current.createCharacterMovesSpells
: S.current.entityPlural(Move),
? tr.createCharacter.movesSpells.title
: tr.entityPlural(Move),
),
),
subtitle: Text(
(cls?.isSpellcaster ?? false)
? S.current.createCharacterMovesSpellsDescription(
? tr.createCharacter.movesSpells
.description(
controller.moves.length,
controller.spells.length,
)
: S.current.movesWithCount(controller.moves.length),
: tr.entityCountNum(
Move,
controller.moves.length,
),
),
onTap: cls != null
? () => Get.toNamed(
@@ -263,8 +311,10 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
onChanged: controller.setMovesSpells,
moves: controller.moves,
spells: controller.spells,
abilityScores: controller.abilityScores.value,
characterClass: controller.characterClass.value!,
abilityScores:
controller.abilityScores.value,
characterClass:
controller.characterClass.value!,
),
preventDuplicates: false,
)
@@ -287,7 +337,6 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
class _AbilityScoreChipList extends StatelessWidget {
const _AbilityScoreChipList({
super.key,
required this.controller,
});
@@ -331,14 +380,13 @@ class _AbilityScoreChipList extends StatelessWidget {
class _Card extends StatelessWidget {
const _Card({
Key? key,
this.contentPadding,
this.leading,
required this.title,
required this.subtitle,
this.valid = true,
required this.onTap,
}) : super(key: key);
});
final EdgeInsets? contentPadding;
final Widget? leading;

View File

@@ -3,18 +3,20 @@ import 'package:dungeon_paper/app/data/services/user_service.dart';
import 'package:dungeon_paper/app/modules/Migration/controllers/migration_controller.dart';
import 'package:dungeon_paper/app/routes/app_pages.dart';
import 'package:dungeon_paper/app/widgets/atoms/user_menu.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HomeAppBar extends StatelessWidget with LoadingServiceMixin, UserServiceMixin implements PreferredSizeWidget {
class HomeAppBar extends StatelessWidget
with LoadingServiceMixin, UserServiceMixin
implements PreferredSizeWidget {
const HomeAppBar({super.key});
@override
Widget build(BuildContext context) {
return Obx(
() => AppBar(
title: Text(S.current.appName),
title: Text(tr.app.name),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.search),

View File

@@ -28,15 +28,14 @@ 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/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'local_widgets/home_character_actions_summary.dart';
class HomeCharacterActionsView extends GetView<CharacterService> {
const HomeCharacterActionsView({Key? key}) : super(key: key);
const HomeCharacterActionsView({super.key});
Character get char => controller.current;
@@ -106,19 +105,19 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
children: [
Expanded(
child: ElevatedButton(
child: Text(
S.current.actionsBasicMoves,
),
onPressed: _openBasicMoves,
child: Text(
tr.actions.moves.basic,
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
child: Text(
S.current.actionsSpecialMoves,
),
onPressed: _openSpecialMoves,
child: Text(
tr.actions.moves.special,
),
),
),
],
@@ -128,16 +127,18 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
const SizedBox(height: 4),
if (char.settings.racePosition == RacePosition.start) raceCard,
],
trailing: char.settings.racePosition == RacePosition.end ? [raceCard] : [],
trailing:
char.settings.racePosition == RacePosition.end ? [raceCard] : [],
menuTrailing: [
if (char.settings.racePosition != RacePosition.start)
// Move to start of list
MenuEntry(
value: 'move_to_start',
label: Text(S.current.moveToStartGeneric(S.current.entity(Race))),
label: Text(tr.sort.moveEntityToTop(tr.entity(Race))),
onSelect: () => controller.updateCharacter(
char.copyWith(
settings: char.settings.copyWith(racePosition: RacePosition.start),
settings:
char.settings.copyWith(racePosition: RacePosition.start),
),
),
),
@@ -145,10 +146,11 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
// Move to end of list
MenuEntry(
value: 'move_to_end',
label: Text(S.current.moveToEndGeneric(S.current.entity(Race))),
label: Text(tr.sort.moveEntityToBottom(tr.entity(Race))),
onSelect: () => controller.updateCharacter(
char.copyWith(
settings: char.settings.copyWith(racePosition: RacePosition.end),
settings:
char.settings.copyWith(racePosition: RacePosition.end),
),
),
),
@@ -225,7 +227,8 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
onSelected: (items) => onSelected(
items
.map(
(x) => x.copyWithInherited(amount: x.amount == 0 ? 1 : x.amount),
(x) =>
x.copyWithInherited(amount: x.amount == 0 ? 1 : x.amount),
)
.toList(),
),
@@ -245,7 +248,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
ChecklistMenuEntry(
value: 'countArmor',
checked: item.settings.countArmor,
label: Text(S.current.itemSettingsCountArmor),
label: Text(tr.items.settings.countArmor),
onChanged: (value) => onSave(false)(
item.copyWithInherited(
settings: item.settings.copyWith(countArmor: value!),
@@ -255,7 +258,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
ChecklistMenuEntry(
value: 'countDamage',
checked: item.settings.countDamage,
label: Text(S.current.itemSettingsCountDamage),
label: Text(tr.items.settings.countDamage),
onChanged: (value) => onSave(false)(
item.copyWithInherited(
settings: item.settings.copyWith(countDamage: value!),
@@ -265,7 +268,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
ChecklistMenuEntry(
value: 'countWeight',
checked: item.settings.countWeight,
label: Text(S.current.itemSettingsCountWeight),
label: Text(tr.items.settings.countWeight),
onChanged: (value) => onSave(false)(
item.copyWithInherited(
settings: item.settings.copyWith(countWeight: value!),
@@ -319,7 +322,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
class ActionsCardList<T extends WithMeta> extends GetView<CharacterService>
with LibraryServiceMixin, RepositoryServiceMixin {
const ActionsCardList({
Key? key,
super.key,
required this.route,
required this.addPageArguments,
required this.cardBuilder,
@@ -330,7 +333,7 @@ class ActionsCardList<T extends WithMeta> extends GetView<CharacterService>
this.menuTrailing = const [],
this.leading = const [],
this.trailing = const [],
}) : super(key: key);
});
final String route;
final List<Widget> leading;
@@ -356,17 +359,18 @@ class ActionsCardList<T extends WithMeta> extends GetView<CharacterService>
Widget build(BuildContext context) {
return CategorizedList(
initiallyExpanded: true,
title: Text(S.current.entityPlural(T)),
title: Text(tr.entityPlural(T)),
itemPadding: const EdgeInsets.only(bottom: 8),
titleTrailing: [
TextButton.icon(
onPressed: () => Get.toNamed(
route,
arguments: addPageArguments(
onSelected: (items) => library.upsertToCharacter(items, forkBehavior: ForkBehavior.fork),
onSelected: (items) => library.upsertToCharacter(items,
forkBehavior: ForkBehavior.fork),
),
),
label: Text(S.current.addGeneric(S.current.entityPlural(T))),
label: Text(tr.generic.addEntity(tr.entityPlural(T))),
icon: const Icon(Icons.add),
),
GroupSortMenu(
@@ -382,19 +386,20 @@ class ActionsCardList<T extends WithMeta> extends GetView<CharacterService>
children: [
...list.map(
(obj) => _wrapChild(
key: PageStorageKey('type-$T-' + obj.key),
key: PageStorageKey('type-$T-${obj.key}'),
child: cardBuilder(
obj,
onDelete: _confirmDeleteDlg(context, obj, obj.displayName),
onSave: (fork) => (_obj) {
library.upsertToCharacter([_obj], forkBehavior: ForkBehavior.none);
onSave: (fork) => (obj) {
library
.upsertToCharacter([obj], forkBehavior: ForkBehavior.none);
},
),
),
),
],
onReorder: (oldIndex, newIndex) =>
controller.updateCharacter(CharacterUtils.reorderByType<T>(char, oldIndex, newIndex)),
onReorder: (oldIndex, newIndex) => controller.updateCharacter(
CharacterUtils.reorderByType<T>(char, oldIndex, newIndex)),
);
}
@@ -404,12 +409,13 @@ class ActionsCardList<T extends WithMeta> extends GetView<CharacterService>
child: child,
);
void Function() _confirmDeleteDlg(BuildContext context, T object, String name) {
void Function() _confirmDeleteDlg(
BuildContext context, T object, String name) {
return () => deleteDialog.confirm(
context,
DeleteDialogOptions(
entityName: name,
entityKind: S.current.entity(T),
entityKind: tr.entity(T),
),
() => controller.updateCharacter(
CharacterUtils.removeByType<T>(char, [object]),

View File

@@ -12,13 +12,12 @@ 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/storage_handler/storage_handler.dart';
import 'package:dungeon_paper/core/utils/list_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HomeCharacterJournalView extends GetView<CharacterService> {
const HomeCharacterJournalView({Key? key}) : super(key: key);
const HomeCharacterJournalView({super.key});
Character get char => controller.current;
@@ -47,9 +46,10 @@ class HomeCharacterJournalView extends GetView<CharacterService> {
children: [
for (final cat in enumerate(char.noteCategories))
CategorizedList(
key: Key('note-category-' + cat.value),
key: Key('note-category-${cat.value}'),
initiallyExpanded: true,
title: Text(cat.value.isEmpty ? S.current.noteNoCategory : cat.value),
title:
Text(cat.value.isEmpty ? tr.notes.noCategory : cat.value),
titleTrailing: [
GroupSortMenu(
index: cat.index,
@@ -58,33 +58,37 @@ class HomeCharacterJournalView extends GetView<CharacterService> {
),
],
onReorder: (oldIndex, newIndex) => controller.updateCharacter(
CharacterUtils.reorderByType<Note>(char, oldIndex, newIndex, extraData: cat.value),
CharacterUtils.reorderByType<Note>(char, oldIndex, newIndex,
extraData: cat.value),
),
children: char.notes
.where((note) => note.localizedCategory == cat.value)
.map(
(note) => Padding(
key: Key('note-' + note.key),
key: Key('note-${note.key}'),
padding: const EdgeInsets.symmetric(vertical: 4),
child: NoteCard(
note: note,
reorderablePadding: true,
actions: [
EntityEditMenu(
onDelete: confirmDelete(context, note, note.title),
onDelete:
confirmDelete(context, note, note.title),
onEdit: () => ModelPages.openNotePage(
note: note,
onSave: (_note) {
onSave: (note) {
controller.updateCharacter(
CharacterUtils.updateByType<Note>(char, [_note]),
CharacterUtils.updateByType<Note>(
char, [note]),
);
StorageHandler.instance.create('Notes', note.key, note.toJson());
StorageHandler.instance
.create('Notes', note.key, note.toJson());
},
),
),
],
onSave: (_note) => controller.updateCharacter(
CharacterUtils.updateNotes(char, [_note]),
onSave: (note) => controller.updateCharacter(
CharacterUtils.updateNotes(char, [note]),
),
),
),
@@ -97,22 +101,25 @@ class HomeCharacterJournalView extends GetView<CharacterService> {
);
}
void Function() confirmDelete<T>(BuildContext context, T object, String name) {
// TODO use existing confirmDelete
void Function() confirmDelete<T>(
BuildContext context, T object, String name) {
return () async {
final result = await Get.dialog<bool>(
AlertDialog(
title: Text(S.current.confirmDeleteTitle(S.current.entity(T))),
content: Text(S.current.confirmDeleteBody(S.current.entity(T), name)),
title: Text(tr.dialogs.confirmations.delete.title(tr.entity(T))),
content:
Text(tr.dialogs.confirmations.delete.body(tr.entity(T), name)),
actions: [
ElevatedButton.icon(
icon: const Icon(Icons.close),
label: Text(S.current.cancel),
label: Text(tr.generic.cancel),
onPressed: () => Get.back(result: false),
style: ButtonThemes.primaryElevated(context),
),
ElevatedButton.icon(
icon: const Icon(Icons.delete),
label: Text(S.current.remove),
label: Text(tr.generic.remove),
onPressed: () => Get.back(result: true),
style: ButtonThemes.errorElevated(context),
),
@@ -130,7 +137,8 @@ class HomeCharacterJournalView extends GetView<CharacterService> {
break;
case Spell:
controller.updateCharacter(
char.copyWith(spells: removeByKey(char.spells, [object as Spell])),
char.copyWith(
spells: removeByKey(char.spells, [object as Spell])),
);
break;
case Item:

View File

@@ -10,15 +10,16 @@ import 'package:dungeon_paper/app/widgets/molecules/ability_scores_grid.dart';
import 'package:dungeon_paper/core/dw_icons.dart';
import 'package:dungeon_paper/core/utils/builder_utils.dart';
import 'package:dungeon_paper/core/utils/math_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'local_widgets/home_character_dynamic_cards.dart';
import 'local_widgets/home_character_header_view.dart';
import 'local_widgets/home_character_hp_xp_view.dart';
class HomeCharacterView extends GetView<CharacterService> with HomeCharacterPaddingMixin {
const HomeCharacterView({Key? key}) : super(key: key);
class HomeCharacterView extends GetView<CharacterService>
with HomeCharacterPaddingMixin {
const HomeCharacterView({super.key});
@override
Widget build(BuildContext context) {
@@ -45,7 +46,7 @@ class HomeCharacterView extends GetView<CharacterService> with HomeCharacterPadd
const SizedBox(height: 8),
pad(Text(
char.displayName,
textScaleFactor: 1.4,
textScaler: const TextScaler.linear(1.4),
textAlign: TextAlign.center,
)),
CharacterSubtitle(character: char),
@@ -69,7 +70,7 @@ class HomeCharacterView extends GetView<CharacterService> with HomeCharacterPadd
icon: const Icon(DwIcons.swords),
// visualDensity: VisualDensity.compact,
label: char.damageDice.toString(),
tooltip: S.current.damageDice,
tooltip: tr.character.data.damageDice,
onPressed: () => Get.dialog(
DamageDiceDialog(
damage: char.stats.damageDice,
@@ -85,7 +86,7 @@ class HomeCharacterView extends GetView<CharacterService> with HomeCharacterPadd
),
const SizedBox(width: 8),
PrimaryChip(
tooltip: S.current.armor,
tooltip: tr.armor.title,
icon: const Icon(DwIcons.armor),
// visualDensity: VisualDensity.compact,
label: char.armor.toString(),
@@ -120,7 +121,8 @@ class HomeCharacterView extends GetView<CharacterService> with HomeCharacterPadd
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => DiceUtils.openRollDialog(char.rollButtons[0].diceFor(char)),
onPressed: () => DiceUtils.openRollDialog(
char.rollButtons[0].diceFor(char)),
style: ButtonThemes.primaryElevated(context),
label: Text(char.rollButtons[0].label),
icon: const Icon(DwIcons.dice_d6),
@@ -129,7 +131,8 @@ class HomeCharacterView extends GetView<CharacterService> with HomeCharacterPadd
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: () => DiceUtils.openRollDialog(char.rollButtons[1].diceFor(char)),
onPressed: () => DiceUtils.openRollDialog(
char.rollButtons[1].diceFor(char)),
style: ButtonThemes.primaryElevated(context),
label: Text(char.rollButtons[1].label),
icon: const Icon(DwIcons.dice_d6),
@@ -144,7 +147,8 @@ class HomeCharacterView extends GetView<CharacterService> with HomeCharacterPadd
}
}
class HomeCharacterLayout extends StatelessWidget with HomeCharacterPaddingMixin {
class HomeCharacterLayout extends StatelessWidget
with HomeCharacterPaddingMixin {
const HomeCharacterLayout({
super.key,
required this.leftCol,
@@ -194,7 +198,10 @@ class HomeCharacterLayout extends StatelessWidget with HomeCharacterPaddingMixin
if (!scrollable) {
return Column(
children: [for (final i in range(builder.itemCount)) builder.itemBuilder(context, i)],
children: [
for (final i in range(builder.itemCount))
builder.itemBuilder(context, i)
],
);
}

View File

@@ -3,7 +3,7 @@ import 'package:dungeon_paper/app/data/services/character_service.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/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
class HomeFAB extends StatefulWidget {
@@ -50,7 +50,7 @@ class _HomeFABState extends State<HomeFAB> with CharacterServiceMixin {
scale: inPageRange ? 1.0 : 0.0,
duration: duration,
child: Text(
S.current.createGeneric(Note),
tr.generic.createEntity(tr.entity(Note)),
),
),
icon: AnimatedScale(

View File

@@ -3,25 +3,24 @@ import 'package:dungeon_paper/app/modules/Home/views/home_character_view.dart';
import 'package:dungeon_paper/app/modules/Home/views/local_widgets/home_character_header_view.dart';
import 'package:dungeon_paper/app/widgets/atoms/character_avatar.dart';
import 'package:dungeon_paper/core/utils/math_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:skeleton_loader/skeleton_loader.dart';
class HomeLoaderView extends GetView with LoadingServiceMixin {
const HomeLoaderView({Key? key}) : super(key: key);
const HomeLoaderView({super.key});
String get title {
if (loadingService.loadingUser) {
return S.current.loadingUser;
return tr.loading.user;
}
if (loadingService.loadingCharacters) {
return S.current.loadingCharacters;
return tr.loading.characters;
}
return S.current.loadingGeneral;
return tr.loading.general;
}
@override
@@ -29,11 +28,14 @@ class HomeLoaderView extends GetView with LoadingServiceMixin {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final brightness = theme.brightness;
final skeletonColor =
brightness == Brightness.light ? theme.cardColor.withOpacity(0.65) : colorScheme.surfaceVariant;
final skeletonColor = brightness == Brightness.light
? theme.cardColor.withOpacity(0.65)
: colorScheme.surfaceVariant;
final skeletonHighlightColor = brightness == Brightness.light
? Color.alphaBlend(theme.cardColor.withOpacity(0.65), colorScheme.surfaceTint.withOpacity(0.5))
: Color.alphaBlend(theme.cardColor.withOpacity(0.65), colorScheme.surfaceTint);
? Color.alphaBlend(theme.cardColor.withOpacity(0.65),
colorScheme.surfaceTint.withOpacity(0.5))
: Color.alphaBlend(
theme.cardColor.withOpacity(0.65), colorScheme.surfaceTint);
return SingleChildScrollView(
child: SkeletonLoader(
@@ -159,7 +161,8 @@ class HomeLoaderView extends GetView with LoadingServiceMixin {
children: [
for (final _ in range(3))
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2).copyWith(bottom: 4),
padding: const EdgeInsets.symmetric(horizontal: 2)
.copyWith(bottom: 4),
child: Container(
height: 56,
width: 118,

View File

@@ -1,13 +1,13 @@
import 'package:dungeon_paper/core/dw_icons.dart';
import 'package:dungeon_paper/core/utils/list_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
class HomeNavBar extends StatefulWidget {
const HomeNavBar({
Key? key,
super.key,
required this.pageController,
}) : super(key: key);
});
final PageController pageController;
@@ -34,12 +34,14 @@ class _CharacterHomeNavBarState extends State<HomeNavBar> {
@override
Widget build(BuildContext context) {
final currentIndex = widget.pageController.positions.length == 1 ? widget.pageController.page?.round() ?? 1 : 1;
final currentIndex = widget.pageController.positions.length == 1
? widget.pageController.page?.round() ?? 1
: 1;
final items = <String, Icon>{
S.current.navActions: const Icon(DwIcons.hand_rock),
S.current.navCharacter: const Icon(Icons.person),
S.current.navJournal: const Icon(DwIcons.scroll_quill),
tr.nav.actions: const Icon(DwIcons.hand_rock),
tr.nav.character: const Icon(Icons.person),
tr.nav.journal: const Icon(DwIcons.scroll_quill),
};
return Material(
@@ -118,17 +120,17 @@ class _NavItem extends StatelessWidget {
clipper: const ShapeBorderClipper(shape: StadiumBorder()),
child: AnimatedContainer(
duration: duration,
width: selected ? 60 : 40,
color: selected ? selectedColor : Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: IconTheme(
child: icon,
data: IconThemeData(
color: selected ? selectedFgColor : unselectedFgColor,
),
child: icon,
),
),
width: selected ? 60 : 40,
color: selected ? selectedColor : Colors.transparent,
),
),
const SizedBox(height: 2),

View File

@@ -9,9 +9,8 @@ import 'package:dungeon_paper/app/routes/app_pages.dart';
import 'package:dungeon_paper/app/themes/button_themes.dart';
import 'package:dungeon_paper/app/widgets/atoms/icon_span.dart';
import 'package:dungeon_paper/app/widgets/atoms/page_controller_fractional_box.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// import '../../../widgets/atoms/debug_menu.dart';
@@ -20,7 +19,8 @@ import 'home_character_view.dart';
import 'home_fab.dart';
import 'home_nav_bar.dart';
class HomeView extends GetView<CharacterService> with UserServiceMixin, LoadingServiceMixin, CharacterServiceMixin {
class HomeView extends GetView<CharacterService>
with UserServiceMixin, LoadingServiceMixin, CharacterServiceMixin {
const HomeView({super.key});
@override
@@ -45,14 +45,18 @@ class HomeView extends GetView<CharacterService> with UserServiceMixin, LoadingS
: const HomeEmptyState();
},
),
floatingActionButton: Obx(() => maybeChar != null ? const HomeFAB() : const SizedBox.shrink()),
floatingActionButton: Obx(
() => maybeChar != null ? const HomeFAB() : const SizedBox.shrink()),
bottomNavigationBar: Obx(
() => maybeChar != null ? HomeNavBar(pageController: controller.pageController) : const SizedBox.shrink(),
() => maybeChar != null
? HomeNavBar(pageController: controller.pageController)
: const SizedBox.shrink(),
),
);
}
PageControllerFractionalBox _fractionalSizedBox(Widget child) => PageControllerFractionalBox(
PageControllerFractionalBox _fractionalSizedBox(Widget child) =>
PageControllerFractionalBox(
controller: controller.pageController,
child: child,
);
@@ -61,7 +65,8 @@ class HomeView extends GetView<CharacterService> with UserServiceMixin, LoadingS
debugPrint('afterFirstLoad: ${loadingService.afterFirstLoad}, '
'loadingUser: ${loadingService.loadingUser}, '
'loadingCharacters: ${loadingService.loadingCharacters}');
return !loadingService.afterFirstLoad && (loadingService.loadingUser || loadingService.loadingCharacters);
return !loadingService.afterFirstLoad &&
(loadingService.loadingUser || loadingService.loadingCharacters);
}
}
@@ -83,7 +88,8 @@ class HomeEmptyState extends StatelessWidget with UserServiceMixin {
child: Card(
margin: const EdgeInsets.all(32),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
padding: const EdgeInsets.symmetric(
vertical: 24, horizontal: 16),
child: Column(
children: [
RichText(
@@ -91,20 +97,21 @@ class HomeEmptyState extends StatelessWidget with UserServiceMixin {
text: TextSpan(
children: [
IconSpan(context, icon: Icons.person, size: 24),
TextSpan(text: ' ${S.current.homeEmptyStateLoginTitle}'),
TextSpan(
text: ' ${tr.home.emptyState.guest.title}'),
],
style: textTheme.titleLarge,
),
),
const SizedBox(height: 16),
Text(
S.current.homeEmptyStateLoginSubtitle,
tr.home.emptyState.guest.subtitle,
style: textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
label: Text(S.current.signinButton),
label: Text(tr.auth.login.button),
icon: const Icon(Icons.login),
onPressed: () => Get.toNamed(Routes.login),
style: ButtonThemes.primaryElevated(context),
@@ -118,19 +125,19 @@ class HomeEmptyState extends StatelessWidget with UserServiceMixin {
const SizedBox(height: 16),
],
Text(
S.current.homeEmptyStateTitle,
tr.home.emptyState.title,
style: textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
S.current.homeEmptyStateSubtitle,
tr.home.emptyState.subtitle,
style: textTheme.titleMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
label: Text(S.current.createGeneric(S.current.entity(Character))),
label: Text(tr.generic.createEntity(tr.entity(Character))),
icon: const Icon(Icons.person_add),
onPressed: () => Get.toNamed(Routes.createCharacter),
),

View File

@@ -3,15 +3,15 @@ import 'package:dungeon_paper/app/data/models/move.dart';
import 'package:dungeon_paper/app/data/models/spell.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/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
class HomeCharacterActionsFilters extends StatelessWidget {
const HomeCharacterActionsFilters({
Key? key,
super.key,
required this.hidden,
required this.onUpdateHidden,
}) : super(key: key);
});
final Set<Type> hidden;
final void Function(Set<Type> filters) onUpdateHidden;
@@ -27,10 +27,12 @@ class HomeCharacterActionsFilters extends StatelessWidget {
checked: !hidden.contains(type),
onChanged: (show) {
onUpdateHidden(
!show! ? {...hidden, type} : {...hidden.where((element) => element != type)},
!show!
? {...hidden, type}
: {...hidden.where((element) => element != type)},
);
},
label: Expanded(child: Text(S.current.entityPlural(type))),
label: Expanded(child: Text(tr.entityPlural(type))),
),
)
.toList(),

View File

@@ -4,7 +4,7 @@ import 'package:dungeon_paper/app/widgets/chips/primary_chip.dart';
import 'package:dungeon_paper/app/widgets/dialogs/coins_dialog.dart';
import 'package:dungeon_paper/app/widgets/dialogs/load_dialog.dart';
import 'package:dungeon_paper/core/dw_icons.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
@@ -13,8 +13,8 @@ import 'home_character_actions_filters.dart';
class HomeCharacterActionsSummary extends GetView<CharacterService> {
const HomeCharacterActionsSummary({
Key? key,
}) : super(key: key);
super.key,
});
Character get char => controller.current;
@@ -33,8 +33,9 @@ class HomeCharacterActionsSummary extends GetView<CharacterService> {
PrimaryChip(
// visualDensity: VisualDensity.compact,
icon: const Icon(DwIcons.dumbbell, size: 16),
label: S.current.actionSummaryChipLoad(char.currentLoad, char.maxLoad),
tooltip: S.current.maxLoad,
label: tr.home.summary.load
.label(char.currentLoad, char.maxLoad),
tooltip: tr.home.summary.load.tooltip,
onPressed: () => Get.dialog(
LoadDialog(
load: char.stats.load,
@@ -50,14 +51,15 @@ class HomeCharacterActionsSummary extends GetView<CharacterService> {
PrimaryChip(
// visualDensity: VisualDensity.compact,
icon: const Icon(DwIcons.coin_stack, size: 16),
label: S.current.actionSummaryChipCoins(
label: tr.home.summary.coins.label(
NumberFormat.compact().format(char.coins),
),
tooltip: S.current.coins,
tooltip: tr.home.summary.coins.tooltip,
onPressed: () => Get.dialog(
CoinsDialog(
coins: char.coins,
onChanged: (coins) => controller.updateCharacter(char.copyWith(coins: coins)),
onChanged: (coins) => controller
.updateCharacter(char.copyWith(coins: coins)),
),
),
),
@@ -70,7 +72,8 @@ class HomeCharacterActionsSummary extends GetView<CharacterService> {
controller.updateCharacter(
char.copyWith(
settings: char.settings.copyWith(
actionCategories: char.settings.actionCategories.copyWithInherited(
actionCategories:
char.settings.actionCategories.copyWithInherited(
hidden: filters,
),
),

View File

@@ -21,358 +21,397 @@ 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/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../expanded_card_dialog_view.dart';
import 'horizontal_list_card_view.dart';
class HomeCharacterDynamicCards extends GetView<CharacterService> with LibraryServiceMixin {
const HomeCharacterDynamicCards({Key? key}) : super(key: key);
class HomeCharacterDynamicCards extends GetView<CharacterService>
with LibraryServiceMixin {
const HomeCharacterDynamicCards({super.key});
List<Move> get moves => (controller.maybeCurrent?.moves ?? <Move>[]).where((m) => m.favorite).toList();
List<Spell> get spells => (controller.maybeCurrent?.spells ?? <Spell>[]).where((m) => m.prepared).toList();
List<Item> get items => (controller.maybeCurrent?.items ?? <Item>[]).where((m) => m.equipped).toList();
List<Note> get notes => (controller.maybeCurrent?.notes ?? <Note>[]).where((n) => n.favorite).toList();
List<Move> get moves => (controller.maybeCurrent?.moves ?? <Move>[])
.where((m) => m.favorite)
.toList();
List<Spell> get spells => (controller.maybeCurrent?.spells ?? <Spell>[])
.where((m) => m.prepared)
.toList();
List<Item> get items => (controller.maybeCurrent?.items ?? <Item>[])
.where((m) => m.equipped)
.toList();
List<Note> get notes => (controller.maybeCurrent?.notes ?? <Note>[])
.where((n) => n.favorite)
.toList();
@override
Widget build(BuildContext context) {
const cardSize = Size(210, 151);
final maxContentHeight = MediaQuery.of(context).size.height - 250;
return Obx(
() => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
//
// NOTES
//
if (notes.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(S.current.dynamicCategoriesNotes),
),
],
HorizontalCardListView<Note>(
cardSize: cardSize,
items: notes,
cardBuilder: (context, note, index, onTap) => Obx(
() => NoteCardMini(
note: notes[index],
onTap: onTap,
onSave: (_note) => controller.updateCharacter(
CharacterUtils.updateNotes(controller.current, [_note]),
() => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
// NOTES
//
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) => Obx(
() => NoteCardMini(
note: notes[index],
onTap: onTap,
onSave: (note) => controller.updateCharacter(
CharacterUtils.updateNotes(controller.current, [note]),
),
),
),
expandedCardBuilder: (context, note, index) => Obx(
() {
return notes.isNotEmpty && index < notes.length
? NoteCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
note: notes[index],
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openNotePage(
note: notes[index],
onSave: (note) => controller.updateCharacter(
CharacterUtils.updateNotes(
controller.current, [note]),
),
),
onDelete: _delete(
context,
note,
note.title,
() => controller.updateCharacter(
CharacterUtils.removeNotes(
controller.current, [note]),
),
),
),
],
onSave: (note) {
controller.updateCharacter(
CharacterUtils.updateNotes(
controller.current, [note]),
);
if (!note.favorite) {
Get.back();
}
},
)
: const SizedBox.shrink();
},
),
),
),
expandedCardBuilder: (context, note, index) => Obx(
() {
return notes.isNotEmpty && index < notes.length
? NoteCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
note: notes[index],
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openNotePage(
note: notes[index],
onSave: (note) => controller.updateCharacter(
CharacterUtils.updateNotes(controller.current, [note]),
),
),
onDelete: _delete(
context,
note,
note.title,
() => controller.updateCharacter(
CharacterUtils.removeNotes(controller.current, [note]),
//
// MOVES
//
if (moves.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(tr.home.categories.moves),
),
Builder(builder: (context) {
final raceCardMini = controller.current.race.favorite
? RaceCardMini(
race: controller.current.race,
onTap: () => Get.dialog(
ExpandedCardDialogView<Race>(
// heroTag: getKeyFor(item.value),
heroTag: null,
builder: (context) => RaceCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
race: controller.current.race,
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openRacePage(
abilityScores:
controller.current.abilityScores,
race: controller.current.race,
onSave: (race) => controller.updateCharacter(
controller.current.copyWith(race: race),
),
),
onDelete: null,
),
],
onSave: (race) => controller.updateCharacter(
controller.current.copyWith(race: race),
),
),
),
],
onSave: (_note) {
controller.updateCharacter(
CharacterUtils.updateNotes(controller.current, [_note]),
);
if (!_note.favorite) {
Get.back();
}
},
),
onSave: (race) => controller.updateCharacter(
controller.current.copyWith(race: race),
),
)
: const SizedBox.shrink();
},
),
),
//
// MOVES
//
if (moves.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(S.current.dynamicCategoriesMoves),
),
Builder(builder: (context) {
final raceCardMini = controller.current.race.favorite
? RaceCardMini(
race: controller.current.race,
onTap: () => Get.dialog(
ExpandedCardDialogView<Race>(
// heroTag: getKeyFor(item.value),
heroTag: null,
builder: (context) => RaceCard(
: null;
return HorizontalCardListView<Move>(
cardSize: cardSize,
items: moves,
cardBuilder: (context, move, index, onTap) => Obx(
() => MoveCardMini(
move: moves[index],
onTap: onTap,
onSave: (move) => controller.updateCharacter(
CharacterUtils.updateMoves(controller.current, [move]),
),
abilityScores: controller.current.abilityScores,
),
),
expandedCardBuilder: (context, move, index) => Obx(
() => moves.isNotEmpty && index < moves.length
? MoveCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
move: moves[index],
abilityScores: controller.current.abilityScores,
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openMovePage(
abilityScores: controller.current.abilityScores,
move: moves[index],
onSave: (move) => library.upsertToCharacter(
[move],
forkBehavior: ForkBehavior.increaseVersion),
),
onDelete: _delete(
context,
move,
move.name,
() => controller.updateCharacter(
CharacterUtils.removeMoves(
controller.current, [move]),
),
),
),
],
onSave: (move) {
controller.updateCharacter(
CharacterUtils.updateMoves(
controller.current, [move]),
);
if (!move.favorite) {
Get.back();
}
},
)
: const SizedBox.shrink(),
),
leading: raceCardMini != null &&
controller.current.settings.racePosition ==
RacePosition.start
? [raceCardMini]
: [],
trailing: raceCardMini != null &&
controller.current.settings.racePosition ==
RacePosition.end
? [raceCardMini]
: [],
);
}),
//
// SPELLS
//
if (spells.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(tr.home.categories.spells),
),
],
HorizontalCardListView<Spell>(
cardSize: cardSize,
items: spells,
cardBuilder: (context, spell, index, onTap) => Obx(
() => SpellCardMini(
spell: spells[index],
onTap: onTap,
onSave: (spell) => controller.updateCharacter(
CharacterUtils.updateSpells(controller.current, [spell]),
),
abilityScores: controller.current.abilityScores,
),
),
expandedCardBuilder: (context, spell, index) => Obx(
() => spells.isNotEmpty && index < spells.length
? SpellCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
race: controller.current.race,
spell: spells[index],
abilityScores: controller.current.abilityScores,
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openRacePage(
onEdit: () => ModelPages.openSpellPage(
abilityScores: controller.current.abilityScores,
race: controller.current.race,
onSave: (_race) => controller.updateCharacter(
controller.current.copyWith(race: _race),
classKeys: spells[index].classKeys,
spell: spells[index],
onSave: (spell) => controller.updateCharacter(
CharacterUtils.updateSpells(
controller.current, [spell]),
),
),
onDelete: _delete(
context,
spell,
spell.name,
() => controller.updateCharacter(
CharacterUtils.removeSpells(
controller.current, [spell]),
),
),
onDelete: null,
),
],
onSave: (_race) => controller.updateCharacter(
controller.current.copyWith(race: _race),
),
),
),
onSave: (spell) {
controller.updateCharacter(
CharacterUtils.updateSpells(
controller.current, [spell]),
);
if (!spell.prepared) {
Get.back();
}
},
)
: const SizedBox.shrink(),
),
),
//
// ITEMS
//
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) => Obx(
() => ItemCardMini(
item: items[index],
onTap: onTap,
onSave: (item) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [item]),
),
onSave: (_race) => controller.updateCharacter(
controller.current.copyWith(race: _race),
),
)
: null;
return HorizontalCardListView<Move>(
cardSize: cardSize,
items: moves,
cardBuilder: (context, move, index, onTap) => Obx(
() => MoveCardMini(
move: moves[index],
onTap: onTap,
onSave: (_move) => controller.updateCharacter(
CharacterUtils.updateMoves(controller.current, [_move]),
),
abilityScores: controller.current.abilityScores,
),
),
expandedCardBuilder: (context, move, index) => Obx(
() => moves.isNotEmpty && index < moves.length
? MoveCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
move: moves[index],
abilityScores: controller.current.abilityScores,
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openMovePage(
abilityScores: controller.current.abilityScores,
move: moves[index],
onSave: (move) =>
library.upsertToCharacter([move], forkBehavior: ForkBehavior.increaseVersion),
),
onDelete: _delete(
context,
move,
move.name,
() => controller.updateCharacter(
CharacterUtils.removeMoves(controller.current, [move]),
expandedCardBuilder: (context, item, index) => Obx(
() => items.isNotEmpty && index < items.length
? ItemCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
item: items[index],
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openItemPage(
item: items[index],
onSave: (item) => controller.updateCharacter(
CharacterUtils.updateItems(
controller.current, [item]),
),
),
),
),
],
onSave: (_move) {
controller.updateCharacter(
CharacterUtils.updateMoves(controller.current, [_move]),
);
if (!_move.favorite) {
Get.back();
}
},
)
: const SizedBox.shrink(),
),
leading: raceCardMini != null && controller.current.settings.racePosition == RacePosition.start
? [raceCardMini]
: [],
trailing: raceCardMini != null && controller.current.settings.racePosition == RacePosition.end
? [raceCardMini]
: [],
);
}),
//
// SPELLS
//
if (spells.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(S.current.dynamicCategoriesSpells),
),
],
HorizontalCardListView<Spell>(
cardSize: cardSize,
items: spells,
cardBuilder: (context, spell, index, onTap) => Obx(
() => SpellCardMini(
spell: spells[index],
onTap: onTap,
onSave: (_spell) => controller.updateCharacter(
CharacterUtils.updateSpells(controller.current, [_spell]),
),
abilityScores: controller.current.abilityScores,
),
),
expandedCardBuilder: (context, spell, index) => Obx(
() => spells.isNotEmpty && index < spells.length
? SpellCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
spell: spells[index],
abilityScores: controller.current.abilityScores,
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openSpellPage(
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,
() => controller.updateCharacter(
CharacterUtils.removeSpells(controller.current, [spell]),
),
),
),
],
onSave: (_spell) {
controller.updateCharacter(
CharacterUtils.updateSpells(controller.current, [_spell]),
);
if (!_spell.prepared) {
Get.back();
}
},
)
: const SizedBox.shrink(),
),
),
//
// ITEMS
//
if (items.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(S.current.dynamicCategoriesItems),
),
],
HorizontalCardListView<Item>(
cardSize: cardSize,
items: items,
cardBuilder: (context, item, index, onTap) => Obx(
() => ItemCardMini(
item: items[index],
onTap: onTap,
onSave: (_item) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [_item]),
),
),
),
expandedCardBuilder: (context, item, index) => Obx(
() => items.isNotEmpty && index < items.length
? ItemCard(
maxContentHeight: maxContentHeight,
expandable: false,
initiallyExpanded: true,
item: items[index],
actions: [
EntityEditMenu(
onEdit: () => ModelPages.openItemPage(
item: items[index],
onSave: (item) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [item]),
),
),
onDelete: _delete(
context,
item,
item.name,
() => controller.updateCharacter(
CharacterUtils.removeItems(controller.current, [item]),
),
),
leading: [
ChecklistMenuEntry(
value: 'countArmor',
checked: item.settings.countArmor,
label: Text(S.current.itemSettingsCountArmor),
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(S.current.itemSettingsCountDamage),
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(S.current.itemSettingsCountWeight),
onChanged: (value) => controller.updateCharacter(
CharacterUtils.updateItems(controller.current, [
item.copyWithInherited(
settings: item.settings.copyWith(countWeight: value!),
)
]),
onDelete: _delete(
context,
item,
item.name,
() => 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) {
Get.back();
}
},
)
: const SizedBox.shrink(),
),
),
]),
onSave: (item) {
controller.updateCharacter(
CharacterUtils.updateItems(
controller.current, [item]),
);
if (!item.equipped) {
Get.back();
}
},
)
: const SizedBox.shrink(),
),
),
]),
);
}
void Function() _delete<T>(BuildContext context, T item, String itemName, void Function() onRemove) {
void Function() _delete<T>(
BuildContext context, T item, String itemName, void Function() onRemove) {
return () => deleteDialog.confirm(
context,
DeleteDialogOptions(entityName: itemName, entityKind: S.current.entity(T)),
DeleteDialogOptions(entityName: itemName, entityKind: tr.entity(T)),
() {
onRemove();
Get.back();

View File

@@ -13,12 +13,12 @@ import 'package:dungeon_paper/app/widgets/dialogs/character_bonds_flags_dialog.d
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';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HomeCharacterExtras extends GetView<CharacterService> {
const HomeCharacterExtras({Key? key}) : super(key: key);
const HomeCharacterExtras({super.key});
@override
Widget build(BuildContext context) {
@@ -27,66 +27,56 @@ class HomeCharacterExtras extends GetView<CharacterService> {
children: [
MenuButton<String>(
icon: const Icon(Icons.person),
tooltip: S.current.characterMenu,
tooltip: tr.home.menu.character.tooltip,
items: [
MenuEntry(
value: 'name_photo',
icon: const Icon(Icons.photo),
label: Text(S.current.basicInformationTitle),
label: Text(tr.home.menu.character.basicInfo),
onSelect: _openBasicInfo,
),
// MenuEntry(
// value: 'bio',
// icon: const Icon(Icons.text_snippet),
// label: Text(S.current.characterBioDialogTitle),
// onSelect: _openBio,
// ),
MenuEntry(
value: 'ability_scores',
icon: const Icon(Icons.format_list_numbered_rtl),
label: Text(S.current.characterRollsTitle),
label: Text(tr.home.menu.character.abilityScores),
onSelect: _openAbilityScores,
),
MenuEntry(
value: 'class',
icon: Icon(CharacterClass.genericIcon),
label: Text(S.current.changeGeneric(S.current.entity(CharacterClass))),
label: Text(tr.generic.changeEntity(tr.entity(CharacterClass))),
onSelect: _openCharClass,
),
MenuEntry(
value: 'race',
icon: Icon(Race.genericIcon),
label: Text(S.current.changeGeneric(S.current.entity(Race))),
label: Text(tr.generic.changeEntity(tr.entity(Race))),
onSelect: _openRace,
),
MenuEntry(
value: 'roll_buttons',
icon: const Icon(DwIcons.dice_d6),
label: Text(S.current.customRollButtons),
label: Text(tr.home.menu.character.customRolls),
onSelect: _openRollButtons,
),
MenuEntry(
value: 'theme',
icon: const Icon(Icons.brush),
label: Text(S.current.characterSelectTheme),
label: Text(tr.home.menu.character.theme),
onSelect: _openThemeSelect,
),
],
),
// IconButton(
// onPressed: _openAbilityScores,
// icon: const Icon(Icons.format_list_numbered_rtl),
// tooltip: S.current.characterRollsTitle,
// ),
IconButton(
icon: const Icon(Icons.text_snippet),
tooltip: S.current.characterBioDialogTitle,
tooltip: tr.home.menu.bio,
onPressed: _openBio,
),
Obx(
() => IconButton(
onPressed: _openBondsFlags,
icon: Transform.scale(child: const Icon(Icons.handshake), scaleX: -1),
icon:
Transform.scale(scaleX: -1, child: const Icon(Icons.handshake)),
tooltip: SessionMark.categoryTitle(
bonds: controller.maybeCurrent?.bonds ?? [],
flags: controller.maybeCurrent?.flags ?? [],
@@ -96,12 +86,12 @@ class HomeCharacterExtras extends GetView<CharacterService> {
IconButton(
onPressed: _openDebilities,
icon: const Icon(Icons.personal_injury),
tooltip: S.current.characterDebilitiesDialogTitle,
tooltip: tr.home.menu.debilities,
),
IconButton(
onPressed: null,
icon: const Icon(Icons.groups),
tooltip: S.current.entity(S.current.entityPlural(Campaign)),
tooltip: tr.entityPlural(Campaign),
),
],
);
@@ -111,8 +101,8 @@ class HomeCharacterExtras extends GetView<CharacterService> {
Routes.abilityScores,
arguments: AbilityScoresFormArguments(
abilityScores: controller.current.abilityScores,
onChanged: (abilityScores) =>
controller.updateCharacter(controller.current.copyWith(abilityScores: abilityScores)),
onChanged: (abilityScores) => controller.updateCharacter(
controller.current.copyWith(abilityScores: abilityScores)),
),
preventDuplicates: false,
);
@@ -138,9 +128,10 @@ class HomeCharacterExtras extends GetView<CharacterService> {
ModelPages.openRacesList(
character: controller.current,
preSelection: controller.current.race,
onSelected: (_race) => controller.updateCharacter(
onSelected: (race) => controller.updateCharacter(
controller.current.copyWithInherited(
race: _race.copyWithInherited(favorite: controller.current.race.favorite),
race: race.copyWithInherited(
favorite: controller.current.race.favorite),
),
),
);
@@ -149,10 +140,10 @@ class HomeCharacterExtras extends GetView<CharacterService> {
void _openCharClass() {
ModelPages.openCharacterClassesList(
character: controller.current,
onSelected: (_cls) => controller.updateCharacter(
onSelected: (cls) => controller.updateCharacter(
// TODO add a reset dialog to confirm + ask what to reset: moves, spells, alignment, rac
controller.current.copyWithInherited(
characterClass: _cls,
characterClass: cls,
),
),
);
@@ -172,7 +163,8 @@ class HomeCharacterExtras extends GetView<CharacterService> {
character: controller.current,
onChanged: (rollButtons) => controller.updateCharacter(
controller.current.copyWith(
settings: controller.current.settings.copyWith(rollButtons: rollButtons),
settings:
controller.current.settings.copyWith(rollButtons: rollButtons),
),
),
),

View File

@@ -17,7 +17,9 @@ class HorizontalCardListView<T extends WithMeta> extends StatelessWidget {
}) : super(key: key);
final Size cardSize;
final Widget Function(BuildContext context, T item, int index, void Function() onTap) cardBuilder;
final Widget Function(
BuildContext context, T item, int index, void Function() onTap)
cardBuilder;
final Widget Function(
BuildContext context,
T item,

View File

@@ -20,7 +20,10 @@ import 'package:dungeon_paper/app/modules/ImportExport/platforms/abstract_import
if (dart.library.html) 'package:dungeon_paper/app/modules/ImportExport/platforms/web_export.dart';
class ExportController extends GetxController
with GetSingleTickerProviderStateMixin, CharacterServiceMixin, RepositoryServiceMixin
with
GetSingleTickerProviderStateMixin,
CharacterServiceMixin,
RepositoryServiceMixin
implements ImportExportSelectionData {
final toExport = ExportSelections().obs;
@@ -43,30 +46,38 @@ class ExportController extends GetxController
}
@override
void toggle<T extends WithMeta>(T item, bool state) => _toggleExportList<T>([item], state);
void toggle<T extends WithMeta>(T item, bool state) =>
_toggleExportList<T>([item], state);
@override
void toggleAll<T extends WithMeta>(bool state) => _toggleExportList<T>(listByType<T>(), state);
void toggleAll<T extends WithMeta>(bool state) =>
_toggleExportList<T>(listByType<T>(), state);
void _toggleExportList<T>(List<T> items, bool state) {
switch (T) {
case Character:
toExport.value.characters = _toggleInList(toExport.value.characters, items.cast<Character>(), state);
toExport.value.characters = _toggleInList(
toExport.value.characters, items.cast<Character>(), state);
break;
case Move:
toExport.value.moves = _toggleInList(toExport.value.moves, items.cast<Move>(), state);
toExport.value.moves =
_toggleInList(toExport.value.moves, items.cast<Move>(), state);
break;
case Spell:
toExport.value.spells = _toggleInList(toExport.value.spells, items.cast<Spell>(), state);
toExport.value.spells =
_toggleInList(toExport.value.spells, items.cast<Spell>(), state);
break;
case Item:
toExport.value.items = _toggleInList(toExport.value.items, items.cast<Item>(), state);
toExport.value.items =
_toggleInList(toExport.value.items, items.cast<Item>(), state);
break;
case CharacterClass:
toExport.value.classes = _toggleInList(toExport.value.classes, items.cast<CharacterClass>(), state);
toExport.value.classes = _toggleInList(
toExport.value.classes, items.cast<CharacterClass>(), state);
break;
case Race:
toExport.value.races = _toggleInList(toExport.value.races, items.cast<Race>(), state);
toExport.value.races =
_toggleInList(toExport.value.races, items.cast<Race>(), state);
break;
}
toExport.refresh();

View File

@@ -11,7 +11,7 @@ import 'package:dungeon_paper/app/data/models/spell.dart';
import 'package:dungeon_paper/app/modules/ImportExport/local_widgets/import_progress_dialog.dart';
import 'package:dungeon_paper/core/storage_handler/storage_handler.dart';
import 'package:dungeon_paper/core/utils/list_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:get/get.dart';
@@ -29,7 +29,8 @@ class ImportController extends GetxController
List<CharacterClass> get classes => toImport.value!.allClasses.toList();
List<Race> get races => toImport.value!.allRaces.toList();
int get selectionsCount => [characters, moves, spells, items, classes].fold(0, (total, list) => total + list.length);
int get selectionsCount => [characters, moves, spells, items, classes]
.fold(0, (total, list) => total + list.length);
bool get hasData => toImport.value != null;
@@ -37,30 +38,38 @@ class ImportController extends GetxController
final leftCount = 0.obs;
@override
void toggle<T extends WithMeta>(T item, bool state) => _toggleImportList<T>([item], state);
void toggle<T extends WithMeta>(T item, bool state) =>
_toggleImportList<T>([item], state);
@override
void toggleAll<T extends WithMeta>(bool state) => _toggleImportList<T>(listByType<T>(), state);
void toggleAll<T extends WithMeta>(bool state) =>
_toggleImportList<T>(listByType<T>(), state);
void _toggleImportList<T>(List<T> items, bool state) {
switch (T) {
case Character:
toImport.value!.characters = _toggleInList(toImport.value!.characters, items.cast<Character>(), state);
toImport.value!.characters = _toggleInList(
toImport.value!.characters, items.cast<Character>(), state);
break;
case Move:
toImport.value!.moves = _toggleInList(toImport.value!.moves, items.cast<Move>(), state);
toImport.value!.moves =
_toggleInList(toImport.value!.moves, items.cast<Move>(), state);
break;
case Spell:
toImport.value!.spells = _toggleInList(toImport.value!.spells, items.cast<Spell>(), state);
toImport.value!.spells =
_toggleInList(toImport.value!.spells, items.cast<Spell>(), state);
break;
case Item:
toImport.value!.items = _toggleInList(toImport.value!.items, items.cast<Item>(), state);
toImport.value!.items =
_toggleInList(toImport.value!.items, items.cast<Item>(), state);
break;
case CharacterClass:
toImport.value!.classes = _toggleInList(toImport.value!.classes, items.cast<CharacterClass>(), state);
toImport.value!.classes = _toggleInList(
toImport.value!.classes, items.cast<CharacterClass>(), state);
break;
case Race:
toImport.value!.races = _toggleInList(toImport.value!.races, items.cast<Race>(), state);
toImport.value!.races =
_toggleInList(toImport.value!.races, items.cast<Race>(), state);
break;
}
toImport.refresh();
@@ -68,7 +77,10 @@ class ImportController extends GetxController
@override
bool isSelected<T extends WithMeta>(T item) {
return toImport.value!.listByType<T>(selected: true).map((x) => x.key).contains(item.key);
return toImport.value!
.listByType<T>(selected: true)
.map((x) => x.key)
.contains(item.key);
}
List<T> _toggleInList<T>(List<T> list, List<T> items, bool state) {
@@ -100,26 +112,27 @@ class ImportController extends GetxController
void pickImportFile() async {
try {
final _path = await FlutterFileDialog.pickFile(
final importPath = await FlutterFileDialog.pickFile(
params: const OpenFileDialogParams(
fileExtensionsFilter: ['json'],
mimeTypesFilter: ['application/json'],
),
);
if (_path == null) {
if (importPath == null) {
return;
}
final js = json.decode(await File(_path).readAsString()) as Map<String, dynamic>;
final js = json.decode(await File(importPath).readAsString())
as Map<String, dynamic>;
toImport.value = ImportSelections.fromJson(js);
} catch (e) {
// unawaited(analytics.logEvent(name: Events.ImportFail, parameters: {
// 'reason': e.toString(),
// }));
Get.rawSnackbar(
title: S.current.importFailedTitle,
message: S.current.importFailedMessage,
title: tr.backup.importing.error.title,
message: tr.backup.importing.error.message,
);
rethrow;
}
@@ -139,12 +152,14 @@ class ImportController extends GetxController
await Future.delayed(const Duration(milliseconds: 500));
for (final char in characters) {
await StorageHandler.instance.create('Characters', char.key, char.toJson());
await StorageHandler.instance
.create('Characters', char.key, char.toJson());
leftCount.value -= 1;
}
importStep.value = CharacterClass;
for (final cls in classes) {
await StorageHandler.instance.create('CharacterClasses', cls.key, cls.toJson());
await StorageHandler.instance
.create('CharacterClasses', cls.key, cls.toJson());
leftCount.value -= 1;
}
importStep.value = Move;
@@ -154,20 +169,22 @@ class ImportController extends GetxController
}
importStep.value = Spell;
for (final spell in spells) {
await StorageHandler.instance.create('Spells', spell.key, spell.toJson());
await StorageHandler.instance
.create('Spells', spell.key, spell.toJson());
leftCount.value -= 1;
}
importStep.value = Item;
for (final items in items) {
await StorageHandler.instance.create('Items', items.key, items.toJson());
await StorageHandler.instance
.create('Items', items.key, items.toJson());
leftCount.value -= 1;
}
await Future.delayed(const Duration(milliseconds: 500));
Get.back();
Get.rawSnackbar(
title: S.current.importSuccessTitle,
message: S.current.importSuccessMessage,
title: tr.backup.importing.success.title,
message: tr.backup.importing.success.message,
);
};
}
@@ -204,13 +221,30 @@ class ImportSelections {
List<Race> races;
factory ImportSelections.fromJson(Map<String, dynamic> json) {
final allClasses = (json['classes'] ?? []).map((x) => CharacterClass.fromJson(x)).toList().cast<CharacterClass>();
final allCharacters =
List<dynamic>.from(json['characters'] ?? []).map((x) => Character.fromJson(x)).toList().cast<Character>();
final allMoves = List<dynamic>.from(json['moves'] ?? []).map((x) => Move.fromJson(x)).toList().cast<Move>();
final allSpells = List<dynamic>.from(json['spells'] ?? []).map((x) => Spell.fromJson(x)).toList().cast<Spell>();
final allItems = List<dynamic>.from(json['items'] ?? []).map((x) => Item.fromJson(x)).toList().cast<Item>();
final allRaces = List<dynamic>.from(json['races'] ?? []).map((x) => Race.fromJson(x)).toList().cast<Race>();
final allClasses = (json['classes'] ?? [])
.map((x) => CharacterClass.fromJson(x))
.toList()
.cast<CharacterClass>();
final allCharacters = List<dynamic>.from(json['characters'] ?? [])
.map((x) => Character.fromJson(x))
.toList()
.cast<Character>();
final allMoves = List<dynamic>.from(json['moves'] ?? [])
.map((x) => Move.fromJson(x))
.toList()
.cast<Move>();
final allSpells = List<dynamic>.from(json['spells'] ?? [])
.map((x) => Spell.fromJson(x))
.toList()
.cast<Spell>();
final allItems = List<dynamic>.from(json['items'] ?? [])
.map((x) => Item.fromJson(x))
.toList()
.cast<Item>();
final allRaces = List<dynamic>.from(json['races'] ?? [])
.map((x) => Race.fromJson(x))
.toList()
.cast<Race>();
return ImportSelections(
allClasses: allClasses,

View File

@@ -5,7 +5,8 @@ import 'package:get/get.dart';
import 'import_controller.dart';
class ImportExportController extends GetxController with GetSingleTickerProviderStateMixin {
class ImportExportController extends GetxController
with GetSingleTickerProviderStateMixin {
late final Rx<TabController> tab;
@override

View File

@@ -1,33 +1,45 @@
import 'package:dungeon_paper/app/modules/ImportExport/controllers/import_controller.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ImportProgressDialog extends GetView<ImportController> {
const ImportProgressDialog({Key? key}) : super(key: key);
const ImportProgressDialog({super.key});
@override
Widget build(BuildContext context) {
return Obx(
() => SimpleDialog(
title: Text(S.current.importProgressTitle),
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32).copyWith(top: 8),
children: [
Text(S.current.importProgressProcessing(S.current.entityPlural(controller.importStep.value!))),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: 1 - controller.leftCount / controller.selectionsCount,
),
() {
final completedCount =
controller.selectionsCount - controller.leftCount.value;
final totalCount = controller.selectionsCount;
return SimpleDialog(
title: Text(tr.backup.importing.progress.title),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 32)
.copyWith(top: 8),
children: [
Text(
tr.backup.importing.progress.processing(
tr.entityPlural(controller.importStep.value!),
),
const SizedBox(width: 16),
Text('${controller.selectionsCount - controller.leftCount.value} / ${controller.selectionsCount}'),
],
)
],
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
value:
1 - controller.leftCount / controller.selectionsCount,
),
),
const SizedBox(width: 16),
Text('$completedCount / $totalCount'),
],
)
],
);
},
);
}
}

View File

@@ -2,14 +2,15 @@ import 'package:dungeon_paper/app/data/models/meta.dart';
import 'package:dungeon_paper/app/modules/ImportExport/controllers/import_export_controller.dart';
import 'package:dungeon_paper/app/widgets/atoms/custom_expansion_panel.dart';
import 'package:dungeon_paper/app/widgets/atoms/menu_button.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ListCard<T extends WithMeta, C extends ImportExportSelectionData> extends GetView<C> {
class ListCard<T extends WithMeta, C extends ImportExportSelectionData>
extends GetView<C> {
const ListCard({
Key? key,
}) : super(key: key);
super.key,
});
List<T> get list => controller.listByType<T>();
@@ -31,7 +32,7 @@ class ListCard<T extends WithMeta, C extends ImportExportSelectionData> extends
const SizedBox(width: 8),
Expanded(
child: Text(
S.current.myGeneric(S.current.entityPlural(T)),
tr.generic.myEntity(tr.entityPlural(T)),
style: textTheme.titleLarge,
),
),
@@ -43,13 +44,13 @@ class ListCard<T extends WithMeta, C extends ImportExportSelectionData> extends
MenuEntry<bool>(
value: true,
icon: const Icon(Icons.select_all),
label: Text(S.current.selectAll),
label: Text(tr.generic.selectAll),
onSelect: () => controller.toggleAll<T>(true),
),
MenuEntry<bool>(
value: false,
icon: const Icon(Icons.clear),
label: Text(S.current.selectNone),
label: Text(tr.generic.selectNone),
onSelect: () => controller.toggleAll<T>(false),
),
],
@@ -58,7 +59,8 @@ class ListCard<T extends WithMeta, C extends ImportExportSelectionData> extends
children: [
for (final item in list)
ListTile(
onTap: () => controller.toggle<T>(item, !controller.isSelected<T>(item)),
onTap: () =>
controller.toggle<T>(item, !controller.isSelected<T>(item)),
title: Text(item.displayName),
leading: Checkbox(
value: controller.isSelected<T>(item),
@@ -69,7 +71,7 @@ class ListCard<T extends WithMeta, C extends ImportExportSelectionData> extends
Padding(
padding: const EdgeInsets.all(8),
child: Text(
S.current.noGeneric(S.current.entityPlural(T)),
tr.generic.noEntity(tr.entityPlural(T)),
textAlign: TextAlign.center,
),
),

View File

@@ -1,11 +1,11 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:path/path.dart' as path;
import 'package:get/get.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/app/modules/ImportExport/platforms/abstract_import_export.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class Exporter extends AbstractExporter {
@@ -21,19 +21,19 @@ class Exporter extends AbstractExporter {
final path = await FlutterFileDialog.saveFile(params: params);
if (path == null) {
Get.rawSnackbar(
title: S.current.exportFailedTitle,
message: S.current.errorUserOperationCanceled,
title: tr.backup.exporting.error.title,
message: tr.errors.userOperationCanceled,
);
} else {
Get.rawSnackbar(
title: S.current.exportSuccessfulTitle,
message: S.current.exportSuccessfulMessage,
title: tr.backup.exporting.success.title,
message: tr.backup.exporting.success.message,
);
}
} catch (e) {
Get.rawSnackbar(
title: S.current.exportFailedTitle,
message: S.current.exportFailedMessage,
title: tr.backup.exporting.error.title,
message: tr.backup.exporting.error.message,
);
rethrow;
}

View File

@@ -4,9 +4,10 @@ import 'dart:html' as html;
import 'package:dungeon_paper/app/modules/ImportExport/platforms/abstract_import_export.dart';
void downloadFileFromDataURL(String dataURL, String fileName) => html.AnchorElement(href: dataURL)
..setAttribute('download', fileName)
..click();
void downloadFileFromDataURL(String dataURL, String fileName) =>
html.AnchorElement(href: dataURL)
..setAttribute('download', fileName)
..click();
class Exporter extends AbstractExporter {
@override

View File

@@ -1,7 +1,6 @@
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/import_export_controller.dart';
@@ -17,7 +16,7 @@ class ImportExportView extends GetView<ImportExportController> {
return Scaffold(
appBar: AppBar(
title: Text(S.current.importExportTitle),
title: Text(tr.backup.title),
centerTitle: true,
),
body: Column(
@@ -25,8 +24,8 @@ class ImportExportView extends GetView<ImportExportController> {
TabBar(
controller: controller.tab.value,
tabs: [
Tab(child: Text(S.current.export, style: textStyle)),
Tab(child: Text(S.current.import, style: textStyle)),
Tab(child: Text(tr.backup.exporting.title, style: textStyle)),
Tab(child: Text(tr.backup.importing.title, style: textStyle)),
],
),
Expanded(
@@ -42,9 +41,14 @@ class ImportExportView extends GetView<ImportExportController> {
),
floatingActionButton: Obx(
() => AdvancedFloatingActionButton.extended(
label: Text(controller.tab.value.index == 0 ? S.current.export : S.current.import),
icon: Icon(controller.tab.value.index == 0 ? Icons.upload : Icons.download),
onPressed: controller.tab.value.index == 0 ? controller.doExport : controller.doImport,
label: Text(controller.tab.value.index == 0
? tr.backup.exporting.button
: tr.backup.importing.button),
icon: Icon(
controller.tab.value.index == 0 ? Icons.upload : Icons.download),
onPressed: controller.tab.value.index == 0
? controller.doExport
: controller.doImport,
),
),
);

View File

@@ -6,15 +6,14 @@ import 'package:dungeon_paper/app/data/models/race.dart';
import 'package:dungeon_paper/app/data/models/spell.dart';
import 'package:dungeon_paper/app/modules/ImportExport/controllers/import_controller.dart';
import 'package:dungeon_paper/core/utils/builder_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../local_widgets/list_card.dart';
class ImportView extends GetView<ImportController> {
const ImportView({Key? key}) : super(key: key);
const ImportView({super.key});
@override
Widget build(BuildContext context) {
@@ -28,7 +27,7 @@ class ImportView extends GetView<ImportController> {
() => ElevatedButton.icon(
onPressed: () => controller.toImport.value = null,
icon: const Icon(Icons.clear),
label: Text(S.current.importClearFile),
label: Text(tr.backup.importing.file.clearFile),
),
() => const ListCard<Character, ImportController>(),
() => const ListCard<CharacterClass, ImportController>(),
@@ -40,11 +39,11 @@ class ImportView extends GetView<ImportController> {
)
: ItemBuilder.lazyChildren(
children: [
() => Text(S.current.importBrowseHelp),
() => Text(tr.backup.importing.file.info),
() => ElevatedButton.icon(
onPressed: controller.pickImportFile,
icon: const Icon(Icons.file_open),
label: Text(S.current.importBrowseFile),
label: Text(tr.backup.importing.file.browse),
)
],
);

View File

@@ -13,8 +13,12 @@ enum FiltersGroup {
// online,
}
class LibraryListController<T extends WithMeta, F extends EntityFilters<T>> extends GetxController
with GetSingleTickerProviderStateMixin, LibraryServiceMixin, CharacterServiceMixin {
class LibraryListController<T extends WithMeta, F extends EntityFilters<T>>
extends GetxController
with
GetSingleTickerProviderStateMixin,
LibraryServiceMixin,
CharacterServiceMixin {
final repo = Get.find<RepositoryService>().obs;
final chars = Get.find<CharacterService>().obs;
@@ -33,11 +37,14 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>> exte
bool get selectable => onSelected != null;
Iterable<T> get builtInList => filterList(builtInListRaw, FiltersGroup.playbook, filterFn, sortFn);
Iterable<T> get builtInList =>
filterList(builtInListRaw, FiltersGroup.playbook, filterFn, sortFn);
Iterable<T> get builtInListRaw => repo.value.builtIn.listByType<T>().values.toList();
Iterable<T> get builtInListRaw =>
repo.value.builtIn.listByType<T>().values.toList();
Iterable<T> get myList => filterList(myListRaw, FiltersGroup.my, filterFn, sortFn);
Iterable<T> get myList =>
filterList(myListRaw, FiltersGroup.my, filterFn, sortFn);
Iterable<T> get myListRaw => repo.value.my.listByType<T>().values.toList();
String get storageKey => Meta.storageKeyFor(T);
@@ -110,7 +117,8 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>> exte
_compare(T item) {
return (T element) {
return (element.meta.sharing?.sourceKey ?? element.key) == item.key || element.key == item.key;
return (element.meta.sharing?.sourceKey ?? element.key) == item.key ||
element.key == item.key;
};
}
@@ -136,14 +144,18 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>> exte
:
// single: if is pre-selected, then only if it was not removed,
// if not pre-selected, then only if nothing else is selected
(isPreSelected(item) && !isRemoved(item)) || isInCurrentSelectedList(item);
(isPreSelected(item) && !isRemoved(item)) ||
isInCurrentSelectedList(item);
bool isInCurrentSelectedList(T item) =>
selected.firstWhereOrNull((element) => [element.meta.sharing?.sourceKey, element.key].contains(item.key)) != null;
selected.firstWhereOrNull((element) =>
[element.meta.sharing?.sourceKey, element.key].contains(item.key)) !=
null;
bool isRemoved(T item) => removed.firstWhereOrNull(_compare(item)) != null;
bool isPreSelected(T item) => preSelections.toList().firstWhereOrNull(_compare(item)) != null;
bool isPreSelected(T item) =>
preSelections.toList().firstWhereOrNull(_compare(item)) != null;
bool isEnabled(T item) => multiple
?
@@ -161,14 +173,20 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>> exte
int Function(T a, T b) Function(F filters)? sortFn,
F? initialFilters,
]) {
final filtered = filterFn != null && (filters[group] != null || initialFilters != null)
? list.where((x) => filterFn(x, filters[group] ?? initialFilters!)).toList()
: list.toList();
return sortFn != null ? (filtered..sort(sortFn(filters[group] ?? initialFilters!))) : filtered;
final filtered =
filterFn != null && (filters[group] != null || initialFilters != null)
? list
.where((x) => filterFn(x, filters[group] ?? initialFilters!))
.toList()
: list.toList();
return sortFn != null
? (filtered..sort(sortFn(filters[group] ?? initialFilters!)))
: filtered;
}
void _updatePlaybookSearch() {
filters[FiltersGroup.playbook]?.setSearch(search[FiltersGroup.playbook]!.text);
filters[FiltersGroup.playbook]
?.setSearch(search[FiltersGroup.playbook]!.text);
search.refresh();
repo.refresh();
}
@@ -191,7 +209,8 @@ abstract class EntityFilters<T> {
List<bool?> get filterActiveList;
int get activeFilterCount => filterActiveList.where((element) => element == true).length;
int get activeFilterCount =>
filterActiveList.where((element) => element == true).length;
int get totalFilterCount => filterActiveList.length;
@@ -201,7 +220,8 @@ abstract class EntityFilters<T> {
int sortByScore(T a, T b) => getScore(b).compareTo(getScore(a));
}
abstract class LibraryListArguments<T extends WithMeta, F extends EntityFilters<T>> {
abstract class LibraryListArguments<T extends WithMeta,
F extends EntityFilters<T>> {
final Map<FiltersGroup, F?> filters;
final void Function(Iterable<T> items)? onSelected;

View File

@@ -13,7 +13,8 @@ import 'package:get/get.dart';
import 'filters/character_class_filters.dart';
class CharacterClassesLibraryListView extends GetView<LibraryListController<CharacterClass, CharacterClassFilters>> {
class CharacterClassesLibraryListView extends GetView<
LibraryListController<CharacterClass, CharacterClassFilters>> {
const CharacterClassesLibraryListView({
Key? key,
}) : super(key: key);
@@ -43,7 +44,8 @@ class CharacterClassesLibraryListView extends GetView<LibraryListController<Char
onSave: data.onUpdate!,
)
: null,
onDelete: data.onDelete != null ? () => data.onDelete!(data.item) : null,
onDelete:
data.onDelete != null ? () => data.onDelete!(data.item) : null,
),
if (data.selectable)
ElevatedButton.icon(
@@ -58,7 +60,8 @@ class CharacterClassesLibraryListView extends GetView<LibraryListController<Char
}
}
class CharacterClassLibraryListArguments extends LibraryListArguments<CharacterClass, CharacterClassFilters> {
class CharacterClassLibraryListArguments
extends LibraryListArguments<CharacterClass, CharacterClassFilters> {
CharacterClassLibraryListArguments({
required void Function(CharacterClass cls)? onSelected,
required super.preSelections,
@@ -70,7 +73,8 @@ class CharacterClassLibraryListArguments extends LibraryListArguments<CharacterC
FiltersGroup.playbook: CharacterClassFilters(),
FiltersGroup.my: CharacterClassFilters(),
},
onSelected: onSelected != null ? (cls) => onSelected.call(cls.first) : null,
onSelected:
onSelected != null ? (cls) => onSelected.call(cls.first) : null,
extraData: const {},
multiple: false,
);

View File

@@ -5,14 +5,14 @@ import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_c
import 'package:dungeon_paper/app/widgets/atoms/search_field.dart';
import 'package:dungeon_paper/app/widgets/chips/primary_chip.dart';
import 'package:dungeon_paper/core/utils/list_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:popover/popover.dart';
class EntityFiltersView<T, F extends EntityFilters<T>> extends StatelessWidget {
EntityFiltersView({
Key? key,
super.key,
required this.filters,
required this.emptyFilters,
required this.onChange,
@@ -20,11 +20,12 @@ class EntityFiltersView<T, F extends EntityFilters<T>> extends StatelessWidget {
this.filterWidgetsBuilder,
this.leading = const [],
this.trailing = const [],
}) : super(key: key);
});
final F filters;
final F emptyFilters;
final List<Widget> Function(BuildContext context, F filters)? filterWidgetsBuilder;
final List<Widget> Function(BuildContext context, F filters)?
filterWidgetsBuilder;
final service = Get.find<RepositoryService>();
final void Function(F) onChange;
final TextEditingController searchController;
@@ -39,7 +40,7 @@ class EntityFiltersView<T, F extends EntityFilters<T>> extends StatelessWidget {
if (leading.isNotEmpty) const SizedBox(height: 8),
SearchField(
controller: searchController,
hintText: S.current.searchPlaceholderGeneric(S.current.entity(T)),
hintText: tr.search.placeholderEntity(tr.entity(T)),
trailing: filterWidgetsBuilder != null
? [
_FiltersWidgetsBuilder<F>(
@@ -60,16 +61,17 @@ class EntityFiltersView<T, F extends EntityFilters<T>> extends StatelessWidget {
class _FiltersWidgetsBuilder<F extends EntityFilters> extends StatelessWidget {
const _FiltersWidgetsBuilder({
Key? key,
super.key,
required this.filters,
required this.emptyFilters,
required this.filterWidgetsBuilder,
required this.onChange,
}) : super(key: key);
});
final F filters;
final F emptyFilters;
final List<Widget> Function(BuildContext context, F filters) filterWidgetsBuilder;
final List<Widget> Function(BuildContext context, F filters)
filterWidgetsBuilder;
final void Function(F) onChange;
@override
@@ -88,12 +90,14 @@ class _FiltersWidgetsBuilder<F extends EntityFilters> extends StatelessWidget {
filters.controller.add(emptyFilters);
}
: null,
deleteButtonTooltip: S.current.libraryListNoItemsFoundClearFiltersButton,
deleteButtonTooltip: tr.myLibrary.filters.clear,
onPressed: () => showPopover(
context: context,
height: max(
96,
filters.totalFilterCount * 64 + 32 + (16 * (filters.totalFilterCount - 1)),
filters.totalFilterCount * 64 +
32 +
(16 * (filters.totalFilterCount - 1)),
),
width: 300,
backgroundColor: Theme.of(context).cardColor,
@@ -109,13 +113,14 @@ class _FiltersWidgetsBuilder<F extends EntityFilters> extends StatelessWidget {
class _FiltersPopover<F extends EntityFilters> extends StatelessWidget {
const _FiltersPopover({
Key? key,
super.key,
required this.filters,
required this.filterWidgetsBuilder,
}) : super(key: key);
});
final F filters;
final List<Widget> Function(BuildContext context, F filters) filterWidgetsBuilder;
final List<Widget> Function(BuildContext context, F filters)
filterWidgetsBuilder;
@override
Widget build(BuildContext context) {
@@ -131,8 +136,9 @@ class _FiltersPopover<F extends EntityFilters> extends StatelessWidget {
children: enumerate(filterWidgetsBuilder(context, filters))
.map(
(e) => Container(
padding:
e.index == filters.totalFilterCount - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 16),
padding: e.index == filters.totalFilterCount - 1
? EdgeInsets.zero
: const EdgeInsets.only(bottom: 16),
height: e.index == filters.totalFilterCount - 1 ? 64 : 80,
child: e.value,
),

View File

@@ -70,7 +70,9 @@ class CharacterClassFilters extends EntityFilters<CharacterClass> {
double getScore(CharacterClass cls) {
return avg(
[cls.name, cls.description].map(
(e) => (search?.isEmpty ?? true) || e.isEmpty ? 0.0 : StringSimilarity.compareTwoStrings(search!, e),
(e) => (search?.isEmpty ?? true) || e.isEmpty
? 0.0
: StringSimilarity.compareTwoStrings(search!, e),
),
);
}

View File

@@ -64,7 +64,9 @@ class ItemFilters extends EntityFilters<Item> {
double getScore(Item item) {
return avg(
[item.name, item.description].map(
(e) => (search?.isEmpty ?? true) || e.isEmpty ? 0.0 : StringSimilarity.compareTwoStrings(search!, e),
(e) => (search?.isEmpty ?? true) || e.isEmpty
? 0.0
: StringSimilarity.compareTwoStrings(search!, e),
),
);
}

View File

@@ -6,20 +6,19 @@ import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart'
import 'package:dungeon_paper/app/widgets/atoms/select_box.dart';
import 'package:dungeon_paper/core/utils/math_utils.dart';
import 'package:dungeon_paper/core/utils/string_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:string_similarity/string_similarity.dart';
class MoveFiltersView extends StatelessWidget {
MoveFiltersView({
Key? key,
super.key,
required this.filters,
required this.group,
required this.onChange,
required this.searchController,
}) : super(key: key);
});
final MoveFilters filters;
final FiltersGroup group;
@@ -37,21 +36,18 @@ class MoveFiltersView extends StatelessWidget {
filterWidgetsBuilder: (context, f) => [
SelectBox<MoveCategory?>(
isExpanded: true,
label: Text(S.current.entityPlural(MoveCategory)),
label: Text(tr.entityPlural(MoveCategory)),
value: f.category,
items: [
DropdownMenuItem<MoveCategory?>(
child: Text(S.current.allGeneric(S.current.entityPlural(MoveCategory))),
value: null,
child:
Text(tr.generic.allEntities(tr.entityPlural(MoveCategory))),
),
...MoveCategory.values.map(
(cat) => DropdownMenuItem<MoveCategory?>(
child: Text(
![MoveCategory.advanced1, MoveCategory.advanced2].contains(cat)
? S.current.moveCategory(cat)
: S.current.moveCategoryWithLevel(cat),
),
value: cat,
child: Text(tr.moves.category.longName(cat.name)),
),
),
],
@@ -61,18 +57,22 @@ class MoveFiltersView extends StatelessWidget {
},
),
SelectBox<String>(
label: Text(S.current.entityPlural(CharacterClass)),
label: Text(tr.entityPlural(CharacterClass)),
isExpanded: true,
value: f.classKey,
items: [
DropdownMenuItem<String>(
child: Text(S.current.allGeneric(S.current.entityPlural(CharacterClass))),
value: null,
child:
Text(tr.generic.allEntities(tr.entityPlural(CharacterClass))),
),
...<CharacterClass>{...repo.builtIn.classes.values, ...repo.my.classes.values}.map(
...<CharacterClass>{
...repo.builtIn.classes.values,
...repo.my.classes.values
}.map(
(cls) => DropdownMenuItem<String>(
child: Text(cls.name),
value: cls.key,
child: Text(cls.name),
),
),
],
@@ -119,7 +119,9 @@ class MoveFilters extends EntityFilters<Move> {
if (classKey != null) {
if (![MoveCategory.basic, MoveCategory.special].contains(category) &&
!move.classKeys.map((x) => cleanStr(x.key)).contains(cleanStr(classKey!))) {
!move.classKeys
.map((x) => cleanStr(x.key))
.contains(cleanStr(classKey!))) {
return false;
}
}
@@ -139,11 +141,18 @@ class MoveFilters extends EntityFilters<Move> {
return avg(
[
category == move.category ? 1.0 : 0.0,
classKey != null && move.classKeys.map((x) => cleanStr(x.key)).contains(cleanStr(classKey!)) ? 1.0 : 0.0,
classKey != null &&
move.classKeys
.map((x) => cleanStr(x.key))
.contains(cleanStr(classKey!))
? 1.0
: 0.0,
] +
[move.name, move.description, move.explanation]
.map(
(e) => (search?.isEmpty ?? true) || e.isEmpty ? 0.0 : StringSimilarity.compareTwoStrings(search!, e),
(e) => (search?.isEmpty ?? true) || e.isEmpty
? 0.0
: StringSimilarity.compareTwoStrings(search!, e),
)
.toList(),
);

View File

@@ -64,7 +64,9 @@ class NoteFilters extends EntityFilters<Note> {
double getScore(Note note) {
return avg(
[note.title, note.description].map(
(e) => (search?.isEmpty ?? true) || e.isEmpty ? 0.0 : StringSimilarity.compareTwoStrings(search!, e),
(e) => (search?.isEmpty ?? true) || e.isEmpty
? 0.0
: StringSimilarity.compareTwoStrings(search!, e),
),
);
}

View File

@@ -6,20 +6,19 @@ import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart'
import 'package:dungeon_paper/app/widgets/atoms/select_box.dart';
import 'package:dungeon_paper/core/utils/math_utils.dart';
import 'package:dungeon_paper/core/utils/string_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:string_similarity/string_similarity.dart';
class RaceFiltersView extends StatelessWidget {
RaceFiltersView({
Key? key,
super.key,
required this.filters,
required this.group,
required this.onChange,
required this.searchController,
}) : super(key: key);
});
final RaceFilters filters;
final FiltersGroup group;
@@ -36,18 +35,22 @@ class RaceFiltersView extends StatelessWidget {
searchController: searchController,
filterWidgetsBuilder: (context, f) => [
SelectBox<String>(
label: Text(S.current.entityPlural(CharacterClass)),
label: Text(tr.entityPlural(CharacterClass)),
isExpanded: true,
value: f.classKey,
items: [
DropdownMenuItem<String>(
child: Text(S.current.allGeneric(S.current.entityPlural(CharacterClass))),
value: null,
child:
Text(tr.generic.allEntities(tr.entityPlural(CharacterClass))),
),
...<CharacterClass>{...repo.builtIn.classes.values, ...repo.my.classes.values}.map(
...<CharacterClass>{
...repo.builtIn.classes.values,
...repo.my.classes.values
}.map(
(cls) => DropdownMenuItem<String>(
child: Text(cls.name),
value: cls.key,
child: Text(cls.name),
),
),
],
@@ -85,7 +88,9 @@ class RaceFilters extends EntityFilters<Race> {
}
if (classKey != null) {
if (!race.classKeys.map((x) => cleanStr(x.key)).contains(cleanStr(classKey!))) {
if (!race.classKeys
.map((x) => cleanStr(x.key))
.contains(cleanStr(classKey!))) {
return false;
}
}
@@ -104,11 +109,18 @@ class RaceFilters extends EntityFilters<Race> {
double getScore(Race race) {
return avg(
[
classKey != null && race.classKeys.map((x) => cleanStr(x.key)).contains(cleanStr(classKey!)) ? 1.0 : 0.0,
classKey != null &&
race.classKeys
.map((x) => cleanStr(x.key))
.contains(cleanStr(classKey!))
? 1.0
: 0.0,
] +
[race.name, race.description, race.explanation]
.map(
(e) => (search?.isEmpty ?? true) || e.isEmpty ? 0.0 : StringSimilarity.compareTwoStrings(search!, e),
(e) => (search?.isEmpty ?? true) || e.isEmpty
? 0.0
: StringSimilarity.compareTwoStrings(search!, e),
)
.toList(),
);

View File

@@ -6,19 +6,19 @@ import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart'
import 'package:dungeon_paper/app/widgets/atoms/select_box.dart';
import 'package:dungeon_paper/core/utils/math_utils.dart';
import 'package:dungeon_paper/core/utils/string_utils.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:string_similarity/string_similarity.dart';
class SpellFiltersView extends StatelessWidget {
SpellFiltersView({
Key? key,
super.key,
required this.group,
required this.filters,
required this.onChange,
required this.searchController,
}) : super(key: key);
});
final SpellFilters filters;
final FiltersGroup group;
@@ -35,17 +35,21 @@ class SpellFiltersView extends StatelessWidget {
searchController: searchController,
filterWidgetsBuilder: (context, f) => [
SelectBox<String>(
label: Text(S.current.entityPlural(Spell)),
label: Text(tr.entityPlural(Spell)),
value: f.classKey,
items: [
DropdownMenuItem<String>(
child: Text(S.current.allGeneric(S.current.entityPlural(CharacterClass))),
value: null,
child:
Text(tr.generic.allEntities(tr.entityPlural(CharacterClass))),
),
...<CharacterClass>{...repo.builtIn.classes.values, ...repo.my.classes.values}.map(
...<CharacterClass>{
...repo.builtIn.classes.values,
...repo.my.classes.values
}.map(
(cls) => DropdownMenuItem<String>(
child: Text(cls.name),
value: cls.key,
child: Text(cls.name),
),
),
],
@@ -91,7 +95,9 @@ class SpellFilters extends EntityFilters<Spell> {
}
if (classKey != null) {
if (!spell.classKeys.map((x) => cleanStr(x.key)).contains(cleanStr(classKey!))) {
if (!spell.classKeys
.map((x) => cleanStr(x.key))
.contains(cleanStr(classKey!))) {
return false;
}
}
@@ -109,11 +115,18 @@ class SpellFilters extends EntityFilters<Spell> {
return avg(
[
level == spell.level ? 1.0 : 0.0,
classKey != null && spell.classKeys.map((x) => cleanStr(x.key)).contains(cleanStr(classKey!)) ? 1.0 : 0.0,
classKey != null &&
spell.classKeys
.map((x) => cleanStr(x.key))
.contains(cleanStr(classKey!))
? 1.0
: 0.0,
] +
[spell.name, spell.description, spell.explanation]
.map(
(e) => (search?.isEmpty ?? true) || e.isEmpty ? 0.0 : StringSimilarity.compareTwoStrings(search!, e),
(e) => (search?.isEmpty ?? true) || e.isEmpty
? 0.0
: StringSimilarity.compareTwoStrings(search!, e),
)
.toList(),
);

View File

@@ -12,7 +12,8 @@ import 'package:get/get.dart';
import 'filters/item_filters.dart';
class ItemsLibraryListView extends GetView<LibraryListController<Item, ItemFilters>> {
class ItemsLibraryListView
extends GetView<LibraryListController<Item, ItemFilters>> {
const ItemsLibraryListView({
Key? key,
}) : super(key: key);
@@ -40,7 +41,8 @@ class ItemsLibraryListView extends GetView<LibraryListController<Item, ItemFilte
onSave: data.onUpdate!,
)
: null,
onDelete: data.onDelete != null ? () => data.onDelete!(data.item) : null,
onDelete:
data.onDelete != null ? () => data.onDelete!(data.item) : null,
),
if (data.selectable)
ElevatedButton.icon(

View File

@@ -16,13 +16,14 @@ import 'package:dungeon_paper/app/widgets/forms/race_form.dart';
import 'package:dungeon_paper/app/widgets/forms/spell_form.dart';
import 'package:dungeon_paper/core/utils/builder_utils.dart';
import 'package:dungeon_paper/core/utils/enums.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class LibraryCardList<T extends WithMeta, F extends EntityFilters<T>> extends GetView<LibraryListController<T, F>> {
class LibraryCardList<T extends WithMeta, F extends EntityFilters<T>>
extends GetView<LibraryListController<T, F>> {
LibraryCardList({
Key? key,
super.key,
required this.useFilters,
required this.filtersBuilder,
required this.filters,
@@ -32,11 +33,10 @@ class LibraryCardList<T extends WithMeta, F extends EntityFilters<T>> extends Ge
this.extraData = const {},
this.onSave,
}) : itemBuilder = ((BuildContext context, int index) => children[index]),
itemCount = children.length,
super(key: key);
itemCount = children.length;
const LibraryCardList.builder({
Key? key,
super.key,
required this.useFilters,
required this.filtersBuilder,
required this.filters,
@@ -46,7 +46,7 @@ class LibraryCardList<T extends WithMeta, F extends EntityFilters<T>> extends Ge
required this.itemCount,
this.extraData = const {},
this.onSave,
}) : super(key: key);
});
final bool useFilters;
final Widget Function(
@@ -110,7 +110,7 @@ class LibraryCardList<T extends WithMeta, F extends EntityFilters<T>> extends Ge
if (itemCount == 0) ...[
() => const SizedBox(height: 32),
() => Text(
S.current.libraryListNoItemsFoundTitle(S.current.entityPlural(T)),
tr.myLibrary.emptyState.title(tr.entityPlural(T)),
style: textTheme.titleLarge,
textAlign: TextAlign.center,
),
@@ -120,8 +120,10 @@ class LibraryCardList<T extends WithMeta, F extends EntityFilters<T>> extends Ge
width: 300,
child: Text(
filters.isEmpty
? S.current.libraryListNoItemsFoundSubtitleNoFilters(S.current.entityPlural(T))
: S.current.libraryListNoItemsFoundSubtitleFilters(S.current.entityPlural(T)),
? tr.myLibrary.emptyState.subtitle
.noFilters(tr.entityPlural(T))
: tr.myLibrary.emptyState.subtitle
.filters(tr.entityPlural(T)),
style: textTheme.titleMedium,
textAlign: TextAlign.center,
),
@@ -138,7 +140,7 @@ class LibraryCardList<T extends WithMeta, F extends EntityFilters<T>> extends Ge
Routes.editByType<T>(),
arguments: createPageArgsByType(extraData),
),
label: Text(S.current.createGeneric(S.current.entity(T))),
label: Text(tr.generic.createEntity(tr.entity(T))),
icon: const Icon(Icons.add),
),
),

View File

@@ -10,9 +10,8 @@ import 'package:dungeon_paper/app/data/services/repository_service.dart';
import 'package:dungeon_paper/app/data/services/user_service.dart';
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
import 'package:dungeon_paper/app/widgets/atoms/menu_button.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
@@ -22,7 +21,7 @@ class LibraryCollectionView extends GetView<LibraryCollectionController>
with RepositoryServiceMixin, UserServiceMixin, CharacterServiceMixin {
static const List<Type> types = [Move, Spell, Item, CharacterClass, Race];
const LibraryCollectionView({Key? key}) : super(key: key);
const LibraryCollectionView({super.key});
@override
Widget build(BuildContext context) {
@@ -30,13 +29,13 @@ class LibraryCollectionView extends GetView<LibraryCollectionController>
final textTheme = theme.textTheme;
return Scaffold(
appBar: AppBar(
title: Text(S.current.libraryCollectionTitle),
title: Text(tr.myLibrary.title),
centerTitle: true,
actions: [
MenuButton(
items: [
MenuEntry(
label: Text(S.current.reloadLibrary),
label: Text(tr.myLibrary.reload),
icon: const Icon(Icons.refresh),
disabled: repo.my.isLoading || repo.builtIn.isLoading,
value: 'refresh',
@@ -60,7 +59,8 @@ class LibraryCollectionView extends GetView<LibraryCollectionController>
child: ListTile(
onTap: () => ModelPages.openLibraryList(
type: type,
abilityScores: maybeChar?.abilityScores ?? AbilityScores.dungeonWorldAll(10),
abilityScores: maybeChar?.abilityScores ??
AbilityScores.dungeonWorldAll(10),
classKeys: [],
moveCategory: null,
// initialTab: charService.maybeCurrent != null
@@ -79,18 +79,20 @@ class LibraryCollectionView extends GetView<LibraryCollectionController>
),
),
title: Text(
S.current.entityPlural(type),
tr.entityPlural(type),
style: textTheme.titleLarge,
),
subtitle: Text(
[
S.current.libraryCollectionListItemSubtitle(
NumberFormat('#,###,###').format(repo.builtIn.listByType(type).length),
S.current.libraryCollectionListItemSubtitleType('builtIn'),
tr.myLibrary.itemCount(
NumberFormat('#,###,###')
.format(repo.builtIn.listByType(type).length),
tr.myLibrary.libraryType('builtIn'),
),
S.current.libraryCollectionListItemSubtitle(
NumberFormat('#,###,###').format(repo.my.listByType(type).length),
S.current.libraryCollectionListItemSubtitleType('my'),
tr.myLibrary.itemCount(
NumberFormat('#,###,###')
.format(repo.my.listByType(type).length),
tr.myLibrary.libraryType('my'),
),
].join(' | '),
),

View File

@@ -4,14 +4,14 @@ import 'package:dungeon_paper/app/themes/colors.dart';
import 'package:dungeon_paper/app/themes/themes.dart';
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/app/widgets/dialogs/confirm_delete_dialog.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'library_card_list.dart';
typedef CardBuilder<T> = Widget Function(BuildContext context, CardBuilderData<T> data);
typedef CardBuilder<T> = Widget Function(
BuildContext context, CardBuilderData<T> data);
class CardBuilderData<T> {
final T item;
@@ -37,19 +37,20 @@ class CardBuilderData<T> {
});
}
class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends GetView<LibraryListController<T, F>> {
class LibraryListView<T extends WithMeta, F extends EntityFilters<T>>
extends GetView<LibraryListController<T, F>> {
LibraryListView({
Key? key,
super.key,
this.title,
required this.cardBuilder,
required this.filtersBuilder,
}) : super(key: key);
});
final Widget? title;
final CardBuilder<T> cardBuilder;
final pageStorageBucket = PageStorageBucket();
final Widget Function(FiltersGroup group, F filters, void Function(FiltersGroup group, F filters) update)?
filtersBuilder;
final Widget Function(FiltersGroup group, F filters,
void Function(FiltersGroup group, F filters) update)? filtersBuilder;
bool get useFilters => filtersBuilder != null;
F get playbookFilters => controller.filters[FiltersGroup.playbook]!;
@@ -57,14 +58,17 @@ class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends Ge
@override
Widget build(BuildContext context) {
final entityTitleName =
controller.multiple || !controller.selectable ? S.current.entityPlural(T) : S.current.entity(T);
final entityTitleName = controller.multiple || !controller.selectable
? tr.entityPlural(T)
: tr.entity(T);
return Scaffold(
appBar: AppBar(
title: title ??
Text(
controller.selectable ? S.current.selectGeneric(entityTitleName) : S.current.viewGeneric(entityTitleName),
controller.selectable
? tr.generic.selectEntity(entityTitleName)
: tr.generic.viewEntity(entityTitleName),
),
centerTitle: true,
),
@@ -79,9 +83,9 @@ class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends Ge
labelColor: Theme.of(context).colorScheme.onSurface,
unselectedLabelColor: Theme.of(context).colorScheme.onSurface,
tabs: [
Tab(child: Text(S.current.addRepoItemTabPlaybook)),
Tab(child: Text(S.current.myGeneric(S.current.entityPlural(T)))),
// Tab(child: Text(S.current.addRepoItemTabOnline)),
Tab(child: Text(tr.myLibrary.itemTab.playbook)),
Tab(child: Text(tr.generic.myEntity(tr.entityPlural(T)))),
// Tab(child: Text(tr.myLibrary.itemTab.online)),
],
),
Expanded(
@@ -98,12 +102,14 @@ class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends Ge
itemCount: controller.builtInList.length,
itemBuilder: (context, index) {
final item = controller.builtInList.elementAt(index);
return _wrapWithSelection(context, item, FiltersGroup.playbook);
return _wrapWithSelection(
context, item, FiltersGroup.playbook);
},
),
LibraryCardList<T, F>.builder(
group: FiltersGroup.my,
onSave: (item) => controller.saveCustomItem(controller.storageKey, item),
onSave: (item) => controller.saveCustomItem(
controller.storageKey, item),
extraData: controller.extraData,
useFilters: useFilters,
filtersBuilder: filtersBuilder,
@@ -112,7 +118,8 @@ class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends Ge
itemCount: controller.myList.length,
itemBuilder: (context, index) {
final item = controller.myList.elementAt(index);
return _wrapWithSelection(context, item, FiltersGroup.my);
return _wrapWithSelection(
context, item, FiltersGroup.my);
},
),
// Container(),
@@ -140,16 +147,15 @@ class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends Ge
label: Text(
controller.selected.isNotEmpty
? controller.multiple
? S.current.addGeneric(
S.current.pluralize(
controller.selected.length,
S.current.entity(T),
S.current.entityPlural(T),
),
? tr.generic.addEntity(
tr.entityCount(T, controller.selected.length),
)
: S.current.selectGeneric(controller.selected.first.displayName)
: S.current.selectToAdd(
controller.multiple ? S.current.entityPlural(T) : S.current.entity(T),
: tr.generic.selectEntity(
controller.selected.first.displayName)
: tr.generic.selectToAdd(
controller.multiple
? tr.entityPlural(T)
: tr.entity(T),
),
),
)
@@ -158,13 +164,15 @@ class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends Ge
);
}
Widget _wrapWithSelection(BuildContext context, T item, FiltersGroup group) => Obx(
Widget _wrapWithSelection(BuildContext context, T item, FiltersGroup group) =>
Obx(
() {
final isPreSelected = controller.isPreSelected(item);
final selected = controller.isSelected(item);
final enabled = controller.isEnabled(item);
final selectable = controller.selectable;
final onToggle = enabled ? () => controller.toggleItem(item, !selected) : null;
final onToggle =
enabled ? () => controller.toggleItem(item, !selected) : null;
final cardData = CardBuilderData<T>(
item: item,
selected: selected,
@@ -173,13 +181,13 @@ class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends Ge
label: Text(
enabled
? !selected
? S.current.select
? tr.generic.select
: controller.multiple
? S.current.remove
: S.current.unselect
? tr.generic.remove
: tr.generic.unselect
: controller.multiple
? S.current.alreadyAdded
: S.current.select,
? tr.myLibrary.alreadyAdded
: tr.generic.select,
),
icon: Icon(
enabled
@@ -192,14 +200,16 @@ class LibraryListView<T extends WithMeta, F extends EntityFilters<T>> extends Ge
? Icons.add
: Icons.check,
),
onUpdate:
group == FiltersGroup.my ? (item) => controller.saveCustomItem(controller.storageKey, item) : null,
onUpdate: group == FiltersGroup.my
? (item) =>
controller.saveCustomItem(controller.storageKey, item)
: null,
onDelete: group == FiltersGroup.my
? (item) => deleteDialog.confirm(
context,
DeleteDialogOptions(
entityName: item.displayName,
entityKind: S.current.entity(item.runtimeType),
entityKind: tr.entity(item.runtimeType),
),
() => controller.deleteCustomItem(
controller.storageKey,

View File

@@ -16,7 +16,9 @@ import 'package:get/get.dart';
import 'filters/move_filters.dart';
class MovesLibraryListView extends GetView<LibraryListController<Move, MoveFilters>> with CharacterServiceMixin {
class MovesLibraryListView
extends GetView<LibraryListController<Move, MoveFilters>>
with CharacterServiceMixin {
const MovesLibraryListView({
Key? key,
}) : super(key: key);
@@ -41,12 +43,14 @@ class MovesLibraryListView extends GetView<LibraryListController<Move, MoveFilte
EntityEditMenu(
onEdit: data.onUpdate != null
? () => ModelPages.openMovePage(
abilityScores: maybeChar?.abilityScores ?? AbilityScores.dungeonWorldAll(10),
abilityScores: maybeChar?.abilityScores ??
AbilityScores.dungeonWorldAll(10),
move: data.item,
onSave: data.onUpdate!,
)
: null,
onDelete: data.onDelete != null ? () => data.onDelete!(data.item) : null,
onDelete:
data.onDelete != null ? () => data.onDelete!(data.item) : null,
),
if (data.selectable)
ElevatedButton.icon(
@@ -74,8 +78,10 @@ class MoveLibraryListArguments extends LibraryListArguments<Move, MoveFilters> {
sortFn: Move.sorter,
filterFn: (move, filters) => filters.filter(move),
filters: {
FiltersGroup.playbook: MoveFilters(classKey: character?.characterClass.key, category: category),
FiltersGroup.my: MoveFilters(classKey: character?.characterClass.key, category: category),
FiltersGroup.playbook: MoveFilters(
classKey: character?.characterClass.key, category: category),
FiltersGroup.my: MoveFilters(
classKey: character?.characterClass.key, category: category),
},
extraData: {
'abilityScores': abilityScores ?? character?.abilityScores,

View File

@@ -13,7 +13,8 @@ import 'package:get/get.dart';
import 'filters/note_filters.dart';
class NotesLibraryListView extends GetView<LibraryListController<Note, NoteFilters>> {
class NotesLibraryListView
extends GetView<LibraryListController<Note, NoteFilters>> {
const NotesLibraryListView({
Key? key,
}) : super(key: key);
@@ -41,7 +42,8 @@ class NotesLibraryListView extends GetView<LibraryListController<Note, NoteFilte
onSave: data.onUpdate!,
)
: null,
onDelete: data.onDelete != null ? () => data.onDelete!(data.item) : null,
onDelete:
data.onDelete != null ? () => data.onDelete!(data.item) : null,
),
if (data.selectable)
ElevatedButton.icon(

View File

@@ -15,7 +15,9 @@ import 'package:get/get.dart';
import 'filters/race_filters.dart';
class RacesLibraryListView extends GetView<LibraryListController<Race, RaceFilters>> with CharacterServiceMixin {
class RacesLibraryListView
extends GetView<LibraryListController<Race, RaceFilters>>
with CharacterServiceMixin {
const RacesLibraryListView({
Key? key,
}) : super(key: key);
@@ -40,12 +42,14 @@ class RacesLibraryListView extends GetView<LibraryListController<Race, RaceFilte
EntityEditMenu(
onEdit: data.onUpdate != null
? () => ModelPages.openRacePage(
abilityScores: maybeChar?.abilityScores ?? AbilityScores.dungeonWorldAll(10),
abilityScores: maybeChar?.abilityScores ??
AbilityScores.dungeonWorldAll(10),
race: data.item,
onSave: data.onUpdate!,
)
: null,
onDelete: data.onDelete != null ? () => data.onDelete!(data.item) : null,
onDelete:
data.onDelete != null ? () => data.onDelete!(data.item) : null,
),
if (data.selectable)
ElevatedButton.icon(
@@ -70,14 +74,18 @@ class RaceLibraryListArguments extends LibraryListArguments<Race, RaceFilters> {
sortFn: Race.sorter,
filterFn: (race, filters) => filters.filter(race),
filters: {
FiltersGroup.playbook: RaceFilters(classKey: character?.characterClass.key),
FiltersGroup.my: RaceFilters(classKey: character?.characterClass.key),
FiltersGroup.playbook:
RaceFilters(classKey: character?.characterClass.key),
FiltersGroup.my:
RaceFilters(classKey: character?.characterClass.key),
},
extraData: {
'abilityScores': character?.abilityScores,
'classKeys': character != null ? [character.characterClass.key] : null,
'classKeys':
character != null ? [character.characterClass.key] : null,
},
onSelected: onSelected != null ? (race) => onSelected.call(race.first) : null,
onSelected:
onSelected != null ? (race) => onSelected.call(race.first) : null,
multiple: false,
);
}

View File

@@ -13,7 +13,8 @@ import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
import 'filters/spell_filters.dart';
class SpellsLibraryListView extends GetView<LibraryListController<Spell, SpellFilters>> {
class SpellsLibraryListView
extends GetView<LibraryListController<Spell, SpellFilters>> {
const SpellsLibraryListView({
Key? key,
}) : super(key: key);
@@ -44,7 +45,8 @@ class SpellsLibraryListView extends GetView<LibraryListController<Spell, SpellFi
onSave: data.onUpdate!,
)
: null,
onDelete: data.onDelete != null ? () => data.onDelete!(data.item) : null,
onDelete:
data.onDelete != null ? () => data.onDelete!(data.item) : null,
),
if (data.selectable)
ElevatedButton.icon(
@@ -59,7 +61,8 @@ class SpellsLibraryListView extends GetView<LibraryListController<Spell, SpellFi
}
}
class SpellLibraryListArguments extends LibraryListArguments<Spell, SpellFilters> {
class SpellLibraryListArguments
extends LibraryListArguments<Spell, SpellFilters> {
SpellLibraryListArguments({
required Character? character,
required super.onSelected,

View File

@@ -7,7 +7,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class LoginController extends GetxController with AuthServiceMixin, LoadingServiceMixin, CharacterServiceMixin {
class LoginController extends GetxController
with AuthServiceMixin, LoadingServiceMixin, CharacterServiceMixin {
final formKey = GlobalKey<FormState>(debugLabel: 'loginForm');
final email = TextEditingController();
final password = TextEditingController();
@@ -41,7 +42,8 @@ class LoginController extends GetxController with AuthServiceMixin, LoadingServi
void loginWithPassword() async {
_loginWrapper(
() => authService.loginWithPassword(email: email.text, password: password.text),
() => authService.loginWithPassword(
email: email.text, password: password.text),
);
}
@@ -84,4 +86,3 @@ class LoginController extends GetxController with AuthServiceMixin, LoadingServi
return _valid.value;
}
}

View File

@@ -1,8 +1,7 @@
import 'package:dungeon_paper/app/data/services/character_service.dart';
import 'package:dungeon_paper/app/data/services/loading_service.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
enum LoginProgressStep {
@@ -10,19 +9,20 @@ enum LoginProgressStep {
loadChars,
}
class LoginProgressDialogView extends GetView<LoadingService> with CharacterServiceMixin {
const LoginProgressDialogView({Key? key}) : super(key: key);
class LoginProgressDialogView extends GetView<LoadingService>
with CharacterServiceMixin {
const LoginProgressDialogView({super.key});
String get title {
if (controller.loadingUser) {
return S.current.loadingUser;
return tr.loading.user;
}
if (controller.loadingCharacters) {
return S.current.loadingCharacters;
return tr.loading.characters;
}
return S.current.loadingGeneral;
return tr.loading.general;
}
@override

View File

@@ -4,17 +4,16 @@ import 'package:dungeon_paper/core/dw_icons.dart';
import 'package:dungeon_paper/core/platform_helper.dart';
import 'package:dungeon_paper/core/utils/list_utils.dart';
import 'package:dungeon_paper/core/utils/password_validator.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:email_validator/email_validator.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
import '../controllers/login_controller.dart';
class LoginView extends GetView<LoginController> {
const LoginView({Key? key}) : super(key: key);
const LoginView({super.key});
@override
Widget build(BuildContext context) {
@@ -23,7 +22,7 @@ class LoginView extends GetView<LoginController> {
return Scaffold(
appBar: AppBar(
title: Text(S.current.appName),
title: Text(tr.app.name),
),
body: Center(
child: SingleChildScrollView(
@@ -43,11 +42,15 @@ class LoginView extends GetView<LoginController> {
onPressed: !controller.loadingService.loadingUser
? controller.loginWithGoogle
: null,
label: Text(controller.isLogin
? S.current.signinWithButton(
S.current.signinProvider('google'))
: S.current.signupWithButton(
S.current.signinProvider('google'))),
label: Text(
controller.isLogin
? tr.auth.providers.loginWith(
tr.auth.providers.name('google'),
)
: tr.auth.providers.signupWith(
tr.auth.providers.name('google'),
),
),
icon: const Icon(DwIcons.google),
),
],
@@ -56,11 +59,15 @@ class LoginView extends GetView<LoginController> {
onPressed: !controller.loadingService.loadingUser
? controller.loginWithApple
: null,
label: Text(controller.isLogin
? S.current.signinWithButton(
S.current.signinProvider('apple'))
: S.current.signupWithButton(
S.current.signinProvider('apple'))),
label: Text(
controller.isLogin
? tr.auth.providers.loginWith(
tr.auth.providers.name('apple'),
)
: tr.auth.providers.signupWith(
tr.auth.providers.name('apple'),
),
),
icon: const Icon(DwIcons.apple),
),
],
@@ -69,16 +76,16 @@ class LoginView extends GetView<LoginController> {
children: [
Text(
controller.isLogin
? S.current.signinTitle
: S.current.signupTitle,
? tr.auth.login.title
: tr.auth.signup.title,
style: textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
controller.isLogin
? S.current.signinSubtitle
: S.current.signupSubtitle,
? tr.auth.login.subtitle
: tr.auth.signup.subtitle,
style: textTheme.titleMedium,
textAlign: TextAlign.center,
),
@@ -87,7 +94,7 @@ class LoginView extends GetView<LoginController> {
.joinObjects(const SizedBox(height: 8)),
if (providerSignIns.isNotEmpty) ...[
const SizedBox(height: 16),
LabeledDivider(label: Text(S.current.separatorOr)),
LabeledDivider(label: Text(tr.auth.orSeparator)),
],
TextFormField(
controller: controller.email,
@@ -95,15 +102,15 @@ class LoginView extends GetView<LoginController> {
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
filled: true,
label: Text(S.current.signupEmail),
label: Text(tr.auth.signup.email.label),
enabled: !controller.loadingService.loadingUser,
// floatingLabelBehavior: FloatingLabelBehavior.auto,
hintText: S.current.signupEmailPlaceholder,
hintText: tr.auth.signup.email.placeholder,
),
validator: (email) =>
email == null || EmailValidator.validate(email)
? null
: S.current.signupEmailValidation,
: tr.auth.signup.email.error,
),
const SizedBox(height: 16),
PasswordField(
@@ -116,8 +123,8 @@ class LoginView extends GetView<LoginController> {
],
decoration: InputDecoration(
filled: true,
label: Text(S.current.signupPassword),
hintText: S.current.signupPasswordPlaceholder,
label: Text(tr.auth.signup.password.label),
hintText: tr.auth.signup.password.placeholder,
enabled: !controller.loadingService.loadingUser,
// floatingLabelBehavior: FloatingLabelBehavior.auto,
),
@@ -131,9 +138,10 @@ class LoginView extends GetView<LoginController> {
autofillHints: const [AutofillHints.newPassword],
decoration: InputDecoration(
filled: true,
label: Text(S.current.signupPasswordConfirm),
label:
Text(tr.auth.signup.password.confirm.label),
hintText:
S.current.signupPasswordConfirmPlaceholder,
tr.auth.signup.password.confirm.placeholder,
enabled: !controller.loadingService.loadingUser,
// floatingLabelBehavior: FloatingLabelBehavior.auto,
),
@@ -141,8 +149,7 @@ class LoginView extends GetView<LoginController> {
PasswordValidator().validator(pwd) ??
(pwd == controller.password.text
? null
: S.current
.signupPasswordValidationMatch),
: tr.auth.signup.password.confirm.error),
),
],
const SizedBox(height: 16),
@@ -154,9 +161,9 @@ class LoginView extends GetView<LoginController> {
: null,
label: Text(
controller.isLogin
? S.current.signinButton
: S.current.signupButton,
textScaleFactor: 1.5,
? tr.auth.login.button
: tr.auth.signup.button,
textScaler: const TextScaler.linear(1.5),
),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
@@ -175,12 +182,10 @@ class LoginView extends GetView<LoginController> {
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(S.current.signinGoToSignupLabel),
Text(tr.auth.login.noAccount.label),
ElevatedButton(
onPressed: controller.toggleSignup,
child: Text(
S.current.signinGoToSignupButton,
),
child: Text(tr.auth.login.noAccount.button),
)
],
),
@@ -191,7 +196,7 @@ class LoginView extends GetView<LoginController> {
'https://dungeonpaper.app/privacy-policy.html?utm_medium=app&utm_source=login',
),
),
label: Text(S.current.privacyPolicy),
label: Text(tr.auth.privacyPolicy),
icon: const Icon(Icons.lock),
),
const SizedBox(height: 8),
@@ -202,7 +207,7 @@ class LoginView extends GetView<LoginController> {
'https://dungeonpaper.app/changelog.html',
),
),
label: Text(S.current.whatsNew),
label: Text(tr.auth.changelog),
icon: const Icon(Icons.new_releases),
),
],
@@ -218,4 +223,3 @@ class LoginView extends GetView<LoginController> {
);
}
}

View File

@@ -1,14 +1,13 @@
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
import 'package:dungeon_paper/app/widgets/atoms/select_box.dart';
import 'package:dungeon_paper/generated/l10n.dart';
import 'package:dungeon_paper/i18n.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/migration_controller.dart';
class MigrationView extends GetView<MigrationController> {
const MigrationView({Key? key}) : super(key: key);
const MigrationView({super.key});
@override
Widget build(BuildContext context) {
@@ -20,7 +19,7 @@ class MigrationView extends GetView<MigrationController> {
floatingActionButton: Obx(
() => AdvancedFloatingActionButton.extended(
onPressed: controller.isValid ? controller.done : null,
label: Text(S.current.continueLabel),
label: Text(tr.generic.continue_),
icon: const Icon(Icons.check),
),
),
@@ -41,13 +40,13 @@ class MigrationView extends GetView<MigrationController> {
),
const SizedBox(height: 16),
Text(
S.current.migrationTitle,
tr.migration.title,
style: textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Text(
S.current.migrationSubtitle,
tr.migration.subtitle,
style: textTheme.titleMedium,
textAlign: TextAlign.center,
),
@@ -56,20 +55,20 @@ class MigrationView extends GetView<MigrationController> {
() => TextFormField(
controller: controller.username,
decoration: InputDecoration(
label: Text(S.current.signupUsername),
hintText: S.current.signupUsernamePlaceholder,
label: Text(tr.migration.username.label),
hintText: tr.migration.username.placeholder,
),
),
),
const SizedBox(height: 8),
Text(S.current.migrationUsernameInfo, style: textTheme.bodySmall),
Text(tr.migration.username.info, style: textTheme.bodySmall),
const SizedBox(height: 16),
Obx(
() => SelectBox(
value: controller.language,
label: Text(S.current.signupDefaultDataLanguage),
label: Text(tr.migration.language.data),
items: const [
DropdownMenuItem(child: Text('English'), value: 'EN'),
DropdownMenuItem(value: 'EN', child: Text('English')),
],
onChanged: null,
),

View File

@@ -4,7 +4,8 @@ import 'package:dungeon_paper/app/themes/themes.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SelectCharacterThemeController extends GetxController with CharacterServiceMixin, UserServiceMixin {
class SelectCharacterThemeController extends GetxController
with CharacterServiceMixin, UserServiceMixin {
final seeAll = {Brightness.light: false, Brightness.dark: false}.obs;
final lightTheme = Rx<int?>(AppThemes.parchment);
final darkTheme = Rx<int?>(AppThemes.dark);
@@ -14,10 +15,12 @@ class SelectCharacterThemeController extends GetxController with CharacterServic
super.onReady();
lightTheme.value = char.settings.lightTheme;
darkTheme.value = char.settings.darkTheme;
if (lightTheme.value != null && !AppThemes.allLightThemes.contains(lightTheme.value)) {
if (lightTheme.value != null &&
!AppThemes.allLightThemes.contains(lightTheme.value)) {
seeAll[Brightness.light] = true;
}
if (darkTheme.value != null && !AppThemes.allDarkThemes.contains(darkTheme.value)) {
if (darkTheme.value != null &&
!AppThemes.allDarkThemes.contains(darkTheme.value)) {
seeAll[Brightness.dark] = true;
}
}

Some files were not shown because too many files have changed in this diff Show More