mirror of
https://github.com/DungeonPaper/dungeon-paper-app.git
synced 2026-05-17 17:58:11 +00:00
refactor: remove GetX (#33)
* refactor(getx): start removing character service * refactor: more char provider usages * refactor: many more files * refactor: many more refactors * refactor: back to working state * fix: route, controller & library fixes * fix: refactor bug fixes, add getArgs util * chore: dart fix --apply * fix: transitions * chore: cleanup
This commit is contained in:
2
devtools_options.yaml
Normal file
2
devtools_options.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
extensions:
|
||||
- provider: true
|
||||
@@ -121,18 +121,12 @@ class AlignmentValue extends dw.Alignment with WithIcon implements WithMeta {
|
||||
class AlignmentValues extends dw.AlignmentValues {
|
||||
AlignmentValues({
|
||||
required this.meta,
|
||||
required String good,
|
||||
required String evil,
|
||||
required String lawful,
|
||||
required String neutral,
|
||||
required String chaotic,
|
||||
}) : super(
|
||||
good: good,
|
||||
evil: evil,
|
||||
lawful: lawful,
|
||||
neutral: neutral,
|
||||
chaotic: chaotic,
|
||||
);
|
||||
required super.good,
|
||||
required super.evil,
|
||||
required super.lawful,
|
||||
required super.neutral,
|
||||
required super.chaotic,
|
||||
});
|
||||
|
||||
final Meta meta;
|
||||
|
||||
|
||||
@@ -5,19 +5,12 @@ import 'item.dart';
|
||||
|
||||
class GearChoice extends dw.GearChoice {
|
||||
GearChoice({
|
||||
required String key,
|
||||
required String description,
|
||||
required List<GearSelection> selections,
|
||||
List<int> preselect = const [],
|
||||
int? maxSelections,
|
||||
}) : _selections = selections,
|
||||
super(
|
||||
key: key,
|
||||
description: description,
|
||||
selections: selections,
|
||||
preselect: preselect,
|
||||
maxSelections: maxSelections,
|
||||
);
|
||||
required super.key,
|
||||
required super.description,
|
||||
required List<GearSelection> super.selections,
|
||||
super.preselect,
|
||||
super.maxSelections,
|
||||
}) : _selections = selections;
|
||||
|
||||
@override
|
||||
List<GearSelection> get selections => _selections;
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'dart:convert';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/filters/item_filters.dart';
|
||||
import 'package:dungeon_paper/core/dw_icons.dart';
|
||||
import 'package:dungeon_paper/core/utils/icon_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/uuid.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'item_settings.dart';
|
||||
import 'meta.dart';
|
||||
|
||||
@@ -5,21 +5,22 @@ import 'package:dungeon_paper/app/data/models/campaign.dart';
|
||||
import 'package:dungeon_paper/app/data/models/session_marks.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/data/models/user.dart';
|
||||
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/data/services/repository_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.dart';
|
||||
import 'package:dungeon_paper/core/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/utils/date_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/uuid.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
import 'package:dungeon_world_data/gear_option.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
|
||||
import 'alignment.dart';
|
||||
import 'bio.dart';
|
||||
import 'character.dart';
|
||||
import 'character_class.dart';
|
||||
import 'character_stats.dart';
|
||||
import 'character.dart';
|
||||
import 'gear_choice.dart';
|
||||
import 'gear_selection.dart';
|
||||
import 'item.dart';
|
||||
@@ -28,7 +29,7 @@ import 'move.dart';
|
||||
import 'note.dart';
|
||||
import 'race.dart';
|
||||
|
||||
class Meta<DataType> with RepositoryServiceMixin {
|
||||
class Meta<DataType> with RepositoryProviderMixin {
|
||||
Meta._({
|
||||
required this.version,
|
||||
DateTime? created,
|
||||
@@ -220,9 +221,8 @@ class Meta<DataType> with RepositoryServiceMixin {
|
||||
);
|
||||
}
|
||||
|
||||
static User get user => Get.find<UserService>().current;
|
||||
|
||||
static T forkOrIncrease<T extends WithMeta>(T object) {
|
||||
final user = UserProvider.of(appGlobalKey.currentContext!).current;
|
||||
if (object.meta.isOwnedBy(user)) {
|
||||
return increaseMetaVersion(object);
|
||||
}
|
||||
@@ -483,4 +483,3 @@ mixin WithMeta<T, MetaDataType>
|
||||
String get storageKey;
|
||||
dw.EntityReference get reference => Meta.referenceFor(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,23 +5,14 @@ import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
|
||||
class Monster extends dw.Monster implements WithMeta {
|
||||
Monster({
|
||||
required Meta meta,
|
||||
required String key,
|
||||
required String name,
|
||||
required String description,
|
||||
required String instinct,
|
||||
required List<dw.Tag> tags,
|
||||
required List<String> moves,
|
||||
}) : _meta = meta,
|
||||
super(
|
||||
meta: meta,
|
||||
key: key,
|
||||
name: name,
|
||||
description: description,
|
||||
instinct: instinct,
|
||||
tags: tags,
|
||||
moves: moves,
|
||||
);
|
||||
required Meta super.meta,
|
||||
required super.key,
|
||||
required super.name,
|
||||
required super.description,
|
||||
required super.instinct,
|
||||
required super.tags,
|
||||
required super.moves,
|
||||
}) : _meta = meta;
|
||||
|
||||
@override
|
||||
Meta get meta => _meta;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import 'dart:convert';
|
||||
import 'package:dungeon_paper/app/data/models/user.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/filters/spell_filters.dart';
|
||||
import 'package:dungeon_paper/core/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/utils/icon_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/uuid.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../core/dw_icons.dart';
|
||||
import 'meta.dart';
|
||||
|
||||
class Spell extends dw.Spell with WithIcon implements WithMeta {
|
||||
class Spell extends dw.Spell
|
||||
with WithIcon, UserProviderMixin
|
||||
implements WithMeta {
|
||||
Spell({
|
||||
required Meta super.meta,
|
||||
required super.key,
|
||||
@@ -74,18 +75,21 @@ class Spell extends dw.Spell with WithIcon implements WithMeta {
|
||||
factory Spell.fromJson(Map<String, dynamic> json) =>
|
||||
Spell.fromDwSpell(dw.Spell.fromJson(json), prepared: json['prepared']);
|
||||
|
||||
factory Spell.empty() => Spell(
|
||||
meta: Meta.empty(createdBy: user.username),
|
||||
classKeys: [],
|
||||
description: '',
|
||||
dice: [],
|
||||
explanation: '',
|
||||
level: '',
|
||||
key: uuid(),
|
||||
name: '',
|
||||
tags: [],
|
||||
prepared: false,
|
||||
);
|
||||
factory Spell.empty() {
|
||||
final user = UserProvider.of(appGlobalKey.currentContext!).current;
|
||||
return Spell(
|
||||
meta: Meta.empty(createdBy: user.username),
|
||||
classKeys: [],
|
||||
description: '',
|
||||
dice: [],
|
||||
explanation: '',
|
||||
level: '',
|
||||
key: uuid(),
|
||||
name: '',
|
||||
tags: [],
|
||||
prepared: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -111,8 +115,6 @@ class Spell extends dw.Spell with WithIcon implements WithMeta {
|
||||
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||
};
|
||||
|
||||
static User get user => Get.find<UserService>().current;
|
||||
|
||||
@override
|
||||
String get displayName => name;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class User {
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
String? get documentPath => isLoggedIn ? 'Data/$email' : null;
|
||||
String? get fileStoragePath => isLoggedIn ? documentPath! + '/Uploads' : null;
|
||||
String? get fileStoragePath => isLoggedIn ? '${documentPath!}/Uploads' : null;
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) => User(
|
||||
username: json['username'],
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/themes/themes.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:wakelock/wakelock.dart';
|
||||
|
||||
class UserSettings with CharacterServiceMixin {
|
||||
import '../../themes/themes.dart';
|
||||
import '../services/character_provider.dart';
|
||||
|
||||
class UserSettings with CharacterProviderMixin {
|
||||
final bool keepScreenAwake;
|
||||
final int defaultLightTheme;
|
||||
final int defaultDarkTheme;
|
||||
@@ -80,7 +81,7 @@ class UserSettings with CharacterServiceMixin {
|
||||
Wakelock.toggle(enable: keepScreenAwake);
|
||||
|
||||
if (maybeChar != null) {
|
||||
charService.switchToCharacterTheme(char);
|
||||
charProvider.switchToCharacterTheme(char);
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dungeon_paper/app/data/services/loading_service.dart';
|
||||
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/data/services/loading_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_provider.dart';
|
||||
import 'package:dungeon_paper/core/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/platform_helper.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
|
||||
import '../../model_utils/user_utils.dart';
|
||||
import 'user_provider.dart';
|
||||
|
||||
class AuthService extends GetxService
|
||||
with UserServiceMixin, LoadingServiceMixin, RepositoryServiceMixin {
|
||||
class AuthProvider extends ChangeNotifier
|
||||
with UserProviderMixin, RepositoryProviderMixin {
|
||||
static AuthProvider of(BuildContext context, {bool listen = false}) =>
|
||||
Provider.of<AuthProvider>(context, listen: listen);
|
||||
static Widget consumer(
|
||||
Widget Function(BuildContext, AuthProvider, Widget?) builder) =>
|
||||
Consumer<AuthProvider>(builder: builder);
|
||||
StreamSubscription<User?>? _sub;
|
||||
|
||||
FirebaseAuth get auth => FirebaseAuth.instance;
|
||||
final gSignIn = GoogleSignIn.standard();
|
||||
final _fbUser = Rx<User?>(null);
|
||||
User? get fbUser => _fbUser.value;
|
||||
User? _fbUser;
|
||||
User? get fbUser => _fbUser;
|
||||
|
||||
AuthProvider() {
|
||||
debugPrint('[AUTH PROVIDER] init');
|
||||
_registerAuthListener();
|
||||
}
|
||||
|
||||
Future<UserCredential> loginWithPassword({
|
||||
required String email,
|
||||
@@ -111,9 +122,10 @@ class AuthService extends GetxService
|
||||
return credential;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
Future<void> logout(BuildContext context) async {
|
||||
_clearAuthListener();
|
||||
// await StorageHandler.instance.local.clear();
|
||||
|
||||
await repo.my.dispose();
|
||||
await auth.signOut();
|
||||
try {
|
||||
@@ -127,12 +139,7 @@ class AuthService extends GetxService
|
||||
debugPrint('Error while logging out: $e');
|
||||
}
|
||||
user.applyDefaultTheme();
|
||||
_registerAuthListener();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
notifyListeners();
|
||||
_registerAuthListener();
|
||||
}
|
||||
|
||||
@@ -146,16 +153,25 @@ class AuthService extends GetxService
|
||||
return;
|
||||
}
|
||||
debugPrint('fb user changed: $user');
|
||||
_fbUser.value = user;
|
||||
_fbUser = user;
|
||||
|
||||
final context = appGlobalKey.currentContext!;
|
||||
final userProvider = Provider.of<UserProvider>(context, listen: false);
|
||||
if (user != null) {
|
||||
loadingService.loadingCharacters = !loadingService.afterFirstLoad;
|
||||
loadingService.afterFirstLoad = false;
|
||||
userService.loadUserData(user);
|
||||
final loadingProvider = Provider.of<LoadingProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
loadingProvider.loadingCharacters = !loadingProvider.afterFirstLoad;
|
||||
loadingProvider.afterFirstLoad = false;
|
||||
|
||||
userProvider.loadUserData(user);
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
userService.loadGuestData();
|
||||
userProvider.loadGuestData();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<UserCredential> signUp(
|
||||
@@ -169,6 +185,8 @@ class AuthService extends GetxService
|
||||
}
|
||||
}
|
||||
|
||||
mixin AuthServiceMixin {
|
||||
AuthService get authService => Get.find();
|
||||
mixin AuthProviderMixin {
|
||||
AuthProvider get authProvider =>
|
||||
AuthProvider.of(appGlobalKey.currentContext!);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,41 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.dart';
|
||||
import 'package:dungeon_paper/app/themes/themes.dart';
|
||||
import 'package:dungeon_paper/core/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/storage_handler/storage_handler.dart';
|
||||
import 'package:dungeon_paper/core/utils/date_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/enums.dart';
|
||||
import 'package:dynamic_themes/dynamic_themes.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/character.dart';
|
||||
import 'loading_service.dart';
|
||||
import 'loading_provider.dart';
|
||||
|
||||
class CharacterService extends GetxService
|
||||
with LoadingServiceMixin, UserServiceMixin {
|
||||
static CharacterService find() => Get.find();
|
||||
class CharacterProvider extends ChangeNotifier
|
||||
with LoadingProviderMixin, UserProviderMixin {
|
||||
static CharacterProvider of(BuildContext context, {bool listen = false}) =>
|
||||
Provider.of(context, listen: listen);
|
||||
|
||||
final all = <String, Character>{}.obs;
|
||||
final _currentKey = Rx<String?>(null);
|
||||
static Widget consumer(
|
||||
Widget Function(BuildContext, CharacterProvider, Widget?) builder,
|
||||
) =>
|
||||
Consumer<CharacterProvider>(builder: builder);
|
||||
|
||||
final all = <String, Character>{};
|
||||
String? _currentKey;
|
||||
final _pageController = PageController(initialPage: 1, viewportFraction: 1.1);
|
||||
StreamSubscription? _sub;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
// pageController.addListener(refreshPage);
|
||||
await registerCharacterListener();
|
||||
CharacterProvider() {
|
||||
debugPrint('[PROVIDER] initializing character provider');
|
||||
registerCharacterListener();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
// pageController.removeListener(refreshPage);
|
||||
_sub?.cancel();
|
||||
}
|
||||
@@ -40,8 +46,7 @@ class CharacterService extends GetxService
|
||||
? pageController.page ?? 0
|
||||
: 0;
|
||||
|
||||
Character? get maybeCurrent =>
|
||||
_currentKey.value != null ? all[_currentKey.value] : null;
|
||||
Character? get maybeCurrent => _currentKey != null ? all[_currentKey] : null;
|
||||
Character get current => maybeCurrent!;
|
||||
|
||||
List<Character> get allAsList => all.values.toList();
|
||||
@@ -68,19 +73,21 @@ class CharacterService extends GetxService
|
||||
|
||||
Future<void> registerCharacterListener() async {
|
||||
_clearCharListener();
|
||||
debugPrint('registering character listener');
|
||||
debugPrint('[PROVIDER] registering character listener');
|
||||
_sub =
|
||||
StorageHandler.instance.collectionListener('Characters', charsListener);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
all.clear();
|
||||
_currentKey.value = null;
|
||||
_currentKey = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCurrent(String key) {
|
||||
if (all.containsKey(key)) {
|
||||
_currentKey.value = key;
|
||||
_currentKey = key;
|
||||
notifyListeners();
|
||||
switchToCharacterTheme(current);
|
||||
updateCharacter(
|
||||
current.copyWith(
|
||||
@@ -96,13 +103,17 @@ class CharacterService extends GetxService
|
||||
switchToTheme(character.getCurrentTheme(user));
|
||||
|
||||
void switchToTheme(int themeId) {
|
||||
final dynamicTheme = DynamicTheme.of(Get.context!)!;
|
||||
if (appGlobalKey.currentContext == null) {
|
||||
debugPrint('[PROVIDER] no context, cannot switch theme');
|
||||
return;
|
||||
}
|
||||
final dynamicTheme = DynamicTheme.of(appGlobalKey.currentContext!)!;
|
||||
final currentTheme = dynamicTheme.themeId;
|
||||
if (currentTheme == themeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('switching to theme $themeId');
|
||||
debugPrint('[PROVIDER] switching to theme $themeId');
|
||||
AppThemes.setTheme(themeId);
|
||||
}
|
||||
|
||||
@@ -111,7 +122,7 @@ class CharacterService extends GetxService
|
||||
|
||||
all.addAll(Map.fromIterable(list, key: (c) => c.key));
|
||||
|
||||
if (all.isNotEmpty && _currentKey.value == null) {
|
||||
if (all.isNotEmpty && _currentKey == null) {
|
||||
switchToLastUsedChar();
|
||||
}
|
||||
|
||||
@@ -119,33 +130,38 @@ class CharacterService extends GetxService
|
||||
switchToCharacterTheme(current);
|
||||
}
|
||||
|
||||
loadingService.loadingCharacters = false;
|
||||
loadingService.afterFirstLoad = !loadingService.loadingUser;
|
||||
loadingProvider.loadingCharacters = false;
|
||||
loadingProvider.afterFirstLoad = !loadingProvider.loadingUser;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void switchToLastUsedChar() {
|
||||
final hasLastChar = all.values.any((c) => c.meta.data?.lastUsed != null);
|
||||
if (hasLastChar) {
|
||||
final lastChar = charsByLastUsed.first;
|
||||
_currentKey.value = lastChar.key;
|
||||
_currentKey = lastChar.key;
|
||||
} else if (all.isNotEmpty) {
|
||||
_currentKey.value = all.keys.first;
|
||||
_currentKey = all.keys.first;
|
||||
} else {
|
||||
_currentKey.value = null;
|
||||
_currentKey = null;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateCharacter(Character character,
|
||||
{bool switchToCharacter = false}) {
|
||||
// (StorageHandler.instance.delegate as LocalStorageDelegate).storage.collection('Characters');
|
||||
Future<void> updateCharacter(
|
||||
Character character, {
|
||||
bool switchToCharacter = false,
|
||||
}) {
|
||||
character = character.copyWithInherited(meta: character.meta.stampUpdate());
|
||||
all[character.key] = character;
|
||||
notifyListeners();
|
||||
if (switchToCharacter ||
|
||||
_currentKey.value == null ||
|
||||
!all.containsKey(_currentKey.value)) {
|
||||
_currentKey == null ||
|
||||
!all.containsKey(_currentKey)) {
|
||||
setCurrent(character.key);
|
||||
}
|
||||
debugPrint('Updated char: ${character.key} (${character.displayName})');
|
||||
debugPrint(
|
||||
'[PROVIDER] Updated char: ${character.key} (${character.displayName})');
|
||||
debugPrint(character.toRawJson());
|
||||
return StorageHandler.instance
|
||||
.update('Characters', character.key, character.toJson());
|
||||
@@ -155,11 +171,13 @@ class CharacterService extends GetxService
|
||||
all[character.key] = character;
|
||||
StorageHandler.instance
|
||||
.create('Characters', character.key, character.toJson());
|
||||
if (switchToCharacter || _currentKey.value == null) {
|
||||
_currentKey.value = character.key;
|
||||
if (switchToCharacter || _currentKey == null) {
|
||||
_currentKey = character.key;
|
||||
}
|
||||
debugPrint('Created char: ${character.key} (${character.displayName})');
|
||||
debugPrint(
|
||||
'[PROVIDER] Created char: ${character.key} (${character.displayName})');
|
||||
debugPrint(character.toRawJson());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void deleteCharacter(Character character) {
|
||||
@@ -167,12 +185,14 @@ class CharacterService extends GetxService
|
||||
try {
|
||||
StorageHandler.instance.delete('Characters', character.key);
|
||||
} catch (e) {
|
||||
debugPrint('Error deleting character: $e');
|
||||
debugPrint('[PROVIDER] Error deleting character: $e');
|
||||
}
|
||||
if (character.key == _currentKey.value) {
|
||||
_currentKey.value = all.keys.first;
|
||||
if (character.key == _currentKey) {
|
||||
_currentKey = all.keys.first;
|
||||
}
|
||||
debugPrint('Deleted char: ${character.key} (${character.displayName})');
|
||||
debugPrint(
|
||||
'[PROVIDER] Deleted char: ${character.key} (${character.displayName})');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateAll(Iterable<Character> chars) {
|
||||
@@ -182,18 +202,19 @@ class CharacterService extends GetxService
|
||||
}
|
||||
|
||||
void _clearCharListener() {
|
||||
debugPrint('clearing char listener');
|
||||
debugPrint('[PROVIDER] clearing char listener');
|
||||
_sub?.cancel();
|
||||
_sub = null;
|
||||
}
|
||||
}
|
||||
|
||||
mixin CharacterServiceMixin {
|
||||
CharacterService get characterService => Get.find();
|
||||
CharacterService get charService => characterService;
|
||||
mixin CharacterProviderMixin {
|
||||
CharacterProvider get characterProvider =>
|
||||
CharacterProvider.of(appGlobalKey.currentContext!);
|
||||
CharacterProvider get charProvider => characterProvider;
|
||||
|
||||
Character get character => characterService.current;
|
||||
Character? get maybeCharacter => characterService.maybeCurrent;
|
||||
Character get character => characterProvider.current;
|
||||
Character? get maybeCharacter => characterProvider.maybeCurrent;
|
||||
Character get char => character;
|
||||
Character? get maybeChar => maybeCharacter;
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../i18n/messages.i18n.dart';
|
||||
|
||||
class IntlService extends GetxService {
|
||||
class IntlService extends ChangeNotifier {
|
||||
static final Map<Locale, Messages> _m = {};
|
||||
static late Locale _locale;
|
||||
static Locale _locale = const Locale('en');
|
||||
static Locale get locale => _locale;
|
||||
|
||||
static Messages get m => _m[Get.locale] ?? _loadMessages(_locale);
|
||||
static Messages get m => _m[locale] ?? _loadMessages(_locale);
|
||||
List<Locale> get supportedLocales => _m.keys.toList();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadMessages(Get.deviceLocale ?? const Locale('en'));
|
||||
IntlService() {
|
||||
_loadMessages(locale);
|
||||
}
|
||||
|
||||
static void changeLocale(Locale locale) {
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/models/meta.dart';
|
||||
import 'package:dungeon_paper/app/data/models/user.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/character_utils.dart';
|
||||
import 'package:dungeon_paper/core/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/storage_handler/storage_handler.dart';
|
||||
import 'package:dungeon_paper/core/utils/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LibraryService extends GetxService {
|
||||
import 'character_provider.dart';
|
||||
import 'user_provider.dart';
|
||||
|
||||
class LibraryProvider extends ChangeNotifier
|
||||
with CharacterProviderMixin, UserProviderMixin {
|
||||
StorageHandler get storage => StorageHandler.instance;
|
||||
CharacterService get chars => Get.find();
|
||||
User get user => Get.find<UserService>().current;
|
||||
|
||||
static LibraryProvider of(BuildContext context, {bool listen = false}) =>
|
||||
Provider.of<LibraryProvider>(context, listen: listen);
|
||||
|
||||
static Widget consumer(
|
||||
Widget Function(
|
||||
BuildContext context, LibraryProvider library, Widget? child)
|
||||
builder) =>
|
||||
Consumer<LibraryProvider>(builder: builder);
|
||||
|
||||
Future<bool> existsInLibrary<T extends WithMeta>(T item) async {
|
||||
final res = await storage.getDocument(item.storageKey, item.key);
|
||||
@@ -46,6 +55,7 @@ class LibraryService extends GetxService {
|
||||
// items = items.map((e) => (e.meta.createdBy == user.username)
|
||||
// ? increaseMetaVersion(e)
|
||||
// : forkMeta(e, user));
|
||||
|
||||
items = items.map(
|
||||
(e) {
|
||||
debugPrint('forking $e: $forkBehavior');
|
||||
@@ -59,17 +69,21 @@ class LibraryService extends GetxService {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
var m = items.elementAt(0).meta;
|
||||
debugPrint('upserting meta ${m.toJson()}');
|
||||
chars.updateCharacter(
|
||||
CharacterUtils.upsertByType<T>(char ?? chars.current, items),
|
||||
charProvider.updateCharacter(
|
||||
CharacterUtils.upsertByType<T>(char ?? charProvider.current, items),
|
||||
);
|
||||
}
|
||||
|
||||
void removeFromCharacter<T extends WithMeta>(Iterable<T> items,
|
||||
[Character? char]) async {
|
||||
chars.updateCharacter(
|
||||
CharacterUtils.removeByType<T>(char ?? chars.current, items),
|
||||
void removeFromCharacter<T extends WithMeta>(
|
||||
BuildContext context,
|
||||
Iterable<T> items, [
|
||||
Character? char,
|
||||
]) async {
|
||||
charProvider.updateCharacter(
|
||||
CharacterUtils.removeByType<T>(char ?? charProvider.current, items),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,6 +95,7 @@ enum ForkBehavior {
|
||||
both,
|
||||
}
|
||||
|
||||
mixin LibraryServiceMixin {
|
||||
LibraryService get library => Get.find();
|
||||
mixin LibraryProviderMixin {
|
||||
LibraryProvider get libraryProvider =>
|
||||
LibraryProvider.of(appGlobalKey.currentContext!);
|
||||
}
|
||||
62
lib/app/data/services/loading_provider.dart
Normal file
62
lib/app/data/services/loading_provider.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:dungeon_paper/core/global_keys.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
enum LoadKey {
|
||||
user,
|
||||
characters,
|
||||
repo,
|
||||
library,
|
||||
afterFirstLoad,
|
||||
}
|
||||
|
||||
class LoadingProvider extends ChangeNotifier {
|
||||
static LoadingProvider of(BuildContext context, {bool listen = false}) =>
|
||||
Provider.of<LoadingProvider>(context, listen: listen);
|
||||
|
||||
static Widget consumer(
|
||||
Widget Function(BuildContext, LoadingProvider, Widget?) builder) =>
|
||||
Consumer<LoadingProvider>(builder: builder);
|
||||
final _map = <LoadKey, bool>{
|
||||
LoadKey.user: true,
|
||||
LoadKey.characters: false,
|
||||
LoadKey.repo: true,
|
||||
LoadKey.library: true,
|
||||
LoadKey.afterFirstLoad: false,
|
||||
};
|
||||
|
||||
bool get loadingUser => _map[LoadKey.user] == true;
|
||||
set loadingUser(bool value) {
|
||||
_map[LoadKey.user] = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get loadingCharacters => _map[LoadKey.characters] == true;
|
||||
set loadingCharacters(bool value) {
|
||||
_map[LoadKey.characters] = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get loadingRepo => _map[LoadKey.repo] == true;
|
||||
set loadingRepo(bool value) {
|
||||
_map[LoadKey.repo] = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get loadingLibrary => _map[LoadKey.library] == true;
|
||||
set loadingLibrary(bool value) {
|
||||
_map[LoadKey.library] = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool get afterFirstLoad => _map[LoadKey.afterFirstLoad] == true;
|
||||
set afterFirstLoad(bool value) {
|
||||
_map[LoadKey.afterFirstLoad] = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
mixin LoadingProviderMixin {
|
||||
LoadingProvider get loadingProvider =>
|
||||
LoadingProvider.of(appGlobalKey.currentContext!);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
enum LoadKey {
|
||||
user,
|
||||
characters,
|
||||
repo,
|
||||
library,
|
||||
afterFirstLoad,
|
||||
}
|
||||
|
||||
class LoadingService extends GetxService {
|
||||
final _map = <LoadKey, bool>{
|
||||
LoadKey.user: true,
|
||||
LoadKey.characters: false,
|
||||
LoadKey.repo: true,
|
||||
LoadKey.library: true,
|
||||
LoadKey.afterFirstLoad: false,
|
||||
}.obs;
|
||||
|
||||
bool get loadingUser => _map[LoadKey.user] == true;
|
||||
set loadingUser(bool value) => _map[LoadKey.user] = value;
|
||||
|
||||
bool get loadingCharacters => _map[LoadKey.characters] == true;
|
||||
set loadingCharacters(bool value) => _map[LoadKey.characters] = value;
|
||||
|
||||
bool get loadingRepo => _map[LoadKey.repo] == true;
|
||||
set loadingRepo(bool value) => _map[LoadKey.repo] = value;
|
||||
|
||||
bool get loadingLibrary => _map[LoadKey.library] == true;
|
||||
set loadingLibrary(bool value) => _map[LoadKey.library] = value;
|
||||
|
||||
bool get afterFirstLoad => _map[LoadKey.afterFirstLoad] == true;
|
||||
set afterFirstLoad(bool value) => _map[LoadKey.afterFirstLoad] = value;
|
||||
}
|
||||
|
||||
mixin LoadingServiceMixin {
|
||||
LoadingService get loadingService => Get.find();
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
@@ -10,6 +8,7 @@ import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/note.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/core/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/http/api.dart';
|
||||
import 'package:dungeon_paper/core/http/api_requests/search.dart';
|
||||
import 'package:dungeon_paper/core/storage_handler/storage_handler.dart';
|
||||
@@ -17,22 +16,41 @@ import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class RepositoryService extends GetxService {
|
||||
class RepositoryProvider extends ChangeNotifier {
|
||||
final builtIn = BuiltInRepository(id: 'playbook');
|
||||
final my = PersonalRepository(id: 'personal');
|
||||
|
||||
static RepositoryProvider of(BuildContext context) =>
|
||||
Provider.of<RepositoryProvider>(context, listen: false);
|
||||
|
||||
static Widget consumer(
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
RepositoryProvider repo,
|
||||
Widget? child,
|
||||
) builder,
|
||||
) =>
|
||||
Consumer<RepositoryProvider>(builder: builder);
|
||||
StorageDelegate get storage => StorageHandler.instance;
|
||||
|
||||
RepositoryProvider() {
|
||||
// loadAllData();
|
||||
builtIn.addListener(notifyListeners);
|
||||
my.addListener(notifyListeners);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
builtIn._clearValues();
|
||||
my._clearValues();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() async {
|
||||
super.onClose();
|
||||
void dispose() async {
|
||||
super.dispose();
|
||||
builtIn.removeListener(notifyListeners);
|
||||
my.removeListener(notifyListeners);
|
||||
await Future.wait([builtIn.dispose(), my.dispose()]);
|
||||
}
|
||||
|
||||
@@ -63,21 +81,21 @@ enum RepositoryStatus {
|
||||
error,
|
||||
}
|
||||
|
||||
abstract class RepositoryCache {
|
||||
abstract class RepositoryCache extends ChangeNotifier {
|
||||
RepositoryCache({required this.id});
|
||||
|
||||
String? get cachePrefix;
|
||||
final String id;
|
||||
abstract final RemoteBehavior loadRemote;
|
||||
|
||||
final classes = <String, CharacterClass>{}.obs;
|
||||
final items = <String, Item>{}.obs;
|
||||
final monsters = <String, Monster>{}.obs;
|
||||
final moves = <String, Move>{}.obs;
|
||||
final races = <String, Race>{}.obs;
|
||||
final spells = <String, Spell>{}.obs;
|
||||
final tags = <String, dw.Tag>{}.obs;
|
||||
final notes = <String, Note>{}.obs;
|
||||
var classes = <String, CharacterClass>{};
|
||||
var items = <String, Item>{};
|
||||
var monsters = <String, Monster>{};
|
||||
var moves = <String, Move>{};
|
||||
var races = <String, Race>{};
|
||||
var spells = <String, Spell>{};
|
||||
var tags = <String, dw.Tag>{};
|
||||
var notes = <String, Note>{};
|
||||
|
||||
final subs = <StreamSubscription>[];
|
||||
|
||||
@@ -115,8 +133,8 @@ abstract class RepositoryCache {
|
||||
try {
|
||||
resp = await getFromRemote;
|
||||
await setAllFrom(resp, saveIntoCache: true);
|
||||
} catch (e) {
|
||||
debugPrint('[$id] Error loading from remote: $e');
|
||||
} catch (e, stack) {
|
||||
debugPrint('[$id] Error loading from remote: $e\n$stack');
|
||||
resp = SearchResponse.empty();
|
||||
}
|
||||
} else {
|
||||
@@ -183,7 +201,7 @@ abstract class RepositoryCache {
|
||||
_parseMap<CharacterClass>(
|
||||
d,
|
||||
key: (x) => x.key,
|
||||
save: (x) => classes.value = x,
|
||||
save: (x) => classes = x,
|
||||
parse: (x) => CharacterClass.fromJson(x),
|
||||
);
|
||||
},
|
||||
@@ -194,7 +212,7 @@ abstract class RepositoryCache {
|
||||
_parseMap<Item>(
|
||||
d,
|
||||
key: (x) => x.key,
|
||||
save: (x) => items.value = x,
|
||||
save: (x) => items = x,
|
||||
parse: (x) => Item.fromJson(x),
|
||||
);
|
||||
},
|
||||
@@ -205,7 +223,7 @@ abstract class RepositoryCache {
|
||||
_parseMap<Monster>(
|
||||
d,
|
||||
key: (x) => x.key,
|
||||
save: (x) => monsters.value = x,
|
||||
save: (x) => monsters = x,
|
||||
parse: (x) => Monster.fromJson(x),
|
||||
);
|
||||
},
|
||||
@@ -216,7 +234,7 @@ abstract class RepositoryCache {
|
||||
_parseMap<Move>(
|
||||
d,
|
||||
key: (x) => x.key,
|
||||
save: (x) => moves.value = x,
|
||||
save: (x) => moves = x,
|
||||
parse: (x) => Move.fromJson(x),
|
||||
);
|
||||
},
|
||||
@@ -227,7 +245,7 @@ abstract class RepositoryCache {
|
||||
_parseMap<Race>(
|
||||
d,
|
||||
key: (x) => x.key,
|
||||
save: (x) => races.value = x,
|
||||
save: (x) => races = x,
|
||||
parse: (x) => Race.fromJson(x),
|
||||
);
|
||||
},
|
||||
@@ -238,7 +256,7 @@ abstract class RepositoryCache {
|
||||
_parseMap<Spell>(
|
||||
d,
|
||||
key: (x) => x.key,
|
||||
save: (x) => spells.value = x,
|
||||
save: (x) => spells = x,
|
||||
parse: (x) => Spell.fromJson(x),
|
||||
);
|
||||
},
|
||||
@@ -249,7 +267,7 @@ abstract class RepositoryCache {
|
||||
_parseMap<dw.Tag>(
|
||||
d,
|
||||
key: (x) => x.name,
|
||||
save: (x) => tags.value = x,
|
||||
save: (x) => tags = x,
|
||||
parse: (x) => dw.Tag.fromJson(x),
|
||||
);
|
||||
},
|
||||
@@ -260,7 +278,7 @@ abstract class RepositoryCache {
|
||||
_parseMap<Note>(
|
||||
d,
|
||||
key: (x) => x.key,
|
||||
save: (x) => notes.value = x,
|
||||
save: (x) => notes = x,
|
||||
parse: (x) => Note.fromJson(x),
|
||||
);
|
||||
},
|
||||
@@ -300,7 +318,9 @@ abstract class RepositoryCache {
|
||||
subs.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
super.dispose();
|
||||
clearListeners();
|
||||
_clearValues();
|
||||
await cache.clear();
|
||||
@@ -345,34 +365,34 @@ abstract class RepositoryCache {
|
||||
tags.clear();
|
||||
}
|
||||
|
||||
RxMap<String, T> listByType<T>([Type? type]) {
|
||||
Map<String, T> listByType<T>([Type? type]) {
|
||||
assert(T != dynamic || type != null);
|
||||
final t = T != dynamic ? T : type;
|
||||
|
||||
switch (t) {
|
||||
case == CharacterClass:
|
||||
return classes as RxMap<String, T>;
|
||||
return classes as Map<String, T>;
|
||||
case == Item:
|
||||
return items as RxMap<String, T>;
|
||||
return items as Map<String, T>;
|
||||
case == Monster:
|
||||
return monsters as RxMap<String, T>;
|
||||
return monsters as Map<String, T>;
|
||||
case == Move:
|
||||
return moves as RxMap<String, T>;
|
||||
return moves as Map<String, T>;
|
||||
case == Race:
|
||||
return races as RxMap<String, T>;
|
||||
return races as Map<String, T>;
|
||||
case == Spell:
|
||||
return spells as RxMap<String, T>;
|
||||
return spells as Map<String, T>;
|
||||
case == Note:
|
||||
return notes as RxMap<String, T>;
|
||||
return notes as Map<String, T>;
|
||||
case == dw.Tag:
|
||||
return tags as RxMap<String, T>;
|
||||
return tags as Map<String, T>;
|
||||
}
|
||||
throw TypeError();
|
||||
}
|
||||
|
||||
Future<void> updateList<T>(
|
||||
String collectionName,
|
||||
RxMap<String, T> list,
|
||||
Map<String, T> list,
|
||||
Iterable<T>? resp, {
|
||||
required bool saveIntoCache,
|
||||
}) async {
|
||||
@@ -383,8 +403,10 @@ abstract class RepositoryCache {
|
||||
list.addAll(Map.fromIterable(resp, key: (x) => x.key));
|
||||
|
||||
if (saveIntoCache && list.isNotEmpty) {
|
||||
for (final x in list.values)
|
||||
for (final x in list.values) {
|
||||
await cache.create(collectionName, Meta.keyFor(x), Meta.toJsonFor(x));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,7 +473,9 @@ class PersonalRepository extends RepositoryCache {
|
||||
RemoteBehavior get loadRemote => RemoteBehavior.always;
|
||||
}
|
||||
|
||||
mixin RepositoryServiceMixin {
|
||||
RepositoryService get repository => Get.find();
|
||||
RepositoryService get repo => repository;
|
||||
mixin RepositoryProviderMixin {
|
||||
RepositoryProvider get repo =>
|
||||
RepositoryProvider.of(appGlobalKey.currentContext!);
|
||||
RepositoryProvider get repository => repo;
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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';
|
||||
import 'user_service.dart';
|
||||
import 'auth_service.dart';
|
||||
|
||||
Future<void> initServices() async {
|
||||
debugPrint('Starting services...');
|
||||
|
||||
/// 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()));
|
||||
await Get.putAsync(() => Future.value(UserService()));
|
||||
await Get.putAsync(() => Future.value(AuthService()));
|
||||
await Get.putAsync(() => Future.value(CharacterService()));
|
||||
debugPrint('All services started');
|
||||
}
|
||||
@@ -2,12 +2,13 @@ import 'dart:async';
|
||||
|
||||
import 'package:dungeon_paper/app/data/models/user.dart';
|
||||
import 'package:dungeon_paper/app/data/models/user_settings.dart';
|
||||
import 'package:dungeon_paper/app/data/services/auth_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/loading_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/auth_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/loading_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_provider.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/core/global_keys.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';
|
||||
@@ -15,30 +16,37 @@ 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';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../core/utils/secrets_base.dart';
|
||||
|
||||
class UserService extends GetxService
|
||||
with
|
||||
RepositoryServiceMixin,
|
||||
AuthServiceMixin,
|
||||
CharacterServiceMixin,
|
||||
LoadingServiceMixin {
|
||||
final _current = User.guest().obs;
|
||||
class UserProvider extends ChangeNotifier with RepositoryProviderMixin {
|
||||
var _current = User.guest();
|
||||
|
||||
User get current => _current.value;
|
||||
User get current => _current;
|
||||
StreamSubscription? _userDataSub;
|
||||
|
||||
UserProvider() {
|
||||
// loadBuiltInRepo();
|
||||
}
|
||||
|
||||
static UserProvider of(BuildContext context) =>
|
||||
Provider.of<UserProvider>(context, listen: false);
|
||||
|
||||
static consumer(BuildContext context, Widget Function(User) builder) =>
|
||||
Consumer<UserProvider>(
|
||||
builder: (context, provider, _) => builder(provider.current),
|
||||
);
|
||||
|
||||
Future<void> loadBuiltInRepo({bool ignoreCache = false}) async {
|
||||
await repo.builtIn.dispose();
|
||||
// await repo.builtIn.dispose();
|
||||
return repo.builtIn.init(ignoreCache: ignoreCache);
|
||||
}
|
||||
|
||||
Future<void> loadMyRepo({bool ignoreCache = false}) async {
|
||||
await repo.my.dispose();
|
||||
// await repo.my.dispose();
|
||||
return repo.my.init(ignoreCache: ignoreCache);
|
||||
}
|
||||
|
||||
@@ -54,14 +62,20 @@ class UserService extends GetxService
|
||||
.getDocument('Data', email!);
|
||||
await _setUserAfterMigration(user, dbUser);
|
||||
_registerUserListener();
|
||||
charService.registerCharacterListener();
|
||||
final charProvider = Provider.of<CharacterProvider>(
|
||||
appGlobalKey.currentContext!,
|
||||
listen: false);
|
||||
charProvider.registerCharacterListener();
|
||||
|
||||
if (shouldLoadRepo) {
|
||||
loadMyRepo();
|
||||
}
|
||||
|
||||
loadingService.loadingUser = false;
|
||||
loadingService.afterFirstLoad = !loadingService.loadingCharacters;
|
||||
final loadingProvider = Provider.of<LoadingProvider>(
|
||||
appGlobalKey.currentContext!,
|
||||
listen: false);
|
||||
loadingProvider.loadingUser = false;
|
||||
loadingProvider.afterFirstLoad = !loadingProvider.loadingCharacters;
|
||||
}
|
||||
|
||||
bool get isGuest => current.isGuest;
|
||||
@@ -71,28 +85,39 @@ class UserService extends GetxService
|
||||
_clearUserListener();
|
||||
StorageHandler.instance.currentDelegate = 'local';
|
||||
StorageHandler.instance.setCollectionPrefix(null);
|
||||
notifyListeners();
|
||||
await loadMyRepo(ignoreCache: true);
|
||||
charService.registerCharacterListener();
|
||||
loadingService.loadingUser = false;
|
||||
loadingService.afterFirstLoad = !loadingService.loadingCharacters;
|
||||
final charProvider = Provider.of<CharacterProvider>(
|
||||
appGlobalKey.currentContext!,
|
||||
listen: false);
|
||||
final loadingProvider = Provider.of<LoadingProvider>(
|
||||
appGlobalKey.currentContext!,
|
||||
listen: false);
|
||||
charProvider.registerCharacterListener();
|
||||
loadingProvider.loadingUser = false;
|
||||
loadingProvider.afterFirstLoad = !loadingProvider.loadingCharacters;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void logout() async {
|
||||
_clearUserListener();
|
||||
charService.clear();
|
||||
_current.value = User.guest();
|
||||
await authService.logout();
|
||||
final charProvider = Provider.of<CharacterProvider>(
|
||||
appGlobalKey.currentContext!,
|
||||
listen: false);
|
||||
charProvider.clear();
|
||||
_current = User.guest();
|
||||
final context = appGlobalKey.currentContext!;
|
||||
final authService = Provider.of<AuthProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
await authService.logout(context);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadBuiltInRepo();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
super.onClose();
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_userDataSub?.cancel();
|
||||
}
|
||||
|
||||
@@ -106,7 +131,8 @@ class UserService extends GetxService
|
||||
}
|
||||
|
||||
Future<User?> _migrateUser(fba.User user) async {
|
||||
final migrationDetails = await Get.toNamed(
|
||||
final context = appGlobalKey.currentContext!;
|
||||
final migrationDetails = await Navigator.of(context).pushNamed(
|
||||
Routes.migration,
|
||||
arguments: MigrationArguments(email: user.email ?? ''),
|
||||
) as MigrationDetails?;
|
||||
@@ -142,7 +168,8 @@ class UserService extends GetxService
|
||||
return;
|
||||
}
|
||||
final user = User.fromJson(data);
|
||||
_current.value = user;
|
||||
_current = user;
|
||||
notifyListeners();
|
||||
user.applySettings();
|
||||
_initUserExternal(user);
|
||||
}
|
||||
@@ -150,6 +177,10 @@ class UserService extends GetxService
|
||||
void _initUserExternal(User user) async {
|
||||
final pkg = await PackageInfo.fromPlatform();
|
||||
if (secrets.sentryDsn.isNotEmpty) {
|
||||
final authService = Provider.of<AuthProvider>(
|
||||
appGlobalKey.currentContext!,
|
||||
listen: false,
|
||||
);
|
||||
Sentry.configureScope(
|
||||
(scope) => scope.setUser(
|
||||
SentryUser(
|
||||
@@ -170,17 +201,24 @@ class UserService extends GetxService
|
||||
final needsMigration = dbUser == null;
|
||||
|
||||
if (needsMigration) {
|
||||
final context = appGlobalKey.currentContext!;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final loadingProvider =
|
||||
Provider.of<LoadingProvider>(context, listen: false);
|
||||
final resp = await _migrateUser(user);
|
||||
if (resp == null) {
|
||||
Get.rawSnackbar(title: tr.errors.userOperationCanceled);
|
||||
loadingService.loadingUser = false;
|
||||
loadingService.afterFirstLoad = !loadingService.loadingCharacters;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(tr.errors.userOperationCanceled)));
|
||||
|
||||
loadingProvider.loadingUser = false;
|
||||
loadingProvider.afterFirstLoad = !loadingProvider.loadingCharacters;
|
||||
return;
|
||||
}
|
||||
_current.value = resp;
|
||||
_current = resp;
|
||||
} else {
|
||||
_current.value = User.fromJson(dbUser);
|
||||
_current = User.fromJson(dbUser);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updateEmail(String email) async {
|
||||
@@ -189,6 +227,10 @@ class UserService extends GetxService
|
||||
}
|
||||
assert(EmailValidator.validate(email));
|
||||
final updatedUser = current.copyWith(email: email);
|
||||
final authService = Provider.of<AuthProvider>(
|
||||
appGlobalKey.currentContext!,
|
||||
listen: false,
|
||||
);
|
||||
await authService.fbUser!.updateEmail(email);
|
||||
await updateUser(updatedUser);
|
||||
loadUserData(fba.FirebaseAuth.instance.currentUser!);
|
||||
@@ -201,7 +243,8 @@ class UserService extends GetxService
|
||||
}
|
||||
}
|
||||
|
||||
mixin UserServiceMixin {
|
||||
UserService get userService => Get.find();
|
||||
User get user => userService.current;
|
||||
mixin UserProviderMixin {
|
||||
UserProvider get userProvider =>
|
||||
UserProvider.of(appGlobalKey.currentContext!);
|
||||
User get user => userProvider.current;
|
||||
}
|
||||
@@ -2,13 +2,12 @@ import 'package:dungeon_paper/app/routes/app_pages.dart';
|
||||
import 'package:dungeon_paper/core/dw_icons.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class DiceUtils {
|
||||
static Widget iconOf(dw.Dice? tag) => const Icon(DwIcons.dice_d6);
|
||||
|
||||
static void openRollDialog(List<dw.Dice> dice) {
|
||||
Get.toNamed(Routes.rollDice, arguments: dice);
|
||||
static void openRollDialog(BuildContext context, List<dw.Dice> dice) {
|
||||
Navigator.of(context).pushNamed(Routes.rollDice, arguments: dice);
|
||||
}
|
||||
|
||||
static Offset iconCenterOffset(dw.Dice dice) =>
|
||||
|
||||
@@ -7,8 +7,8 @@ import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/models/ability_scores.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/library_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/library_provider.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/character_classes_library_list_view.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/items_library_list_view.dart';
|
||||
@@ -23,16 +23,15 @@ import 'package:dungeon_paper/app/widgets/forms/move_form.dart';
|
||||
import 'package:dungeon_paper/app/widgets/forms/note_form.dart';
|
||||
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/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/utils/enums.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ModelPages {
|
||||
static CharacterService get controller => Get.find();
|
||||
static LibraryService get library => Get.find();
|
||||
|
||||
static void openLibraryList<T extends WithMeta>({
|
||||
class ModelPages with LibraryProviderMixin, CharacterProviderMixin {
|
||||
static void openLibraryList<T extends WithMeta>(
|
||||
BuildContext context, {
|
||||
Character? character,
|
||||
void Function(Iterable<T> list)? onSelected,
|
||||
Iterable<T>? preSelections,
|
||||
@@ -44,6 +43,7 @@ class ModelPages {
|
||||
}) {
|
||||
final map = <Type, Function()>{
|
||||
Move: () => openMovesList(
|
||||
context,
|
||||
character: character,
|
||||
onSelected: onSelected as void Function(Iterable<Move>)?,
|
||||
preSelections: preSelections as Iterable<Move>?,
|
||||
@@ -53,6 +53,7 @@ class ModelPages {
|
||||
initialTab: initialTab,
|
||||
),
|
||||
Spell: () => openSpellsList(
|
||||
context,
|
||||
character: character,
|
||||
onSelected: onSelected as void Function(Iterable<Spell>)?,
|
||||
preSelections: preSelections as Iterable<Spell>?,
|
||||
@@ -61,12 +62,14 @@ class ModelPages {
|
||||
initialTab: initialTab,
|
||||
),
|
||||
Item: () => openItemsList(
|
||||
context,
|
||||
character: character,
|
||||
onSelected: onSelected as void Function(Iterable<Item>)?,
|
||||
preSelections: preSelections as Iterable<Item>?,
|
||||
initialTab: initialTab,
|
||||
),
|
||||
CharacterClass: () => openCharacterClassesList(
|
||||
context,
|
||||
character: character,
|
||||
onSelected: onSelected != null
|
||||
? (x) => onSelected.call(asList<T>(x))
|
||||
@@ -75,6 +78,7 @@ class ModelPages {
|
||||
initialTab: initialTab,
|
||||
),
|
||||
Race: () => openRacesList(
|
||||
context,
|
||||
character: character,
|
||||
onSelected: onSelected != null
|
||||
? (x) => onSelected.call(asList<T>(x))
|
||||
@@ -93,7 +97,8 @@ class ModelPages {
|
||||
map[t]!.call();
|
||||
}
|
||||
|
||||
static void openMovesList({
|
||||
static void openMovesList(
|
||||
BuildContext context, {
|
||||
Character? character,
|
||||
Iterable<Move>? preSelections,
|
||||
MoveCategory? category,
|
||||
@@ -103,7 +108,7 @@ class ModelPages {
|
||||
List<dw.EntityReference>? classKeys,
|
||||
}) {
|
||||
final char = character;
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.moves,
|
||||
arguments: MoveLibraryListArguments(
|
||||
initialTab: initialTab,
|
||||
@@ -117,14 +122,15 @@ class ModelPages {
|
||||
);
|
||||
}
|
||||
|
||||
static void openRacesList({
|
||||
static void openRacesList(
|
||||
BuildContext context, {
|
||||
Character? character,
|
||||
Race? preSelection,
|
||||
void Function(Race race)? onSelected,
|
||||
FiltersGroup? initialTab,
|
||||
}) {
|
||||
final char = character;
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.races,
|
||||
arguments: RaceLibraryListArguments(
|
||||
initialTab: initialTab,
|
||||
@@ -135,12 +141,13 @@ class ModelPages {
|
||||
);
|
||||
}
|
||||
|
||||
static void openMovePage({
|
||||
static void openMovePage(
|
||||
BuildContext context, {
|
||||
required Move? move,
|
||||
required void Function(Move move) onSave,
|
||||
required AbilityScores abilityScores,
|
||||
}) =>
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.editMove,
|
||||
arguments: MoveFormArguments(
|
||||
entity: move,
|
||||
@@ -150,12 +157,13 @@ class ModelPages {
|
||||
),
|
||||
);
|
||||
|
||||
static void openRacePage({
|
||||
static void openRacePage(
|
||||
BuildContext context, {
|
||||
required Race? race,
|
||||
required void Function(Race race) onSave,
|
||||
required AbilityScores abilityScores,
|
||||
}) =>
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.editRace,
|
||||
arguments: RaceFormArguments(
|
||||
entity: race,
|
||||
@@ -165,7 +173,8 @@ class ModelPages {
|
||||
),
|
||||
);
|
||||
|
||||
static void openSpellsList({
|
||||
static void openSpellsList(
|
||||
BuildContext context, {
|
||||
Character? character,
|
||||
Iterable<Spell>? list,
|
||||
void Function(Iterable<Spell> list)? onSelected,
|
||||
@@ -175,7 +184,7 @@ class ModelPages {
|
||||
List<dw.EntityReference>? classKeys,
|
||||
}) {
|
||||
final char = character;
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.spells,
|
||||
arguments: SpellLibraryListArguments(
|
||||
initialTab: initialTab,
|
||||
@@ -188,13 +197,14 @@ class ModelPages {
|
||||
);
|
||||
}
|
||||
|
||||
static void openSpellPage({
|
||||
static void openSpellPage(
|
||||
BuildContext context, {
|
||||
required Spell? spell,
|
||||
required void Function(Spell spell) onSave,
|
||||
required AbilityScores abilityScores,
|
||||
required List<dw.EntityReference> classKeys,
|
||||
}) =>
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.editSpell,
|
||||
arguments: SpellFormArguments(
|
||||
entity: spell,
|
||||
@@ -204,7 +214,8 @@ class ModelPages {
|
||||
),
|
||||
);
|
||||
|
||||
static void openItemsList({
|
||||
static void openItemsList(
|
||||
BuildContext context, {
|
||||
Character? character,
|
||||
Iterable<Item>? list,
|
||||
void Function(Iterable<Item> list)? onSelected,
|
||||
@@ -212,7 +223,7 @@ class ModelPages {
|
||||
Iterable<Item>? preSelections,
|
||||
}) {
|
||||
final char = character;
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.items,
|
||||
arguments: ItemLibraryListArguments(
|
||||
initialTab: initialTab,
|
||||
@@ -222,11 +233,12 @@ class ModelPages {
|
||||
);
|
||||
}
|
||||
|
||||
static void openItemPage({
|
||||
static void openItemPage(
|
||||
BuildContext context, {
|
||||
required Item? item,
|
||||
required void Function(Item item) onSave,
|
||||
}) =>
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.editItem,
|
||||
arguments: ItemFormArgumentsNew(
|
||||
entity: item,
|
||||
@@ -235,7 +247,8 @@ class ModelPages {
|
||||
),
|
||||
);
|
||||
|
||||
static void openNotesList({
|
||||
static void openNotesList(
|
||||
BuildContext context, {
|
||||
Character? character,
|
||||
Iterable<Note>? list,
|
||||
void Function(Iterable<Note> list)? onSelected,
|
||||
@@ -243,7 +256,7 @@ class ModelPages {
|
||||
Iterable<Note>? preSelections,
|
||||
}) {
|
||||
final char = character;
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.notes,
|
||||
arguments: NoteLibraryListArguments(
|
||||
initialTab: initialTab,
|
||||
@@ -253,11 +266,12 @@ class ModelPages {
|
||||
);
|
||||
}
|
||||
|
||||
static void openNotePage({
|
||||
static void openNotePage(
|
||||
BuildContext context, {
|
||||
required Note? note,
|
||||
required void Function(Note note) onSave,
|
||||
}) =>
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.editNote,
|
||||
arguments: NoteFormArguments(
|
||||
entity: note,
|
||||
@@ -266,20 +280,23 @@ class ModelPages {
|
||||
),
|
||||
);
|
||||
|
||||
static void openCharacterClassesList({
|
||||
static void openCharacterClassesList(
|
||||
BuildContext context, {
|
||||
Character? character,
|
||||
CharacterClass? preSelection,
|
||||
void Function(CharacterClass cls)? onSelected,
|
||||
FiltersGroup? initialTab,
|
||||
}) {
|
||||
final char = character;
|
||||
Get.toNamed(
|
||||
final charProvider = CharacterProvider.of(appGlobalKey.currentContext!);
|
||||
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.classes,
|
||||
arguments: CharacterClassLibraryListArguments(
|
||||
initialTab: initialTab,
|
||||
onSelected: onSelected ??
|
||||
(char != null
|
||||
? (cls) => controller
|
||||
? (cls) => charProvider
|
||||
.updateCharacter(char.copyWith(characterClass: cls))
|
||||
: null),
|
||||
preSelections: asList(preSelection ?? char?.characterClass),
|
||||
@@ -287,11 +304,12 @@ class ModelPages {
|
||||
);
|
||||
}
|
||||
|
||||
static void openCharacterClassPage({
|
||||
static void openCharacterClassPage(
|
||||
BuildContext context, {
|
||||
required CharacterClass? characterClass,
|
||||
required void Function(CharacterClass item) onSave,
|
||||
}) =>
|
||||
Get.toNamed(
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.editClass,
|
||||
arguments: CharacterClassFormArguments(
|
||||
entity: characterClass,
|
||||
|
||||
@@ -9,7 +9,7 @@ class TagUtils {
|
||||
.toList();
|
||||
|
||||
static Widget iconOf(dw.Tag tag) => Transform.rotate(
|
||||
child: const Icon(Icons.label),
|
||||
angle: degToRad(-45.0),
|
||||
child: const Icon(Icons.label),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/ability_score_form_controller.dart';
|
||||
|
||||
class AbilityScoreFormBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<AbilityScoreFormController>(
|
||||
() => AbilityScoreFormController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +1,60 @@
|
||||
import 'package:dungeon_paper/app/data/models/ability_scores.dart';
|
||||
import 'package:dungeon_paper/core/route_arguments.dart';
|
||||
import 'package:dungeon_paper/core/utils/enums.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_validator.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AbilityScoreFormController extends GetxController {
|
||||
final entity = AbilityScore.empty().obs;
|
||||
class AbilityScoreFormController extends ChangeNotifier {
|
||||
late final AbilityScore entity;
|
||||
|
||||
late final Rx<TextEditingController> _key = TextEditingController().obs;
|
||||
TextEditingController get key => _key.value;
|
||||
late final TextEditingController _key;
|
||||
TextEditingController get key => _key;
|
||||
|
||||
late final Rx<TextEditingController> _name = TextEditingController().obs;
|
||||
TextEditingController get name => _name.value;
|
||||
late final TextEditingController _name;
|
||||
TextEditingController get name => _name;
|
||||
|
||||
late final Rx<TextEditingController> _description =
|
||||
TextEditingController().obs;
|
||||
TextEditingController get description => _description.value;
|
||||
late final TextEditingController _description;
|
||||
TextEditingController get description => _description;
|
||||
|
||||
late final Rx<TextEditingController> _debilityName =
|
||||
TextEditingController().obs;
|
||||
TextEditingController get debilityName => _debilityName.value;
|
||||
late final TextEditingController _debilityName;
|
||||
TextEditingController get debilityName => _debilityName;
|
||||
|
||||
late final Rx<TextEditingController> _debilityDescription =
|
||||
TextEditingController().obs;
|
||||
TextEditingController get debilityDescription => _debilityDescription.value;
|
||||
late final TextEditingController _debilityDescription;
|
||||
TextEditingController get debilityDescription => _debilityDescription;
|
||||
|
||||
late final Rx<IconData?> _icon = Rx(null);
|
||||
IconData? get icon => _icon.value;
|
||||
late final IconData? _icon;
|
||||
IconData? get icon => _icon;
|
||||
|
||||
late final void Function(AbilityScore abilityScore) onSave;
|
||||
|
||||
late final FormContext formContext;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
final AbilityScoreFormArguments args = Get.arguments;
|
||||
AbilityScoreFormController(BuildContext context) {
|
||||
final AbilityScoreFormArguments args = getArgs(context);
|
||||
formContext =
|
||||
args.abilityScore != null ? FormContext.edit : FormContext.create;
|
||||
if (args.abilityScore != null) {
|
||||
entity.value = args.abilityScore!;
|
||||
entity = 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);
|
||||
_icon.value = entity.value.customIcon;
|
||||
_key = TextEditingController(text: entity.key);
|
||||
_name = TextEditingController(text: entity.name);
|
||||
_description = TextEditingController(text: entity.description);
|
||||
_debilityName = TextEditingController(text: entity.debilityName);
|
||||
_debilityDescription =
|
||||
TextEditingController(text: entity.debilityDescription);
|
||||
_icon = entity.customIcon;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_key.value.dispose();
|
||||
_name.value.dispose();
|
||||
_description.value.dispose();
|
||||
_debilityName.value.dispose();
|
||||
_debilityDescription.value.dispose();
|
||||
_icon.close();
|
||||
super.onClose();
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_key.dispose();
|
||||
_name.dispose();
|
||||
_description.dispose();
|
||||
_debilityName.dispose();
|
||||
_debilityDescription.dispose();
|
||||
}
|
||||
|
||||
bool get isValid => [
|
||||
@@ -89,18 +78,9 @@ class AbilityScoreFormController extends GetxController {
|
||||
String? requiredValidator(String? value) =>
|
||||
StringValidator(minLength: 1).validator(value);
|
||||
|
||||
void _update() {
|
||||
_key.refresh();
|
||||
_name.refresh();
|
||||
_description.refresh();
|
||||
_debilityName.refresh();
|
||||
_debilityDescription.refresh();
|
||||
_icon.refresh();
|
||||
}
|
||||
|
||||
void save() {
|
||||
void save(BuildContext context) {
|
||||
onSave(
|
||||
entity.value.copyWith(
|
||||
entity.copyWith(
|
||||
key: key.text,
|
||||
name: name.text,
|
||||
description: description.text,
|
||||
@@ -109,7 +89,7 @@ class AbilityScoreFormController extends GetxController {
|
||||
icon: icon,
|
||||
),
|
||||
);
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,3 +102,4 @@ class AbilityScoreFormArguments {
|
||||
required this.onSave,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,105 +4,110 @@ import 'package:dungeon_paper/app/widgets/atoms/help_text.dart';
|
||||
import 'package:dungeon_paper/core/utils/enums.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controllers/ability_score_form_controller.dart';
|
||||
|
||||
class AbilityScoreFormView extends GetView<AbilityScoreFormController> {
|
||||
class AbilityScoreFormView extends StatelessWidget {
|
||||
const AbilityScoreFormView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const separator = SizedBox(height: 16);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
controller.formContext == FormContext.create
|
||||
? tr.generic.addEntity(tr.entity(tn(AbilityScore)))
|
||||
: tr.generic.editEntity(tr.entity(tn(AbilityScore))),
|
||||
title: Consumer<AbilityScoreFormController>(
|
||||
builder: (context, controller, _) => Text(
|
||||
controller.formContext == FormContext.create
|
||||
? tr.generic.addEntity(tr.entity(tn(AbilityScore)))
|
||||
: tr.generic.editEntity(tr.entity(tn(AbilityScore))),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: Obx(
|
||||
() => AdvancedFloatingActionButton.extended(
|
||||
onPressed: controller.isValid ? controller.save : null,
|
||||
floatingActionButton: Consumer<AbilityScoreFormController>(
|
||||
builder: (context, controller, _) =>
|
||||
AdvancedFloatingActionButton.extended(
|
||||
onPressed: controller.isValid ? () => controller.save(context) : null,
|
||||
label: Text(tr.generic.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
),
|
||||
body: Form(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16).copyWith(bottom: 80),
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: controller.key,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.key.label,
|
||||
child: Consumer<AbilityScoreFormController>(
|
||||
builder: (context, controller, _) => ListView(
|
||||
padding: const EdgeInsets.all(16).copyWith(bottom: 80),
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: controller.key,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.key.label,
|
||||
),
|
||||
validator: controller.keyValidator,
|
||||
),
|
||||
validator: controller.keyValidator,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: HelpText(text: tr.abilityScores.form.key.description),
|
||||
),
|
||||
separator,
|
||||
TextFormField(
|
||||
controller: controller.name,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.name.label,
|
||||
hintText: tr.abilityScores.form.name.description,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: HelpText(text: tr.abilityScores.form.key.description),
|
||||
),
|
||||
validator: controller.requiredValidator,
|
||||
),
|
||||
separator,
|
||||
TextFormField(
|
||||
controller: controller.description,
|
||||
minLines: 3,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.description.label,
|
||||
hintText: tr.abilityScores.form.description.description,
|
||||
separator,
|
||||
TextFormField(
|
||||
controller: controller.name,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.name.label,
|
||||
hintText: tr.abilityScores.form.name.description,
|
||||
),
|
||||
validator: controller.requiredValidator,
|
||||
),
|
||||
validator: controller.requiredValidator,
|
||||
),
|
||||
const Divider(height: 48),
|
||||
TextFormField(
|
||||
controller: controller.debilityName,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.debilityName.label,
|
||||
hintText: tr.abilityScores.form.debilityName.description,
|
||||
separator,
|
||||
TextFormField(
|
||||
controller: controller.description,
|
||||
minLines: 3,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.description.label,
|
||||
hintText: tr.abilityScores.form.description.description,
|
||||
),
|
||||
validator: controller.requiredValidator,
|
||||
),
|
||||
validator: controller.requiredValidator,
|
||||
),
|
||||
separator,
|
||||
TextFormField(
|
||||
controller: controller.debilityDescription,
|
||||
minLines: 3,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.debilityDescription.label,
|
||||
hintText: tr.abilityScores.form.debilityDescription.description,
|
||||
const Divider(height: 48),
|
||||
TextFormField(
|
||||
controller: controller.debilityName,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.debilityName.label,
|
||||
hintText: tr.abilityScores.form.debilityName.description,
|
||||
),
|
||||
validator: controller.requiredValidator,
|
||||
),
|
||||
validator: controller.requiredValidator,
|
||||
),
|
||||
separator,
|
||||
Text(tr.abilityScores.form.icon.label),
|
||||
separator,
|
||||
Obx(
|
||||
() => Align(
|
||||
separator,
|
||||
TextFormField(
|
||||
controller: controller.debilityDescription,
|
||||
minLines: 3,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: tr.abilityScores.form.debilityDescription.label,
|
||||
hintText:
|
||||
tr.abilityScores.form.debilityDescription.description,
|
||||
),
|
||||
validator: controller.requiredValidator,
|
||||
),
|
||||
separator,
|
||||
Text(tr.abilityScores.form.icon.label),
|
||||
separator,
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Icon(controller.icon ??
|
||||
AbilityScore.iconFor(controller.key.text)),
|
||||
),
|
||||
),
|
||||
separator,
|
||||
ElevatedButton(
|
||||
onPressed: () => controller.pickIcon(context),
|
||||
child: Text(
|
||||
tr.abilityScores.form.icon.button,
|
||||
separator,
|
||||
ElevatedButton(
|
||||
onPressed: () => controller.pickIcon(context),
|
||||
child: Text(
|
||||
tr.abilityScores.form.icon.button,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/ability_scores_form_controller.dart';
|
||||
|
||||
class AbilityScoresFormBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.put<AbilityScoresFormController>(
|
||||
AbilityScoresFormController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,25 @@
|
||||
import 'package:dungeon_paper/app/data/models/ability_scores.dart';
|
||||
import 'package:dungeon_paper/core/route_arguments.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AbilityScoresFormController extends GetxController {
|
||||
final dirty = false.obs;
|
||||
class AbilityScoresFormController extends ChangeNotifier {
|
||||
var dirty = false;
|
||||
|
||||
final Rx<AbilityScores> abilityScores = AbilityScores.dungeonWorld(
|
||||
dex: 10, str: 10, wis: 10, con: 10, intl: 10, cha: 10)
|
||||
.obs;
|
||||
AbilityScores abilityScores = AbilityScores.dungeonWorldAll(10);
|
||||
final textControllers = <String, TextEditingController>{};
|
||||
late final void Function(AbilityScores abilityScores) onChanged;
|
||||
|
||||
AbilityScoresFormController();
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
final AbilityScoresFormArguments args = Get.arguments;
|
||||
AbilityScoresFormController(BuildContext context) {
|
||||
final AbilityScoresFormArguments args = getArgs(context);
|
||||
if (args.abilityScores != null) {
|
||||
abilityScores.value = args.abilityScores!;
|
||||
abilityScores = args.abilityScores!;
|
||||
}
|
||||
for (final ctrl in textControllers.values) {
|
||||
ctrl.removeListener(validate);
|
||||
}
|
||||
textControllers.clear();
|
||||
for (final stat in abilityScores.value.stats) {
|
||||
for (final stat in abilityScores.stats) {
|
||||
textControllers[stat.key] =
|
||||
TextEditingController(text: stat.value.toString())
|
||||
..addListener(validate);
|
||||
@@ -34,16 +28,17 @@ class AbilityScoresFormController extends GetxController {
|
||||
}
|
||||
|
||||
void validate() {
|
||||
dirty.value = true;
|
||||
abilityScores.value = abilityScores.value.copyWithStatValues({
|
||||
for (final stat in abilityScores.value.stats)
|
||||
dirty = true;
|
||||
abilityScores = abilityScores.copyWithStatValues({
|
||||
for (final stat in abilityScores.stats)
|
||||
stat.key: int.tryParse(textControllers[stat.key]!.text) ?? stat.value
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateStat(AbilityScore stat) {
|
||||
abilityScores.value = abilityScores.value
|
||||
.copyWith(stats: updateByKey(abilityScores.value.stats, [stat]));
|
||||
abilityScores =
|
||||
abilityScores.copyWith(stats: updateByKey(abilityScores.stats, [stat]));
|
||||
textControllers[stat.key] ??=
|
||||
TextEditingController(text: stat.value.toString())
|
||||
..addListener(validate);
|
||||
@@ -51,8 +46,8 @@ class AbilityScoresFormController extends GetxController {
|
||||
}
|
||||
|
||||
void removeStat(AbilityScore stat) {
|
||||
abilityScores.value = abilityScores.value
|
||||
.copyWith(stats: removeByKey(abilityScores.value.stats, [stat]));
|
||||
abilityScores =
|
||||
abilityScores.copyWith(stats: removeByKey(abilityScores.stats, [stat]));
|
||||
textControllers.remove(stat.key);
|
||||
}
|
||||
|
||||
@@ -60,12 +55,19 @@ class AbilityScoresFormController extends GetxController {
|
||||
if (textControllers.containsKey(abilityScore.key)) {
|
||||
return;
|
||||
}
|
||||
abilityScores.value = abilityScores.value
|
||||
.copyWith(stats: [...abilityScores.value.stats, abilityScore]);
|
||||
abilityScores =
|
||||
abilityScores.copyWith(stats: [...abilityScores.stats, abilityScore]);
|
||||
textControllers[abilityScore.key] =
|
||||
TextEditingController(text: abilityScore.value.toString())
|
||||
..addListener(validate);
|
||||
}
|
||||
|
||||
void onReorder(int oldIndex, int newIndex) {
|
||||
abilityScores = abilityScores.copyWith(
|
||||
stats: reorder(abilityScores.stats, oldIndex, newIndex),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class AbilityScoresFormArguments {
|
||||
@@ -77,3 +79,4 @@ class AbilityScoresFormArguments {
|
||||
required this.onChanged,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,25 +16,23 @@ import 'package:dungeon_paper/core/utils/list_utils.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';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
|
||||
const AbilityScoresFormView({
|
||||
super.key,
|
||||
});
|
||||
class AbilityScoresFormView extends StatelessWidget {
|
||||
const AbilityScoresFormView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => ConfirmExitView(
|
||||
dirty: controller.dirty.value,
|
||||
return Consumer<AbilityScoresFormController>(
|
||||
builder: (context, controller, _) => ConfirmExitView(
|
||||
dirty: controller.dirty,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr.entityPlural(tn(AbilityScore))),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: AdvancedFloatingActionButton.extended(
|
||||
onPressed: _save,
|
||||
onPressed: () => _save(context),
|
||||
label: Text(tr.generic.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
@@ -47,22 +45,19 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
|
||||
child: ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
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));
|
||||
},
|
||||
itemBuilder: (context, index) => _buildCard(context, index),
|
||||
itemCount: controller.abilityScores.stats.length,
|
||||
onReorder: controller.onReorder,
|
||||
itemBuilder: (context, index) =>
|
||||
_buildCard(context, controller, index),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => Get.toNamed(Routes.abilityScoreForm,
|
||||
arguments: AbilityScoreFormArguments(
|
||||
abilityScore: null,
|
||||
onSave: controller.addStat,
|
||||
)),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pushNamed(Routes.abilityScoreForm,
|
||||
arguments: AbilityScoreFormArguments(
|
||||
abilityScore: null,
|
||||
onSave: controller.addStat,
|
||||
)),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(
|
||||
tr.generic.addEntity(
|
||||
@@ -77,15 +72,15 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard(BuildContext context, int index) {
|
||||
Widget _buildCard(
|
||||
BuildContext context, AbilityScoresFormController controller, int index) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final statKey = sortByPredefined(
|
||||
controller.textControllers.keys.toList(),
|
||||
order:
|
||||
controller.abilityScores.value.stats.map((stat) => stat.key).toList(),
|
||||
order: controller.abilityScores.stats.map((stat) => stat.key).toList(),
|
||||
).elementAt(index);
|
||||
final stat = controller.abilityScores.value.stats
|
||||
final stat = controller.abilityScores.stats
|
||||
.firstWhere((stat) => stat.key == statKey);
|
||||
return Padding(
|
||||
key: Key('stat-$statKey'),
|
||||
@@ -151,19 +146,18 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: EntityEditMenu(
|
||||
onEdit: () => Get.toNamed(
|
||||
onEdit: () => Navigator.of(context).pushNamed(
|
||||
Routes.abilityScoreForm,
|
||||
arguments: AbilityScoreFormArguments(
|
||||
abilityScore: stat,
|
||||
onSave: (stat) => controller.updateStat(stat),
|
||||
),
|
||||
),
|
||||
onDelete: () => deleteDialog.confirm(
|
||||
onDelete: () => awaitDeleteConfirmation(
|
||||
context,
|
||||
DeleteDialogOptions(
|
||||
entityName: stat.name,
|
||||
entityKind: tr.entity(tn(AbilityScore))),
|
||||
stat.name,
|
||||
() => controller.removeStat(stat),
|
||||
AbilityScore,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -176,8 +170,10 @@ class AbilityScoresFormView extends GetView<AbilityScoresFormController> {
|
||||
);
|
||||
}
|
||||
|
||||
_save() {
|
||||
controller.onChanged(controller.abilityScores.value);
|
||||
Get.back();
|
||||
_save(BuildContext context) {
|
||||
final controller =
|
||||
Provider.of<AbilityScoresFormController>(context, listen: false);
|
||||
controller.onChanged(controller.abilityScores);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/about_controller.dart';
|
||||
|
||||
class AboutBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<AboutController>(
|
||||
() => AboutController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:pub_semver/pub_semver.dart';
|
||||
|
||||
class AboutController extends GetxController {
|
||||
final version = Rx<Version?>(null);
|
||||
class AboutController extends ChangeNotifier {
|
||||
Version? version;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
AboutController() {
|
||||
getVersion();
|
||||
}
|
||||
|
||||
Future<void> getVersion() async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
version.value = Version.parse(info.version + '+' + info.buildNumber);
|
||||
version = Version.parse('${info.version}+${info.buildNumber}');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import 'package:dungeon_paper/core/dw_icons.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 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
|
||||
import '../../../model_utils/user_utils.dart';
|
||||
import '../controllers/about_controller.dart';
|
||||
|
||||
class AboutView extends GetView<AboutController> {
|
||||
class AboutView extends StatelessWidget {
|
||||
const AboutView({super.key});
|
||||
|
||||
@override
|
||||
@@ -34,9 +34,9 @@ class AboutView extends GetView<AboutController> {
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.headlineMedium,
|
||||
),
|
||||
() => Obx(
|
||||
() => Text(
|
||||
tr.about.version(controller.version.value?.toString() ?? '-'),
|
||||
() => Consumer<AboutController>(
|
||||
builder: (context, controller, _) => Text(
|
||||
tr.about.version(controller.version?.toString() ?? '-'),
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodySmall,
|
||||
),
|
||||
@@ -71,7 +71,8 @@ class AboutView extends GetView<AboutController> {
|
||||
title: Text(tr.about.feedback.title),
|
||||
subtitle: Text(tr.about.feedback.subtitle,
|
||||
style: textTheme.bodySmall),
|
||||
onTap: () => Get.toNamed(Routes.sendFeedback),
|
||||
onTap: () =>
|
||||
Navigator.of(context).pushNamed(Routes.sendFeedback),
|
||||
isThreeLine: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/account_controller.dart';
|
||||
|
||||
class AccountBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<AccountController>(
|
||||
() => AccountController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
import 'package:dungeon_paper/app/data/services/auth_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/auth_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.dart';
|
||||
import 'package:dungeon_paper/core/utils/upload_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/uuid.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 {
|
||||
final uploading = false.obs;
|
||||
class AccountController extends ChangeNotifier
|
||||
with UserProviderMixin, AuthProviderMixin {
|
||||
var uploading = false;
|
||||
|
||||
void updateEmail(String email) async {
|
||||
await userService.updateEmail(email);
|
||||
Get.rawSnackbar(message: tr.account.details.email.success);
|
||||
void updateEmail(BuildContext context, String email) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
await userProvider.updateEmail(email);
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(tr.account.details.email.success)),
|
||||
);
|
||||
}
|
||||
|
||||
void uploadPhoto(BuildContext context) {
|
||||
@@ -22,15 +24,23 @@ class AccountController extends GetxController
|
||||
UploadSettings(
|
||||
uploadPath: '/UserPhoto/${uuid()}',
|
||||
cropStyle: CropStyle.circle,
|
||||
onUploadFile: (_) => uploading.value = true,
|
||||
onSuccess: (url) {
|
||||
uploading.value = false;
|
||||
userService.updateUser(
|
||||
user.copyWith(photoUrl: url),
|
||||
);
|
||||
onUploadFile: (_) {
|
||||
uploading = true;
|
||||
notifyListeners();
|
||||
},
|
||||
onSuccess: (url) {
|
||||
uploading = false;
|
||||
userProvider.updateUser(user.copyWith(photoUrl: url));
|
||||
notifyListeners();
|
||||
},
|
||||
onCancel: () {
|
||||
uploading = false;
|
||||
notifyListeners();
|
||||
},
|
||||
onError: (error) {
|
||||
uploading = false;
|
||||
notifyListeners();
|
||||
},
|
||||
onCancel: () => uploading.value = false,
|
||||
onError: (error) => uploading.value = false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ import 'package:dungeon_paper/core/utils/password_validator.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_validator.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../core/dw_icons.dart';
|
||||
import '../../../../core/http/api.dart';
|
||||
import '../../../model_utils/user_utils.dart';
|
||||
import '../controllers/account_controller.dart';
|
||||
|
||||
class AccountView extends GetView<AccountController> {
|
||||
class AccountView extends StatelessWidget {
|
||||
const AccountView({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -28,8 +28,8 @@ class AccountView extends GetView<AccountController> {
|
||||
final builder = ItemBuilder.lazyChildren(
|
||||
children: [
|
||||
() => Center(
|
||||
child: Obx(
|
||||
() => UserAvatar(
|
||||
child: Consumer<AccountController>(
|
||||
builder: (context, controller, _) => UserAvatar(
|
||||
user: controller.user,
|
||||
size: 100,
|
||||
),
|
||||
@@ -45,19 +45,19 @@ class AccountView extends GetView<AccountController> {
|
||||
style: textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
() => Obx(
|
||||
() => ListTile(
|
||||
() => Consumer<AccountController>(
|
||||
builder: (context, controller, _) => ListTile(
|
||||
title: Text(tr.account.details.displayName.title),
|
||||
subtitle: Text(controller.user.displayName),
|
||||
leading: const Icon(Icons.abc),
|
||||
onTap: _openNameDialog,
|
||||
onTap: () => _openNameDialog(context),
|
||||
),
|
||||
),
|
||||
() => Obx(
|
||||
() => ListTile(
|
||||
() => Consumer<AccountController>(
|
||||
builder: (context, controller, _) => ListTile(
|
||||
title: Text(tr.account.details.image.title),
|
||||
subtitle: Text(tr.account.details.image.subtitle),
|
||||
leading: controller.uploading.value
|
||||
leading: controller.uploading
|
||||
? const SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
@@ -65,24 +65,23 @@ class AccountView extends GetView<AccountController> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.image),
|
||||
enabled: !controller.uploading.value,
|
||||
onTap: !controller.uploading.value
|
||||
? () => _uploadImage(context)
|
||||
: null,
|
||||
enabled: !controller.uploading,
|
||||
onTap:
|
||||
!controller.uploading ? () => _uploadImage(context) : null,
|
||||
),
|
||||
),
|
||||
() => Obx(
|
||||
() => ListTile(
|
||||
() => Consumer<AccountController>(
|
||||
builder: (context, controller, _) => ListTile(
|
||||
title: Text(tr.account.details.email.title),
|
||||
subtitle: Text(controller.user.email),
|
||||
onTap: _openEmailDialog,
|
||||
onTap: () => _openEmailDialog(context),
|
||||
leading: const Icon(Icons.email),
|
||||
),
|
||||
),
|
||||
() => ListTile(
|
||||
title: Text(tr.account.details.password.title),
|
||||
subtitle: Text(tr.account.details.password.subtitle),
|
||||
onTap: _openPasswordDialog,
|
||||
onTap: () => _openPasswordDialog(context),
|
||||
leading: const Icon(Icons.key),
|
||||
),
|
||||
() => const Divider(),
|
||||
@@ -94,7 +93,7 @@ class AccountView extends GetView<AccountController> {
|
||||
style: textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
// ...(controller.authService.fbUser?.providerData ?? []).map((provider) {
|
||||
// ...(controller.authProvider.fbUser?.providerData ?? []).map((provider) {
|
||||
...([
|
||||
// ProviderName.password,
|
||||
// if (PlatformHelper.canUseGoogleSignIn)
|
||||
@@ -103,8 +102,8 @@ class AccountView extends GetView<AccountController> {
|
||||
ProviderName.apple
|
||||
]).map(
|
||||
(provider) {
|
||||
return () => Obx(
|
||||
() => ListTile(
|
||||
return () => Consumer<AccountController>(
|
||||
builder: (context, controller, _) => ListTile(
|
||||
title: Text(tr.auth.providers.name(provider.name)),
|
||||
// subtitle: Text(provider.),
|
||||
leading: Icon(DwIcons.providerIcon(provider)),
|
||||
@@ -112,19 +111,19 @@ class AccountView extends GetView<AccountController> {
|
||||
? Text(
|
||||
tr.auth.providers.unusable(
|
||||
tr.auth.providers.name(provider.name)),
|
||||
textScaleFactor: 0.8,
|
||||
textScaler: const TextScaler.linear(0.8),
|
||||
)
|
||||
: null,
|
||||
trailing: ElevatedButton(
|
||||
onPressed: providerCount > 1
|
||||
? isProviderLinked(provider)
|
||||
onPressed: providerCount(controller) > 1
|
||||
? isProviderLinked(controller, provider)
|
||||
? unlinkProvider(context, provider)
|
||||
: PlatformHelper.canUseProvider(provider)
|
||||
? linkProvider(provider)
|
||||
? linkProvider(context, provider)
|
||||
: null
|
||||
: null,
|
||||
child: Text(
|
||||
isProviderLinked(provider)
|
||||
isProviderLinked(controller, provider)
|
||||
? tr.auth.providers.unlink
|
||||
: tr.auth.providers.link,
|
||||
),
|
||||
@@ -134,22 +133,25 @@ class AccountView extends GetView<AccountController> {
|
||||
},
|
||||
),
|
||||
// delete account
|
||||
() => ListTile(
|
||||
title: Text(tr.account.deleteAccount.title),
|
||||
leading: const Icon(Icons.delete_forever),
|
||||
onTap: () => awaitDeleteAccountConfirmation(
|
||||
context,
|
||||
() {
|
||||
api.requests.sendFeedback(
|
||||
email: controller.user.email,
|
||||
subject: 'Account Deletion Request',
|
||||
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: tr.account.deleteAccount.success);
|
||||
},
|
||||
() => Consumer<AccountController>(
|
||||
builder: (context, controller, _) => ListTile(
|
||||
title: Text(tr.account.deleteAccount.title),
|
||||
leading: const Icon(Icons.delete_forever),
|
||||
onTap: () => awaitDeleteAccountConfirmation(
|
||||
context,
|
||||
() {
|
||||
api.requests.sendFeedback(
|
||||
email: controller.user.email,
|
||||
subject: 'Account Deletion Request',
|
||||
body:
|
||||
'Automated: Request Account Deletion for ${controller.user.email}',
|
||||
username: controller.user.username,
|
||||
);
|
||||
// A deletion request for your account was sent successfully
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(tr.account.deleteAccount.success)));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// () => const SizedBox(height: 32),
|
||||
@@ -173,65 +175,92 @@ class AccountView extends GetView<AccountController> {
|
||||
],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(controller.user.username),
|
||||
centerTitle: true,
|
||||
return Consumer<AccountController>(
|
||||
builder: (context, controller, _) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(controller.user.username),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: builder.asListView(),
|
||||
),
|
||||
body: builder.asListView(),
|
||||
);
|
||||
}
|
||||
|
||||
int get providerCount =>
|
||||
controller.authService.fbUser?.providerData.length ?? 0;
|
||||
int providerCount(AccountController controller) {
|
||||
return controller.authProvider.fbUser?.providerData.length ?? 0;
|
||||
}
|
||||
|
||||
bool isProviderLinked(ProviderName provider) =>
|
||||
controller.authService.fbUser?.providerData
|
||||
bool isProviderLinked(AccountController controller, ProviderName provider) =>
|
||||
controller.authProvider.fbUser?.providerData
|
||||
.any((pr) => pr.providerId == domainFromProviderName(provider)) ==
|
||||
true;
|
||||
|
||||
void _openNameDialog() {
|
||||
Get.dialog(
|
||||
SingleTextFieldDialog(
|
||||
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: tr.account.details.displayName.success);
|
||||
controller.userService.updateUser(
|
||||
controller.user.copyWith(displayName: displayName),
|
||||
);
|
||||
},
|
||||
),
|
||||
void _openNameDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final controller =
|
||||
Provider.of<AccountController>(context, listen: false);
|
||||
return SingleTextFieldDialog(
|
||||
title: tr.account.details.displayName.title,
|
||||
inputLabel: tr.account.details.displayName.label,
|
||||
inputHint: tr.account.details.displayName.placeholder,
|
||||
value: controller.user.displayName,
|
||||
onSave: (displayName) {
|
||||
controller.userProvider.updateUser(
|
||||
controller.user.copyWith(displayName: displayName),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(tr.account.details.displayName.success),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _openEmailDialog() {
|
||||
Get.dialog(
|
||||
SingleTextFieldDialog(
|
||||
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,
|
||||
),
|
||||
void _openEmailDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final controller =
|
||||
Provider.of<AccountController>(context, listen: false);
|
||||
return SingleTextFieldDialog(
|
||||
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: (email) => controller.updateEmail(context, email),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _openPasswordDialog() {
|
||||
Get.dialog(
|
||||
PasswordFieldDialog(
|
||||
onSave: (password) {
|
||||
Get.rawSnackbar(message: tr.account.details.password.success);
|
||||
controller.authService.fbUser!.updatePassword(password);
|
||||
},
|
||||
),
|
||||
void _openPasswordDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final controller =
|
||||
Provider.of<AccountController>(context, listen: false);
|
||||
return PasswordFieldDialog(
|
||||
onSave: (password) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(tr.account.details.password.success),
|
||||
),
|
||||
);
|
||||
controller.authProvider.fbUser!.updatePassword(password);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _uploadImage(BuildContext context) {
|
||||
final controller = Provider.of<AccountController>(context, listen: false);
|
||||
controller.uploadPhoto(context);
|
||||
}
|
||||
|
||||
@@ -241,16 +270,22 @@ class AccountView extends GetView<AccountController> {
|
||||
context,
|
||||
provider,
|
||||
() {
|
||||
controller.authService.logoutFromProvider(provider);
|
||||
controller.authService.fbUser!
|
||||
final controller =
|
||||
Provider.of<AccountController>(context, listen: false);
|
||||
controller.authProvider.logoutFromProvider(provider);
|
||||
controller.authProvider.fbUser!
|
||||
.unlink(domainFromProviderName(provider));
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> Function() linkProvider(ProviderName provider) => () async {
|
||||
Future<void> Function() linkProvider(
|
||||
BuildContext context, ProviderName provider) =>
|
||||
() async {
|
||||
final controller =
|
||||
Provider.of<AccountController>(context, listen: false);
|
||||
final cred =
|
||||
await controller.authService.getProviderCredential(provider);
|
||||
controller.authService.fbUser!.linkWithCredential(cred);
|
||||
await controller.authProvider.getProviderCredential(provider);
|
||||
controller.authProvider.fbUser!.linkWithCredential(cred);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -335,9 +370,9 @@ class _SingleTextFieldDialogState extends State<SingleTextFieldDialog> {
|
||||
context,
|
||||
onSave: () {
|
||||
widget.onSave(widget.value.text);
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onCancel: () => Get.back(),
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -415,10 +450,10 @@ class _PasswordFieldDialogState extends State<PasswordFieldDialog> {
|
||||
onSave: valid
|
||||
? () {
|
||||
widget.onSave(password.text);
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
: null,
|
||||
onCancel: () => Get.back(),
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/basic_info_form_controller.dart';
|
||||
|
||||
class BasicInfoFormBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<BasicInfoFormController>(
|
||||
() => BasicInfoFormController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +1,76 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.dart';
|
||||
import 'package:dungeon_paper/core/route_arguments.dart';
|
||||
import 'package:dungeon_paper/core/utils/upload_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class BasicInfoFormController extends GetxController with UserServiceMixin {
|
||||
final Rx<TextEditingController> name = TextEditingController().obs;
|
||||
final Rx<TextEditingController> avatarUrl = TextEditingController().obs;
|
||||
class BasicInfoFormController extends ChangeNotifier with UserProviderMixin {
|
||||
final TextEditingController name = TextEditingController();
|
||||
final TextEditingController avatarUrl = TextEditingController();
|
||||
late final void Function(String name, String avatar) onChanged;
|
||||
final uploading = false.obs;
|
||||
var uploading = false;
|
||||
File? photoFile;
|
||||
var dirty = false;
|
||||
|
||||
final Rx<File?> photoFile = Rx(null);
|
||||
final dirty = false.obs;
|
||||
|
||||
bool get hasPhotoFile => photoFile.value != null;
|
||||
bool get isUploading => uploading.value;
|
||||
bool get hasPhotoFile => photoFile != null;
|
||||
bool get isUploading => uploading;
|
||||
|
||||
void startUploadFlow(BuildContext context) {
|
||||
cropAndUploadPhoto(
|
||||
context,
|
||||
UploadSettings(
|
||||
uploadPath: '/CharacterPhoto/' + uuid(),
|
||||
onUploadFile: (_) => uploading.value = true,
|
||||
onSuccess: (url) {
|
||||
avatarUrl.value.text = url;
|
||||
uploading.value = false;
|
||||
uploadPath: '/CharacterPhoto/${uuid()}',
|
||||
onUploadFile: (_) {
|
||||
uploading = true;
|
||||
notifyListeners();
|
||||
},
|
||||
onSuccess: (url) {
|
||||
avatarUrl.text = url;
|
||||
uploading = false;
|
||||
notifyListeners();
|
||||
},
|
||||
onCancel: () {
|
||||
uploading = false;
|
||||
notifyListeners();
|
||||
},
|
||||
onError: (error) {
|
||||
uploading = false;
|
||||
notifyListeners();
|
||||
},
|
||||
onCancel: () => uploading.value = false,
|
||||
onError: (error) => uploading.value = false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void resetPhoto() {
|
||||
photoFile.value = null;
|
||||
avatarUrl.value.text = '';
|
||||
photoFile = null;
|
||||
avatarUrl.text = '';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
final BasicInfoFormArguments args = Get.arguments;
|
||||
BasicInfoFormController(BuildContext context) {
|
||||
final BasicInfoFormArguments args = getArgs(context);
|
||||
onChanged = args.onChanged;
|
||||
name.value = TextEditingController(text: args.name);
|
||||
avatarUrl.value = TextEditingController(text: args.avatarUrl);
|
||||
name.text = args.name;
|
||||
avatarUrl.text = args.avatarUrl;
|
||||
|
||||
name.value.addListener(_refreshName);
|
||||
avatarUrl.value.addListener(_refreshAvatarUrl);
|
||||
name.addListener(_setDirty);
|
||||
avatarUrl.addListener(_setDirty);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
name.value.removeListener(_refreshName);
|
||||
avatarUrl.value.removeListener(_refreshAvatarUrl);
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void _refreshName() {
|
||||
name.refresh();
|
||||
_setDirty();
|
||||
}
|
||||
|
||||
void _refreshAvatarUrl() {
|
||||
avatarUrl.refresh();
|
||||
_setDirty();
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
name.removeListener(_setDirty);
|
||||
avatarUrl.removeListener(_setDirty);
|
||||
}
|
||||
|
||||
void _setDirty() {
|
||||
dirty.value = true;
|
||||
if (!dirty) {
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.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/advanced_floating_action_button.dart';
|
||||
@@ -12,12 +12,11 @@ import 'package:dungeon_paper/core/platform_helper.dart';
|
||||
import 'package:dungeon_paper/core/utils/content_generators/character_name_generator.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controllers/basic_info_form_controller.dart';
|
||||
|
||||
class BasicInfoFormView extends GetView<BasicInfoFormController>
|
||||
with UserServiceMixin {
|
||||
class BasicInfoFormView extends StatelessWidget with UserProviderMixin {
|
||||
const BasicInfoFormView({
|
||||
super.key,
|
||||
});
|
||||
@@ -25,16 +24,16 @@ class BasicInfoFormView extends GetView<BasicInfoFormController>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Obx(
|
||||
() => ConfirmExitView(
|
||||
dirty: controller.dirty.value,
|
||||
return Consumer<BasicInfoFormController>(
|
||||
builder: (context, controller, _) => ConfirmExitView(
|
||||
dirty: controller.dirty,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr.basicInfo.title),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: AdvancedFloatingActionButton.extended(
|
||||
onPressed: _save,
|
||||
onPressed: () => _save(context),
|
||||
label: Text(tr.generic.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
@@ -46,7 +45,7 @@ class BasicInfoFormView extends GetView<BasicInfoFormController>
|
||||
children: [
|
||||
TextFormField(
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
controller: controller.name.value,
|
||||
controller: controller.name,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (val) =>
|
||||
val == null || val.isEmpty ? 'Cannot be empty' : null,
|
||||
@@ -64,7 +63,7 @@ class BasicInfoFormView extends GetView<BasicInfoFormController>
|
||||
),
|
||||
icon: const Icon(DwIcons.dice_d6_numbered),
|
||||
onPressed: () {
|
||||
controller.name.value.text =
|
||||
controller.name.text =
|
||||
CharacterNameGenerator().generate();
|
||||
},
|
||||
),
|
||||
@@ -113,14 +112,14 @@ class BasicInfoFormView extends GetView<BasicInfoFormController>
|
||||
height: 40,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed:
|
||||
!controller.isUploading && userService.isLoggedIn
|
||||
!controller.isUploading && userProvider.isLoggedIn
|
||||
? () => controller.startUploadFlow(context)
|
||||
: null,
|
||||
icon: const Icon(Icons.upload_file),
|
||||
label: Text(tr.basicInfo.form.photo.choose),
|
||||
),
|
||||
),
|
||||
if (userService.isGuest) ...[
|
||||
if (userProvider.isGuest) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: RichText(
|
||||
@@ -137,7 +136,8 @@ class BasicInfoFormView extends GetView<BasicInfoFormController>
|
||||
Hyperlink.textSpan(
|
||||
context,
|
||||
tr.basicInfo.form.photo.guest.label,
|
||||
onTap: () => Get.toNamed(Routes.login),
|
||||
onTap: () =>
|
||||
Navigator.of(context).pushNamed(Routes.login),
|
||||
),
|
||||
TextSpan(text: tr.basicInfo.form.photo.guest.suffix),
|
||||
],
|
||||
@@ -162,7 +162,7 @@ class BasicInfoFormView extends GetView<BasicInfoFormController>
|
||||
: Text(tr.basicInfo.form.photo.orSeparator),
|
||||
),
|
||||
TextFormField(
|
||||
controller: controller.avatarUrl.value,
|
||||
controller: controller.avatarUrl,
|
||||
textInputAction: TextInputAction.done,
|
||||
enabled: !controller.isUploading,
|
||||
// onChanged: (val) => updateControllers(),
|
||||
@@ -181,9 +181,11 @@ class BasicInfoFormView extends GetView<BasicInfoFormController>
|
||||
);
|
||||
}
|
||||
|
||||
_save() {
|
||||
_save(BuildContext context) {
|
||||
final controller =
|
||||
Provider.of<BasicInfoFormController>(context, listen: false);
|
||||
controller.onChanged(
|
||||
controller.name.value.text, controller.avatarUrl.value.text);
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/bio_form_controller.dart';
|
||||
|
||||
class BioFormBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<BioFormController>(
|
||||
() => BioFormController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,47 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/core/utils/enum_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
|
||||
class BioFormController extends GetxController with CharacterServiceMixin {
|
||||
final bioDesc = TextEditingController().obs;
|
||||
final looks = TextEditingController().obs;
|
||||
final alignmentName = 'good'.obs;
|
||||
final alignmentValue = TextEditingController().obs;
|
||||
final bonds = <TextEditingController>[].obs;
|
||||
final dirty = false.obs;
|
||||
class BioFormController extends ChangeNotifier with CharacterProviderMixin {
|
||||
var bioDesc = TextEditingController();
|
||||
var looks = TextEditingController();
|
||||
var alignmentName = 'good';
|
||||
var alignmentValue = TextEditingController();
|
||||
var bonds = <TextEditingController>[];
|
||||
var dirty = false;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
final BioFormArguments args = Get.arguments;
|
||||
final char = args.character ?? this.char;
|
||||
bioDesc.value = TextEditingController(text: char.bio.description);
|
||||
looks.value = TextEditingController(text: char.bio.looks);
|
||||
alignmentName.value = char.bio.alignment.key;
|
||||
alignmentValue.value =
|
||||
BioFormController(BuildContext context) {
|
||||
bioDesc = TextEditingController(text: char.bio.description);
|
||||
looks = TextEditingController(text: char.bio.looks);
|
||||
alignmentName = char.bio.alignment.key;
|
||||
alignmentValue =
|
||||
TextEditingController(text: char.bio.alignment.description);
|
||||
bonds.value = char.sessionMarks
|
||||
bonds = char.sessionMarks
|
||||
.map((e) => TextEditingController(text: e.description))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void save() {
|
||||
charService.updateCharacter(char.copyWith(
|
||||
void save(BuildContext context) {
|
||||
final charProvider = CharacterProvider.of(context);
|
||||
final char = charProvider.current;
|
||||
charProvider.updateCharacter(char.copyWith(
|
||||
bio: char.bio.copyWith(
|
||||
description: bioDesc.value.text,
|
||||
looks: looks.value.text.replaceAll(RegExp('\\s*\n'), ' \n'),
|
||||
alignment: char.bio.alignment.copyWith(
|
||||
description: alignmentValue.value.text,
|
||||
type: getEnumByName(dw.AlignmentType.values, alignmentName.value),
|
||||
type: getEnumByName(dw.AlignmentType.values, alignmentName),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void setDirty([String? value]) {
|
||||
if (!dirty.value) {
|
||||
dirty.value = true;
|
||||
if (!dirty) {
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:dungeon_paper/app/data/models/alignment.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/modules/BioForm/controllers/bio_form_controller.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/confirm_exit_view.dart';
|
||||
@@ -7,23 +6,22 @@ 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/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class BioFormView extends GetView<BioFormController>
|
||||
with CharacterServiceMixin {
|
||||
class BioFormView extends StatelessWidget {
|
||||
const BioFormView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => ConfirmExitView(
|
||||
dirty: controller.dirty.value,
|
||||
return Consumer<BioFormController>(
|
||||
builder: (context, controller, _) => ConfirmExitView(
|
||||
dirty: controller.dirty,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr.bio.dialog.title),
|
||||
),
|
||||
floatingActionButton: AdvancedFloatingActionButton.extended(
|
||||
onPressed: _save,
|
||||
onPressed: _save(context),
|
||||
label: Text(tr.generic.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
@@ -31,7 +29,7 @@ class BioFormView extends GetView<BioFormController>
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
RichTextField(
|
||||
controller: controller.bioDesc.value,
|
||||
controller: controller.bioDesc,
|
||||
minLines: 5,
|
||||
maxLines: 10,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
@@ -43,7 +41,7 @@ class BioFormView extends GetView<BioFormController>
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RichTextField(
|
||||
controller: controller.looks.value,
|
||||
controller: controller.looks,
|
||||
minLines: 4,
|
||||
maxLines: 8,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
@@ -55,7 +53,7 @@ class BioFormView extends GetView<BioFormController>
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SelectBox<String>(
|
||||
value: controller.alignmentName.value,
|
||||
value: controller.alignmentName,
|
||||
items: AlignmentValue.allKeys
|
||||
.map(
|
||||
(a) => DropdownMenuItem<String>(
|
||||
@@ -71,7 +69,7 @@ class BioFormView extends GetView<BioFormController>
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (v) {
|
||||
controller.alignmentName.value = v!;
|
||||
controller.alignmentName = v!;
|
||||
controller.setDirty();
|
||||
},
|
||||
isExpanded: true,
|
||||
@@ -79,7 +77,7 @@ class BioFormView extends GetView<BioFormController>
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
RichTextField(
|
||||
controller: controller.alignmentValue.value,
|
||||
controller: controller.alignmentValue,
|
||||
minLines: 4,
|
||||
maxLines: 8,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
@@ -97,8 +95,11 @@ class BioFormView extends GetView<BioFormController>
|
||||
);
|
||||
}
|
||||
|
||||
void _save() {
|
||||
controller.save();
|
||||
Get.back();
|
||||
void Function() _save(BuildContext context) {
|
||||
return () {
|
||||
final controller = Provider.of<BioFormController>(context, listen: false);
|
||||
controller.save(context);
|
||||
Navigator.of(context).pop();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/bonds_flags_form_controller.dart';
|
||||
|
||||
class BondsFlagsFormBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<BondsFlagsFormController>(
|
||||
() => BondsFlagsFormController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,30 @@
|
||||
import 'package:dungeon_paper/app/data/models/session_marks.dart';
|
||||
import 'package:dungeon_paper/core/route_arguments.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class BondsFlagsFormController extends GetxController {
|
||||
final bonds = <SessionMark>[].obs;
|
||||
final flags = <SessionMark>[].obs;
|
||||
final bondsDesc = <TextEditingController>[].obs;
|
||||
final flagsDesc = <TextEditingController>[].obs;
|
||||
class BondsFlagsFormController extends ChangeNotifier {
|
||||
final bonds = <SessionMark>[];
|
||||
final flags = <SessionMark>[];
|
||||
final bondsDesc = <TextEditingController>[];
|
||||
final flagsDesc = <TextEditingController>[];
|
||||
late final void Function(List<SessionMark> bonds, List<SessionMark> flags)
|
||||
onChanged;
|
||||
final dirty = false.obs;
|
||||
var dirty = false;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
final BondsFlagsFormArguments args = Get.arguments;
|
||||
bonds.value = args.bonds;
|
||||
bondsDesc.value = args.bonds
|
||||
BondsFlagsFormController(BuildContext context) {
|
||||
final BondsFlagsFormArguments args = getArgs(context);
|
||||
bonds.addAll(args.bonds);
|
||||
bondsDesc.addAll(args.bonds
|
||||
.map((e) =>
|
||||
TextEditingController(text: e.description)..addListener(_setDirty))
|
||||
.toList();
|
||||
flags.value = args.flags;
|
||||
flagsDesc.value = args.flags
|
||||
.toList());
|
||||
flags.addAll(args.flags);
|
||||
flagsDesc.addAll(args.flags
|
||||
.map((e) =>
|
||||
TextEditingController(text: e.description)..addListener(_setDirty))
|
||||
.toList();
|
||||
.toList());
|
||||
onChanged = args.onChanged;
|
||||
}
|
||||
|
||||
@@ -61,14 +59,14 @@ class BondsFlagsFormController extends GetxController {
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
for (final ctrl in [...bondsDesc, ...flagsDesc]) {
|
||||
ctrl.removeListener(_setDirty);
|
||||
}
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void save() {
|
||||
void save(BuildContext context) {
|
||||
final newBonds = enumerate(bonds)
|
||||
.map((e) =>
|
||||
e.value.copyWithInherited(description: bondsDesc[e.index].text))
|
||||
@@ -81,12 +79,13 @@ class BondsFlagsFormController extends GetxController {
|
||||
.toList();
|
||||
|
||||
onChanged(newBonds, newFlags);
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _setDirty() {
|
||||
if (!dirty.value) {
|
||||
dirty.value = true;
|
||||
if (!dirty) {
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,26 @@ import 'package:dungeon_paper/app/widgets/atoms/confirm_exit_view.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controllers/bonds_flags_form_controller.dart';
|
||||
|
||||
class BondsFlagsFormView extends GetView<BondsFlagsFormController> {
|
||||
class BondsFlagsFormView extends StatelessWidget {
|
||||
const BondsFlagsFormView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Obx(
|
||||
() => ConfirmExitView(
|
||||
dirty: controller.dirty.value,
|
||||
return Consumer<BondsFlagsFormController>(
|
||||
builder: (context, controller, _) => ConfirmExitView(
|
||||
dirty: controller.dirty,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr.sessionMarks.title),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: AdvancedFloatingActionButton.extended(
|
||||
onPressed: controller.save,
|
||||
onPressed: () => controller.save(context),
|
||||
icon: const Icon(Icons.save),
|
||||
label: Text(tr.generic.save),
|
||||
),
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/campaigns_list_controller.dart';
|
||||
|
||||
class CampaignsListBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<CampaignsListController>(
|
||||
() => CampaignsListController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,29 +2,32 @@ import 'dart:async';
|
||||
|
||||
import 'package:dungeon_paper/app/data/models/campaign.dart';
|
||||
import 'package:dungeon_paper/core/storage_handler/storage_handler.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CampaignsListController extends GetxController {
|
||||
class CampaignsListController extends ChangeNotifier {
|
||||
StreamSubscription? _campaignsListenerSubscription;
|
||||
final _campaigns = <Campaign>[].obs;
|
||||
final _campaigns = <Campaign>[];
|
||||
var count = 0;
|
||||
|
||||
List<Campaign> get campaigns => _campaigns.toList();
|
||||
|
||||
final count = 0.obs;
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
CampaignsListController() {
|
||||
_campaignsListenerSubscription = StorageHandler.instance
|
||||
.collectionListener('Campaigns', _campaignsListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_campaignsListenerSubscription?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void _campaignsListener(List<DocData> data) {
|
||||
_campaigns.value = data.map((e) => Campaign.fromJson(e)).toList();
|
||||
_campaigns
|
||||
..clear()
|
||||
..addAll(
|
||||
data.map((e) => Campaign.fromJson(e)).toList(),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:dungeon_paper/app/data/models/campaign.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controllers/campaigns_list_controller.dart';
|
||||
|
||||
class CampaignsListView extends GetView<CampaignsListController> {
|
||||
class CampaignsListView extends StatelessWidget {
|
||||
const CampaignsListView({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -14,8 +14,8 @@ class CampaignsListView extends GetView<CampaignsListController> {
|
||||
title: Text(tr.generic.myEntity(tr.entityPlural(tn(Campaign)))),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Obx(
|
||||
() => controller.campaigns.isEmpty
|
||||
body: Consumer<CampaignsListController>(
|
||||
builder: (context, controller, _) => controller.campaigns.isEmpty
|
||||
? Center(
|
||||
child: Text(tr.generic.noEntity(tr.entityPlural(tn(Campaign)))),
|
||||
)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/campaign_controller.dart';
|
||||
|
||||
class CampaignBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<CampaignController>(
|
||||
() => CampaignController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CampaignController extends GetxController {
|
||||
//TODO: Implement CampaignController
|
||||
|
||||
final count = 0.obs;
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void increment() => count.value++;
|
||||
class CampaignController extends ChangeNotifier {
|
||||
//
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/campaign_controller.dart';
|
||||
|
||||
class CampaignView extends GetView<CampaignController> {
|
||||
const CampaignView({Key? key}) : super(key: key);
|
||||
class CampaignView extends StatelessWidget {
|
||||
const CampaignView({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CharacterListPageBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/character_utils.dart';
|
||||
import 'package:dungeon_paper/app/routes/app_pages.dart';
|
||||
import 'package:dungeon_paper/app/themes/themes.dart';
|
||||
@@ -13,10 +13,8 @@ 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';
|
||||
|
||||
class CharacterListPageView extends GetView<CharacterService>
|
||||
with UserServiceMixin {
|
||||
class CharacterListPageView extends StatelessWidget with UserProviderMixin {
|
||||
const CharacterListPageView({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -26,81 +24,78 @@ class CharacterListPageView extends GetView<CharacterService>
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: AdvancedFloatingActionButton.extended(
|
||||
onPressed: () => Get.toNamed(Routes.createCharacter),
|
||||
onPressed: () => Navigator.pushNamed(context, Routes.createCharacter),
|
||||
label: Text(tr.generic.createEntity(tr.entity(tn(Character)))),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
body: Obx(
|
||||
() {
|
||||
body: CharacterProvider.consumer(
|
||||
(context, controller, _) {
|
||||
final builder = ItemBuilder.lazyChildren(
|
||||
children: [
|
||||
for (final cat in controller.charsByCategory.keys)
|
||||
() => CategorizedList(
|
||||
title:
|
||||
Text(cat.isNotEmpty ? cat : tr.character.noCategory),
|
||||
onReorder: (oldIndex, newIndex) => controller.updateAll(
|
||||
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));
|
||||
return Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color: charTheme.scaffoldBackgroundColor,
|
||||
child: ListTileTheme.merge(
|
||||
minLeadingWidth: 48,
|
||||
minVerticalPadding: 16,
|
||||
horizontalTitleGap: 10,
|
||||
textColor:
|
||||
charTheme.colorScheme.onBackground,
|
||||
// textColor: ThemeData.estimateBrightnessForColor(charTheme.scaffoldBackgroundColor) == Brightness.light ? Colors.black : Colors.white,
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
splashColor:
|
||||
Theme.of(context).splashColor,
|
||||
onTap: () {
|
||||
controller.setCurrent(char.key);
|
||||
Get.offAllNamed(Routes.home);
|
||||
},
|
||||
child: ListTile(
|
||||
leading: CharacterAvatar.squircle(
|
||||
character: char, size: 48),
|
||||
title: Text(char.displayName),
|
||||
subtitle: CharacterSubtitle(
|
||||
character: char,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
trailing: EntityEditMenu(
|
||||
onEdit: null,
|
||||
onDelete: () => deleteDialog.confirm(
|
||||
() {
|
||||
return CategorizedList(
|
||||
title: Text(cat.isNotEmpty ? cat : tr.character.noCategory),
|
||||
onReorder: (oldIndex, newIndex) => controller.updateAll(
|
||||
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));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color: charTheme.scaffoldBackgroundColor,
|
||||
child: ListTileTheme.merge(
|
||||
minLeadingWidth: 48,
|
||||
minVerticalPadding: 16,
|
||||
horizontalTitleGap: 10,
|
||||
textColor: charTheme.colorScheme.onBackground,
|
||||
// textColor: ThemeData.estimateBrightnessForColor(charTheme.scaffoldBackgroundColor) == Brightness.light ? Colors.black : Colors.white,
|
||||
child: InkWell(
|
||||
borderRadius: borderRadius,
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
onTap: () {
|
||||
controller.setCurrent(char.key);
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
child: ListTile(
|
||||
leading: CharacterAvatar.squircle(
|
||||
character: char, size: 48),
|
||||
title: Text(char.displayName),
|
||||
subtitle: CharacterSubtitle(
|
||||
character: char,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
trailing: EntityEditMenu(
|
||||
onEdit: null,
|
||||
onDelete: () {
|
||||
awaitDeleteConfirmation<Character>(
|
||||
context,
|
||||
DeleteDialogOptions(
|
||||
entityName: char.displayName,
|
||||
entityKind:
|
||||
tr.entity(tn(Character)),
|
||||
),
|
||||
char.displayName,
|
||||
() => controller
|
||||
.deleteCharacter(char),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
return ListView.builder(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/class_alignments_controller.dart';
|
||||
|
||||
class ClassAlignmentsBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ClassAlignmentsController>(
|
||||
() => ClassAlignmentsController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,36 @@
|
||||
import 'package:dungeon_paper/app/data/models/alignment.dart';
|
||||
import 'package:dungeon_paper/core/route_arguments.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ClassAlignmentsController extends GetxController {
|
||||
final alignments = AlignmentValues.empty().obs;
|
||||
final selected = Rx<dw.AlignmentType?>(null);
|
||||
class ClassAlignmentsController extends ChangeNotifier {
|
||||
var alignments = AlignmentValues.empty();
|
||||
dw.AlignmentType? selected;
|
||||
bool selectable = false;
|
||||
bool editable = false;
|
||||
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;
|
||||
final editing = <dw.AlignmentType, bool>{};
|
||||
final textControllers = <dw.AlignmentType, TextEditingController>{};
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
final ClassAlignmentsArguments? args = Get.arguments;
|
||||
ClassAlignmentsController(BuildContext context) {
|
||||
final ClassAlignmentsArguments? args = getArgs(context, nullOk: true);
|
||||
if (args != null) {
|
||||
if (args.alignments != null) {
|
||||
alignments.value = args.alignments!;
|
||||
alignments = args.alignments!;
|
||||
}
|
||||
selectable = args.selectable;
|
||||
editable = args.editable;
|
||||
onChanged = args.onChanged;
|
||||
if (args.preselected != null) {
|
||||
selected.value = args.preselected!;
|
||||
selected = args.preselected!;
|
||||
}
|
||||
}
|
||||
textControllers.addAll({
|
||||
for (final alignment in sortedAlignmentTypes)
|
||||
alignment: TextEditingController(
|
||||
text: alignments.value.byType(alignment),
|
||||
text: alignments.byType(alignment),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -43,15 +41,15 @@ class ClassAlignmentsController extends GetxController {
|
||||
}
|
||||
|
||||
void select(dw.AlignmentType type) {
|
||||
selected.value = type;
|
||||
selected = type;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
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 == type;
|
||||
|
||||
void save() {
|
||||
final updated = alignments.value.copyWithInherited(
|
||||
void save(BuildContext context) {
|
||||
final updated = alignments.copyWithInherited(
|
||||
good: textControllers[dw.AlignmentType.good]!.text,
|
||||
lawful: textControllers[dw.AlignmentType.lawful]!.text,
|
||||
neutral: textControllers[dw.AlignmentType.neutral]!.text,
|
||||
@@ -59,8 +57,8 @@ class ClassAlignmentsController extends GetxController {
|
||||
evil: textControllers[dw.AlignmentType.evil]!.text,
|
||||
);
|
||||
|
||||
onChanged?.call(updated, selected.value);
|
||||
Get.back();
|
||||
onChanged?.call(updated, selected);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,3 +78,4 @@ class ClassAlignmentsArguments {
|
||||
this.preselected,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,97 +5,101 @@ import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.
|
||||
import 'package:dungeon_paper/app/widgets/molecules/dialog_controls.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controllers/class_alignments_controller.dart';
|
||||
|
||||
class ClassAlignmentsView extends GetView<ClassAlignmentsController> {
|
||||
class ClassAlignmentsView extends StatelessWidget {
|
||||
const ClassAlignmentsView({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr.generic.selectEntity(tr.entity(tn(AlignmentValue)))),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: controller.onChanged != null
|
||||
? AdvancedFloatingActionButton.extended(
|
||||
onPressed: controller.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),
|
||||
children: [
|
||||
for (final alignment in controller.sortedAlignmentTypes)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Obx(() {
|
||||
final description =
|
||||
controller.alignments.value.byType(alignment);
|
||||
final isEditing = controller.isEditing(alignment);
|
||||
final isSelected = controller.isSelected(alignment);
|
||||
return Consumer<ClassAlignmentsController>(
|
||||
builder: (context, controller, _) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(tr.generic.selectEntity(tr.entity(tn(AlignmentValue)))),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: controller.onChanged != null
|
||||
? AdvancedFloatingActionButton.extended(
|
||||
onPressed: () => controller.save(context),
|
||||
label: Text(tr.generic.save),
|
||||
icon: const Icon(Icons.save),
|
||||
)
|
||||
: null,
|
||||
body: ListView(
|
||||
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: Builder(
|
||||
builder: (context) {
|
||||
final description = controller.alignments.byType(alignment);
|
||||
final isEditing = controller.isEditing(alignment);
|
||||
final isSelected = controller.isSelected(alignment);
|
||||
|
||||
return _wrapWithSelection(
|
||||
isSelected,
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ListTile(
|
||||
minLeadingWidth: 16,
|
||||
leading: Icon(AlignmentValue.iconOf(alignment)),
|
||||
title: Text(tr.alignment.name(alignment.name)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: !isEditing
|
||||
? [
|
||||
if (controller.editable)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => controller.toggleEdit(
|
||||
alignment, true),
|
||||
iconSize: 16,
|
||||
),
|
||||
if (controller.selectable)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(!isSelected
|
||||
? tr.generic.select
|
||||
: tr.generic.selected),
|
||||
onPressed: !isSelected
|
||||
? () => controller.select(alignment)
|
||||
: null,
|
||||
),
|
||||
]
|
||||
: DialogControls.done(
|
||||
context,
|
||||
() => controller.toggleEdit(
|
||||
alignment, false)),
|
||||
),
|
||||
return _wrapWithSelection(
|
||||
isSelected,
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ListTile(
|
||||
minLeadingWidth: 16,
|
||||
leading: Icon(AlignmentValue.iconOf(alignment)),
|
||||
title: Text(tr.alignment.name(alignment.name)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: !isEditing
|
||||
? [
|
||||
if (controller.editable)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () => controller
|
||||
.toggleEdit(alignment, true),
|
||||
iconSize: 16,
|
||||
),
|
||||
if (controller.selectable)
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(!isSelected
|
||||
? tr.generic.select
|
||||
: tr.generic.selected),
|
||||
onPressed: !isSelected
|
||||
? () =>
|
||||
controller.select(alignment)
|
||||
: null,
|
||||
),
|
||||
]
|
||||
: DialogControls.done(
|
||||
context,
|
||||
() => controller.toggleEdit(
|
||||
alignment, false)),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8)
|
||||
.copyWith(left: 56, top: 0),
|
||||
child: !isEditing
|
||||
? Text(description.isEmpty
|
||||
? tr.generic.noDescription
|
||||
: description)
|
||||
: TextField(
|
||||
controller: controller
|
||||
.textControllers[alignment]!,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8)
|
||||
.copyWith(left: 56, top: 0),
|
||||
child: !isEditing
|
||||
? Text(description.isEmpty
|
||||
? tr.generic.noDescription
|
||||
: description)
|
||||
: TextField(
|
||||
controller:
|
||||
controller.textControllers[alignment]!,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/select_moves_spells_controller.dart';
|
||||
|
||||
class SelectMovesSpellsBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.put<SelectMovesSpellsController>(
|
||||
SelectMovesSpellsController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,27 @@
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/ability_scores.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_utils.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/core/route_arguments.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_utils.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class SelectMovesSpellsController extends GetxController {
|
||||
final dirty = false.obs;
|
||||
final repo = Get.find<RepositoryService>();
|
||||
class SelectMovesSpellsController extends ChangeNotifier {
|
||||
var dirty = false;
|
||||
|
||||
final moves = <Move>[].obs;
|
||||
final spells = <Spell>[].obs;
|
||||
late final Rx<AbilityScores> abilityScores;
|
||||
late final Rx<CharacterClass> characterClass;
|
||||
var moves = <Move>[];
|
||||
var spells = <Spell>[];
|
||||
late AbilityScores abilityScores;
|
||||
late CharacterClass characterClass;
|
||||
late final void Function(List<Move> moves, List<Spell> spells) onChanged;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
final SelectMovesSpellsArguments args = Get.arguments;
|
||||
moves.value = args.moves.toList();
|
||||
spells.value = args.spells.toList();
|
||||
abilityScores = args.abilityScores.obs;
|
||||
characterClass = args.characterClass.obs;
|
||||
SelectMovesSpellsController(BuildContext context) {
|
||||
final SelectMovesSpellsArguments args = getArgs(context);
|
||||
moves = args.moves.toList();
|
||||
spells = args.spells.toList();
|
||||
abilityScores = args.abilityScores;
|
||||
characterClass = args.characterClass;
|
||||
onChanged = args.onChanged;
|
||||
}
|
||||
|
||||
@@ -35,6 +33,42 @@ class SelectMovesSpellsController extends GetxController {
|
||||
: b.category == MoveCategory.basic
|
||||
? 1
|
||||
: 0);
|
||||
|
||||
void addMoves(Iterable<Move> added) {
|
||||
moves = addByKey(moves, added);
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateMove(Move move) {
|
||||
moves = updateByKey(moves, [move]);
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void deleteMove(Move move) {
|
||||
moves = removeByKey(moves, [move]);
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addSpells(Iterable<Spell> added) {
|
||||
spells = addByKey(spells, added);
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateSpell(Spell spell) {
|
||||
spells = updateByKey(spells, [spell]);
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void deleteSpell(Spell spell) {
|
||||
spells = removeByKey(spells, [spell]);
|
||||
dirty = true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class SelectMovesSpellsArguments {
|
||||
@@ -52,3 +86,4 @@ class SelectMovesSpellsArguments {
|
||||
required this.characterClass,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,13 @@ import 'package:dungeon_paper/app/widgets/atoms/confirm_exit_view.dart';
|
||||
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/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controllers/select_moves_spells_controller.dart';
|
||||
|
||||
class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
|
||||
class SelectMovesSpellsView extends StatelessWidget {
|
||||
const SelectMovesSpellsView({
|
||||
super.key,
|
||||
});
|
||||
@@ -23,38 +22,38 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var titleStyle = Theme.of(context).textTheme.titleLarge;
|
||||
return ConfirmExitView(
|
||||
dirty: controller.dirty.value,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr.generic.selectEntity(tr.createCharacter.movesSpells.title)),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: AdvancedFloatingActionButton.extended(
|
||||
onPressed: _save,
|
||||
label: Text(tr.generic.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// MOVES TITLE
|
||||
Obx(() => Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
tr.entityCountNum(
|
||||
tn(Move),
|
||||
controller.moves.length,
|
||||
),
|
||||
style: titleStyle),
|
||||
)),
|
||||
// MOVES CARDS
|
||||
Obx(
|
||||
() => ListView(
|
||||
return Consumer<SelectMovesSpellsController>(
|
||||
builder: (context, controller, _) => ConfirmExitView(
|
||||
dirty: controller.dirty,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
tr.generic.selectEntity(tr.createCharacter.movesSpells.title)),
|
||||
centerTitle: true,
|
||||
),
|
||||
floatingActionButton: AdvancedFloatingActionButton.extended(
|
||||
onPressed: () => _save(context),
|
||||
label: Text(tr.generic.save),
|
||||
icon: const Icon(Icons.save),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// MOVES TITLE
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(
|
||||
tr.entityCountNum(
|
||||
tn(Move),
|
||||
controller.moves.length,
|
||||
),
|
||||
style: titleStyle),
|
||||
),
|
||||
// MOVES CARDS
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(8),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -68,63 +67,60 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: () => ModelPages.openMovePage(
|
||||
abilityScores:
|
||||
controller.abilityScores.value,
|
||||
context,
|
||||
abilityScores: controller.abilityScores,
|
||||
move: move,
|
||||
onSave: (move) => controller.moves.value =
|
||||
updateByKey(controller.moves, [move]),
|
||||
onSave: controller.updateMove,
|
||||
),
|
||||
onDelete: () => controller.moves.value =
|
||||
removeByKey(controller.moves, [move]),
|
||||
onDelete: () => controller.deleteMove(move),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
// ADD MOVES
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: OutlinedButton.icon(
|
||||
style: ButtonThemes.primaryOutlined(context),
|
||||
onPressed: () => ModelPages.openMovesList(
|
||||
character: Character.empty().copyWith(
|
||||
characterClass: controller.characterClass.value,
|
||||
// ADD MOVES
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: OutlinedButton.icon(
|
||||
style: ButtonThemes.primaryOutlined(context),
|
||||
onPressed: () => ModelPages.openMovesList(
|
||||
context,
|
||||
character: Character.empty().copyWith(
|
||||
characterClass: controller.characterClass,
|
||||
),
|
||||
preSelections: controller.moves,
|
||||
category: MoveCategory.advanced1,
|
||||
onSelected: (moves) {
|
||||
controller.addMoves(
|
||||
moves.map(
|
||||
(m) => m.copyWithInherited(favorite: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
preSelections: controller.moves,
|
||||
category: MoveCategory.advanced1,
|
||||
onSelected: (moves) {
|
||||
controller.dirty.value = true;
|
||||
controller.moves.value = addByKey(
|
||||
controller.moves,
|
||||
moves.map((m) => m.copyWithInherited(favorite: true)),
|
||||
);
|
||||
},
|
||||
label:
|
||||
Text(tr.generic.addEntity(tr.entityPlural(tn(Move)))),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
label:
|
||||
Text(tr.generic.addEntity(tr.entityPlural(tn(Move)))),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
// SPELLS TITLE
|
||||
Obx(() => Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8)
|
||||
.copyWith(top: 24),
|
||||
child: Text(
|
||||
tr.entityCount(
|
||||
tn(Spell),
|
||||
controller.spells.length,
|
||||
),
|
||||
style: titleStyle),
|
||||
)),
|
||||
// SPELL CARDS
|
||||
Obx(
|
||||
() => ListView(
|
||||
// SPELLS TITLE
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8)
|
||||
.copyWith(top: 24),
|
||||
child: Text(
|
||||
tr.entityCount(
|
||||
tn(Spell),
|
||||
controller.spells.length,
|
||||
),
|
||||
style: titleStyle),
|
||||
),
|
||||
// SPELL CARDS
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(8),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -139,8 +135,7 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
|
||||
ElevatedButton.icon(
|
||||
style: ButtonThemes.primaryElevated(context),
|
||||
onPressed: () {
|
||||
controller.spells.value =
|
||||
removeByKey(controller.spells, [spell]);
|
||||
controller.deleteSpell(spell);
|
||||
},
|
||||
label: Text(tr.generic.remove),
|
||||
icon: const Icon(Icons.remove),
|
||||
@@ -150,44 +145,50 @@ class SelectMovesSpellsView extends GetView<SelectMovesSpellsController> {
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
// ADD SPELLS
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: OutlinedButton.icon(
|
||||
style: ButtonThemes.primaryOutlined(context),
|
||||
onPressed: () => ModelPages.openSpellsList(
|
||||
character: Character.empty().copyWith(
|
||||
characterClass: controller.characterClass.value,
|
||||
// ADD SPELLS
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: OutlinedButton.icon(
|
||||
style: ButtonThemes.primaryOutlined(context),
|
||||
onPressed: () => ModelPages.openSpellsList(
|
||||
context,
|
||||
character: Character.empty().copyWith(
|
||||
characterClass: controller.characterClass,
|
||||
),
|
||||
list: controller.spells,
|
||||
onSelected: (spells) {
|
||||
controller.addSpells(
|
||||
spells.map(
|
||||
(m) => m.copyWithInherited(prepared: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
list: controller.spells,
|
||||
onSelected: (spells) {
|
||||
controller.dirty.value = true;
|
||||
controller.spells.value = addByKey(
|
||||
controller.spells,
|
||||
spells
|
||||
.map((m) => m.copyWithInherited(prepared: true)),
|
||||
);
|
||||
},
|
||||
label: Text(
|
||||
tr.generic.addEntity(tr.entityPlural(tn(Spell))),
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
label:
|
||||
Text(tr.generic.addEntity(tr.entityPlural(tn(Spell)))),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
const SizedBox(height: 80),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_save() {
|
||||
_save(BuildContext context) {
|
||||
final controller = Provider.of<SelectMovesSpellsController>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
controller.onChanged(controller.moves, controller.spells);
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/create_character_controller.dart';
|
||||
|
||||
class CreateCharacterBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.put<CreateCharacterController>(
|
||||
CreateCharacterController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:dungeon_paper/app/data/models/ability_scores.dart';
|
||||
import 'package:dungeon_paper/app/data/models/alignment.dart';
|
||||
import 'package:dungeon_paper/app/data/models/bio.dart';
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
@@ -7,40 +8,40 @@ import 'package:dungeon_paper/app/data/models/gear_choice.dart';
|
||||
import 'package:dungeon_paper/app/data/models/gear_selection.dart';
|
||||
import 'package:dungeon_paper/app/data/models/item.dart';
|
||||
import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/ability_scores.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/session_marks.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/data/models/user.dart';
|
||||
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/data/services/repository_provider.dart';
|
||||
import 'package:dungeon_paper/core/utils/uuid.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:dungeon_world_data/dungeon_world_data.dart' as dw;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
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 startingGear = <GearSelection>[].obs;
|
||||
final moves = <Move>[].obs;
|
||||
final spells = <Spell>[].obs;
|
||||
final alignment = Rx<AlignmentValue?>(null);
|
||||
final race = Rx<Race?>(null);
|
||||
class CreateCharacterController extends ChangeNotifier with RepositoryProviderMixin {
|
||||
var name = '';
|
||||
var avatarUrl = '';
|
||||
CharacterClass? characterClass;
|
||||
var abilityScores = AbilityScores.dungeonWorldAll(10);
|
||||
var startingGear = <GearSelection>[];
|
||||
var moves = <Move>[];
|
||||
var spells = <Spell>[];
|
||||
AlignmentValue? alignment;
|
||||
Race? race;
|
||||
|
||||
final repo = Get.find<RepositoryService>();
|
||||
final dirty = false.obs;
|
||||
var dirty = false;
|
||||
|
||||
User get user => Get.find<UserService>().current;
|
||||
static CreateCharacterController of(BuildContext context, {bool listen = false}) =>
|
||||
Provider.of<CreateCharacterController>(context, listen: listen);
|
||||
static Widget consumer(
|
||||
Widget Function(BuildContext, CreateCharacterController, Widget?) builder,
|
||||
) =>
|
||||
Consumer<CreateCharacterController>(builder: builder);
|
||||
|
||||
bool get isValid => [
|
||||
name.isNotEmpty,
|
||||
characterClass.value != null,
|
||||
alignment.value != null,
|
||||
race.value != null,
|
||||
characterClass != null,
|
||||
alignment != null,
|
||||
race != null,
|
||||
].every((element) => element == true);
|
||||
|
||||
List<Item> get items =>
|
||||
@@ -49,23 +50,23 @@ class CreateCharacterController extends GetxController {
|
||||
double get coins => GearChoice.selectionToCoins(startingGear);
|
||||
|
||||
void setBasicInfo(String name, String avatar) {
|
||||
this.name.value = name;
|
||||
avatarUrl.value = avatar;
|
||||
this.name = name;
|
||||
avatarUrl = avatar;
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void setClass(CharacterClass cls) {
|
||||
characterClass.value = cls;
|
||||
void setClass(BuildContext context, CharacterClass cls) {
|
||||
characterClass = cls;
|
||||
setStartingGear(
|
||||
cls.gearChoices
|
||||
.fold([], (all, cur) => [...all, ...cur.preselectedGearSelections]),
|
||||
);
|
||||
addStartingMoves();
|
||||
addStartingMoves(context);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void setAbilityScores(AbilityScores stats) {
|
||||
abilityScores.value = stats;
|
||||
abilityScores = stats;
|
||||
setDirty();
|
||||
}
|
||||
|
||||
@@ -73,13 +74,13 @@ class CreateCharacterController extends GetxController {
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
alignment.value = AlignmentValue.empty(type: selected).copyWith(
|
||||
alignment = AlignmentValue.empty(type: selected).copyWith(
|
||||
description: alignments.byType(selected),
|
||||
);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void setMovesSpells(List<Move> moves, List<Spell> spells) {
|
||||
void setMovesAndSpells(List<Move> moves, List<Spell> spells) {
|
||||
this.moves.clear();
|
||||
this.spells.clear();
|
||||
this.moves.addAll(moves.map((e) => e.copyWithInherited(favorite: true)));
|
||||
@@ -87,7 +88,7 @@ class CreateCharacterController extends GetxController {
|
||||
}
|
||||
|
||||
void setDirty() {
|
||||
dirty.value = true;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void setStartingGear(List<GearSelection> selections) {
|
||||
@@ -95,13 +96,12 @@ class CreateCharacterController extends GetxController {
|
||||
startingGear.addAll(selections);
|
||||
}
|
||||
|
||||
void addStartingMoves() {
|
||||
void addStartingMoves(BuildContext context) {
|
||||
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!.reference) &&
|
||||
m.category == MoveCategory.starting))
|
||||
.map(
|
||||
// favorite: move.category != MoveCategory.basic
|
||||
(move) => Move.fromDwMove(move, favorite: true),
|
||||
@@ -111,22 +111,22 @@ class CreateCharacterController extends GetxController {
|
||||
}
|
||||
|
||||
Character getAsCharacter() => Character.empty().copyWith(
|
||||
displayName: name.value,
|
||||
avatarUrl: avatarUrl.value,
|
||||
characterClass: characterClass.value,
|
||||
abilityScores: abilityScores.value,
|
||||
displayName: name,
|
||||
avatarUrl: avatarUrl,
|
||||
characterClass: characterClass,
|
||||
abilityScores: abilityScores,
|
||||
moves: moves,
|
||||
spells: spells,
|
||||
items: items,
|
||||
coins: coins,
|
||||
race: race.value,
|
||||
race: race,
|
||||
stats: CharacterStats(
|
||||
level: 1,
|
||||
currentHp: characterClass.value!.hp + abilityScores.value.conMod!,
|
||||
currentHp: characterClass!.hp + abilityScores.conMod!,
|
||||
currentXp: 0,
|
||||
),
|
||||
sessionMarks: [
|
||||
...(characterClass.value?.bonds
|
||||
...(characterClass?.bonds
|
||||
.map((bond) => SessionMark.bond(
|
||||
description: bond, completed: false, key: uuid()))
|
||||
.toList() ??
|
||||
@@ -136,7 +136,7 @@ class CreateCharacterController extends GetxController {
|
||||
bio: Bio(
|
||||
looks: '',
|
||||
description: '',
|
||||
alignment: alignment.value ?? AlignmentValue.empty(),
|
||||
alignment: alignment ?? AlignmentValue.empty(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/gear_selection.dart';
|
||||
import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
||||
import 'package:dungeon_paper/app/modules/AbilityScoresForm/controllers/ability_scores_form_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/BasicInfoForm/controllers/basic_info_form_controller.dart';
|
||||
@@ -22,41 +22,40 @@ 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/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../../core/dw_icons.dart';
|
||||
import '../../../widgets/chips/advanced_chip.dart';
|
||||
import '../controllers/create_character_controller.dart';
|
||||
|
||||
class CreateCharacterView extends GetView<CreateCharacterController> {
|
||||
class CreateCharacterView extends StatelessWidget with CharacterProviderMixin {
|
||||
const CreateCharacterView({super.key});
|
||||
|
||||
CharacterClass? get cls => controller.characterClass.value;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => ConfirmExitView(
|
||||
dirty: controller.dirty.value,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
// blendMode: BlendMode.darken,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black.withOpacity(0.85),
|
||||
appBar: AppBar(
|
||||
title: Container(),
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
floatingActionButton: Obx(
|
||||
() => AdvancedFloatingActionButton.extended(
|
||||
return CreateCharacterController.consumer(
|
||||
(context, controller, _) {
|
||||
final cls = controller.characterClass;
|
||||
return ConfirmExitView(
|
||||
dirty: controller.dirty,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
// blendMode: BlendMode.darken,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black.withOpacity(0.85),
|
||||
appBar: AppBar(
|
||||
title: Container(),
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
floatingActionButton: AdvancedFloatingActionButton.extended(
|
||||
onPressed: controller.isValid
|
||||
? () {
|
||||
Get.find<CharacterService>().createCharacter(
|
||||
charProvider.createCharacter(
|
||||
controller.getAsCharacter(),
|
||||
switchToCharacter: true,
|
||||
);
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.person_add),
|
||||
@@ -64,266 +63,267 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
|
||||
tr.generic.createEntity(tr.entity(tn(Character))),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
child: SizedBox(
|
||||
width: 340,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Basic Info
|
||||
_Card(
|
||||
leading: CharacterAvatar.squircle(
|
||||
size: 48,
|
||||
character: Character.empty().copyWith(
|
||||
avatarUrl: controller.avatarUrl.value,
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
child: SizedBox(
|
||||
width: 340,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Basic Info
|
||||
_Card(
|
||||
leading: CharacterAvatar.squircle(
|
||||
size: 48,
|
||||
character: Character.empty().copyWith(
|
||||
avatarUrl: controller.avatarUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: controller.name.isEmpty
|
||||
? Text(
|
||||
tr.createCharacter.basicInfo.defaultName,
|
||||
)
|
||||
: Text(controller.name.value),
|
||||
subtitle: controller.name.isEmpty
|
||||
? Text(tr.createCharacter.basicInfo.helpText)
|
||||
: Text(
|
||||
tr.createCharacter.basicInfo.description(
|
||||
cls?.name ?? '',
|
||||
),
|
||||
),
|
||||
valid: controller.name.isNotEmpty,
|
||||
onTap: () => Get.toNamed(
|
||||
Routes.createCharacterBasicInfo,
|
||||
arguments: BasicInfoFormArguments(
|
||||
avatarUrl: controller.avatarUrl.value,
|
||||
name: controller.name.value,
|
||||
onChanged: controller.setBasicInfo,
|
||||
),
|
||||
preventDuplicates: false,
|
||||
),
|
||||
),
|
||||
// Class
|
||||
_Card(
|
||||
title: cls == null
|
||||
? Text(tr.generic.selectEntity(
|
||||
tr.entity(tn(CharacterClass))))
|
||||
: Text(cls!.name),
|
||||
subtitle: cls == null
|
||||
? Text(tr.createCharacter.characterClass
|
||||
.noSelection)
|
||||
: Text(
|
||||
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!]
|
||||
: [],
|
||||
onSelected: (cls) => controller.setClass(cls),
|
||||
),
|
||||
preventDuplicates: false,
|
||||
),
|
||||
),
|
||||
// Race
|
||||
_Card(
|
||||
title: controller.race.value == null
|
||||
? Text(tr.generic
|
||||
.selectEntity(tr.entity(tn(Race))))
|
||||
: Text(controller.race.value!.name),
|
||||
subtitle: controller.race.value == null
|
||||
? Text(tr.generic
|
||||
.noEntitySelected(tr.entity(tn(Race))))
|
||||
: Text(
|
||||
controller.race.value!.description,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: cls != null
|
||||
? () => ModelPages.openRacesList(
|
||||
character: controller.getAsCharacter(),
|
||||
preSelection: controller.race.value,
|
||||
onSelected: (race) =>
|
||||
controller.race.value = race,
|
||||
title: controller.name.isEmpty
|
||||
? Text(
|
||||
tr.createCharacter.basicInfo
|
||||
.defaultName,
|
||||
)
|
||||
: null,
|
||||
valid: controller.race.value != null,
|
||||
),
|
||||
// Ability Scores
|
||||
_Card(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
title: Text(tr.generic.selectEntity(
|
||||
tr.entityPlural(tn(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),
|
||||
),
|
||||
onTap: () => Get.toNamed(
|
||||
Routes.createCharacterAbilityScores,
|
||||
arguments: AbilityScoresFormArguments(
|
||||
onChanged: (abilityScores) => controller
|
||||
.setAbilityScores(abilityScores),
|
||||
abilityScores: controller.abilityScores.value,
|
||||
),
|
||||
preventDuplicates: false,
|
||||
),
|
||||
),
|
||||
// Alignment
|
||||
_Card(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 0),
|
||||
valid: controller.alignment.value != null,
|
||||
title: Text(
|
||||
controller.alignment.value != null
|
||||
? [
|
||||
tr.entity(tn(AlignmentValue)),
|
||||
tr.alignment.name(controller
|
||||
.alignment.value!.type.name)
|
||||
].join(': ')
|
||||
: tr.generic.selectEntity(
|
||||
tr.entity(tn(AlignmentValue)),
|
||||
),
|
||||
),
|
||||
subtitle: controller.alignment.value != null
|
||||
? Text(
|
||||
controller.alignment.value!.description
|
||||
.isNotEmpty
|
||||
? controller
|
||||
.alignment.value!.description
|
||||
: tr.generic.noDescription,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(
|
||||
tr.generic.noEntitySelectedRequired(
|
||||
tr.entity(tn(AlignmentValue))),
|
||||
),
|
||||
onTap: cls != null
|
||||
? () => Get.toNamed(
|
||||
Routes.classAlignments,
|
||||
arguments: ClassAlignmentsArguments(
|
||||
onChanged: controller.setAlignment,
|
||||
alignments: controller
|
||||
.characterClass.value!.alignments,
|
||||
preselected:
|
||||
controller.alignment.value?.type,
|
||||
selectable: true,
|
||||
editable: true,
|
||||
: Text(controller.name),
|
||||
subtitle: controller.name.isEmpty
|
||||
? Text(
|
||||
tr.createCharacter.basicInfo.helpText)
|
||||
: Text(
|
||||
tr.createCharacter.basicInfo
|
||||
.description(
|
||||
cls?.name ?? '',
|
||||
),
|
||||
preventDuplicates: false,
|
||||
),
|
||||
valid: controller.name.isNotEmpty,
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.createCharacterBasicInfo,
|
||||
arguments: BasicInfoFormArguments(
|
||||
avatarUrl: controller.avatarUrl,
|
||||
name: controller.name,
|
||||
onChanged: controller.setBasicInfo,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Class
|
||||
_Card(
|
||||
title: cls == null
|
||||
? Text(tr.generic.selectEntity(
|
||||
tr.entity(tn(CharacterClass))))
|
||||
: Text(cls.name),
|
||||
subtitle: cls == null
|
||||
? Text(tr.createCharacter.characterClass
|
||||
.noSelection)
|
||||
: Text(
|
||||
tr.createCharacter.characterClass
|
||||
.description(
|
||||
cls.hp,
|
||||
cls.load,
|
||||
cls.damageDice.toString(),
|
||||
),
|
||||
),
|
||||
valid: cls != null,
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.createCharacterSelectClass,
|
||||
arguments: CharacterClassLibraryListArguments(
|
||||
preSelections:
|
||||
controller.characterClass != null
|
||||
? [controller.characterClass!]
|
||||
: [],
|
||||
onSelected: (cls) =>
|
||||
controller.setClass(context, cls),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Race
|
||||
_Card(
|
||||
title: controller.race == null
|
||||
? Text(tr.generic
|
||||
.selectEntity(tr.entity(tn(Race))))
|
||||
: Text(controller.race!.name),
|
||||
subtitle: controller.race == null
|
||||
? Text(tr.generic
|
||||
.noEntitySelected(tr.entity(tn(Race))))
|
||||
: Text(
|
||||
controller.race!.description,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: cls != null
|
||||
? () => ModelPages.openRacesList(
|
||||
context,
|
||||
character:
|
||||
controller.getAsCharacter(),
|
||||
preSelection: controller.race,
|
||||
onSelected: (race) =>
|
||||
controller.race = race,
|
||||
)
|
||||
: null,
|
||||
valid: controller.race != null,
|
||||
),
|
||||
// Ability Scores
|
||||
_Card(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
title: Text(tr.generic.selectEntity(
|
||||
tr.entityPlural(tn(AbilityScore)))),
|
||||
// subtitle: Text(
|
||||
// controller.abilityScores.stats
|
||||
// .map((stat) => '${stat.key}: ${stat}')
|
||||
// .join(', '),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: _AbilityScoreChipList(
|
||||
controller: controller),
|
||||
),
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.createCharacterAbilityScores,
|
||||
arguments: AbilityScoresFormArguments(
|
||||
onChanged: (abilityScores) => controller
|
||||
.setAbilityScores(abilityScores),
|
||||
abilityScores: controller.abilityScores,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Alignment
|
||||
_Card(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 0),
|
||||
valid: controller.alignment != null,
|
||||
title: Text(
|
||||
controller.alignment != null
|
||||
? [
|
||||
tr.entity(tn(AlignmentValue)),
|
||||
tr.alignment.name(
|
||||
controller.alignment!.type.name)
|
||||
].join(': ')
|
||||
: tr.generic.selectEntity(
|
||||
tr.entity(tn(AlignmentValue)),
|
||||
),
|
||||
),
|
||||
subtitle: controller.alignment != null
|
||||
? Text(
|
||||
controller.alignment!.description
|
||||
.isNotEmpty
|
||||
? controller.alignment!.description
|
||||
: tr.generic.noDescription,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
: Text(
|
||||
tr.generic.noEntitySelectedRequired(
|
||||
tr.entity(tn(AlignmentValue))),
|
||||
),
|
||||
onTap: cls != null
|
||||
? () => Navigator.of(context).pushNamed(
|
||||
Routes.classAlignments,
|
||||
arguments: ClassAlignmentsArguments(
|
||||
onChanged: controller.setAlignment,
|
||||
alignments: controller
|
||||
.characterClass!.alignments,
|
||||
preselected:
|
||||
controller.alignment?.type,
|
||||
selectable: true,
|
||||
editable: true,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
||||
// Starting Gear
|
||||
_Card(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
title: Text(
|
||||
tr.generic.selectEntity(
|
||||
tr.entity(tn(GearSelection)),
|
||||
// Starting Gear
|
||||
_Card(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
title: Text(
|
||||
tr.generic.selectEntity(
|
||||
tr.entity(tn(GearSelection)),
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Text(controller.items.isEmpty &&
|
||||
controller.coins == 0
|
||||
? tr.createCharacter.startingGear.helpText
|
||||
: [
|
||||
controller.coins > 0
|
||||
? tr.createCharacter.startingGear
|
||||
.coins(
|
||||
NumberFormat('#0.#')
|
||||
.format(controller.coins),
|
||||
)
|
||||
: null,
|
||||
controller.items
|
||||
.map((i) => tr
|
||||
.createCharacter.startingGear
|
||||
.item(
|
||||
subtitle: Text(controller.items.isEmpty &&
|
||||
controller.coins == 0
|
||||
? tr.createCharacter.startingGear.helpText
|
||||
: [
|
||||
controller.coins > 0
|
||||
? tr.createCharacter.startingGear
|
||||
.coins(
|
||||
NumberFormat('#0.#')
|
||||
.format(i.amount),
|
||||
i.name,
|
||||
))
|
||||
.join(', '),
|
||||
].whereType<String>().join(', ')),
|
||||
onTap: cls != null
|
||||
? () => Get.toNamed(
|
||||
Routes.createCharacterStartingGear,
|
||||
arguments: StartingGearFormArguments(
|
||||
onChanged: controller.setStartingGear,
|
||||
selectedOptions:
|
||||
controller.startingGear,
|
||||
characterClass: cls!,
|
||||
.format(controller.coins),
|
||||
)
|
||||
: null,
|
||||
controller.items
|
||||
.map((i) => tr.createCharacter
|
||||
.startingGear
|
||||
.item(
|
||||
NumberFormat('#0.#')
|
||||
.format(i.amount),
|
||||
i.name,
|
||||
))
|
||||
.join(', '),
|
||||
].whereType<String>().join(', ')),
|
||||
onTap: cls != null
|
||||
? () => Navigator.of(context).pushNamed(
|
||||
Routes.createCharacterStartingGear,
|
||||
arguments: StartingGearFormArguments(
|
||||
onChanged:
|
||||
controller.setStartingGear,
|
||||
selectedOptions:
|
||||
controller.startingGear,
|
||||
characterClass: cls,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
valid: cls == null
|
||||
? false
|
||||
: cls.gearChoices.every(
|
||||
(c) => c.selections.any(
|
||||
(s) => controller.startingGear
|
||||
.map((x) => x.key)
|
||||
.contains(s.key),
|
||||
),
|
||||
preventDuplicates: false,
|
||||
)
|
||||
: null,
|
||||
valid: cls == null
|
||||
? false
|
||||
: cls!.gearChoices.every(
|
||||
(c) => c.selections.any(
|
||||
(s) => controller.startingGear
|
||||
.map((x) => x.key)
|
||||
.contains(s.key),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Moves & Spells
|
||||
_Card(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
title: Text(
|
||||
tr.generic.selectEntity(
|
||||
(cls?.isSpellcaster ?? false)
|
||||
? tr.createCharacter.movesSpells.title
|
||||
: tr.entityPlural(tn(Move)),
|
||||
),
|
||||
// Moves & Spells
|
||||
_Card(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
title: Text(
|
||||
tr.generic.selectEntity(
|
||||
(cls?.isSpellcaster ?? false)
|
||||
? tr.createCharacter.movesSpells.title
|
||||
: tr.entityPlural(tn(Move)),
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
(cls?.isSpellcaster ?? false)
|
||||
? tr.createCharacter.movesSpells
|
||||
.description(
|
||||
controller.moves.length,
|
||||
controller.spells.length,
|
||||
)
|
||||
: tr.entityCountNum(
|
||||
tn(Move),
|
||||
controller.moves.length,
|
||||
),
|
||||
),
|
||||
onTap: cls != null
|
||||
? () => Get.toNamed(
|
||||
Routes.createCharacterMovesSpells,
|
||||
arguments: SelectMovesSpellsArguments(
|
||||
onChanged: controller.setMovesSpells,
|
||||
moves: controller.moves,
|
||||
spells: controller.spells,
|
||||
abilityScores:
|
||||
controller.abilityScores.value,
|
||||
characterClass:
|
||||
controller.characterClass.value!,
|
||||
subtitle: Text(
|
||||
(cls?.isSpellcaster ?? false)
|
||||
? tr.createCharacter.movesSpells
|
||||
.description(
|
||||
controller.moves.length,
|
||||
controller.spells.length,
|
||||
)
|
||||
: tr.entityCountNum(
|
||||
tn(Move),
|
||||
controller.moves.length,
|
||||
),
|
||||
preventDuplicates: false,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: cls != null
|
||||
? () => Navigator.of(context).pushNamed(
|
||||
Routes.createCharacterMovesSpells,
|
||||
arguments: SelectMovesSpellsArguments(
|
||||
onChanged:
|
||||
controller.setMovesAndSpells,
|
||||
moves: controller.moves,
|
||||
spells: controller.spells,
|
||||
abilityScores:
|
||||
controller.abilityScores,
|
||||
characterClass:
|
||||
controller.characterClass!,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -332,8 +332,8 @@ class CreateCharacterView extends GetView<CreateCharacterController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -350,7 +350,7 @@ class _AbilityScoreChipList extends StatelessWidget {
|
||||
return Wrap(
|
||||
spacing: 2,
|
||||
runSpacing: 2,
|
||||
children: controller.abilityScores.value.stats
|
||||
children: controller.abilityScores.stats
|
||||
.map(
|
||||
(stat) => ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
@@ -371,7 +371,7 @@ class _AbilityScoreChipList extends StatelessWidget {
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
label: Text(
|
||||
'${stat.key}: ${stat.value}',
|
||||
textScaleFactor: 0.8,
|
||||
textScaler: const TextScaler.linear(0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -437,3 +437,4 @@ class _Card extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class HomeBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ExpandedCardDialogView<T> extends GetView {
|
||||
class ExpandedCardDialogView<T> extends StatelessWidget {
|
||||
const ExpandedCardDialogView({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.builder,
|
||||
required this.heroTag,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final Widget Function(BuildContext context) builder;
|
||||
final String? heroTag;
|
||||
@@ -16,7 +14,7 @@ class ExpandedCardDialogView<T> extends GetView {
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
// color: Colors.black.withOpacity(0.75),
|
||||
padding: const EdgeInsets.all(32),
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'package:dungeon_paper/app/data/services/loading_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/loading_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.dart';
|
||||
import 'package:dungeon_paper/app/routes/app_pages.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/user_menu.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class HomeAppBar extends StatelessWidget
|
||||
with LoadingServiceMixin, UserServiceMixin
|
||||
with LoadingProviderMixin, UserProviderMixin
|
||||
implements PreferredSizeWidget {
|
||||
const HomeAppBar({super.key});
|
||||
|
||||
@@ -18,7 +17,8 @@ class HomeAppBar extends StatelessWidget
|
||||
automaticallyImplyLeading: false,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () => Get.toNamed(Routes.universalSearch),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pushNamed(Routes.universalSearch),
|
||||
),
|
||||
actions: const [
|
||||
// if (user.flags['su'] == true)
|
||||
@@ -37,4 +37,3 @@ class HomeAppBar extends StatelessWidget
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(64);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ import 'package:dungeon_paper/app/data/models/meta.dart';
|
||||
import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/library_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/library_provider.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/character_utils.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
@@ -30,41 +29,23 @@ import 'package:dungeon_paper/core/utils/builder_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'local_widgets/home_character_actions_summary.dart';
|
||||
|
||||
class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
class HomeCharacterActionsView extends StatelessWidget
|
||||
with CharacterProviderMixin {
|
||||
const HomeCharacterActionsView({super.key});
|
||||
|
||||
Character get char => controller.current;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final builder = ItemBuilder.builder(
|
||||
leadingBuilder: (context, index) => const HomeCharacterActionsSummary(),
|
||||
leadingCount: 1,
|
||||
itemBuilder: (context, index) {
|
||||
switch (char.actionCategories.elementAt(index)) {
|
||||
case 'Move':
|
||||
return movesList ?? const SizedBox.shrink();
|
||||
case 'Spell':
|
||||
return spellsList ?? const SizedBox.shrink();
|
||||
case 'Item':
|
||||
return itemsList ?? const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
itemCount: char.actionCategories.length,
|
||||
);
|
||||
return PageStorage(
|
||||
bucket: PageStorageBucket(),
|
||||
child: Obx(
|
||||
() {
|
||||
child: CharacterProvider.consumer(
|
||||
(context, controller, _) {
|
||||
if (controller.maybeCurrent == null) {
|
||||
return Container();
|
||||
}
|
||||
final builder = _getBuilder(controller);
|
||||
return builder.asListView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
);
|
||||
@@ -73,7 +54,28 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget? get movesList {
|
||||
ItemBuilder _getBuilder(CharacterProvider ctrl) {
|
||||
final char = ctrl.current;
|
||||
return ItemBuilder.builder(
|
||||
leadingBuilder: (context, index) => const HomeCharacterActionsSummary(),
|
||||
leadingCount: 1,
|
||||
itemCount: char.actionCategories.length,
|
||||
itemBuilder: (context, index) {
|
||||
switch (char.actionCategories.elementAt(index)) {
|
||||
case 'Move':
|
||||
return movesList(context, ctrl) ?? const SizedBox.shrink();
|
||||
case 'Spell':
|
||||
return spellsList(context, ctrl) ?? const SizedBox.shrink();
|
||||
case 'Item':
|
||||
return itemsList(context, ctrl) ?? const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget? movesList(BuildContext context, CharacterProvider controller) {
|
||||
if (char.settings.actionCategories.hidden.contains('Move')) {
|
||||
return null;
|
||||
}
|
||||
@@ -86,6 +88,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
EntityEditMenu(
|
||||
onDelete: null,
|
||||
onEdit: () => ModelPages.openRacePage(
|
||||
context,
|
||||
race: char.race,
|
||||
abilityScores: char.abilityScores,
|
||||
onSave: (race) => controller.updateCharacter(
|
||||
@@ -106,7 +109,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _openBasicMoves,
|
||||
onPressed: () => _openBasicMoves(context),
|
||||
child: Text(
|
||||
tr.actions.moves.basic,
|
||||
),
|
||||
@@ -115,7 +118,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _openSpecialMoves,
|
||||
onPressed: () => _openSpecialMoves(context),
|
||||
child: Text(
|
||||
tr.actions.moves.special,
|
||||
),
|
||||
@@ -170,6 +173,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
EntityEditMenu(
|
||||
onDelete: onDelete,
|
||||
onEdit: () => ModelPages.openMovePage(
|
||||
context,
|
||||
move: move,
|
||||
abilityScores: char.abilityScores,
|
||||
onSave: onSave(true),
|
||||
@@ -181,7 +185,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget? get spellsList {
|
||||
Widget? spellsList(BuildContext context, CharacterProvider controller) {
|
||||
if (char.settings.actionCategories.hidden.contains('Spell')) {
|
||||
return null;
|
||||
}
|
||||
@@ -204,6 +208,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
EntityEditMenu(
|
||||
onDelete: onDelete,
|
||||
onEdit: () => ModelPages.openSpellPage(
|
||||
context,
|
||||
spell: spell,
|
||||
classKeys: spell.classKeys,
|
||||
abilityScores: char.abilityScores,
|
||||
@@ -216,7 +221,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget? get itemsList {
|
||||
Widget? itemsList(BuildContext context, CharacterProvider controller) {
|
||||
if (char.settings.actionCategories.hidden.contains('Item')) {
|
||||
return null;
|
||||
}
|
||||
@@ -244,6 +249,7 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
EntityEditMenu(
|
||||
onDelete: onDelete,
|
||||
onEdit: () => ModelPages.openItemPage(
|
||||
context,
|
||||
item: item,
|
||||
onSave: onSave(true),
|
||||
),
|
||||
@@ -286,8 +292,8 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
);
|
||||
}
|
||||
|
||||
void _onReorder(int oldIndex, int newIndex) {
|
||||
controller.updateCharacter(
|
||||
void _onReorder(BuildContext context, int oldIndex, int newIndex) {
|
||||
charProvider.updateCharacter(
|
||||
char.copyWith(
|
||||
settings: char.settings.copyWith(
|
||||
actionCategories: char.settings.actionCategories.copyWithInherited(
|
||||
@@ -305,16 +311,18 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
);
|
||||
}
|
||||
|
||||
void _openBasicMoves() {
|
||||
void _openBasicMoves(BuildContext context) {
|
||||
ModelPages.openMovesList(
|
||||
context,
|
||||
category: MoveCategory.basic,
|
||||
initialTab: FiltersGroup.playbook,
|
||||
abilityScores: char.abilityScores,
|
||||
);
|
||||
}
|
||||
|
||||
void _openSpecialMoves() {
|
||||
void _openSpecialMoves(BuildContext context) {
|
||||
ModelPages.openMovesList(
|
||||
context,
|
||||
category: MoveCategory.special,
|
||||
initialTab: FiltersGroup.playbook,
|
||||
abilityScores: char.abilityScores,
|
||||
@@ -322,8 +330,8 @@ class HomeCharacterActionsView extends GetView<CharacterService> {
|
||||
}
|
||||
}
|
||||
|
||||
class ActionsCardList<T extends WithMeta> extends GetView<CharacterService>
|
||||
with LibraryServiceMixin, RepositoryServiceMixin {
|
||||
class ActionsCardList<T extends WithMeta> extends StatelessWidget
|
||||
with CharacterProviderMixin {
|
||||
const ActionsCardList({
|
||||
super.key,
|
||||
required this.route,
|
||||
@@ -354,58 +362,62 @@ class ActionsCardList<T extends WithMeta> extends GetView<CharacterService>
|
||||
}) cardBuilder;
|
||||
final List<T> list;
|
||||
final int index;
|
||||
final void Function(int oldIndex, int newIndex) onReorder;
|
||||
final void Function(BuildContext context, int oldIndex, int newIndex)
|
||||
onReorder;
|
||||
final List<MenuEntry<String>> menuLeading;
|
||||
final List<MenuEntry<String>> menuTrailing;
|
||||
|
||||
Character get char => controller.current;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CategorizedList(
|
||||
initiallyExpanded: true,
|
||||
title: Text(tr.entityPlural(typeName)),
|
||||
itemPadding: const EdgeInsets.only(bottom: 8),
|
||||
titleTrailing: [
|
||||
TextButton.icon(
|
||||
onPressed: () => Get.toNamed(
|
||||
route,
|
||||
arguments: addPageArguments(
|
||||
onSelected: (items) => library.upsertToCharacter(items,
|
||||
forkBehavior: ForkBehavior.fork),
|
||||
return CharacterProvider.consumer((context, controller, _) {
|
||||
return CategorizedList(
|
||||
initiallyExpanded: true,
|
||||
title: Text(tr.entityPlural(typeName)),
|
||||
itemPadding: const EdgeInsets.only(bottom: 8),
|
||||
titleTrailing: [
|
||||
LibraryProvider.consumer((context, library, _) => TextButton.icon(
|
||||
onPressed: () => Navigator.pushNamed(
|
||||
context,
|
||||
route,
|
||||
arguments: addPageArguments(
|
||||
onSelected: (items) => library.upsertToCharacter(items,
|
||||
forkBehavior: ForkBehavior.fork),
|
||||
),
|
||||
),
|
||||
label: Text(tr.generic.addEntity(tr.entityPlural(typeName))),
|
||||
icon: const Icon(Icons.add),
|
||||
)),
|
||||
GroupSortMenu(
|
||||
index: index,
|
||||
totalItemCount: Character.allActionCategories.length,
|
||||
onReorder: onReorder,
|
||||
leading: menuLeading,
|
||||
trailing: menuTrailing,
|
||||
)
|
||||
],
|
||||
leading: leading.map((obj) => _wrapChild(child: obj)).toList(),
|
||||
trailing: trailing.map((obj) => _wrapChild(child: obj)).toList(),
|
||||
children: [
|
||||
...list.map(
|
||||
(obj) => _wrapChild(
|
||||
key: PageStorageKey('type-$T-${obj.key}'),
|
||||
child: cardBuilder(
|
||||
obj,
|
||||
onDelete: _confirmDeleteDlg(context, obj, obj.displayName),
|
||||
onSave: (fork) => (obj) {
|
||||
final library = LibraryProvider.of(context);
|
||||
library.upsertToCharacter([obj],
|
||||
forkBehavior: ForkBehavior.none);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
label: Text(tr.generic.addEntity(tr.entityPlural(typeName))),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
GroupSortMenu(
|
||||
index: index,
|
||||
totalItemCount: Character.allActionCategories.length,
|
||||
onReorder: onReorder,
|
||||
leading: menuLeading,
|
||||
trailing: menuTrailing,
|
||||
)
|
||||
],
|
||||
leading: leading.map((obj) => _wrapChild(child: obj)).toList(),
|
||||
trailing: trailing.map((obj) => _wrapChild(child: obj)).toList(),
|
||||
children: [
|
||||
...list.map(
|
||||
(obj) => _wrapChild(
|
||||
key: PageStorageKey('type-$T-${obj.key}'),
|
||||
child: cardBuilder(
|
||||
obj,
|
||||
onDelete: _confirmDeleteDlg(context, obj, obj.displayName),
|
||||
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>(
|
||||
controller.current, oldIndex, newIndex)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _wrapChild({Key? key, required Widget child}) => Padding(
|
||||
@@ -415,16 +427,18 @@ class ActionsCardList<T extends WithMeta> extends GetView<CharacterService>
|
||||
);
|
||||
|
||||
void Function() _confirmDeleteDlg(
|
||||
BuildContext context, T object, String name) {
|
||||
return () => deleteDialog.confirm(
|
||||
context,
|
||||
DeleteDialogOptions(
|
||||
entityName: name,
|
||||
entityKind: tr.entity(typeName),
|
||||
),
|
||||
() => controller.updateCharacter(
|
||||
CharacterUtils.removeByType<T>(char, [object]),
|
||||
BuildContext context,
|
||||
T object,
|
||||
String name,
|
||||
) {
|
||||
return () {
|
||||
awaitDeleteConfirmation(context, name, () {
|
||||
charProvider.updateCharacter(
|
||||
char.copyWithInherited(
|
||||
moves: char.moves.where((x) => x.key != object.key).toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/models/item.dart';
|
||||
import 'package:dungeon_paper/app/data/models/note.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/character_utils.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
||||
import 'package:dungeon_paper/app/themes/button_themes.dart';
|
||||
import 'package:dungeon_paper/app/widgets/cards/note_card.dart';
|
||||
import 'package:dungeon_paper/app/widgets/dialogs/confirm_delete_dialog.dart';
|
||||
import 'package:dungeon_paper/app/widgets/menus/entity_edit_menu.dart';
|
||||
import 'package:dungeon_paper/app/widgets/menus/group_sort_menu.dart';
|
||||
import 'package:dungeon_paper/app/widgets/molecules/categorized_list.dart';
|
||||
@@ -14,21 +11,20 @@ import 'package:dungeon_paper/core/storage_handler/storage_handler.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class HomeCharacterJournalView extends GetView<CharacterService> {
|
||||
class HomeCharacterJournalView extends StatelessWidget
|
||||
with CharacterProviderMixin {
|
||||
const HomeCharacterJournalView({super.key});
|
||||
|
||||
Character get char => controller.current;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageStorage(
|
||||
bucket: PageStorageBucket(),
|
||||
child: Obx(() {
|
||||
child: CharacterProvider.consumer((context, controller, _) {
|
||||
if (controller.maybeCurrent == null) {
|
||||
return Container();
|
||||
}
|
||||
final char = controller.current;
|
||||
// return ReorderableListView(
|
||||
return ListView(
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -79,6 +75,7 @@ class HomeCharacterJournalView extends GetView<CharacterService> {
|
||||
tn(Note),
|
||||
),
|
||||
onEdit: () => ModelPages.openNotePage(
|
||||
context,
|
||||
note: note,
|
||||
onSave: (note) {
|
||||
controller.updateCharacter(
|
||||
@@ -105,7 +102,6 @@ class HomeCharacterJournalView extends GetView<CharacterService> {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO use existing confirmDelete
|
||||
void Function() confirmDelete<T>(
|
||||
BuildContext context,
|
||||
T object,
|
||||
@@ -113,56 +109,17 @@ class HomeCharacterJournalView extends GetView<CharacterService> {
|
||||
String typeName,
|
||||
) {
|
||||
return () async {
|
||||
final result = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title:
|
||||
Text(tr.dialogs.confirmations.delete.title(tr.entity(typeName))),
|
||||
content: Text(
|
||||
tr.dialogs.confirmations.delete.body(tr.entity(typeName), name)),
|
||||
actions: [
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.close),
|
||||
label: Text(tr.generic.cancel),
|
||||
onPressed: () => Get.back(result: false),
|
||||
style: ButtonThemes.primaryElevated(context),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.delete),
|
||||
label: Text(tr.generic.remove),
|
||||
onPressed: () => Get.back(result: true),
|
||||
style: ButtonThemes.errorElevated(context),
|
||||
),
|
||||
const SizedBox(width: 0),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
switch (T) {
|
||||
case == Note:
|
||||
controller.updateCharacter(
|
||||
char.copyWith(notes: removeByKey(char.notes, [object as Note])),
|
||||
);
|
||||
break;
|
||||
case == Spell:
|
||||
controller.updateCharacter(
|
||||
char.copyWith(
|
||||
spells: removeByKey(char.spells, [object as Spell])),
|
||||
);
|
||||
break;
|
||||
case == Item:
|
||||
controller.updateCharacter(
|
||||
char.copyWith(items: removeByKey(char.items, [object as Item])),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw TypeError();
|
||||
}
|
||||
}
|
||||
awaitDeleteConfirmation(context, name, () {
|
||||
charProvider.updateCharacter(
|
||||
char.copyWith(notes: removeByKey(char.notes, [object as Note])),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _move(int oldIndex, int newIndex) {
|
||||
Future<void> _move(BuildContext context, int oldIndex, int newIndex) {
|
||||
final controller = CharacterProvider.of(context);
|
||||
final char = controller.current;
|
||||
return controller.updateCharacter(
|
||||
char.copyWith(
|
||||
settings: char.settings.copyWith(
|
||||
@@ -181,3 +138,4 @@ class HomeCharacterJournalView extends GetView<CharacterService> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/dice_utils.dart';
|
||||
import 'package:dungeon_paper/app/modules/Home/views/local_widgets/home_character_extras.dart';
|
||||
import 'package:dungeon_paper/app/themes/button_themes.dart';
|
||||
@@ -12,32 +12,33 @@ import 'package:dungeon_paper/core/utils/builder_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/math_utils.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 {
|
||||
class HomeCharacterView extends StatelessWidget with HomeCharacterPaddingMixin {
|
||||
const HomeCharacterView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() {
|
||||
return CharacterProvider.consumer(
|
||||
(context, controller, _) {
|
||||
final char = controller.maybeCurrent;
|
||||
if (char == null) {
|
||||
return Container();
|
||||
}
|
||||
return HomeCharacterLayout(
|
||||
leftCol: _buildLeftCol(context),
|
||||
leftCol: _buildLeftCol(context, controller),
|
||||
rightCol: const HomeCharacterDynamicCards(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildLeftCol(BuildContext context) {
|
||||
List<Widget> _buildLeftCol(
|
||||
BuildContext context,
|
||||
CharacterProvider controller,
|
||||
) {
|
||||
final char = controller.current;
|
||||
final abilityScores = char.abilityScores.stats;
|
||||
|
||||
@@ -71,8 +72,9 @@ class HomeCharacterView extends GetView<CharacterService>
|
||||
// visualDensity: VisualDensity.compact,
|
||||
label: char.damageDice.toString(),
|
||||
tooltip: tr.character.data.damageDice,
|
||||
onPressed: () => Get.dialog(
|
||||
DamageDiceDialog(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => DamageDiceDialog(
|
||||
damage: char.stats.damageDice,
|
||||
defaultDamage: char.defaultDamageDice,
|
||||
abilityScores: char.abilityScores,
|
||||
@@ -90,8 +92,9 @@ class HomeCharacterView extends GetView<CharacterService>
|
||||
icon: const Icon(DwIcons.armor),
|
||||
// visualDensity: VisualDensity.compact,
|
||||
label: char.armor.toString(),
|
||||
onPressed: () => Get.dialog(
|
||||
ArmorDialog(
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => ArmorDialog(
|
||||
armor: char.stats.armor,
|
||||
defaultArmor: char.defaultArmor,
|
||||
onChanged: (armor) => controller.updateCharacter(
|
||||
@@ -122,7 +125,9 @@ class HomeCharacterView extends GetView<CharacterService>
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => DiceUtils.openRollDialog(
|
||||
char.rollButtons[0].diceFor(char)),
|
||||
context,
|
||||
char.rollButtons[0].diceFor(char),
|
||||
),
|
||||
style: ButtonThemes.primaryElevated(context),
|
||||
label: Text(char.rollButtons[0].label),
|
||||
icon: const Icon(DwIcons.dice_d6),
|
||||
@@ -132,7 +137,9 @@ class HomeCharacterView extends GetView<CharacterService>
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => DiceUtils.openRollDialog(
|
||||
char.rollButtons[1].diceFor(char)),
|
||||
context,
|
||||
char.rollButtons[1].diceFor(char),
|
||||
),
|
||||
style: ButtonThemes.primaryElevated(context),
|
||||
label: Text(char.rollButtons[1].label),
|
||||
icon: const Icon(DwIcons.dice_d6),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:dungeon_paper/app/data/models/note.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/character_utils.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
|
||||
@@ -13,24 +13,24 @@ class HomeFAB extends StatefulWidget {
|
||||
State<HomeFAB> createState() => _HomeFABState();
|
||||
}
|
||||
|
||||
class _HomeFABState extends State<HomeFAB> with CharacterServiceMixin {
|
||||
class _HomeFABState extends State<HomeFAB> with CharacterProviderMixin {
|
||||
late bool inPageRange;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
inPageRange = getPageIsInRange();
|
||||
charService.pageController.addListener(_refresh);
|
||||
charProvider.pageController.addListener(_refresh);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
charService.pageController.removeListener(_refresh);
|
||||
charProvider.pageController.removeListener(_refresh);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool getPageIsInRange() {
|
||||
final distance = (charService.page - pageNum.toDouble()).abs();
|
||||
final distance = (charProvider.page - pageNum.toDouble()).abs();
|
||||
return distance <= 0.5;
|
||||
}
|
||||
|
||||
@@ -60,8 +60,9 @@ class _HomeFABState extends State<HomeFAB> with CharacterServiceMixin {
|
||||
),
|
||||
onPressed: inPageRange
|
||||
? () => ModelPages.openNotePage(
|
||||
context,
|
||||
note: null,
|
||||
onSave: (note) => charService.updateCharacter(
|
||||
onSave: (note) => charProvider.updateCharacter(
|
||||
CharacterUtils.addByType<Note>(char, [note]),
|
||||
),
|
||||
)
|
||||
@@ -77,3 +78,4 @@ class _HomeFABState extends State<HomeFAB> with CharacterServiceMixin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import 'package:dungeon_paper/app/data/services/loading_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/loading_provider.dart';
|
||||
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/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/utils/math_utils.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:skeleton_loader/skeleton_loader.dart';
|
||||
|
||||
class HomeLoaderView extends GetView with LoadingServiceMixin {
|
||||
class HomeLoaderView extends StatelessWidget {
|
||||
const HomeLoaderView({super.key});
|
||||
|
||||
String get title {
|
||||
if (loadingService.loadingUser) {
|
||||
final context = appGlobalKey.currentContext!;
|
||||
final loadingProvider = Provider.of<LoadingProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
|
||||
if (loadingProvider.loadingUser) {
|
||||
return tr.loading.user;
|
||||
}
|
||||
|
||||
if (loadingService.loadingCharacters) {
|
||||
if (loadingProvider.loadingCharacters) {
|
||||
return tr.loading.characters;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/services/loading_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/loading_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/user_provider.dart';
|
||||
import 'package:dungeon_paper/app/modules/Home/views/home_app_bar.dart';
|
||||
import 'package:dungeon_paper/app/modules/Home/views/home_character_actions_view.dart';
|
||||
import 'package:dungeon_paper/app/modules/Home/views/home_character_journal_view.dart';
|
||||
@@ -11,24 +12,21 @@ 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/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// import '../../../widgets/atoms/debug_menu.dart';
|
||||
import '../../../data/services/character_service.dart';
|
||||
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 StatelessWidget
|
||||
with UserProviderMixin, LoadingProviderMixin, CharacterProviderMixin {
|
||||
const HomeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const HomeAppBar(),
|
||||
body: Obx(
|
||||
() {
|
||||
body: CharacterProvider.consumer(
|
||||
(context, controller, _) {
|
||||
const children = [
|
||||
HomeCharacterActionsView(),
|
||||
HomeCharacterView(),
|
||||
@@ -37,7 +35,7 @@ class HomeView extends GetView<CharacterService>
|
||||
|
||||
return isLoading
|
||||
? const HomeLoaderView()
|
||||
: maybeChar != null
|
||||
: controller.maybeCurrent != null
|
||||
? PageView(
|
||||
controller: controller.pageController,
|
||||
children: children.map(_fractionalSizedBox).toList(),
|
||||
@@ -45,32 +43,36 @@ class HomeView extends GetView<CharacterService>
|
||||
: const HomeEmptyState();
|
||||
},
|
||||
),
|
||||
floatingActionButton: Obx(
|
||||
() => maybeChar != null ? const HomeFAB() : const SizedBox.shrink()),
|
||||
bottomNavigationBar: Obx(
|
||||
() => maybeChar != null
|
||||
? HomeNavBar(pageController: controller.pageController)
|
||||
floatingActionButton: CharacterProvider.consumer(
|
||||
(context, controller, _) =>
|
||||
maybeChar != null ? const HomeFAB() : const SizedBox.shrink(),
|
||||
),
|
||||
bottomNavigationBar: CharacterProvider.consumer(
|
||||
(context, controller, _) => maybeChar != null
|
||||
? CharacterProvider.consumer((context, controller, _) =>
|
||||
HomeNavBar(pageController: controller.pageController))
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
PageControllerFractionalBox _fractionalSizedBox(Widget child) =>
|
||||
PageControllerFractionalBox(
|
||||
controller: controller.pageController,
|
||||
child: child,
|
||||
Widget _fractionalSizedBox(Widget child) => CharacterProvider.consumer(
|
||||
(context, controller, _) => PageControllerFractionalBox(
|
||||
controller: controller.pageController,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
bool get isLoading {
|
||||
debugPrint('afterFirstLoad: ${loadingService.afterFirstLoad}, '
|
||||
'loadingUser: ${loadingService.loadingUser}, '
|
||||
'loadingCharacters: ${loadingService.loadingCharacters}');
|
||||
return !loadingService.afterFirstLoad &&
|
||||
(loadingService.loadingUser || loadingService.loadingCharacters);
|
||||
debugPrint('afterFirstLoad: ${loadingProvider.afterFirstLoad}, '
|
||||
'loadingUser: ${loadingProvider.loadingUser}, '
|
||||
'loadingCharacters: ${loadingProvider.loadingCharacters}');
|
||||
return !loadingProvider.afterFirstLoad &&
|
||||
(loadingProvider.loadingUser || loadingProvider.loadingCharacters);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeEmptyState extends StatelessWidget with UserServiceMixin {
|
||||
class HomeEmptyState extends StatelessWidget with UserProviderMixin {
|
||||
const HomeEmptyState({super.key});
|
||||
|
||||
@override
|
||||
@@ -113,7 +115,8 @@ class HomeEmptyState extends StatelessWidget with UserServiceMixin {
|
||||
ElevatedButton.icon(
|
||||
label: Text(tr.auth.login.button),
|
||||
icon: const Icon(Icons.login),
|
||||
onPressed: () => Get.toNamed(Routes.login),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pushNamed(Routes.login),
|
||||
style: ButtonThemes.primaryElevated(context),
|
||||
),
|
||||
],
|
||||
@@ -139,7 +142,8 @@ class HomeEmptyState extends StatelessWidget with UserServiceMixin {
|
||||
ElevatedButton.icon(
|
||||
label: Text(tr.generic.createEntity(tr.entity(tn(Character)))),
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () => Get.toNamed(Routes.createCharacter),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pushNamed(Routes.createCharacter),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,89 +1,88 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
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/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'home_character_actions_filters.dart';
|
||||
|
||||
class HomeCharacterActionsSummary extends GetView<CharacterService> {
|
||||
const HomeCharacterActionsSummary({
|
||||
super.key,
|
||||
});
|
||||
|
||||
Character get char => controller.current;
|
||||
class HomeCharacterActionsSummary extends StatelessWidget {
|
||||
const HomeCharacterActionsSummary({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
PrimaryChip(
|
||||
// visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(DwIcons.dumbbell, size: 16),
|
||||
label: tr.home.summary.load
|
||||
.label(char.currentLoad, char.maxLoad),
|
||||
tooltip: tr.home.summary.load.tooltip,
|
||||
backgroundColor: _loadColor(char.currentLoad, char.maxLoad),
|
||||
onPressed: () => Get.dialog(
|
||||
LoadDialog(
|
||||
load: char.stats.load,
|
||||
defaultLoad: char.defaultMaxLoad,
|
||||
onChanged: (load) => controller.updateCharacter(
|
||||
char.copyWith(
|
||||
stats: char.stats.copyWithLoad(load),
|
||||
return CharacterProvider.consumer(
|
||||
(context, charProvider, _) {
|
||||
final char = charProvider.current;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
PrimaryChip(
|
||||
// visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(DwIcons.dumbbell, size: 16),
|
||||
label: tr.home.summary.load
|
||||
.label(char.currentLoad, char.maxLoad),
|
||||
tooltip: tr.home.summary.load.tooltip,
|
||||
backgroundColor: _loadColor(char.currentLoad, char.maxLoad),
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => LoadDialog(
|
||||
load: char.stats.load,
|
||||
defaultLoad: char.defaultMaxLoad,
|
||||
onChanged: (load) => charProvider.updateCharacter(
|
||||
char.copyWith(
|
||||
stats: char.stats.copyWithLoad(load),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PrimaryChip(
|
||||
// visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(DwIcons.coin_stack, size: 16),
|
||||
label: tr.home.summary.coins.label(
|
||||
NumberFormat.compact().format(char.coins),
|
||||
),
|
||||
tooltip: tr.home.summary.coins.tooltip,
|
||||
onPressed: () => Get.dialog(
|
||||
CoinsDialog(
|
||||
coins: char.coins,
|
||||
onChanged: (coins) => controller
|
||||
.updateCharacter(char.copyWith(coins: coins)),
|
||||
PrimaryChip(
|
||||
// visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(DwIcons.coin_stack, size: 16),
|
||||
label: tr.home.summary.coins.label(
|
||||
NumberFormat.compact().format(char.coins),
|
||||
),
|
||||
tooltip: tr.home.summary.coins.tooltip,
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => CoinsDialog(
|
||||
coins: char.coins,
|
||||
onChanged: (coins) => charProvider
|
||||
.updateCharacter(char.copyWith(coins: coins)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
HomeCharacterActionsFilters(
|
||||
hidden: char.settings.actionCategories.hidden,
|
||||
onUpdateHidden: (filters) {
|
||||
controller.updateCharacter(
|
||||
char.copyWith(
|
||||
settings: char.settings.copyWith(
|
||||
actionCategories:
|
||||
char.settings.actionCategories.copyWithInherited(
|
||||
hidden: filters,
|
||||
HomeCharacterActionsFilters(
|
||||
hidden: char.settings.actionCategories.hidden,
|
||||
onUpdateHidden: (filters) {
|
||||
charProvider.updateCharacter(
|
||||
char.copyWith(
|
||||
settings: char.settings.copyWith(
|
||||
actionCategories:
|
||||
char.settings.actionCategories.copyWithInherited(
|
||||
hidden: filters,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,4 +96,3 @@ class HomeCharacterActionsSummary extends GetView<CharacterService> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/note.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/library_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/library_provider.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/character_utils.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/checklist_menu_entry.dart';
|
||||
@@ -23,116 +23,109 @@ 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/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 {
|
||||
class HomeCharacterDynamicCards extends StatelessWidget
|
||||
with CharacterProviderMixin {
|
||||
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>[])
|
||||
List<Move> get moves =>
|
||||
(maybeChar?.moves ?? <Move>[]).where((m) => m.favorite).toList();
|
||||
List<Spell> get spells => (charProvider.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<Item> get items =>
|
||||
(maybeChar?.items ?? <Item>[]).where((m) => m.equipped).toList();
|
||||
List<Note> get notes =>
|
||||
(maybeChar?.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(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,
|
||||
tn(Note),
|
||||
() => controller.updateCharacter(
|
||||
CharacterUtils.removeNotes(
|
||||
controller.current, [note]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onSave: (note) {
|
||||
controller.updateCharacter(
|
||||
CharacterUtils.updateNotes(
|
||||
controller.current, [note]),
|
||||
);
|
||||
if (!note.favorite) {
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
return CharacterProvider.consumer(
|
||||
(context, controller, _) => 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) => NoteCardMini(
|
||||
note: notes[index],
|
||||
onTap: onTap,
|
||||
onSave: (note) => controller.updateCharacter(
|
||||
CharacterUtils.updateNotes(controller.current, [note]),
|
||||
),
|
||||
),
|
||||
//
|
||||
// MOVES
|
||||
//
|
||||
if (moves.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(tr.home.categories.moves),
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
expandedCardBuilder: (context, note, index) => notes.isNotEmpty &&
|
||||
index < notes.length
|
||||
? NoteCard(
|
||||
maxContentHeight: maxContentHeight,
|
||||
expandable: false,
|
||||
initiallyExpanded: true,
|
||||
note: notes[index],
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: () => ModelPages.openNotePage(
|
||||
context,
|
||||
note: notes[index],
|
||||
onSave: (note) => controller.updateCharacter(
|
||||
CharacterUtils.updateNotes(
|
||||
controller.current, [note]),
|
||||
),
|
||||
),
|
||||
onDelete: _delete(
|
||||
context,
|
||||
note,
|
||||
note.title,
|
||||
tn(Note),
|
||||
() => controller.updateCharacter(
|
||||
CharacterUtils.removeNotes(
|
||||
controller.current, [note]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onSave: (note) {
|
||||
controller.updateCharacter(
|
||||
CharacterUtils.updateNotes(controller.current, [note]),
|
||||
);
|
||||
if (!note.favorite) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
//
|
||||
// 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>(
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => ExpandedCardDialogView<Race>(
|
||||
// heroTag: getKeyFor(item.value),
|
||||
heroTag: null,
|
||||
builder: (context) => RaceCard(
|
||||
@@ -143,6 +136,7 @@ class HomeCharacterDynamicCards extends GetView<CharacterService>
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: () => ModelPages.openRacePage(
|
||||
context,
|
||||
abilityScores:
|
||||
controller.current.abilityScores,
|
||||
race: controller.current.race,
|
||||
@@ -167,56 +161,61 @@ class HomeCharacterDynamicCards extends GetView<CharacterService>
|
||||
return HorizontalCardListView<Move>(
|
||||
cardSize: cardSize,
|
||||
items: moves,
|
||||
cardBuilder: (context, move, index, onTap) => Obx(
|
||||
() => MoveCardMini(
|
||||
cardBuilder: (context, move, index, onTap) =>
|
||||
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,
|
||||
tn(Move),
|
||||
() => controller.updateCharacter(
|
||||
CharacterUtils.removeMoves(
|
||||
controller.current, [move]),
|
||||
expandedCardBuilder: (context, move, index) =>
|
||||
LibraryProvider.consumer(
|
||||
(context, library, _) {
|
||||
return moves.isNotEmpty && index < moves.length
|
||||
? MoveCard(
|
||||
maxContentHeight: maxContentHeight,
|
||||
expandable: false,
|
||||
initiallyExpanded: true,
|
||||
move: moves[index],
|
||||
abilityScores: controller.current.abilityScores,
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: () => ModelPages.openMovePage(
|
||||
context,
|
||||
abilityScores:
|
||||
controller.current.abilityScores,
|
||||
move: moves[index],
|
||||
onSave: (move) => library.upsertToCharacter(
|
||||
[move],
|
||||
forkBehavior:
|
||||
ForkBehavior.increaseVersion),
|
||||
),
|
||||
onDelete: _delete(
|
||||
context,
|
||||
move,
|
||||
move.name,
|
||||
tn(Move),
|
||||
() => controller.updateCharacter(
|
||||
CharacterUtils.removeMoves(
|
||||
controller.current, [move]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onSave: (move) {
|
||||
controller.updateCharacter(
|
||||
CharacterUtils.updateMoves(
|
||||
controller.current, [move]),
|
||||
);
|
||||
if (!move.favorite) {
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
onSave: (move) {
|
||||
controller.updateCharacter(
|
||||
CharacterUtils.updateMoves(
|
||||
controller.current, [move]),
|
||||
);
|
||||
if (!move.favorite) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
leading: raceCardMini != null &&
|
||||
controller.current.settings.racePosition ==
|
||||
@@ -229,32 +228,31 @@ class HomeCharacterDynamicCards extends GetView<CharacterService>
|
||||
? [raceCardMini]
|
||||
: [],
|
||||
);
|
||||
}),
|
||||
//
|
||||
// SPELLS
|
||||
//
|
||||
if (spells.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(tr.home.categories.spells),
|
||||
},
|
||||
),
|
||||
//
|
||||
// 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) => SpellCardMini(
|
||||
spell: spells[index],
|
||||
onTap: onTap,
|
||||
onSave: (spell) => controller.updateCharacter(
|
||||
CharacterUtils.updateSpells(controller.current, [spell]),
|
||||
),
|
||||
],
|
||||
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
|
||||
abilityScores: controller.current.abilityScores,
|
||||
),
|
||||
expandedCardBuilder: (context, spell, index) =>
|
||||
spells.isNotEmpty && index < spells.length
|
||||
? SpellCard(
|
||||
maxContentHeight: maxContentHeight,
|
||||
expandable: false,
|
||||
@@ -264,6 +262,7 @@ class HomeCharacterDynamicCards extends GetView<CharacterService>
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: () => ModelPages.openSpellPage(
|
||||
context,
|
||||
abilityScores: controller.current.abilityScores,
|
||||
classKeys: spells[index].classKeys,
|
||||
spell: spells[index],
|
||||
@@ -290,137 +289,134 @@ class HomeCharacterDynamicCards extends GetView<CharacterService>
|
||||
controller.current, [spell]),
|
||||
);
|
||||
if (!spell.prepared) {
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
)
|
||||
: 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) => ItemCardMini(
|
||||
item: items[index],
|
||||
onTap: onTap,
|
||||
onSave: (item) => controller.updateCharacter(
|
||||
CharacterUtils.updateItems(controller.current, [item]),
|
||||
),
|
||||
),
|
||||
//
|
||||
// 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]),
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
tn(Item),
|
||||
() => controller.updateCharacter(
|
||||
CharacterUtils.removeItems(
|
||||
controller.current, [item]),
|
||||
),
|
||||
),
|
||||
leading: [
|
||||
ChecklistMenuEntry(
|
||||
value: 'countArmor',
|
||||
checked: item.settings.countArmor,
|
||||
label: Text(tr.items.settings.countArmor),
|
||||
onChanged: (value) =>
|
||||
controller.updateCharacter(
|
||||
CharacterUtils.updateItems(
|
||||
controller.current, [
|
||||
item.copyWithInherited(
|
||||
settings: item.settings
|
||||
.copyWith(countArmor: value!),
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
ChecklistMenuEntry(
|
||||
value: 'countDamage',
|
||||
checked: item.settings.countDamage,
|
||||
label: Text(tr.items.settings.countDamage),
|
||||
onChanged: (value) =>
|
||||
controller.updateCharacter(
|
||||
CharacterUtils.updateItems(
|
||||
controller.current, [
|
||||
item.copyWithInherited(
|
||||
settings: item.settings
|
||||
.copyWith(countDamage: value!),
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
ChecklistMenuEntry(
|
||||
value: 'countWeight',
|
||||
checked: item.settings.countWeight,
|
||||
label: Text(tr.items.settings.countWeight),
|
||||
onChanged: (value) =>
|
||||
controller.updateCharacter(
|
||||
CharacterUtils.updateItems(
|
||||
controller.current, [
|
||||
item.copyWithInherited(
|
||||
settings: item.settings
|
||||
.copyWith(countWeight: value!),
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onSave: (item) {
|
||||
controller.updateCharacter(
|
||||
expandedCardBuilder: (context, item, index) => items.isNotEmpty &&
|
||||
index < items.length
|
||||
? ItemCard(
|
||||
maxContentHeight: maxContentHeight,
|
||||
expandable: false,
|
||||
initiallyExpanded: true,
|
||||
item: items[index],
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: () => ModelPages.openItemPage(
|
||||
context,
|
||||
item: items[index],
|
||||
onSave: (item) => controller.updateCharacter(
|
||||
CharacterUtils.updateItems(
|
||||
controller.current, [item]),
|
||||
);
|
||||
if (!item.equipped) {
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
onDelete: _delete(
|
||||
context,
|
||||
item,
|
||||
item.name,
|
||||
tn(Item),
|
||||
() => controller.updateCharacter(
|
||||
CharacterUtils.removeItems(
|
||||
controller.current, [item]),
|
||||
),
|
||||
),
|
||||
leading: [
|
||||
ChecklistMenuEntry(
|
||||
value: 'countArmor',
|
||||
checked: item.settings.countArmor,
|
||||
label: Text(tr.items.settings.countArmor),
|
||||
onChanged: (value) => controller.updateCharacter(
|
||||
CharacterUtils.updateItems(controller.current, [
|
||||
item.copyWithInherited(
|
||||
settings: item.settings
|
||||
.copyWith(countArmor: value!),
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
ChecklistMenuEntry(
|
||||
value: 'countDamage',
|
||||
checked: item.settings.countDamage,
|
||||
label: Text(tr.items.settings.countDamage),
|
||||
onChanged: (value) => controller.updateCharacter(
|
||||
CharacterUtils.updateItems(controller.current, [
|
||||
item.copyWithInherited(
|
||||
settings: item.settings
|
||||
.copyWith(countDamage: value!),
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
ChecklistMenuEntry(
|
||||
value: 'countWeight',
|
||||
checked: item.settings.countWeight,
|
||||
label: Text(tr.items.settings.countWeight),
|
||||
onChanged: (value) => controller.updateCharacter(
|
||||
CharacterUtils.updateItems(controller.current, [
|
||||
item.copyWithInherited(
|
||||
settings: item.settings
|
||||
.copyWith(countWeight: value!),
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
onSave: (item) {
|
||||
controller.updateCharacter(
|
||||
CharacterUtils.updateItems(controller.current, [item]),
|
||||
);
|
||||
if (!item.equipped) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void Function() _delete<T>(BuildContext context, T item, String itemName,
|
||||
String typeName, void Function() onRemove) {
|
||||
return () => deleteDialog.confirm(
|
||||
void Function() _delete<T>(
|
||||
BuildContext context,
|
||||
T item,
|
||||
String itemName,
|
||||
String typeName,
|
||||
void Function() onRemove,
|
||||
) {
|
||||
return () => awaitDeleteConfirmation(
|
||||
context,
|
||||
DeleteDialogOptions(
|
||||
entityName: itemName, entityKind: tr.entity(typeName)),
|
||||
itemName,
|
||||
() {
|
||||
onRemove();
|
||||
Get.back();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
T,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:dungeon_paper/app/data/models/campaign.dart';
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/session_marks.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
||||
import 'package:dungeon_paper/app/modules/AbilityScoresForm/controllers/ability_scores_form_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/BasicInfoForm/controllers/basic_info_form_controller.dart';
|
||||
@@ -15,9 +15,8 @@ import 'package:dungeon_paper/app/widgets/dialogs/debilities_dialog.dart';
|
||||
import 'package:dungeon_paper/core/dw_icons.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class HomeCharacterExtras extends GetView<CharacterService> {
|
||||
class HomeCharacterExtras extends StatelessWidget with CharacterProviderMixin {
|
||||
const HomeCharacterExtras({super.key});
|
||||
|
||||
@override
|
||||
@@ -33,49 +32,49 @@ class HomeCharacterExtras extends GetView<CharacterService> {
|
||||
value: 'name_photo',
|
||||
icon: const Icon(Icons.photo),
|
||||
label: Text(tr.home.menu.character.basicInfo),
|
||||
onSelect: _openBasicInfo,
|
||||
onSelect: () => _openBasicInfo(context),
|
||||
),
|
||||
MenuEntry(
|
||||
value: 'ability_scores',
|
||||
icon: const Icon(Icons.format_list_numbered_rtl),
|
||||
label: Text(tr.home.menu.character.abilityScores),
|
||||
onSelect: _openAbilityScores,
|
||||
onSelect: () => _openAbilityScores(context),
|
||||
),
|
||||
MenuEntry(
|
||||
value: 'class',
|
||||
icon: Icon(CharacterClass.genericIcon),
|
||||
label:
|
||||
Text(tr.generic.changeEntity(tr.entity(tn(CharacterClass)))),
|
||||
onSelect: _openCharClass,
|
||||
onSelect: () => _openCharClass(context),
|
||||
),
|
||||
MenuEntry(
|
||||
value: 'race',
|
||||
icon: Icon(Race.genericIcon),
|
||||
label: Text(tr.generic.changeEntity(tr.entity(tn(Race)))),
|
||||
onSelect: _openRace,
|
||||
onSelect: () => _openRace(context),
|
||||
),
|
||||
MenuEntry(
|
||||
value: 'roll_buttons',
|
||||
icon: const Icon(DwIcons.dice_d6),
|
||||
label: Text(tr.home.menu.character.customRolls),
|
||||
onSelect: _openRollButtons,
|
||||
onSelect: () => _openRollButtons(context),
|
||||
),
|
||||
MenuEntry(
|
||||
value: 'theme',
|
||||
icon: const Icon(Icons.brush),
|
||||
label: Text(tr.home.menu.character.theme),
|
||||
onSelect: _openThemeSelect,
|
||||
onSelect: () => _openThemeSelect(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.text_snippet),
|
||||
tooltip: tr.home.menu.bio,
|
||||
onPressed: _openBio,
|
||||
onPressed: () => _openBio(context),
|
||||
),
|
||||
Obx(
|
||||
() => IconButton(
|
||||
onPressed: _openBondsFlags,
|
||||
CharacterProvider.consumer(
|
||||
(context, controller, _) => IconButton(
|
||||
onPressed: () => _openBondsFlags(context),
|
||||
icon:
|
||||
Transform.scale(scaleX: -1, child: const Icon(Icons.handshake)),
|
||||
tooltip: SessionMark.categoryTitle(
|
||||
@@ -85,7 +84,7 @@ class HomeCharacterExtras extends GetView<CharacterService> {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _openDebilities,
|
||||
onPressed: () => _openDebilities(context),
|
||||
icon: const Icon(Icons.personal_injury),
|
||||
tooltip: tr.home.menu.debilities,
|
||||
),
|
||||
@@ -98,81 +97,87 @@ class HomeCharacterExtras extends GetView<CharacterService> {
|
||||
);
|
||||
}
|
||||
|
||||
void _openAbilityScores() => Get.toNamed(
|
||||
Routes.abilityScores,
|
||||
arguments: AbilityScoresFormArguments(
|
||||
abilityScores: controller.current.abilityScores,
|
||||
onChanged: (abilityScores) => controller.updateCharacter(
|
||||
controller.current.copyWith(abilityScores: abilityScores)),
|
||||
),
|
||||
preventDuplicates: false,
|
||||
);
|
||||
void _openAbilityScores(BuildContext context) {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.abilityScores,
|
||||
arguments: AbilityScoresFormArguments(
|
||||
abilityScores: charProvider.current.abilityScores,
|
||||
onChanged: (abilityScores) => charProvider.updateCharacter(
|
||||
charProvider.current.copyWith(abilityScores: abilityScores)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openBasicInfo() {
|
||||
Get.toNamed(
|
||||
void _openBasicInfo(BuildContext context) {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.basicInfo,
|
||||
arguments: BasicInfoFormArguments(
|
||||
onChanged: (name, avatar) => controller.updateCharacter(
|
||||
controller.current.copyWith(displayName: name, avatarUrl: avatar),
|
||||
onChanged: (name, avatar) => charProvider.updateCharacter(
|
||||
charProvider.current.copyWith(displayName: name, avatarUrl: avatar),
|
||||
),
|
||||
name: controller.current.displayName,
|
||||
avatarUrl: controller.current.avatarUrl,
|
||||
name: charProvider.current.displayName,
|
||||
avatarUrl: charProvider.current.avatarUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openBio() {
|
||||
Get.dialog(const CharacterBioDialog());
|
||||
void _openBio(BuildContext context) {
|
||||
showDialog(context: context, builder: (_) => const CharacterBioDialog());
|
||||
}
|
||||
|
||||
void _openRace() {
|
||||
void _openRace(BuildContext context) {
|
||||
ModelPages.openRacesList(
|
||||
character: controller.current,
|
||||
preSelection: controller.current.race,
|
||||
onSelected: (race) => controller.updateCharacter(
|
||||
controller.current.copyWithInherited(
|
||||
context,
|
||||
character: charProvider.current,
|
||||
preSelection: charProvider.current.race,
|
||||
onSelected: (race) => charProvider.updateCharacter(
|
||||
charProvider.current.copyWithInherited(
|
||||
race: race.copyWithInherited(
|
||||
favorite: controller.current.race.favorite),
|
||||
favorite: charProvider.current.race.favorite),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openCharClass() {
|
||||
void _openCharClass(BuildContext context) {
|
||||
ModelPages.openCharacterClassesList(
|
||||
character: controller.current,
|
||||
onSelected: (cls) => controller.updateCharacter(
|
||||
context,
|
||||
character: charProvider.current,
|
||||
onSelected: (cls) => charProvider.updateCharacter(
|
||||
// TODO add a reset dialog to confirm + ask what to reset: moves, spells, alignment, rac
|
||||
controller.current.copyWithInherited(
|
||||
charProvider.current.copyWithInherited(
|
||||
characterClass: cls,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openBondsFlags() {
|
||||
Get.dialog(const CharacterBondsFlagsDialog());
|
||||
void _openBondsFlags(BuildContext context) {
|
||||
showDialog(
|
||||
context: context, builder: (_) => const CharacterBondsFlagsDialog());
|
||||
}
|
||||
|
||||
void _openDebilities() {
|
||||
Get.dialog(const CharacterDebilitiesDialog());
|
||||
void _openDebilities(BuildContext context) {
|
||||
showDialog(
|
||||
context: context, builder: (_) => const CharacterDebilitiesDialog());
|
||||
}
|
||||
|
||||
void _openRollButtons() {
|
||||
Get.dialog(
|
||||
CustomRollButtonsDialog(
|
||||
character: controller.current,
|
||||
onChanged: (rollButtons) => controller.updateCharacter(
|
||||
controller.current.copyWith(
|
||||
settings:
|
||||
controller.current.settings.copyWith(rollButtons: rollButtons),
|
||||
void _openRollButtons(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => CustomRollButtonsDialog(
|
||||
character: charProvider.current,
|
||||
onChanged: (rollButtons) => charProvider.updateCharacter(
|
||||
charProvider.current.copyWith(
|
||||
settings: charProvider.current.settings
|
||||
.copyWith(rollButtons: rollButtons),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openThemeSelect() {
|
||||
Get.toNamed(Routes.selectCharacterTheme);
|
||||
void _openThemeSelect(BuildContext context) {
|
||||
Navigator.of(context).pushNamed(Routes.selectCharacterTheme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/character_avatar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class HomeCharacterHeaderView extends GetView<CharacterService> {
|
||||
const HomeCharacterHeaderView({Key? key}) : super(key: key);
|
||||
class HomeCharacterHeaderView extends StatelessWidget {
|
||||
const HomeCharacterHeaderView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/xp_bar.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/hp_bar.dart';
|
||||
import 'package:dungeon_paper/app/widgets/dialogs/xp_dialog.dart';
|
||||
import 'package:dungeon_paper/app/widgets/dialogs/hp_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class HomeCharacterHpExpView extends GetView<CharacterService> {
|
||||
const HomeCharacterHpExpView({Key? key}) : super(key: key);
|
||||
class HomeCharacterHpExpView extends StatelessWidget {
|
||||
const HomeCharacterHpExpView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -17,24 +14,30 @@ class HomeCharacterHpExpView extends GetView<CharacterService> {
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => const HPDialog(),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: HpBar(),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => Get.dialog(const HPDialog()),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
splashColor: Theme.of(context).splashColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => const EXPDialog(),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: ExpBar(),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => Get.dialog(const EXPDialog()),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,18 +3,17 @@ import 'package:dungeon_paper/app/modules/Home/views/expanded_card_dialog_view.d
|
||||
import 'package:dungeon_paper/core/utils/builder_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class HorizontalCardListView<T extends WithMeta> extends StatelessWidget {
|
||||
HorizontalCardListView({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.cardSize,
|
||||
required this.items,
|
||||
required this.cardBuilder,
|
||||
required this.expandedCardBuilder,
|
||||
this.leading = const [],
|
||||
this.trailing = const [],
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final Size cardSize;
|
||||
final Widget Function(
|
||||
@@ -27,7 +26,7 @@ class HorizontalCardListView<T extends WithMeta> extends StatelessWidget {
|
||||
// void Function(Iterable<T>) onUpdate
|
||||
) expandedCardBuilder;
|
||||
final Iterable<T> items;
|
||||
final itemsObs = <T>[].obs;
|
||||
final itemsObs = ValueNotifier<List<T>>([]);
|
||||
final List<Widget> leading;
|
||||
final List<Widget> trailing;
|
||||
|
||||
@@ -36,7 +35,8 @@ class HorizontalCardListView<T extends WithMeta> extends StatelessWidget {
|
||||
if (items.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final displayedItems = enumerate(itemsObs.isEmpty ? items : itemsObs);
|
||||
final displayedItems =
|
||||
enumerate(itemsObs.value.isEmpty ? items : itemsObs.value);
|
||||
|
||||
final builder = ItemBuilder.builder(
|
||||
leadingCount: leading.length,
|
||||
@@ -87,8 +87,9 @@ class HorizontalCardListView<T extends WithMeta> extends StatelessWidget {
|
||||
context,
|
||||
item.value,
|
||||
item.index,
|
||||
() => Get.dialog(
|
||||
ExpandedCardDialogView<T>(
|
||||
() => showDialog(
|
||||
context: context,
|
||||
builder: (_) => ExpandedCardDialogView<T>(
|
||||
// heroTag: getKeyFor(item.value),
|
||||
heroTag: null,
|
||||
builder: (context) =>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/export_controller.dart';
|
||||
import '../controllers/import_controller.dart';
|
||||
import '../controllers/import_export_controller.dart';
|
||||
|
||||
class ImportExportBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<ImportExportController>(
|
||||
() => ImportExportController(),
|
||||
);
|
||||
Get.lazyPut<ExportController>(
|
||||
() => ExportController(),
|
||||
);
|
||||
Get.lazyPut<ImportController>(
|
||||
() => ImportController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,40 +7,34 @@ import 'package:dungeon_paper/app/data/models/meta.dart';
|
||||
import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_provider.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../data/services/character_provider.dart';
|
||||
import '../platforms/platform_export.dart';
|
||||
import 'import_export_controller.dart';
|
||||
|
||||
|
||||
class ExportController extends GetxController
|
||||
with
|
||||
GetSingleTickerProviderStateMixin,
|
||||
CharacterServiceMixin,
|
||||
RepositoryServiceMixin
|
||||
class ExportController extends ChangeNotifier
|
||||
with CharacterProviderMixin, RepositoryProviderMixin
|
||||
implements ImportExportSelectionData {
|
||||
final toExport = ExportSelections().obs;
|
||||
final toExport = ExportSelections();
|
||||
|
||||
List<Character> get characters => characterService.allAsList;
|
||||
List<Character> get characters => characterProvider.allAsList;
|
||||
List<Move> get moves => repo.my.moves.values.toList();
|
||||
List<Spell> get spells => repo.my.spells.values.toList();
|
||||
List<Item> get items => repo.my.items.values.toList();
|
||||
List<CharacterClass> get classes => repo.my.classes.values.toList();
|
||||
List<Race> get races => repo.my.races.values.toList();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
toExport.value.characters = List.from(characters);
|
||||
toExport.value.moves = List.from(moves);
|
||||
toExport.value.spells = List.from(spells);
|
||||
toExport.value.items = List.from(items);
|
||||
toExport.value.classes = List.from(classes);
|
||||
toExport.value.races = List.from(races);
|
||||
ExportController() {
|
||||
toExport.characters = List.from(characters);
|
||||
toExport.moves = List.from(moves);
|
||||
toExport.spells = List.from(spells);
|
||||
toExport.items = List.from(items);
|
||||
toExport.classes = List.from(classes);
|
||||
toExport.races = List.from(races);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -54,54 +48,57 @@ class ExportController extends GetxController
|
||||
void _toggleExportList<T>(List<T> items, bool state) {
|
||||
switch (T) {
|
||||
case == Character:
|
||||
toExport.value.characters = _toggleInList(
|
||||
toExport.value.characters, items.cast<Character>(), state);
|
||||
toExport.characters =
|
||||
_toggleInList(toExport.characters, items.cast<Character>(), state);
|
||||
break;
|
||||
case == Move:
|
||||
toExport.value.moves =
|
||||
_toggleInList(toExport.value.moves, items.cast<Move>(), state);
|
||||
toExport.moves =
|
||||
_toggleInList(toExport.moves, items.cast<Move>(), state);
|
||||
break;
|
||||
case == Spell:
|
||||
toExport.value.spells =
|
||||
_toggleInList(toExport.value.spells, items.cast<Spell>(), state);
|
||||
toExport.spells =
|
||||
_toggleInList(toExport.spells, items.cast<Spell>(), state);
|
||||
break;
|
||||
case == Item:
|
||||
toExport.value.items =
|
||||
_toggleInList(toExport.value.items, items.cast<Item>(), state);
|
||||
toExport.items =
|
||||
_toggleInList(toExport.items, items.cast<Item>(), state);
|
||||
break;
|
||||
case == CharacterClass:
|
||||
toExport.value.classes = _toggleInList(
|
||||
toExport.value.classes, items.cast<CharacterClass>(), state);
|
||||
toExport.classes = _toggleInList(
|
||||
toExport.classes, items.cast<CharacterClass>(), state);
|
||||
break;
|
||||
case == Race:
|
||||
toExport.value.races =
|
||||
_toggleInList(toExport.value.races, items.cast<Race>(), state);
|
||||
toExport.races =
|
||||
_toggleInList(toExport.races, items.cast<Race>(), state);
|
||||
break;
|
||||
}
|
||||
toExport.refresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSelected<T extends WithMeta>(T item) {
|
||||
return toExport.value.listByType<T>().map((x) => x.key).contains(item.key);
|
||||
return toExport.listByType<T>().map((x) => x.key).contains(item.key);
|
||||
}
|
||||
|
||||
List<T> _toggleInList<T>(List<T> list, List<T> items, bool state) {
|
||||
late List<T> res;
|
||||
if (state) {
|
||||
return addByKey<T>(list, items);
|
||||
res = addByKey<T>(list, items);
|
||||
} else {
|
||||
return removeByKey<T>(list, items);
|
||||
res = removeByKey<T>(list, items);
|
||||
}
|
||||
notifyListeners();
|
||||
return res;
|
||||
}
|
||||
|
||||
void Function()? getDoExport() {
|
||||
void Function()? getDoExport(BuildContext context) {
|
||||
return () async {
|
||||
final strData = utf8.encode(json.encode(toExport.value.toJson()));
|
||||
final strData = utf8.encode(json.encode(toExport.toJson()));
|
||||
|
||||
final dt = DateFormat('yy-MM-dd_HH.mm.ss').format(DateTime.now());
|
||||
final fileName = 'DungeonPaperV2_$dt.json';
|
||||
|
||||
Exporter().export(strData, fileName);
|
||||
Exporter().export(context, strData, fileName);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,29 +12,28 @@ import 'package:dungeon_paper/core/storage_handler/storage_handler.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'import_export_controller.dart';
|
||||
|
||||
class ImportController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin
|
||||
class ImportController extends ChangeNotifier
|
||||
implements ImportExportSelectionData {
|
||||
final Rx<ImportSelections?> toImport = Rx(null);
|
||||
ImportSelections? toImport;
|
||||
|
||||
List<Character> get characters => toImport.value!.allCharacters.toList();
|
||||
List<Move> get moves => toImport.value!.allMoves.toList();
|
||||
List<Spell> get spells => toImport.value!.allSpells.toList();
|
||||
List<Item> get items => toImport.value!.allItems.toList();
|
||||
List<CharacterClass> get classes => toImport.value!.allClasses.toList();
|
||||
List<Race> get races => toImport.value!.allRaces.toList();
|
||||
List<Character> get characters => toImport!.allCharacters.toList();
|
||||
List<Move> get moves => toImport!.allMoves.toList();
|
||||
List<Spell> get spells => toImport!.allSpells.toList();
|
||||
List<Item> get items => toImport!.allItems.toList();
|
||||
List<CharacterClass> get classes => toImport!.allClasses.toList();
|
||||
List<Race> get races => toImport!.allRaces.toList();
|
||||
|
||||
int get selectionsCount => [characters, moves, spells, items, classes]
|
||||
.fold(0, (total, list) => total + list.length);
|
||||
|
||||
bool get hasData => toImport.value != null;
|
||||
bool get hasData => toImport != null;
|
||||
|
||||
final importStep = Rx<Type?>(null);
|
||||
final leftCount = 0.obs;
|
||||
Type? importStep;
|
||||
var leftCount = 0;
|
||||
|
||||
@override
|
||||
void toggle<T extends WithMeta>(T item, bool state) =>
|
||||
@@ -47,36 +46,36 @@ class ImportController extends GetxController
|
||||
void _toggleImportList<T>(List<T> items, bool state) {
|
||||
switch (T) {
|
||||
case == Character:
|
||||
toImport.value!.characters = _toggleInList(
|
||||
toImport.value!.characters, items.cast<Character>(), state);
|
||||
toImport!.characters =
|
||||
_toggleInList(toImport!.characters, items.cast<Character>(), state);
|
||||
break;
|
||||
case == Move:
|
||||
toImport.value!.moves =
|
||||
_toggleInList(toImport.value!.moves, items.cast<Move>(), state);
|
||||
toImport!.moves =
|
||||
_toggleInList(toImport!.moves, items.cast<Move>(), state);
|
||||
break;
|
||||
case == Spell:
|
||||
toImport.value!.spells =
|
||||
_toggleInList(toImport.value!.spells, items.cast<Spell>(), state);
|
||||
toImport!.spells =
|
||||
_toggleInList(toImport!.spells, items.cast<Spell>(), state);
|
||||
break;
|
||||
case == Item:
|
||||
toImport.value!.items =
|
||||
_toggleInList(toImport.value!.items, items.cast<Item>(), state);
|
||||
toImport!.items =
|
||||
_toggleInList(toImport!.items, items.cast<Item>(), state);
|
||||
break;
|
||||
case == CharacterClass:
|
||||
toImport.value!.classes = _toggleInList(
|
||||
toImport.value!.classes, items.cast<CharacterClass>(), state);
|
||||
toImport!.classes = _toggleInList(
|
||||
toImport!.classes, items.cast<CharacterClass>(), state);
|
||||
break;
|
||||
case == Race:
|
||||
toImport.value!.races =
|
||||
_toggleInList(toImport.value!.races, items.cast<Race>(), state);
|
||||
toImport!.races =
|
||||
_toggleInList(toImport!.races, items.cast<Race>(), state);
|
||||
break;
|
||||
}
|
||||
toImport.refresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSelected<T extends WithMeta>(T item) {
|
||||
return toImport.value!
|
||||
return toImport!
|
||||
.listByType<T>(selected: true)
|
||||
.map((x) => x.key)
|
||||
.contains(item.key);
|
||||
@@ -109,13 +108,24 @@ class ImportController extends GetxController
|
||||
throw TypeError();
|
||||
}
|
||||
|
||||
void pickImportFile() async {
|
||||
var result =
|
||||
await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['json']);
|
||||
void pickImportFile(BuildContext context) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final result = await FilePicker.platform
|
||||
.pickFiles(type: FileType.custom, allowedExtensions: ['json']);
|
||||
if (result == null) {
|
||||
Get.rawSnackbar(
|
||||
title: tr.backup.importing.error.title,
|
||||
message: tr.backup.importing.error.message,
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
tr.backup.importing.error.title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
Text(tr.backup.importing.error.message),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -123,56 +133,78 @@ class ImportController extends GetxController
|
||||
final filedata = result.files.single.bytes;
|
||||
final filestring = utf8.decode(filedata as List<int>);
|
||||
final filejson = json.decode(filestring);
|
||||
toImport.value = ImportSelections.fromJson(filejson);
|
||||
toImport = ImportSelections.fromJson(filejson);
|
||||
}
|
||||
|
||||
void Function()? getDoImport() {
|
||||
if (!hasData || toImport.value?.hasSelections != true) {
|
||||
void Function()? getDoImport(BuildContext context) {
|
||||
if (!hasData || toImport?.hasSelections != true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return () async {
|
||||
leftCount.value = selectionsCount;
|
||||
leftCount = selectionsCount;
|
||||
final navigator = Navigator.of(context);
|
||||
|
||||
Get.dialog(const ImportProgressDialog(), barrierDismissible: false);
|
||||
importStep.value = Character;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => const ImportProgressDialog(),
|
||||
barrierDismissible: false);
|
||||
importStep = Character;
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
for (final char in characters) {
|
||||
await StorageHandler.instance
|
||||
.create('Characters', char.key, char.toJson());
|
||||
leftCount.value -= 1;
|
||||
leftCount -= 1;
|
||||
notifyListeners();
|
||||
}
|
||||
importStep.value = CharacterClass;
|
||||
importStep = CharacterClass;
|
||||
for (final cls in classes) {
|
||||
await StorageHandler.instance
|
||||
.create('CharacterClasses', cls.key, cls.toJson());
|
||||
leftCount.value -= 1;
|
||||
leftCount -= 1;
|
||||
notifyListeners();
|
||||
}
|
||||
importStep.value = Move;
|
||||
importStep = Move;
|
||||
for (final move in moves) {
|
||||
await StorageHandler.instance.create('Moves', move.key, move.toJson());
|
||||
leftCount.value -= 1;
|
||||
leftCount -= 1;
|
||||
notifyListeners();
|
||||
}
|
||||
importStep.value = Spell;
|
||||
importStep = Spell;
|
||||
for (final spell in spells) {
|
||||
await StorageHandler.instance
|
||||
.create('Spells', spell.key, spell.toJson());
|
||||
leftCount.value -= 1;
|
||||
leftCount -= 1;
|
||||
notifyListeners();
|
||||
}
|
||||
importStep.value = Item;
|
||||
importStep = Item;
|
||||
for (final items in items) {
|
||||
await StorageHandler.instance
|
||||
.create('Items', items.key, items.toJson());
|
||||
leftCount.value -= 1;
|
||||
leftCount -= 1;
|
||||
notifyListeners();
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
Get.back();
|
||||
navigator.pop();
|
||||
|
||||
Get.rawSnackbar(
|
||||
title: tr.backup.importing.success.title,
|
||||
message: tr.backup.importing.success.message,
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
tr.backup.importing.success.title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
Text(
|
||||
tr.backup.importing.success.message,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
import 'package:dungeon_paper/app/data/models/meta.dart';
|
||||
import 'package:dungeon_paper/app/modules/ImportExport/controllers/export_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'import_controller.dart';
|
||||
|
||||
class ImportExportController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
late final Rx<TabController> tab;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
tab = (TabController(length: 2, vsync: this)..addListener(_refresh)).obs;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
tab.value.removeListener(_refresh);
|
||||
}
|
||||
|
||||
void Function()? get doExport => Get.find<ExportController>().getDoExport();
|
||||
void Function()? get doImport => Get.find<ImportController>().getDoImport();
|
||||
|
||||
void _refresh() {
|
||||
tab.refresh();
|
||||
}
|
||||
// TODO remove?
|
||||
class ImportExportController extends ChangeNotifier {
|
||||
void Function()? doExport(BuildContext context) =>
|
||||
Provider.of<ExportController>(context, listen: false).getDoExport(context);
|
||||
void Function()? doImport(BuildContext context) =>
|
||||
Provider.of<ImportController>(context, listen: false).getDoImport(context);
|
||||
}
|
||||
|
||||
abstract class ImportExportSelectionData {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import 'package:dungeon_paper/app/modules/ImportExport/controllers/import_controller.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ImportProgressDialog extends GetView<ImportController> {
|
||||
class ImportProgressDialog extends StatelessWidget {
|
||||
const ImportProgressDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() {
|
||||
return Consumer<ImportController>(
|
||||
builder: (context, controller, _) {
|
||||
final completedCount =
|
||||
controller.selectionsCount - controller.leftCount.value;
|
||||
controller.selectionsCount - controller.leftCount;
|
||||
final totalCount = controller.selectionsCount;
|
||||
return SimpleDialog(
|
||||
title: Text(tr.backup.importing.progress.title),
|
||||
@@ -21,7 +21,7 @@ class ImportProgressDialog extends GetView<ImportController> {
|
||||
children: [
|
||||
Text(
|
||||
tr.backup.importing.progress.processing(
|
||||
tr.entityPlural(tn(controller.importStep.value!)),
|
||||
tr.entityPlural(tn(controller.importStep!)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -4,7 +4,7 @@ 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/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../core/dw_icons.dart';
|
||||
import '../../../data/models/character.dart';
|
||||
@@ -17,90 +17,94 @@ import '../../../widgets/chips/primary_chip.dart';
|
||||
enum ListCardType { import, export }
|
||||
|
||||
class ListCard<T extends WithMeta, C extends ImportExportSelectionData>
|
||||
extends GetView<C> {
|
||||
extends StatelessWidget {
|
||||
const ListCard({
|
||||
super.key,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
final ListCardType type;
|
||||
List<T> get list => controller.listByType<T>();
|
||||
List<T> list(BuildContext context) =>
|
||||
Provider.of<C>(context, listen: false).listByType<T>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Obx(
|
||||
() => Card(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
child: CustomExpansionPanel(
|
||||
initiallyExpanded: true,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Meta.genericIconFor(T),
|
||||
color: textTheme.titleLarge!.color,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
type == ListCardType.import
|
||||
? tr.entityPlural(tn(T))
|
||||
: tr.generic.myEntity(tr.entityPlural(tn(T))),
|
||||
style: textTheme.titleLarge,
|
||||
return Consumer<C>(
|
||||
builder: (context, controller, _) {
|
||||
final list = this.list(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
child: CustomExpansionPanel(
|
||||
initiallyExpanded: true,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Meta.genericIconFor(T),
|
||||
color: textTheme.titleLarge!.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: [
|
||||
MenuButton<bool>(
|
||||
items: [
|
||||
MenuEntry<bool>(
|
||||
value: true,
|
||||
icon: const Icon(Icons.select_all),
|
||||
label: Text(tr.generic.selectAll),
|
||||
onSelect: () => controller.toggleAll<T>(true),
|
||||
),
|
||||
MenuEntry<bool>(
|
||||
value: false,
|
||||
icon: const Icon(Icons.clear),
|
||||
label: Text(tr.generic.selectNone),
|
||||
onSelect: () => controller.toggleAll<T>(false),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
type == ListCardType.import
|
||||
? tr.entityPlural(tn(T))
|
||||
: tr.generic.myEntity(tr.entityPlural(tn(T))),
|
||||
style: textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
children: [
|
||||
for (final item in list)
|
||||
Builder(builder: (context) {
|
||||
final tags = tagsByType(item);
|
||||
return ListTile(
|
||||
onTap: () => controller.toggle<T>(
|
||||
item, !controller.isSelected<T>(item)),
|
||||
title: Text(item.displayName),
|
||||
subtitle: tags.isNotEmpty
|
||||
? Wrap(
|
||||
spacing: 8,
|
||||
children: tags,
|
||||
)
|
||||
: null,
|
||||
leading: Checkbox(
|
||||
value: controller.isSelected<T>(item),
|
||||
onChanged: (state) => controller.toggle<T>(item, state!),
|
||||
trailing: [
|
||||
MenuButton<bool>(
|
||||
items: [
|
||||
MenuEntry<bool>(
|
||||
value: true,
|
||||
icon: const Icon(Icons.select_all),
|
||||
label: Text(tr.generic.selectAll),
|
||||
onSelect: () => controller.toggleAll<T>(true),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (list.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
tr.generic.noEntity(tr.entityPlural(tn(T))),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
MenuEntry<bool>(
|
||||
value: false,
|
||||
icon: const Icon(Icons.clear),
|
||||
label: Text(tr.generic.selectNone),
|
||||
onSelect: () => controller.toggleAll<T>(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
children: [
|
||||
for (final item in list)
|
||||
Builder(builder: (context) {
|
||||
final tags = tagsByType(item);
|
||||
return ListTile(
|
||||
onTap: () => controller.toggle<T>(
|
||||
item, !controller.isSelected<T>(item)),
|
||||
title: Text(item.displayName),
|
||||
subtitle: tags.isNotEmpty
|
||||
? Wrap(
|
||||
spacing: 8,
|
||||
children: tags,
|
||||
)
|
||||
: null,
|
||||
leading: Checkbox(
|
||||
value: controller.isSelected<T>(item),
|
||||
onChanged: (state) => controller.toggle<T>(item, state!),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (list.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
tr.generic.noEntity(tr.entityPlural(tn(T))),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,4 +171,3 @@ class ListCard<T extends WithMeta, C extends ImportExportSelectionData>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class AbstractExporter {
|
||||
void export(Uint8List data, String filename);
|
||||
void export(BuildContext context, Uint8List data, String filename);
|
||||
}
|
||||
|
||||
class Exporter extends AbstractExporter {
|
||||
@override
|
||||
void export(Uint8List data, String filename) {
|
||||
void export(BuildContext context, Uint8List data, String filename) {
|
||||
throw UnimplementedError('Unsupported platform is unsupported');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,17 @@ import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dungeon_paper/app/modules/ImportExport/platforms/abstract_export.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/custom_snack_bar.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.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 {
|
||||
@override
|
||||
void export(Uint8List data, String filename) async {
|
||||
void export(BuildContext context, Uint8List data, String filename) async {
|
||||
final snackBar = CustomSnackBar.deferred(context);
|
||||
final tmp = await getTemporaryDirectory();
|
||||
|
||||
final tmpFile = File(path.join(tmp.path, filename));
|
||||
@@ -20,20 +22,20 @@ class Exporter extends AbstractExporter {
|
||||
try {
|
||||
final path = await FlutterFileDialog.saveFile(params: params);
|
||||
if (path == null) {
|
||||
Get.rawSnackbar(
|
||||
snackBar.show(
|
||||
title: tr.backup.exporting.error.title,
|
||||
message: tr.errors.userOperationCanceled,
|
||||
content: tr.errors.userOperationCanceled,
|
||||
);
|
||||
} else {
|
||||
Get.rawSnackbar(
|
||||
snackBar.show(
|
||||
title: tr.backup.exporting.success.title,
|
||||
message: tr.backup.exporting.success.message,
|
||||
content: tr.backup.exporting.success.message,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.rawSnackbar(
|
||||
snackBar.show(
|
||||
title: tr.backup.exporting.error.title,
|
||||
message: tr.backup.exporting.error.message,
|
||||
content: tr.backup.exporting.error.message,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export './abstract_export.dart'
|
||||
if (dart.library.io) 'package:dungeon_paper/app/modules/ImportExport/platforms/native_export.dart'
|
||||
if (dart.library.html) 'package:dungeon_paper/app/modules/ImportExport/platforms/web_export.dart';
|
||||
|
||||
|
||||
@@ -8,13 +8,11 @@ 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 '../../../widgets/dialogs/export_class_dialog.dart';
|
||||
import '../controllers/export_controller.dart';
|
||||
import '../local_widgets/list_card.dart';
|
||||
|
||||
class ExportView extends GetView<ExportController> {
|
||||
class ExportView extends StatelessWidget {
|
||||
const ExportView({super.key});
|
||||
|
||||
@override
|
||||
@@ -34,10 +32,13 @@ class ExportView extends GetView<ExportController> {
|
||||
),
|
||||
],
|
||||
),
|
||||
() => const ListCard<Character, ExportController>(type: ListCardType.export),
|
||||
() => const ListCard<CharacterClass, ExportController>(type: ListCardType.export),
|
||||
() => const ListCard<Character, ExportController>(
|
||||
type: ListCardType.export),
|
||||
() => const ListCard<CharacterClass, ExportController>(
|
||||
type: ListCardType.export),
|
||||
() => const ListCard<Move, ExportController>(type: ListCardType.export),
|
||||
() => const ListCard<Spell, ExportController>(type: ListCardType.export),
|
||||
() =>
|
||||
const ListCard<Spell, ExportController>(type: ListCardType.export),
|
||||
() => const ListCard<Item, ExportController>(type: ListCardType.export),
|
||||
() => const ListCard<Race, ExportController>(type: ListCardType.export),
|
||||
],
|
||||
@@ -52,4 +53,3 @@ class ExportView extends GetView<ExportController> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import 'package:dungeon_paper/app/widgets/atoms/advanced_floating_action_button.dart';
|
||||
import 'package:dungeon_paper/i18n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controllers/import_export_controller.dart';
|
||||
import 'export_view.dart';
|
||||
import 'import_view.dart';
|
||||
|
||||
class ImportExportView extends GetView<ImportExportController> {
|
||||
class ImportExportView extends StatefulWidget {
|
||||
const ImportExportView({super.key});
|
||||
|
||||
@override
|
||||
State<ImportExportView> createState() => _ImportExportViewState();
|
||||
}
|
||||
|
||||
class _ImportExportViewState extends State<ImportExportView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController tab;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
tab = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle = TextStyle(color: Theme.of(context).colorScheme.onSurface);
|
||||
@@ -22,7 +36,7 @@ class ImportExportView extends GetView<ImportExportController> {
|
||||
body: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: controller.tab.value,
|
||||
controller: tab,
|
||||
tabs: [
|
||||
Tab(child: Text(tr.backup.exporting.title, style: textStyle)),
|
||||
Tab(child: Text(tr.backup.importing.title, style: textStyle)),
|
||||
@@ -30,7 +44,7 @@ class ImportExportView extends GetView<ImportExportController> {
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tab.value,
|
||||
controller: tab,
|
||||
children: const [
|
||||
ExportView(),
|
||||
ImportView(),
|
||||
@@ -39,18 +53,19 @@ class ImportExportView extends GetView<ImportExportController> {
|
||||
)
|
||||
],
|
||||
),
|
||||
floatingActionButton: Obx(
|
||||
() => AdvancedFloatingActionButton.extended(
|
||||
label: Text(controller.tab.value.index == 0
|
||||
floatingActionButton: Consumer<ImportExportController>(
|
||||
builder: (context, controller, _) =>
|
||||
AdvancedFloatingActionButton.extended(
|
||||
label: Text(tab.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,
|
||||
icon: Icon(tab.index == 0 ? Icons.upload : Icons.download),
|
||||
onPressed: tab.index == 0
|
||||
? () => controller.doExport(context)
|
||||
: () => controller.doImport(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,40 +8,46 @@ import 'package:dungeon_paper/app/modules/ImportExport/controllers/import_contro
|
||||
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 'package:provider/provider.dart';
|
||||
|
||||
import '../local_widgets/list_card.dart';
|
||||
|
||||
class ImportView extends GetView<ImportController> {
|
||||
class ImportView extends StatelessWidget {
|
||||
const ImportView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTileTheme.merge(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
child: Obx(
|
||||
() {
|
||||
child: Consumer<ImportController>(
|
||||
builder: (context, controller, _) {
|
||||
final builder = controller.hasData
|
||||
? ItemBuilder.lazyChildren(
|
||||
children: [
|
||||
() => ElevatedButton.icon(
|
||||
onPressed: () => controller.toImport.value = null,
|
||||
onPressed: () => controller.toImport = null,
|
||||
icon: const Icon(Icons.clear),
|
||||
label: Text(tr.backup.importing.file.clearFile),
|
||||
),
|
||||
() => const ListCard<Character, ImportController>(type: ListCardType.import),
|
||||
() => const ListCard<CharacterClass, ImportController>(type: ListCardType.import),
|
||||
() => const ListCard<Move, ImportController>(type: ListCardType.import),
|
||||
() => const ListCard<Spell, ImportController>(type: ListCardType.import),
|
||||
() => const ListCard<Item, ImportController>(type: ListCardType.import),
|
||||
() => const ListCard<Race, ImportController>(type: ListCardType.import),
|
||||
() => const ListCard<Character, ImportController>(
|
||||
type: ListCardType.import),
|
||||
() => const ListCard<CharacterClass, ImportController>(
|
||||
type: ListCardType.import),
|
||||
() => const ListCard<Move, ImportController>(
|
||||
type: ListCardType.import),
|
||||
() => const ListCard<Spell, ImportController>(
|
||||
type: ListCardType.import),
|
||||
() => const ListCard<Item, ImportController>(
|
||||
type: ListCardType.import),
|
||||
() => const ListCard<Race, ImportController>(
|
||||
type: ListCardType.import),
|
||||
],
|
||||
)
|
||||
: ItemBuilder.lazyChildren(
|
||||
children: [
|
||||
() => Text(tr.backup.importing.file.info),
|
||||
() => ElevatedButton.icon(
|
||||
onPressed: controller.pickImportFile,
|
||||
onPressed: () => controller.pickImportFile(context),
|
||||
icon: const Icon(Icons.file_open),
|
||||
label: Text(tr.backup.importing.file.browse),
|
||||
)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
// import '../controllers/library_collection_controller.dart';
|
||||
|
||||
class LibraryCollectionBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// Get.lazyPut<LibraryCollectionController>(
|
||||
// () => LibraryCollectionController(),
|
||||
// );
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/item.dart';
|
||||
import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/note.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/widgets/forms/character_class_form.dart';
|
||||
import 'package:dungeon_paper/app/widgets/forms/item_form.dart';
|
||||
import 'package:dungeon_paper/app/widgets/forms/move_form.dart';
|
||||
import 'package:dungeon_paper/app/widgets/forms/note_form.dart';
|
||||
import 'package:dungeon_paper/app/widgets/forms/race_form.dart';
|
||||
import 'package:dungeon_paper/app/widgets/forms/spell_form.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class LibraryFormBinding<T> extends Bindings {
|
||||
LibraryFormBinding();
|
||||
|
||||
@override
|
||||
void dependencies() {
|
||||
switch (T) {
|
||||
case == Move:
|
||||
Get.put<MoveFormController>(MoveFormController());
|
||||
break;
|
||||
case == Spell:
|
||||
Get.put<SpellFormController>(SpellFormController());
|
||||
break;
|
||||
case == Item:
|
||||
Get.put<ItemFormController>(ItemFormController());
|
||||
break;
|
||||
case == Note:
|
||||
Get.put<NoteFormController>(NoteFormController());
|
||||
break;
|
||||
case == CharacterClass:
|
||||
Get.put<CharacterClassFormController>(CharacterClassFormController());
|
||||
break;
|
||||
case == Race:
|
||||
Get.put<RaceFormController>(RaceFormController());
|
||||
break;
|
||||
default:
|
||||
throw UnsupportedError('Type $T is unsupported');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/item.dart';
|
||||
import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/filters/race_filters.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
|
||||
import '../views/filters/character_class_filters.dart';
|
||||
import '../views/filters/item_filters.dart';
|
||||
import '../views/filters/move_filters.dart';
|
||||
import '../views/filters/spell_filters.dart';
|
||||
|
||||
class LibraryListBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<LibraryListController<Move, MoveFilters>>(
|
||||
() => LibraryListController<Move, MoveFilters>(),
|
||||
);
|
||||
Get.lazyPut<LibraryListController<Spell, SpellFilters>>(
|
||||
() => LibraryListController<Spell, SpellFilters>(),
|
||||
);
|
||||
Get.lazyPut<LibraryListController<Item, ItemFilters>>(
|
||||
() => LibraryListController<Item, ItemFilters>(),
|
||||
);
|
||||
Get.lazyPut<LibraryListController<CharacterClass, CharacterClassFilters>>(
|
||||
() => LibraryListController<CharacterClass, CharacterClassFilters>(),
|
||||
);
|
||||
Get.lazyPut<LibraryListController<Race, RaceFilters>>(
|
||||
() => LibraryListController<Race, RaceFilters>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class LibraryCollectionController extends GetxController {
|
||||
//
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dungeon_paper/app/data/models/meta.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/library_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/character_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/library_provider.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_provider.dart';
|
||||
import 'package:dungeon_paper/core/global_keys.dart';
|
||||
import 'package:dungeon_paper/core/route_arguments.dart';
|
||||
import 'package:dungeon_paper/core/utils/list_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
enum FiltersGroup {
|
||||
playbook,
|
||||
@@ -14,63 +16,41 @@ enum FiltersGroup {
|
||||
}
|
||||
|
||||
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;
|
||||
extends ChangeNotifier
|
||||
with CharacterProviderMixin, RepositoryProviderMixin {
|
||||
late final LibraryListArguments<T, F> arguments;
|
||||
|
||||
final selected = <T>[].obs;
|
||||
final removed = <T>[].obs;
|
||||
final filters = <FiltersGroup, F?>{}.obs;
|
||||
final search = <FiltersGroup, TextEditingController>{}.obs;
|
||||
final selected = <T>[];
|
||||
final removed = <T>[];
|
||||
final filters = <FiltersGroup, F?>{};
|
||||
final search = <FiltersGroup, TextEditingController>{};
|
||||
// late final TabController tabController;
|
||||
|
||||
late final void Function(Iterable<T> items)? onSelected;
|
||||
late final bool Function(T item, F filters) filterFn;
|
||||
late final int Function(T a, T b) Function(F filters) sortFn;
|
||||
late final bool multiple;
|
||||
late final Iterable<T> preSelections;
|
||||
late final Map<String, dynamic> extraData;
|
||||
late final TabController tabController;
|
||||
bool get selectable => arguments.onSelected != null;
|
||||
|
||||
bool get selectable => onSelected != null;
|
||||
|
||||
Iterable<T> get builtInList =>
|
||||
filterList(builtInListRaw, FiltersGroup.playbook, filterFn, sortFn);
|
||||
Iterable<T> get builtInList => filterList(builtInListRaw,
|
||||
FiltersGroup.playbook, arguments.filterFn, arguments.sortFn);
|
||||
|
||||
Iterable<T> get builtInListRaw =>
|
||||
repo.value.builtIn.listByType<T>().values.toList();
|
||||
repo.builtIn.listByType<T>().values.toList();
|
||||
|
||||
Iterable<T> get myList =>
|
||||
filterList(myListRaw, FiltersGroup.my, filterFn, sortFn);
|
||||
Iterable<T> get myList => filterList(
|
||||
myListRaw, FiltersGroup.my, arguments.filterFn, arguments.sortFn);
|
||||
|
||||
Iterable<T> get myListRaw => repo.value.my.listByType<T>().values.toList();
|
||||
Iterable<T> get myListRaw => repo.my.listByType<T>().values.toList();
|
||||
String get storageKey => Meta.storageKeyFor(T);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
assert(Get.arguments != null);
|
||||
final LibraryListArguments<T, F> args = Get.arguments;
|
||||
filters.addAll(args.filters.cast<FiltersGroup, F?>());
|
||||
onSelected = args.onSelected;
|
||||
filterFn = args.filterFn;
|
||||
sortFn = args.sortFn;
|
||||
multiple = args.multiple;
|
||||
preSelections = args.preSelections;
|
||||
extraData = args.extraData;
|
||||
bool get multiple => arguments.multiple;
|
||||
Map<String, dynamic> get extraData => arguments.extraData;
|
||||
void Function(Iterable<T> items)? get onSelected => arguments.onSelected;
|
||||
|
||||
LibraryListController(BuildContext context) {
|
||||
arguments = getArgs(context);
|
||||
filters.addAll(arguments.filters.cast<FiltersGroup, F?>());
|
||||
search[FiltersGroup.playbook] ??= TextEditingController();
|
||||
search[FiltersGroup.playbook]!.addListener(_updatePlaybookSearch);
|
||||
search[FiltersGroup.my] ??= TextEditingController();
|
||||
search[FiltersGroup.my]!.addListener(_updateMySearch);
|
||||
tabController = TabController(
|
||||
initialIndex: FiltersGroup.values.indexOf(args.initialTab),
|
||||
// length: 3,
|
||||
length: 2,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -82,7 +62,7 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>>
|
||||
|
||||
void setFilters(FiltersGroup group, F? filters) {
|
||||
this.filters[group] = filters;
|
||||
this.filters.refresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleItem(T item, bool state) {
|
||||
@@ -90,29 +70,27 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>>
|
||||
return;
|
||||
}
|
||||
|
||||
if (!multiple) {
|
||||
if (!arguments.multiple) {
|
||||
selected.clear();
|
||||
for (final sel in preSelections) {
|
||||
removed.addIf(
|
||||
removed.firstWhereOrNull(_compare(sel)) == null,
|
||||
sel,
|
||||
);
|
||||
for (final sel in arguments.preSelections) {
|
||||
if (removed.firstWhereOrNull(_compare(sel)) == null) {
|
||||
removed.add(sel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state) {
|
||||
selected.addIf(
|
||||
selected.firstWhereOrNull(_compare(item)) == null,
|
||||
item,
|
||||
);
|
||||
if (selected.firstWhereOrNull(_compare(item)) == null) {
|
||||
selected.add(item);
|
||||
}
|
||||
removed.removeWhere(_compare(item));
|
||||
} else {
|
||||
selected.removeWhere(_compare(item));
|
||||
removed.addIf(
|
||||
removed.firstWhereOrNull(_compare(item)) == null,
|
||||
item,
|
||||
);
|
||||
if (removed.firstWhereOrNull(_compare(item)) == null) {
|
||||
removed.add(item);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
_compare(T item) {
|
||||
@@ -125,19 +103,23 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>>
|
||||
void saveCustomItem(String storageKey, T item) {
|
||||
toggleItem(item, true);
|
||||
debugPrint('Saving $item');
|
||||
final library = LibraryProvider.of(appGlobalKey.currentContext!);
|
||||
library.upsertToLibrary<T>([item]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void deleteCustomItem(String storageKey, T item) {
|
||||
toggleItem(item, false);
|
||||
debugPrint('Deleting $item');
|
||||
final library = LibraryProvider.of(appGlobalKey.currentContext!);
|
||||
library.removeFromLibrary<T>([item]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<T> get selectedWithMeta => selected;
|
||||
// selected.map((e) => forkMeta<T>(e, Get.find<UserService>().current)).toList();
|
||||
|
||||
bool isSelected(T item) => multiple
|
||||
bool isSelected(T item) => arguments.multiple
|
||||
?
|
||||
// multiple: if is selected or pre-selected
|
||||
isInCurrentSelectedList(item) || isPreSelected(item)
|
||||
@@ -155,9 +137,9 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>>
|
||||
bool isRemoved(T item) => removed.firstWhereOrNull(_compare(item)) != null;
|
||||
|
||||
bool isPreSelected(T item) =>
|
||||
preSelections.toList().firstWhereOrNull(_compare(item)) != null;
|
||||
arguments.preSelections.toList().firstWhereOrNull(_compare(item)) != null;
|
||||
|
||||
bool isEnabled(T item) => multiple
|
||||
bool isEnabled(T item) => arguments.multiple
|
||||
?
|
||||
// multiple: if is not pre-selected
|
||||
!isPreSelected(item)
|
||||
@@ -187,14 +169,12 @@ class LibraryListController<T extends WithMeta, F extends EntityFilters<T>>
|
||||
void _updatePlaybookSearch() {
|
||||
filters[FiltersGroup.playbook]
|
||||
?.setSearch(search[FiltersGroup.playbook]!.text);
|
||||
search.refresh();
|
||||
repo.refresh();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _updateMySearch() {
|
||||
filters[FiltersGroup.my]?.setSearch(search[FiltersGroup.my]!.text);
|
||||
search.refresh();
|
||||
repo.refresh();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,3 +223,4 @@ abstract class LibraryListArguments<T extends WithMeta,
|
||||
FiltersGroup? initialTab = FiltersGroup.playbook,
|
||||
}) : initialTab = initialTab ?? FiltersGroup.playbook;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +1,62 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/library_list_view.dart';
|
||||
import 'package:dungeon_paper/app/themes/button_themes.dart';
|
||||
import 'package:dungeon_paper/app/widgets/cards/character_class_card.dart';
|
||||
import 'package:dungeon_paper/app/widgets/menus/entity_edit_menu.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'filters/character_class_filters.dart';
|
||||
import 'library_select_button.dart';
|
||||
|
||||
class CharacterClassesLibraryListView extends GetView<
|
||||
LibraryListController<CharacterClass, CharacterClassFilters>> {
|
||||
class CharacterClassesLibraryListView extends StatelessWidget {
|
||||
const CharacterClassesLibraryListView({super.key});
|
||||
|
||||
RepositoryService get service => controller.repo.value;
|
||||
Character get char => controller.chars.value.current;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LibraryListView<CharacterClass, CharacterClassFilters>(
|
||||
filtersBuilder: (group, filters, onChange) => CharacterClassFiltersView(
|
||||
group: group,
|
||||
filters: filters,
|
||||
onChange: (f) => onChange(group, f),
|
||||
searchController: controller.search[group]!,
|
||||
),
|
||||
cardBuilder: (ctx, data) => CharacterClassCard(
|
||||
characterClass: data.item,
|
||||
showDice: false,
|
||||
showStar: false,
|
||||
highlightWords: data.highlightWords,
|
||||
trailing: [
|
||||
if (controller.selectable)
|
||||
LibrarySelectButton<CharacterClass>.icon(
|
||||
selected: data.selected,
|
||||
onPressed: data.onToggle,
|
||||
)
|
||||
],
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: data.onUpdate != null
|
||||
? () => ModelPages.openCharacterClassPage(
|
||||
characterClass: data.item,
|
||||
onSave: data.onUpdate!,
|
||||
)
|
||||
: null,
|
||||
onDelete:
|
||||
data.onDelete != null ? () => data.onDelete!(data.item) : null,
|
||||
),
|
||||
if (controller.selectable)
|
||||
LibrarySelectButton<CharacterClass>(
|
||||
selected: data.selected,
|
||||
onPressed: data.onToggle,
|
||||
)
|
||||
],
|
||||
return Consumer<
|
||||
LibraryListController<CharacterClass, CharacterClassFilters>>(
|
||||
builder: (context, controller, _) =>
|
||||
LibraryListView<CharacterClass, CharacterClassFilters>(
|
||||
filtersBuilder: (group, filters, onChange) => CharacterClassFiltersView(
|
||||
group: group,
|
||||
filters: filters,
|
||||
onChange: (f) => onChange(group, f),
|
||||
searchController: controller.search[group]!,
|
||||
),
|
||||
cardBuilder: (ctx, data) => CharacterClassCard(
|
||||
characterClass: data.item,
|
||||
showDice: false,
|
||||
showStar: false,
|
||||
highlightWords: data.highlightWords,
|
||||
trailing: [
|
||||
if (controller.selectable)
|
||||
LibrarySelectButton<CharacterClass>.icon(
|
||||
selected: data.selected,
|
||||
onPressed: data.onToggle,
|
||||
)
|
||||
],
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: data.onUpdate != null
|
||||
? () => ModelPages.openCharacterClassPage(
|
||||
context,
|
||||
characterClass: data.item,
|
||||
onSave: data.onUpdate!,
|
||||
)
|
||||
: null,
|
||||
onDelete: data.onDelete != null
|
||||
? () => data.onDelete!(data.item)
|
||||
: null,
|
||||
),
|
||||
if (controller.selectable)
|
||||
LibrarySelectButton<CharacterClass>(
|
||||
selected: data.selected,
|
||||
onPressed: data.onToggle,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -83,4 +81,3 @@ class CharacterClassLibraryListArguments
|
||||
multiple: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
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/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({
|
||||
const EntityFiltersView({
|
||||
super.key,
|
||||
required this.typeName,
|
||||
required this.filters,
|
||||
@@ -27,7 +25,6 @@ class EntityFiltersView<T, F extends EntityFilters<T>> extends StatelessWidget {
|
||||
final F emptyFilters;
|
||||
final List<Widget> Function(BuildContext context, F filters)?
|
||||
filterWidgetsBuilder;
|
||||
final service = Get.find<RepositoryService>();
|
||||
final void Function(F) onChange;
|
||||
final TextEditingController searchController;
|
||||
final Iterable<Widget> leading;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart';
|
||||
import 'package:dungeon_paper/core/utils/math_utils.dart';
|
||||
@@ -7,11 +6,10 @@ import 'package:dungeon_paper/core/utils/string_utils.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 CharacterClassFiltersView extends StatelessWidget {
|
||||
CharacterClassFiltersView({
|
||||
const CharacterClassFiltersView({
|
||||
super.key,
|
||||
required this.filters,
|
||||
required this.group,
|
||||
@@ -21,7 +19,6 @@ class CharacterClassFiltersView extends StatelessWidget {
|
||||
|
||||
final CharacterClassFilters filters;
|
||||
final FiltersGroup group;
|
||||
final repo = Get.find<RepositoryService>();
|
||||
final void Function(CharacterClassFilters) onChange;
|
||||
final TextEditingController searchController;
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import 'package:dungeon_paper/app/data/models/item.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart';
|
||||
import 'package:dungeon_paper/core/utils/math_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_utils.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 ItemFiltersView extends StatelessWidget {
|
||||
ItemFiltersView({
|
||||
const ItemFiltersView({
|
||||
super.key,
|
||||
required this.filters,
|
||||
required this.onChange,
|
||||
@@ -18,7 +16,6 @@ class ItemFiltersView extends StatelessWidget {
|
||||
});
|
||||
|
||||
final ItemFilters filters;
|
||||
final service = Get.find<RepositoryService>();
|
||||
final void Function(ItemFilters) onChange;
|
||||
final TextEditingController searchController;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/move.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_provider.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/select_box.dart';
|
||||
@@ -8,11 +8,10 @@ import 'package:dungeon_paper/core/utils/math_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_utils.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({
|
||||
const MoveFiltersView({
|
||||
super.key,
|
||||
required this.filters,
|
||||
required this.group,
|
||||
@@ -22,67 +21,68 @@ class MoveFiltersView extends StatelessWidget {
|
||||
|
||||
final MoveFilters filters;
|
||||
final FiltersGroup group;
|
||||
final repo = Get.find<RepositoryService>();
|
||||
final void Function(MoveFilters) onChange;
|
||||
final TextEditingController searchController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EntityFiltersView<Move, MoveFilters>(
|
||||
filters: filters,
|
||||
emptyFilters: MoveFilters(classKey: null),
|
||||
onChange: onChange,
|
||||
searchController: searchController,
|
||||
typeName: tn(Move),
|
||||
filterWidgetsBuilder: (context, f) => [
|
||||
SelectBox<MoveCategory?>(
|
||||
isExpanded: true,
|
||||
label: Text(tr.entityPlural('MoveCategory')),
|
||||
value: f.category,
|
||||
items: [
|
||||
DropdownMenuItem<MoveCategory?>(
|
||||
value: null,
|
||||
child:
|
||||
Text(tr.generic.allEntities(tr.entityPlural('MoveCategory'))),
|
||||
),
|
||||
...MoveCategory.values.map(
|
||||
(cat) => DropdownMenuItem<MoveCategory?>(
|
||||
value: cat,
|
||||
child: Text(tr.moves.category.longName(cat.name)),
|
||||
return RepositoryProvider.consumer(
|
||||
(context, repo, _) => EntityFiltersView<Move, MoveFilters>(
|
||||
filters: filters,
|
||||
emptyFilters: MoveFilters(classKey: null),
|
||||
onChange: onChange,
|
||||
searchController: searchController,
|
||||
typeName: tn(Move),
|
||||
filterWidgetsBuilder: (context, f) => [
|
||||
SelectBox<MoveCategory?>(
|
||||
isExpanded: true,
|
||||
label: Text(tr.entityPlural('MoveCategory')),
|
||||
value: f.category,
|
||||
items: [
|
||||
DropdownMenuItem<MoveCategory?>(
|
||||
value: null,
|
||||
child: Text(
|
||||
tr.generic.allEntities(tr.entityPlural('MoveCategory'))),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (cat) {
|
||||
onChange(f..category = cat);
|
||||
f.controller.add(f);
|
||||
},
|
||||
),
|
||||
SelectBox<String>(
|
||||
label: Text(tr.entityPlural(tn(CharacterClass))),
|
||||
isExpanded: true,
|
||||
value: f.classKey,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text(
|
||||
tr.generic.allEntities(tr.entityPlural(tn(CharacterClass)))),
|
||||
),
|
||||
...<CharacterClass>{
|
||||
...repo.builtIn.classes.values,
|
||||
...repo.my.classes.values
|
||||
}.map(
|
||||
(cls) => DropdownMenuItem<String>(
|
||||
value: cls.key,
|
||||
child: Text(cls.name),
|
||||
...MoveCategory.values.map(
|
||||
(cat) => DropdownMenuItem<MoveCategory?>(
|
||||
value: cat,
|
||||
child: Text(tr.moves.category.longName(cat.name)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (key) {
|
||||
onChange(f..classKey = key);
|
||||
f.controller.add(f);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
onChanged: (cat) {
|
||||
onChange(f..category = cat);
|
||||
f.controller.add(f);
|
||||
},
|
||||
),
|
||||
SelectBox<String>(
|
||||
label: Text(tr.entityPlural(tn(CharacterClass))),
|
||||
isExpanded: true,
|
||||
value: f.classKey,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text(tr.generic
|
||||
.allEntities(tr.entityPlural(tn(CharacterClass)))),
|
||||
),
|
||||
...<CharacterClass>{
|
||||
...repo.builtIn.classes.values,
|
||||
...repo.my.classes.values
|
||||
}.map(
|
||||
(cls) => DropdownMenuItem<String>(
|
||||
value: cls.key,
|
||||
child: Text(cls.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (key) {
|
||||
onChange(f..classKey = key);
|
||||
f.controller.add(f);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import 'package:dungeon_paper/app/data/models/note.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart';
|
||||
import 'package:dungeon_paper/core/utils/math_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_utils.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 NoteFiltersView extends StatelessWidget {
|
||||
NoteFiltersView({
|
||||
Key? key,
|
||||
const NoteFiltersView({
|
||||
super.key,
|
||||
required this.filters,
|
||||
required this.onChange,
|
||||
required this.searchController,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final NoteFilters filters;
|
||||
final service = Get.find<RepositoryService>();
|
||||
final void Function(NoteFilters) onChange;
|
||||
final TextEditingController searchController;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/race.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_provider.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/select_box.dart';
|
||||
@@ -8,11 +8,10 @@ import 'package:dungeon_paper/core/utils/math_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_utils.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({
|
||||
const RaceFiltersView({
|
||||
super.key,
|
||||
required this.filters,
|
||||
required this.group,
|
||||
@@ -22,45 +21,46 @@ class RaceFiltersView extends StatelessWidget {
|
||||
|
||||
final RaceFilters filters;
|
||||
final FiltersGroup group;
|
||||
final repo = Get.find<RepositoryService>();
|
||||
final void Function(RaceFilters) onChange;
|
||||
final TextEditingController searchController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EntityFiltersView<Race, RaceFilters>(
|
||||
filters: filters,
|
||||
emptyFilters: RaceFilters(classKey: null),
|
||||
onChange: onChange,
|
||||
searchController: searchController,
|
||||
typeName: tn(Race),
|
||||
filterWidgetsBuilder: (context, f) => [
|
||||
SelectBox<String>(
|
||||
label: Text(tr.entityPlural(tn(CharacterClass))),
|
||||
isExpanded: true,
|
||||
value: f.classKey,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text(
|
||||
tr.generic.allEntities(tr.entityPlural(tn(CharacterClass)))),
|
||||
),
|
||||
...<CharacterClass>{
|
||||
...repo.builtIn.classes.values,
|
||||
...repo.my.classes.values
|
||||
}.map(
|
||||
(cls) => DropdownMenuItem<String>(
|
||||
value: cls.key,
|
||||
child: Text(cls.name),
|
||||
return RepositoryProvider.consumer(
|
||||
(context, repo, _) => EntityFiltersView<Race, RaceFilters>(
|
||||
filters: filters,
|
||||
emptyFilters: RaceFilters(classKey: null),
|
||||
onChange: onChange,
|
||||
searchController: searchController,
|
||||
typeName: tn(Race),
|
||||
filterWidgetsBuilder: (context, f) => [
|
||||
SelectBox<String>(
|
||||
label: Text(tr.entityPlural(tn(CharacterClass))),
|
||||
isExpanded: true,
|
||||
value: f.classKey,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text(tr.generic
|
||||
.allEntities(tr.entityPlural(tn(CharacterClass)))),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (key) {
|
||||
onChange(f..classKey = key);
|
||||
f.controller.add(f);
|
||||
},
|
||||
),
|
||||
],
|
||||
...<CharacterClass>{
|
||||
...repo.builtIn.classes.values,
|
||||
...repo.my.classes.values
|
||||
}.map(
|
||||
(cls) => DropdownMenuItem<String>(
|
||||
value: cls.key,
|
||||
child: Text(cls.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (key) {
|
||||
onChange(f..classKey = key);
|
||||
f.controller.add(f);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:dungeon_paper/app/data/models/character_class.dart';
|
||||
import 'package:dungeon_paper/app/data/models/spell.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_service.dart';
|
||||
import 'package:dungeon_paper/app/data/services/repository_provider.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/entity_filters.dart';
|
||||
import 'package:dungeon_paper/app/widgets/atoms/select_box.dart';
|
||||
@@ -8,11 +8,10 @@ import 'package:dungeon_paper/core/utils/math_utils.dart';
|
||||
import 'package:dungeon_paper/core/utils/string_utils.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({
|
||||
const SpellFiltersView({
|
||||
super.key,
|
||||
required this.group,
|
||||
required this.filters,
|
||||
@@ -22,44 +21,45 @@ class SpellFiltersView extends StatelessWidget {
|
||||
|
||||
final SpellFilters filters;
|
||||
final FiltersGroup group;
|
||||
final repo = Get.find<RepositoryService>();
|
||||
final void Function(SpellFilters) onChange;
|
||||
final TextEditingController searchController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EntityFiltersView<Spell, SpellFilters>(
|
||||
filters: filters,
|
||||
emptyFilters: SpellFilters(classKey: null, level: null),
|
||||
onChange: onChange,
|
||||
searchController: searchController,
|
||||
typeName: tn(Spell),
|
||||
filterWidgetsBuilder: (context, f) => [
|
||||
SelectBox<String>(
|
||||
label: Text(tr.entityPlural(tn(Spell))),
|
||||
value: f.classKey,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text(
|
||||
tr.generic.allEntities(tr.entityPlural(tn(CharacterClass)))),
|
||||
),
|
||||
...<CharacterClass>{
|
||||
...repo.builtIn.classes.values,
|
||||
...repo.my.classes.values
|
||||
}.map(
|
||||
(cls) => DropdownMenuItem<String>(
|
||||
value: cls.key,
|
||||
child: Text(cls.name),
|
||||
return RepositoryProvider.consumer(
|
||||
(context, repo, _) => EntityFiltersView<Spell, SpellFilters>(
|
||||
filters: filters,
|
||||
emptyFilters: SpellFilters(classKey: null, level: null),
|
||||
onChange: onChange,
|
||||
searchController: searchController,
|
||||
typeName: tn(Spell),
|
||||
filterWidgetsBuilder: (context, f) => [
|
||||
SelectBox<String>(
|
||||
label: Text(tr.entityPlural(tn(Spell))),
|
||||
value: f.classKey,
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text(tr.generic
|
||||
.allEntities(tr.entityPlural(tn(CharacterClass)))),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (key) {
|
||||
onChange(f..classKey = key);
|
||||
f.controller.add(f);
|
||||
},
|
||||
),
|
||||
],
|
||||
...<CharacterClass>{
|
||||
...repo.builtIn.classes.values,
|
||||
...repo.my.classes.values
|
||||
}.map(
|
||||
(cls) => DropdownMenuItem<String>(
|
||||
value: cls.key,
|
||||
child: Text(cls.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (key) {
|
||||
onChange(f..classKey = key);
|
||||
f.controller.add(f);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,59 @@
|
||||
import 'package:dungeon_paper/app/data/models/character.dart';
|
||||
import 'package:dungeon_paper/app/data/models/item.dart';
|
||||
import 'package:dungeon_paper/app/model_utils/model_pages.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/controllers/library_list_controller.dart';
|
||||
import 'package:dungeon_paper/app/modules/LibraryList/views/library_list_view.dart';
|
||||
import 'package:dungeon_paper/app/themes/button_themes.dart';
|
||||
import 'package:dungeon_paper/app/widgets/cards/item_card.dart';
|
||||
import 'package:dungeon_paper/app/widgets/menus/entity_edit_menu.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'filters/item_filters.dart';
|
||||
import 'library_select_button.dart';
|
||||
|
||||
class ItemsLibraryListView
|
||||
extends GetView<LibraryListController<Item, ItemFilters>> {
|
||||
class ItemsLibraryListView extends StatelessWidget {
|
||||
const ItemsLibraryListView({super.key});
|
||||
|
||||
Character get char => controller.chars.value.current;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LibraryListView<Item, ItemFilters>(
|
||||
filtersBuilder: (group, filters, onChange) => ItemFiltersView(
|
||||
filters: filters,
|
||||
onChange: (f) => onChange(group, f),
|
||||
searchController: controller.search[group]!,
|
||||
),
|
||||
cardBuilder: (ctx, data) => ItemCard(
|
||||
item: data.item,
|
||||
showStar: false,
|
||||
highlightWords: data.highlightWords,
|
||||
hideCount: true,
|
||||
trailing: [
|
||||
if (controller.selectable)
|
||||
LibrarySelectButton<Item>.icon(
|
||||
selected: data.selected,
|
||||
onPressed: data.onToggle,
|
||||
)
|
||||
],
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: data.onUpdate != null
|
||||
? () => ModelPages.openItemPage(
|
||||
item: data.item,
|
||||
onSave: data.onUpdate!,
|
||||
)
|
||||
: null,
|
||||
onDelete:
|
||||
data.onDelete != null ? () => data.onDelete!(data.item) : null,
|
||||
),
|
||||
if (controller.selectable)
|
||||
LibrarySelectButton<Item>(
|
||||
selected: data.selected,
|
||||
onPressed: data.onToggle,
|
||||
)
|
||||
],
|
||||
return Consumer<LibraryListController<Item, ItemFilters>>(
|
||||
builder: (context, controller, _) => LibraryListView<Item, ItemFilters>(
|
||||
filtersBuilder: (group, filters, onChange) => ItemFiltersView(
|
||||
filters: filters,
|
||||
onChange: (f) => onChange(group, f),
|
||||
searchController: controller.search[group]!,
|
||||
),
|
||||
cardBuilder: (ctx, data) => ItemCard(
|
||||
item: data.item,
|
||||
showStar: false,
|
||||
highlightWords: data.highlightWords,
|
||||
hideCount: true,
|
||||
trailing: [
|
||||
if (controller.selectable)
|
||||
LibrarySelectButton<Item>.icon(
|
||||
selected: data.selected,
|
||||
onPressed: data.onToggle,
|
||||
)
|
||||
],
|
||||
actions: [
|
||||
EntityEditMenu(
|
||||
onEdit: data.onUpdate != null
|
||||
? () => ModelPages.openItemPage(
|
||||
context,
|
||||
item: data.item,
|
||||
onSave: data.onUpdate!,
|
||||
)
|
||||
: null,
|
||||
onDelete: data.onDelete != null
|
||||
? () => data.onDelete!(data.item)
|
||||
: null,
|
||||
),
|
||||
if (controller.selectable)
|
||||
LibrarySelectButton<Item>(
|
||||
selected: data.selected,
|
||||
onPressed: data.onToggle,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -76,4 +74,3 @@ class ItemLibraryListArguments extends LibraryListArguments<Item, ItemFilters> {
|
||||
extraData: const {},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user