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:
Chen Asraf
2023-12-22 00:51:22 +02:00
committed by GitHub
parent 86b660037e
commit b511fc5084
237 changed files with 5207 additions and 5303 deletions

2
devtools_options.yaml Normal file
View File

@@ -0,0 +1,2 @@
extensions:
- provider: true

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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'],

View File

@@ -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 {}
}
}

View File

@@ -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!);
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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!);
}

View 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!);
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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');
}

View File

@@ -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;
}

View File

@@ -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) =>

View File

@@ -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,

View File

@@ -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),
);
}

View File

@@ -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(),
);
}
}

View File

@@ -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,
});
}

View File

@@ -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,
),
),
),
],
],
),
),
),
);

View File

@@ -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(),
);
}
}

View File

@@ -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,
});
}

View File

@@ -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();
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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,
),

View File

@@ -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(),
);
}
}

View File

@@ -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,
),
);
}

View File

@@ -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(),
),
);
}

View File

@@ -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(),
);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
};
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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),
),

View File

@@ -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(),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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)))),
)

View File

@@ -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(),
);
}
}

View File

@@ -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 {
//
}

View File

@@ -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(

View File

@@ -1,8 +0,0 @@
import 'package:get/get.dart';
class CharacterListPageBinding extends Bindings {
@override
void dependencies() {
//
}
}

View File

@@ -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(

View File

@@ -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(),
);
}
}

View File

@@ -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,
});
}

View File

@@ -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]!,
),
)
],
),
),
);
}),
),
],
),
);
},
),
),
],
),
),
);
}

View File

@@ -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(),
);
}
}

View File

@@ -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,
});
}

View File

@@ -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();
}
}

View File

@@ -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(),
);
}
}

View File

@@ -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(),
),
);
}

View File

@@ -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 {
);
}
}

View File

@@ -1,8 +0,0 @@
import 'package:get/get.dart';
class HomeBinding extends Bindings {
@override
void dependencies() {
//
}
}

View File

@@ -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),

View File

@@ -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);
}

View File

@@ -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(),
),
);
});
};
}
}

View File

@@ -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> {
);
}
}

View File

@@ -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),

View File

@@ -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 {
}
}
}

View File

@@ -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;
}

View File

@@ -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),
),
],
),

View File

@@ -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;
}
}

View File

@@ -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,
);
}
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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()),
),
),
],

View File

@@ -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) =>

View File

@@ -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(),
);
}
}

View File

@@ -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);
};
}

View File

@@ -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,
),
],
),
),
);
};
}

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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>
}
}
}

View File

@@ -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');
}
}

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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> {
);
}
}

View File

@@ -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),
),
),
);
}
}

View File

@@ -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),
)

View File

@@ -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(),
// );
}
}

View File

@@ -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');
}
}
}

View File

@@ -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>(),
);
}
}

View File

@@ -1,5 +0,0 @@
import 'package:get/get.dart';
class LibraryCollectionController extends GetxController {
//
}

View File

@@ -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;
}

View File

@@ -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,
);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
},
),
],
),
);
}
}

View File

@@ -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;

View File

@@ -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);
},
),
],
),
);
}
}

View File

@@ -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);
},
),
],
),
);
}
}

View File

@@ -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