mirror of
https://github.com/DungeonPaper/dungeon-paper-app.git
synced 2026-05-17 17:58:11 +00:00
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:
20
README.md
20
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
32
lib/app/data/services/intl_service.dart
Normal file
32
lib/app/data/services/intl_service.dart
Normal 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]!;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]!,
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(' | '),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user