diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 911ec90..de24880 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,6 +24,11 @@ + + + + + diff --git a/icons/system_update_alt.svg b/icons/system_update_alt.svg new file mode 100644 index 0000000..574c13a --- /dev/null +++ b/icons/system_update_alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/components/ability.dart b/lib/components/ability.dart index a08418a..88e3bd4 100644 --- a/lib/components/ability.dart +++ b/lib/components/ability.dart @@ -1,7 +1,7 @@ import 'package:cypher_sheet/components/dialog.dart'; import 'package:cypher_sheet/extensions/pool.dart'; import 'package:cypher_sheet/state/providers/abilities.dart'; -import 'package:cypher_sheet/views/dialogs/view_ability.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/view.dart'; import 'package:flutter/material.dart'; import 'package:cypher_sheet/components/box.dart'; import 'package:cypher_sheet/components/icon.dart'; diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 0e63e4e..e35b9a3 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -33,6 +33,7 @@ class AppBar extends StatelessWidget { child: child, ), ), + automaticallyImplyLeading: false, ); } } diff --git a/lib/components/cypher.dart b/lib/components/cypher.dart index 56f5f62..d65fc98 100644 --- a/lib/components/cypher.dart +++ b/lib/components/cypher.dart @@ -2,8 +2,8 @@ import 'package:cypher_sheet/components/dialog.dart'; import 'package:cypher_sheet/extensions/cypher.dart'; import 'package:cypher_sheet/proto/character.pb.dart'; import 'package:cypher_sheet/state/providers/cyphers.dart'; -import 'package:cypher_sheet/views/dialogs/view_artifact.dart'; -import 'package:cypher_sheet/views/dialogs/view_cypher.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/view.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/view.dart'; import 'package:flutter/material.dart'; import 'package:cypher_sheet/components/box.dart'; import 'package:cypher_sheet/components/icon.dart'; diff --git a/lib/components/equipment.dart b/lib/components/equipment.dart index 8b4e5d9..3d88482 100644 --- a/lib/components/equipment.dart +++ b/lib/components/equipment.dart @@ -2,7 +2,7 @@ import 'package:cypher_sheet/components/dialog.dart'; import 'package:cypher_sheet/extensions/item.dart'; import 'package:cypher_sheet/state/providers/character.dart'; import 'package:cypher_sheet/state/providers/items.dart'; -import 'package:cypher_sheet/views/dialogs/view_item.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/view.dart'; import 'package:flutter/material.dart'; import 'package:cypher_sheet/components/box.dart'; import 'package:cypher_sheet/components/cypher.dart'; diff --git a/lib/components/icons.dart b/lib/components/icons.dart index 27470c6..f163fe4 100644 --- a/lib/components/icons.dart +++ b/lib/components/icons.dart @@ -49,6 +49,7 @@ enum AppIcons { edit, deleteForever, share, + import, devMode; @override @@ -145,6 +146,8 @@ String iconName(AppIcons from) { return "delete_forever"; case AppIcons.share: return "share"; + case AppIcons.import: + return "system_update_alt"; case AppIcons.devMode: return "developer_mode"; } diff --git a/lib/components/meta.dart b/lib/components/meta.dart index 58727cb..1169479 100644 --- a/lib/components/meta.dart +++ b/lib/components/meta.dart @@ -1,3 +1,4 @@ +import 'package:cypher_sheet/main.dart'; import 'package:cypher_sheet/state/providers/character.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -44,6 +45,7 @@ class CharacterMeta extends ConsumerWidget { child: AppBox( onTap: () { ref.invalidate(characterListProvider); + Navigator.of(context).pushReplacementNamed(routeCharacters); ref.read(characterProvider.notifier).reset(); }, flat: true, diff --git a/lib/components/note.dart b/lib/components/note.dart index 9b6146b..4783280 100644 --- a/lib/components/note.dart +++ b/lib/components/note.dart @@ -3,8 +3,8 @@ import 'package:cypher_sheet/components/icons.dart'; import 'package:cypher_sheet/extensions/note.dart'; import 'package:cypher_sheet/proto/character.pb.dart'; import 'package:cypher_sheet/state/providers/notes.dart'; -import 'package:cypher_sheet/views/dialogs/create_note.dart'; -import 'package:cypher_sheet/views/dialogs/view_note.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/update.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/view.dart'; import 'package:flutter/material.dart'; import 'package:cypher_sheet/components/box.dart'; import 'package:cypher_sheet/components/icon.dart'; @@ -51,7 +51,7 @@ class NoteListItem extends ConsumerWidget { padding: 4, size: 24, onTap: () { - showAppDialog(context, CreateNote.fromState(note)); + showAppDialog(context, UpdateNote(note)); }), ], ), diff --git a/lib/components/share.dart b/lib/components/share.dart new file mode 100644 index 0000000..91d358f --- /dev/null +++ b/lib/components/share.dart @@ -0,0 +1,23 @@ +import 'package:cypher_sheet/components/icon.dart'; +import 'package:cypher_sheet/components/icons.dart'; +import 'package:flutter/material.dart'; +import 'package:share_plus/share_plus.dart'; + +class ShareObjectButton extends StatelessWidget { + const ShareObjectButton(this.getFile, {super.key}); + + final Future Function() getFile; + + @override + Widget build(BuildContext context) { + return SVGBox( + padding: 12, + onTap: (() async { + Share.shareXFiles(subject: "Shared from Cypher Sheet", [ + await getFile(), + ]); + }), + icon: AppIcons.share, + ); + } +} diff --git a/lib/components/skill.dart b/lib/components/skill.dart index ab30ee1..b940536 100644 --- a/lib/components/skill.dart +++ b/lib/components/skill.dart @@ -2,7 +2,7 @@ import 'package:cypher_sheet/components/dialog.dart'; import 'package:cypher_sheet/extensions/pool.dart'; import 'package:cypher_sheet/extensions/skill.dart'; import 'package:cypher_sheet/state/providers/skills.dart'; -import 'package:cypher_sheet/views/dialogs/view_skill.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/view.dart'; import 'package:flutter/material.dart'; import 'package:cypher_sheet/components/box.dart'; import 'package:cypher_sheet/components/icon.dart'; diff --git a/lib/extensions/ability.dart b/lib/extensions/ability.dart new file mode 100644 index 0000000..68e2bc4 --- /dev/null +++ b/lib/extensions/ability.dart @@ -0,0 +1,11 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; + +extension ShareHelper on Ability { + SharedObject share() { + return SharedObject( + uuid: uuid, + name: name, + ability: this, + ); + } +} diff --git a/lib/extensions/artifact.dart b/lib/extensions/artifact.dart new file mode 100644 index 0000000..874fa4c --- /dev/null +++ b/lib/extensions/artifact.dart @@ -0,0 +1,11 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; + +extension ShareHelper on Artifact { + SharedObject share() { + return SharedObject( + uuid: uuid, + name: name, + artifact: this, + ); + } +} diff --git a/lib/extensions/cypher.dart b/lib/extensions/cypher.dart index 5645cb7..38e871a 100644 --- a/lib/extensions/cypher.dart +++ b/lib/extensions/cypher.dart @@ -14,3 +14,13 @@ extension CypherTypeHelper on CypherType { } } } + +extension ShareHelper on Cypher { + SharedObject share() { + return SharedObject( + uuid: uuid, + name: name, + cypher: this, + ); + } +} diff --git a/lib/extensions/editable.dart b/lib/extensions/editable.dart new file mode 100644 index 0000000..8465280 --- /dev/null +++ b/lib/extensions/editable.dart @@ -0,0 +1,195 @@ +import 'package:cypher_sheet/components/equipment.dart'; +import 'package:flutter/widgets.dart'; +import 'package:protobuf/protobuf.dart'; + +Map getEditableTextFieldsFrom( + GeneratedMessage msg) { + final Map fields = {}; + mergeEditableTextFieldsFrom(msg, fields); + return fields; +} + +void mergeEditableTextFieldsFrom( + GeneratedMessage msg, Map into) { + // TODO: is byIndex more efficient but still safe? + for (var fieldName in msg.info_.byName.keys) { + final fieldValue = msg.info_.byName[fieldName]; + if (fieldValue == null) { + assert(fieldValue != null); + continue; + } + + if (fieldValue.type != PbFieldType.OS) { + continue; + } + into[fieldName] = + TextEditingController(text: msg.getField(fieldValue.tagNumber)); + } +} + +// Must only be called once per message as it disposes all TextEditingControllers. +Function(String key, TextEditingController value) textFieldsUpdater( + GeneratedMessage msg) { + return (key, value) { + final tagNumber = msg.getTagNumber(key); + if (tagNumber == null) { + assert(tagNumber != null); + return; + } + msg.setField(tagNumber, value.value.text); + value.dispose(); + }; +} + +Map getEditableDoubleFieldsFrom( + GeneratedMessage msg) { + final Map fields = {}; + mergeEditableDoubleFieldsFrom(msg, fields); + return fields; +} + +void mergeEditableDoubleFieldsFrom( + GeneratedMessage msg, Map into) { + // TODO: is byIndex more efficient but still safe? + for (var fieldName in msg.info_.byName.keys) { + final fieldValue = msg.info_.byName[fieldName]; + if (fieldValue == null) { + assert(fieldValue != null); + continue; + } + + if (fieldValue.type != PbFieldType.OD) { + continue; + } + into[fieldName] = TextEditingController( + text: + removeZeroDecimals(msg.getField(fieldValue.tagNumber)).toString()); + } +} + +Function(String key, TextEditingController value) doubleFieldsUpdater( + GeneratedMessage msg) { + return (key, value) { + final tagNumber = msg.getTagNumber(key); + if (tagNumber == null) { + assert(tagNumber != null); + return; + } + final parsed = double.tryParse(value.value.text); + if (value.value.text.isNotEmpty && parsed != null) { + msg.setField(tagNumber, parsed); + } + value.dispose(); + }; +} + +Map getEditableBoolFieldsFrom(GeneratedMessage msg) { + final Map fields = {}; + mergeEditableBoolFieldsFrom(msg, fields); + return fields; +} + +void mergeEditableBoolFieldsFrom(GeneratedMessage msg, Map into) { + // TODO: is byIndex more efficient but still safe? + for (var fieldName in msg.info_.byName.keys) { + final fieldValue = msg.info_.byName[fieldName]; + if (fieldValue == null) { + assert(fieldValue != null); + continue; + } + + if (fieldValue.type != PbFieldType.OB) { + continue; + } + into[fieldName] = msg.getField(fieldValue.tagNumber); + } +} + +Function(String key, bool value) boolFieldsUpdater(GeneratedMessage msg) { + return (key, value) { + final tagNumber = msg.getTagNumber(key); + if (tagNumber == null) { + assert(tagNumber != null); + return; + } + msg.setField(tagNumber, value); + }; +} + +Map getEditableIntFieldsFrom( + GeneratedMessage msg) { + final Map fields = {}; + mergeEditableDoubleFieldsFrom(msg, fields); + return fields; +} + +void mergeEditableIntFieldsFrom( + GeneratedMessage msg, Map into) { + // TODO: is byIndex more efficient but still safe? + for (var fieldName in msg.info_.byName.keys) { + final fieldValue = msg.info_.byName[fieldName]; + if (fieldValue == null) { + assert(fieldValue != null); + continue; + } + + if (fieldValue.type != PbFieldType.OD) { + continue; + } + into[fieldName] = TextEditingController( + text: msg.getField(fieldValue.tagNumber).toString()); + } +} + +Function(String key, TextEditingController value) intFieldsUpdater( + GeneratedMessage msg) { + return (key, value) { + final tagNumber = msg.getTagNumber(key); + if (tagNumber == null) { + assert(tagNumber != null); + return; + } + final parsed = int.tryParse(value.value.text); + if (value.value.text.isNotEmpty && parsed != null) { + msg.setField(tagNumber, parsed); + } + value.dispose(); + }; +} + +void mergeEditableFieldsFrom( + GeneratedMessage msg, { + required Map strings, + required Map bools, + required Map doubles, + required Map ints, +}) { + // TODO: is byIndex more efficient but still safe? + for (var fieldName in msg.info_.byName.keys) { + final fieldValue = msg.info_.byName[fieldName]; + if (fieldValue == null) { + assert(fieldValue != null); + continue; + } + + switch (fieldValue.type) { + case PbFieldType.OS: + strings[fieldName] = + TextEditingController(text: msg.getField(fieldValue.tagNumber)); + break; + case PbFieldType.OB: + bools[fieldName] = msg.getField(fieldValue.tagNumber); + break; + case PbFieldType.OD: + doubles[fieldName] = TextEditingController( + text: removeZeroDecimals(msg.getField(fieldValue.tagNumber)) + .toString()); + break; + case PbFieldType.O3: + ints[fieldName] = TextEditingController( + text: msg.getField(fieldValue.tagNumber).toString()); + break; + default: + } + } +} diff --git a/lib/extensions/item.dart b/lib/extensions/item.dart index e761261..9447385 100644 --- a/lib/extensions/item.dart +++ b/lib/extensions/item.dart @@ -53,3 +53,14 @@ extension Label on ItemType { return name[0].toUpperCase() + name.substring(1); } } + +extension ShareHelper on Item { + SharedObject share() { + // TODO: figure out how to handle subItems. + return SharedObject( + uuid: path.self, + name: name, + item: this, + ); + } +} diff --git a/lib/extensions/note.dart b/lib/extensions/note.dart index bc95369..93ade39 100644 --- a/lib/extensions/note.dart +++ b/lib/extensions/note.dart @@ -30,3 +30,13 @@ extension Label on NoteType { return name[0].toUpperCase() + name.substring(1); } } + +extension ShareHelper on Note { + SharedObject share() { + return SharedObject( + uuid: uuid, + name: title, + note: this, + ); + } +} diff --git a/lib/extensions/shared_object.dart b/lib/extensions/shared_object.dart new file mode 100644 index 0000000..82fa3e5 --- /dev/null +++ b/lib/extensions/shared_object.dart @@ -0,0 +1,109 @@ +import 'dart:io'; + +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/import.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/import.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/import.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/import.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/import.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/import.dart'; +import 'package:flutter/widgets.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +const defaultMimeType = "application/cypher-sheet-object"; +const defaultFileExtension = "cso"; + +extension Shareable on SharedObject { + String mimeType() { + return defaultMimeType; + } + + String printableType() { + final typeName = whichObject().name; + if (typeName.isEmpty) { + return ""; + } + return typeName[0].toUpperCase() + typeName.substring(1); + } + + Future toFile() async { + return _asTempFile(); + } + + Future _asTempFile() async { + final raw = writeToBuffer(); + final String tempPath = (await getTemporaryDirectory()).path; + + // Normalize the name to avoid issues with the path on certain platforms. + final fileName = name.replaceAll(RegExp("[^A-Za-z0-9]"), "_"); + + final path = '$tempPath/$fileName.$defaultFileExtension'; + final tempFile = File(path); + + await tempFile.writeAsBytes(raw); + + // Windows doesn't seem to like our mime type right now, needs further + // investigation but let's just ignore it there. + final String fileMimeType = Platform.isWindows ? "text/plain" : mimeType(); + + return XFile.fromData( + raw, + name: name, + mimeType: fileMimeType, + path: path, + ); + } +} + +extension Importable on SharedObject { + Widget importDialog({ + Function()? onCancel, + Function()? onSuccess, + }) { + switch (whichObject()) { + case SharedObject_Object.cypher: + return ImportCypher( + cypher, + onCancel: onCancel, + onSuccess: onSuccess, + ); + case SharedObject_Object.artifact: + return ImportArtifact( + artifact, + onCancel: onCancel, + onSuccess: onSuccess, + ); + case SharedObject_Object.item: + return ImportItem( + item, + onCancel: onCancel, + onSuccess: onSuccess, + ); + case SharedObject_Object.ability: + return ImportAbility( + ability, + onCancel: onCancel, + onSuccess: onSuccess, + ); + case SharedObject_Object.skill: + return ImportSkill( + skill, + onCancel: onCancel, + onSuccess: onSuccess, + ); + case SharedObject_Object.note: + return ImportNote( + note, + onCancel: onCancel, + onSuccess: onSuccess, + ); + default: + return AppDialog( + child: AppText("Can't import ${printableType()}, yet."), + ); + } + } +} diff --git a/lib/extensions/skill.dart b/lib/extensions/skill.dart index 0883f21..1e029f8 100644 --- a/lib/extensions/skill.dart +++ b/lib/extensions/skill.dart @@ -32,3 +32,13 @@ extension Comparable on SkillLevel { return value.compareTo(other.value); } } + +extension ShareHelper on Skill { + SharedObject share() { + return SharedObject( + uuid: uuid, + name: name, + skill: this, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index a637746..b42dabd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,29 +1,89 @@ -import 'package:cypher_sheet/state/providers/character.dart'; +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:cypher_sheet/components/box.dart'; +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/providers/import.dart'; import 'package:cypher_sheet/state/providers/style.dart'; -import 'package:cypher_sheet/views/notes.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/editable.dart'; +import 'package:cypher_sheet/views/import_character_selection.dart'; +import 'package:cypher_sheet/views/scaffold.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cypher_sheet/components/icons.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:cypher_sheet/state/observer.dart'; -import 'package:cypher_sheet/views/abilities.dart'; import 'package:cypher_sheet/views/characters.dart'; -import 'package:cypher_sheet/views/cyphers.dart'; -import 'package:cypher_sheet/views/equipment.dart'; -import 'package:cypher_sheet/views/skills.dart'; -import 'package:cypher_sheet/views/stats.dart'; -import 'package:cypher_sheet/views/view.dart'; +import 'package:cypher_sheet/views/character_sheet/view.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; void main() { runApp( ProviderScope(observers: [Persister()], child: const CypherSheetApp())); } -class CypherSheetApp extends ConsumerWidget { - const CypherSheetApp({Key? key}) : super(key: key); +class CypherSheetApp extends ConsumerStatefulWidget { + const CypherSheetApp({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _CypherSheetAppState(); +} + +class _CypherSheetAppState extends ConsumerState { + late StreamSubscription _intentDataStreamSubscription; + + @override + void initState() { + super.initState(); + + if (Platform.isAndroid) { + // For sharing coming from outside the app while the app is in the memory + _intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream() + .listen((List value) { + log("received media stream"); + importFile(value); + }, onError: (err) { + log("getIntentDataStream error: $err"); + }); + + // For sharing coming from outside the app while the app is closed + ReceiveSharingIntent.getInitialMedia() + .then((List value) { + log("received initial media"); + importFile(value); + }); + } + } + + @override + void dispose() { + _intentDataStreamSubscription.cancel(); + super.dispose(); + } + + void importFile(List value) { + if (value.isEmpty) { + log("Empty list of files"); + return; + } + if (value.length != 1) { + log("Can't open more than one file"); + return; + } + log("opening shared media: ${value.first.path}"); + final file = File(value.first.path); + final raw = file.readAsBytesSync(); + final obj = SharedObject.fromBuffer(raw); + + ref.read(importObjectProvider.notifier).state = obj; + ReceiveSharingIntent.reset(); + } + + @override + Widget build(BuildContext context) { final baseTheme = ThemeData.dark(useMaterial3: true).copyWith( scaffoldBackgroundColor: const Color.fromRGBO(25, 25, 25, 1), colorScheme: ThemeData.dark(useMaterial3: true).colorScheme.copyWith( @@ -37,6 +97,15 @@ class CypherSheetApp extends ConsumerWidget { ), ); + const charactersView = CharactersView(); + final routes = { + "/": (context) => charactersView, + routeCharacters: (context) => charactersView, + routeCharacter: (context) => const CharacterSheetView(), + routeImportSelectCharacter: (context) => + const ImportCharacterSelectionView(), + }; + return MaterialApp( title: 'Cypher Sheet', theme: baseTheme.copyWith( @@ -66,33 +135,67 @@ class CypherSheetApp extends ConsumerWidget { buttonTheme: baseTheme.buttonTheme.copyWith( alignedDropdown: false, )), - home: ref.watch(characterProvider).uuid.isEmpty - ? const CharactersView() - : const CharacterSheet(), + initialRoute: "/characters", + onGenerateRoute: (settings) { + final route = routes[settings.name]; + if (route == null) { + log("route not found: $settings"); + return MaterialPageRoute(builder: (_) => const UnknownPage()); + } + return MaterialPageRoute( + settings: settings, + builder: route, + ); + }, + routes: routes, ); } } -class CharacterSheet extends StatelessWidget { - const CharacterSheet({Key? key}) : super(key: key); +const String routeCharacters = "/characters"; +const String routeCharacter = "/character"; +const String routeImportSelectCharacter = "/import/select_character"; + +class ImportDialog extends ConsumerWidget { + const ImportDialog(EditableCypher Function() param0, {super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final import = ref.watch(importObjectProvider); return WillPopScope( onWillPop: () async { + ref.read(importObjectProvider.notifier).state = SharedObject(); return false; }, - child: AppView( - views: [ - ViewConfig("Database", AppIcons.database, Container()), - ViewConfig("Skills", AppIcons.skills, const SkillsView()), - ViewConfig("Abilities", AppIcons.abilities, const AbilitiesView()), - ViewConfig("Stats", AppIcons.stats, const StatsView()), - ViewConfig("Cyphers", AppIcons.cypher, const CyphersView()), - ViewConfig("Equipment", AppIcons.equipment, const EquipmentView()), - ViewConfig("Notes", AppIcons.notes, const NotesView()), - ], + child: AppScaffold( + body: AppDialog( + fullscreen: true, + child: import.importDialog(onCancel: () { + ref.read(importObjectProvider.notifier).state = SharedObject(); + }), + ), ), ); } } + +class UnknownPage extends StatelessWidget { + const UnknownPage({super.key}); + + @override + Widget build(BuildContext context) { + return AppScaffold( + body: Column( + children: [ + const AppText("Unknown Page"), + const AppText( + "This should not happen, please report the issue to app-feedback@kwiesmueller.dev"), + AppBox( + onTap: () { + Navigator.of(context).pushReplacementNamed(routeCharacters); + }, + child: const AppText("Back to start")) + ], + )); + } +} diff --git a/lib/proto/character.pb.dart b/lib/proto/character.pb.dart index bffadbb..2583003 100644 --- a/lib/proto/character.pb.dart +++ b/lib/proto/character.pb.dart @@ -2016,3 +2016,201 @@ class Note extends $pb.GeneratedMessage { void clearText() => clearField(5); } +enum SharedObject_Object { + character, + skill, + ability, + cypher, + artifact, + item, + note, + notSet +} + +class SharedObject extends $pb.GeneratedMessage { + static const $core.Map<$core.int, SharedObject_Object> _SharedObject_ObjectByTag = { + 10 : SharedObject_Object.character, + 20 : SharedObject_Object.skill, + 30 : SharedObject_Object.ability, + 40 : SharedObject_Object.cypher, + 50 : SharedObject_Object.artifact, + 60 : SharedObject_Object.item, + 70 : SharedObject_Object.note, + 0 : SharedObject_Object.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SharedObject', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'character'), createEmptyInstance: create) + ..oo(0, [10, 20, 30, 40, 50, 60, 70]) + ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uuid') + ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name') + ..aOM(10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'character', subBuilder: Character.create) + ..aOM(20, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'skill', subBuilder: Skill.create) + ..aOM(30, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ability', subBuilder: Ability.create) + ..aOM(40, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'cypher', subBuilder: Cypher.create) + ..aOM(50, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'artifact', subBuilder: Artifact.create) + ..aOM(60, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'item', subBuilder: Item.create) + ..aOM(70, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'note', subBuilder: Note.create) + ..hasRequiredFields = false + ; + + SharedObject._() : super(); + factory SharedObject({ + $core.String? uuid, + $core.String? name, + Character? character, + Skill? skill, + Ability? ability, + Cypher? cypher, + Artifact? artifact, + Item? item, + Note? note, + }) { + final _result = create(); + if (uuid != null) { + _result.uuid = uuid; + } + if (name != null) { + _result.name = name; + } + if (character != null) { + _result.character = character; + } + if (skill != null) { + _result.skill = skill; + } + if (ability != null) { + _result.ability = ability; + } + if (cypher != null) { + _result.cypher = cypher; + } + if (artifact != null) { + _result.artifact = artifact; + } + if (item != null) { + _result.item = item; + } + if (note != null) { + _result.note = note; + } + return _result; + } + factory SharedObject.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SharedObject.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SharedObject clone() => SharedObject()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SharedObject copyWith(void Function(SharedObject) updates) => super.copyWith((message) => updates(message as SharedObject)) as SharedObject; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static SharedObject create() => SharedObject._(); + SharedObject createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SharedObject getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SharedObject? _defaultInstance; + + SharedObject_Object whichObject() => _SharedObject_ObjectByTag[$_whichOneof(0)]!; + void clearObject() => clearField($_whichOneof(0)); + + @$pb.TagNumber(2) + $core.String get uuid => $_getSZ(0); + @$pb.TagNumber(2) + set uuid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(2) + $core.bool hasUuid() => $_has(0); + @$pb.TagNumber(2) + void clearUuid() => clearField(2); + + @$pb.TagNumber(3) + $core.String get name => $_getSZ(1); + @$pb.TagNumber(3) + set name($core.String v) { $_setString(1, v); } + @$pb.TagNumber(3) + $core.bool hasName() => $_has(1); + @$pb.TagNumber(3) + void clearName() => clearField(3); + + @$pb.TagNumber(10) + Character get character => $_getN(2); + @$pb.TagNumber(10) + set character(Character v) { setField(10, v); } + @$pb.TagNumber(10) + $core.bool hasCharacter() => $_has(2); + @$pb.TagNumber(10) + void clearCharacter() => clearField(10); + @$pb.TagNumber(10) + Character ensureCharacter() => $_ensure(2); + + @$pb.TagNumber(20) + Skill get skill => $_getN(3); + @$pb.TagNumber(20) + set skill(Skill v) { setField(20, v); } + @$pb.TagNumber(20) + $core.bool hasSkill() => $_has(3); + @$pb.TagNumber(20) + void clearSkill() => clearField(20); + @$pb.TagNumber(20) + Skill ensureSkill() => $_ensure(3); + + @$pb.TagNumber(30) + Ability get ability => $_getN(4); + @$pb.TagNumber(30) + set ability(Ability v) { setField(30, v); } + @$pb.TagNumber(30) + $core.bool hasAbility() => $_has(4); + @$pb.TagNumber(30) + void clearAbility() => clearField(30); + @$pb.TagNumber(30) + Ability ensureAbility() => $_ensure(4); + + @$pb.TagNumber(40) + Cypher get cypher => $_getN(5); + @$pb.TagNumber(40) + set cypher(Cypher v) { setField(40, v); } + @$pb.TagNumber(40) + $core.bool hasCypher() => $_has(5); + @$pb.TagNumber(40) + void clearCypher() => clearField(40); + @$pb.TagNumber(40) + Cypher ensureCypher() => $_ensure(5); + + @$pb.TagNumber(50) + Artifact get artifact => $_getN(6); + @$pb.TagNumber(50) + set artifact(Artifact v) { setField(50, v); } + @$pb.TagNumber(50) + $core.bool hasArtifact() => $_has(6); + @$pb.TagNumber(50) + void clearArtifact() => clearField(50); + @$pb.TagNumber(50) + Artifact ensureArtifact() => $_ensure(6); + + @$pb.TagNumber(60) + Item get item => $_getN(7); + @$pb.TagNumber(60) + set item(Item v) { setField(60, v); } + @$pb.TagNumber(60) + $core.bool hasItem() => $_has(7); + @$pb.TagNumber(60) + void clearItem() => clearField(60); + @$pb.TagNumber(60) + Item ensureItem() => $_ensure(7); + + @$pb.TagNumber(70) + Note get note => $_getN(8); + @$pb.TagNumber(70) + set note(Note v) { setField(70, v); } + @$pb.TagNumber(70) + $core.bool hasNote() => $_has(8); + @$pb.TagNumber(70) + void clearNote() => clearField(70); + @$pb.TagNumber(70) + Note ensureNote() => $_ensure(8); +} + diff --git a/lib/proto/character.pbjson.dart b/lib/proto/character.pbjson.dart index 1de0955..067f93e 100644 --- a/lib/proto/character.pbjson.dart +++ b/lib/proto/character.pbjson.dart @@ -371,3 +371,24 @@ const Note$json = const { /// Descriptor for `Note`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List noteDescriptor = $convert.base64Decode('CgROb3RlEhIKBHV1aWQYASABKAlSBHV1aWQSFAoFdGl0bGUYAiABKAlSBXRpdGxlEicKBHR5cGUYAyABKA4yEy5jaGFyYWN0ZXIuTm90ZVR5cGVSBHR5cGUSKgoQc2hvcnREZXNjcmlwdGlvbhgEIAEoCVIQc2hvcnREZXNjcmlwdGlvbhISCgR0ZXh0GAUgASgJUgR0ZXh0'); +@$core.Deprecated('Use sharedObjectDescriptor instead') +const SharedObject$json = const { + '1': 'SharedObject', + '2': const [ + const {'1': 'uuid', '3': 2, '4': 1, '5': 9, '10': 'uuid'}, + const {'1': 'name', '3': 3, '4': 1, '5': 9, '10': 'name'}, + const {'1': 'character', '3': 10, '4': 1, '5': 11, '6': '.character.Character', '9': 0, '10': 'character'}, + const {'1': 'skill', '3': 20, '4': 1, '5': 11, '6': '.character.Skill', '9': 0, '10': 'skill'}, + const {'1': 'ability', '3': 30, '4': 1, '5': 11, '6': '.character.Ability', '9': 0, '10': 'ability'}, + const {'1': 'cypher', '3': 40, '4': 1, '5': 11, '6': '.character.Cypher', '9': 0, '10': 'cypher'}, + const {'1': 'artifact', '3': 50, '4': 1, '5': 11, '6': '.character.Artifact', '9': 0, '10': 'artifact'}, + const {'1': 'item', '3': 60, '4': 1, '5': 11, '6': '.character.Item', '9': 0, '10': 'item'}, + const {'1': 'note', '3': 70, '4': 1, '5': 11, '6': '.character.Note', '9': 0, '10': 'note'}, + ], + '8': const [ + const {'1': 'object'}, + ], +}; + +/// Descriptor for `SharedObject`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sharedObjectDescriptor = $convert.base64Decode('CgxTaGFyZWRPYmplY3QSEgoEdXVpZBgCIAEoCVIEdXVpZBISCgRuYW1lGAMgASgJUgRuYW1lEjQKCWNoYXJhY3RlchgKIAEoCzIULmNoYXJhY3Rlci5DaGFyYWN0ZXJIAFIJY2hhcmFjdGVyEigKBXNraWxsGBQgASgLMhAuY2hhcmFjdGVyLlNraWxsSABSBXNraWxsEi4KB2FiaWxpdHkYHiABKAsyEi5jaGFyYWN0ZXIuQWJpbGl0eUgAUgdhYmlsaXR5EisKBmN5cGhlchgoIAEoCzIRLmNoYXJhY3Rlci5DeXBoZXJIAFIGY3lwaGVyEjEKCGFydGlmYWN0GDIgASgLMhMuY2hhcmFjdGVyLkFydGlmYWN0SABSCGFydGlmYWN0EiUKBGl0ZW0YPCABKAsyDy5jaGFyYWN0ZXIuSXRlbUgAUgRpdGVtEiUKBG5vdGUYRiABKAsyDy5jaGFyYWN0ZXIuTm90ZUgAUgRub3RlQggKBm9iamVjdA=='); diff --git a/lib/proto/characters.pb.dart b/lib/proto/characters.pb.dart index 6624d55..2797206 100644 --- a/lib/proto/characters.pb.dart +++ b/lib/proto/characters.pb.dart @@ -7,67 +7,418 @@ import 'dart:core' as $core; +import 'package:fixnum/fixnum.dart' as $fixnum; import 'package:protobuf/protobuf.dart' as $pb; import 'character.pb.dart' as $1; -class WriteCharacter extends $pb.GeneratedMessage { - static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'WriteCharacter', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) - ..aOM<$1.Character>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'character', subBuilder: $1.Character.create) +class CreateCharacter extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateCharacter', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) ..hasRequiredFields = false ; - WriteCharacter._() : super(); - factory WriteCharacter({ + CreateCharacter._() : super(); + factory CreateCharacter() => create(); + factory CreateCharacter.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateCharacter.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateCharacter clone() => CreateCharacter()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateCharacter copyWith(void Function(CreateCharacter) updates) => super.copyWith((message) => updates(message as CreateCharacter)) as CreateCharacter; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static CreateCharacter create() => CreateCharacter._(); + CreateCharacter createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateCharacter getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateCharacter? _defaultInstance; +} + +class CharacterCreated extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CharacterCreated', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uuid') + ..hasRequiredFields = false + ; + + CharacterCreated._() : super(); + factory CharacterCreated({ + $core.String? uuid, + }) { + final _result = create(); + if (uuid != null) { + _result.uuid = uuid; + } + return _result; + } + factory CharacterCreated.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CharacterCreated.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CharacterCreated clone() => CharacterCreated()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CharacterCreated copyWith(void Function(CharacterCreated) updates) => super.copyWith((message) => updates(message as CharacterCreated)) as CharacterCreated; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static CharacterCreated create() => CharacterCreated._(); + CharacterCreated createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CharacterCreated getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CharacterCreated? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get uuid => $_getSZ(0); + @$pb.TagNumber(1) + set uuid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUuid() => $_has(0); + @$pb.TagNumber(1) + void clearUuid() => clearField(1); +} + +class WriteRevision extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'WriteRevision', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uuid') + ..aOM<$1.Character>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'character', subBuilder: $1.Character.create) + ..a<$fixnum.Int64>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'revision', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + WriteRevision._() : super(); + factory WriteRevision({ + $core.String? uuid, + $1.Character? character, + $fixnum.Int64? revision, + }) { + final _result = create(); + if (uuid != null) { + _result.uuid = uuid; + } + if (character != null) { + _result.character = character; + } + if (revision != null) { + _result.revision = revision; + } + return _result; + } + factory WriteRevision.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory WriteRevision.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WriteRevision clone() => WriteRevision()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WriteRevision copyWith(void Function(WriteRevision) updates) => super.copyWith((message) => updates(message as WriteRevision)) as WriteRevision; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static WriteRevision create() => WriteRevision._(); + WriteRevision createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WriteRevision getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static WriteRevision? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get uuid => $_getSZ(0); + @$pb.TagNumber(1) + set uuid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUuid() => $_has(0); + @$pb.TagNumber(1) + void clearUuid() => clearField(1); + + @$pb.TagNumber(2) + $1.Character get character => $_getN(1); + @$pb.TagNumber(2) + set character($1.Character v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasCharacter() => $_has(1); + @$pb.TagNumber(2) + void clearCharacter() => clearField(2); + @$pb.TagNumber(2) + $1.Character ensureCharacter() => $_ensure(1); + + @$pb.TagNumber(3) + $fixnum.Int64 get revision => $_getI64(2); + @$pb.TagNumber(3) + set revision($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasRevision() => $_has(2); + @$pb.TagNumber(3) + void clearRevision() => clearField(3); +} + +class RevisionWritten extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RevisionWritten', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uuid') + ..a<$fixnum.Int64>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'revision', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + RevisionWritten._() : super(); + factory RevisionWritten({ + $core.String? uuid, + $fixnum.Int64? revision, + }) { + final _result = create(); + if (uuid != null) { + _result.uuid = uuid; + } + if (revision != null) { + _result.revision = revision; + } + return _result; + } + factory RevisionWritten.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory RevisionWritten.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RevisionWritten clone() => RevisionWritten()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RevisionWritten copyWith(void Function(RevisionWritten) updates) => super.copyWith((message) => updates(message as RevisionWritten)) as RevisionWritten; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RevisionWritten create() => RevisionWritten._(); + RevisionWritten createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RevisionWritten getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static RevisionWritten? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get uuid => $_getSZ(0); + @$pb.TagNumber(1) + set uuid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUuid() => $_has(0); + @$pb.TagNumber(1) + void clearUuid() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get revision => $_getI64(1); + @$pb.TagNumber(2) + set revision($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasRevision() => $_has(1); + @$pb.TagNumber(2) + void clearRevision() => clearField(2); +} + +class ReadRevision extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ReadRevision', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uuid') + ..a<$fixnum.Int64>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'revision', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + ReadRevision._() : super(); + factory ReadRevision({ + $core.String? uuid, + $fixnum.Int64? revision, + }) { + final _result = create(); + if (uuid != null) { + _result.uuid = uuid; + } + if (revision != null) { + _result.revision = revision; + } + return _result; + } + factory ReadRevision.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ReadRevision.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ReadRevision clone() => ReadRevision()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ReadRevision copyWith(void Function(ReadRevision) updates) => super.copyWith((message) => updates(message as ReadRevision)) as ReadRevision; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ReadRevision create() => ReadRevision._(); + ReadRevision createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ReadRevision getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ReadRevision? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get uuid => $_getSZ(0); + @$pb.TagNumber(1) + set uuid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUuid() => $_has(0); + @$pb.TagNumber(1) + void clearUuid() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get revision => $_getI64(1); + @$pb.TagNumber(2) + set revision($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasRevision() => $_has(1); + @$pb.TagNumber(2) + void clearRevision() => clearField(2); +} + +class RevisionRead extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RevisionRead', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uuid') + ..a<$fixnum.Int64>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'revision', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..aOM<$1.Character>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'character', subBuilder: $1.Character.create) + ..hasRequiredFields = false + ; + + RevisionRead._() : super(); + factory RevisionRead({ + $core.String? uuid, + $fixnum.Int64? revision, $1.Character? character, }) { final _result = create(); + if (uuid != null) { + _result.uuid = uuid; + } + if (revision != null) { + _result.revision = revision; + } if (character != null) { _result.character = character; } return _result; } - factory WriteCharacter.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory WriteCharacter.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory RevisionRead.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory RevisionRead.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') - WriteCharacter clone() => WriteCharacter()..mergeFromMessage(this); + RevisionRead clone() => RevisionRead()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') - WriteCharacter copyWith(void Function(WriteCharacter) updates) => super.copyWith((message) => updates(message as WriteCharacter)) as WriteCharacter; // ignore: deprecated_member_use + RevisionRead copyWith(void Function(RevisionRead) updates) => super.copyWith((message) => updates(message as RevisionRead)) as RevisionRead; // ignore: deprecated_member_use $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static WriteCharacter create() => WriteCharacter._(); - WriteCharacter createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static RevisionRead create() => RevisionRead._(); + RevisionRead createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static WriteCharacter getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static WriteCharacter? _defaultInstance; + static RevisionRead getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static RevisionRead? _defaultInstance; @$pb.TagNumber(1) - $1.Character get character => $_getN(0); + $core.String get uuid => $_getSZ(0); + @$pb.TagNumber(1) + set uuid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUuid() => $_has(0); + @$pb.TagNumber(1) + void clearUuid() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get revision => $_getI64(1); + @$pb.TagNumber(2) + set revision($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasRevision() => $_has(1); + @$pb.TagNumber(2) + void clearRevision() => clearField(2); + + @$pb.TagNumber(3) + $1.Character get character => $_getN(2); + @$pb.TagNumber(3) + set character($1.Character v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasCharacter() => $_has(2); + @$pb.TagNumber(3) + void clearCharacter() => clearField(3); + @$pb.TagNumber(3) + $1.Character ensureCharacter() => $_ensure(2); +} + +class ReadLatestRevision extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ReadLatestRevision', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uuid') + ..hasRequiredFields = false + ; + + ReadLatestRevision._() : super(); + factory ReadLatestRevision({ + $core.String? uuid, + }) { + final _result = create(); + if (uuid != null) { + _result.uuid = uuid; + } + return _result; + } + factory ReadLatestRevision.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ReadLatestRevision.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ReadLatestRevision clone() => ReadLatestRevision()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ReadLatestRevision copyWith(void Function(ReadLatestRevision) updates) => super.copyWith((message) => updates(message as ReadLatestRevision)) as ReadLatestRevision; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ReadLatestRevision create() => ReadLatestRevision._(); + ReadLatestRevision createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ReadLatestRevision getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ReadLatestRevision? _defaultInstance; + @$pb.TagNumber(1) - set character($1.Character v) { setField(1, v); } + $core.String get uuid => $_getSZ(0); @$pb.TagNumber(1) - $core.bool hasCharacter() => $_has(0); + set uuid($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) - void clearCharacter() => clearField(1); + $core.bool hasUuid() => $_has(0); @$pb.TagNumber(1) - $1.Character ensureCharacter() => $_ensure(0); + void clearUuid() => clearField(1); } -class CharacterWritten extends $pb.GeneratedMessage { - static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CharacterWritten', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) +class DeleteCharacter extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'DeleteCharacter', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'uuid') ..hasRequiredFields = false ; - CharacterWritten._() : super(); - factory CharacterWritten({ + DeleteCharacter._() : super(); + factory DeleteCharacter({ $core.String? uuid, }) { final _result = create(); @@ -76,26 +427,26 @@ class CharacterWritten extends $pb.GeneratedMessage { } return _result; } - factory CharacterWritten.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory CharacterWritten.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + factory DeleteCharacter.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DeleteCharacter.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') - CharacterWritten clone() => CharacterWritten()..mergeFromMessage(this); + DeleteCharacter clone() => DeleteCharacter()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') - CharacterWritten copyWith(void Function(CharacterWritten) updates) => super.copyWith((message) => updates(message as CharacterWritten)) as CharacterWritten; // ignore: deprecated_member_use + DeleteCharacter copyWith(void Function(DeleteCharacter) updates) => super.copyWith((message) => updates(message as DeleteCharacter)) as DeleteCharacter; // ignore: deprecated_member_use $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static CharacterWritten create() => CharacterWritten._(); - CharacterWritten createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static DeleteCharacter create() => DeleteCharacter._(); + DeleteCharacter createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static CharacterWritten getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static CharacterWritten? _defaultInstance; + static DeleteCharacter getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DeleteCharacter? _defaultInstance; @$pb.TagNumber(1) $core.String get uuid => $_getSZ(0); @@ -107,3 +458,32 @@ class CharacterWritten extends $pb.GeneratedMessage { void clearUuid() => clearField(1); } +class CharacterDeleted extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CharacterDeleted', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'characters'), createEmptyInstance: create) + ..hasRequiredFields = false + ; + + CharacterDeleted._() : super(); + factory CharacterDeleted() => create(); + factory CharacterDeleted.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CharacterDeleted.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CharacterDeleted clone() => CharacterDeleted()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CharacterDeleted copyWith(void Function(CharacterDeleted) updates) => super.copyWith((message) => updates(message as CharacterDeleted)) as CharacterDeleted; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static CharacterDeleted create() => CharacterDeleted._(); + CharacterDeleted createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CharacterDeleted getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CharacterDeleted? _defaultInstance; +} + diff --git a/lib/proto/characters.pbgrpc.dart b/lib/proto/characters.pbgrpc.dart index 2197a6c..fe58b79 100644 --- a/lib/proto/characters.pbgrpc.dart +++ b/lib/proto/characters.pbgrpc.dart @@ -14,44 +14,147 @@ import 'characters.pb.dart' as $0; export 'characters.pb.dart'; class CharactersClient extends $grpc.Client { + static final _$create = + $grpc.ClientMethod<$0.CreateCharacter, $0.CharacterCreated>( + '/characters.Characters/Create', + ($0.CreateCharacter value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $0.CharacterCreated.fromBuffer(value)); static final _$writeCharacterRevision = - $grpc.ClientMethod<$0.WriteCharacter, $0.CharacterWritten>( + $grpc.ClientMethod<$0.WriteRevision, $0.RevisionWritten>( '/characters.Characters/WriteCharacterRevision', - ($0.WriteCharacter value) => value.writeToBuffer(), + ($0.WriteRevision value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $0.RevisionWritten.fromBuffer(value)); + static final _$readCharacterRevision = + $grpc.ClientMethod<$0.ReadRevision, $0.RevisionRead>( + '/characters.Characters/ReadCharacterRevision', + ($0.ReadRevision value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.RevisionRead.fromBuffer(value)); + static final _$readLatestCharacterRevision = + $grpc.ClientMethod<$0.ReadLatestRevision, $0.RevisionRead>( + '/characters.Characters/ReadLatestCharacterRevision', + ($0.ReadLatestRevision value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.RevisionRead.fromBuffer(value)); + static final _$delete = + $grpc.ClientMethod<$0.DeleteCharacter, $0.CharacterDeleted>( + '/characters.Characters/Delete', + ($0.DeleteCharacter value) => value.writeToBuffer(), ($core.List<$core.int> value) => - $0.CharacterWritten.fromBuffer(value)); + $0.CharacterDeleted.fromBuffer(value)); CharactersClient($grpc.ClientChannel channel, {$grpc.CallOptions? options, $core.Iterable<$grpc.ClientInterceptor>? interceptors}) : super(channel, options: options, interceptors: interceptors); - $grpc.ResponseFuture<$0.CharacterWritten> writeCharacterRevision( - $0.WriteCharacter request, + $grpc.ResponseFuture<$0.CharacterCreated> create($0.CreateCharacter request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$create, request, options: options); + } + + $grpc.ResponseFuture<$0.RevisionWritten> writeCharacterRevision( + $0.WriteRevision request, {$grpc.CallOptions? options}) { return $createUnaryCall(_$writeCharacterRevision, request, options: options); } + + $grpc.ResponseFuture<$0.RevisionRead> readCharacterRevision( + $0.ReadRevision request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$readCharacterRevision, request, options: options); + } + + $grpc.ResponseFuture<$0.RevisionRead> readLatestCharacterRevision( + $0.ReadLatestRevision request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$readLatestCharacterRevision, request, + options: options); + } + + $grpc.ResponseFuture<$0.CharacterDeleted> delete($0.DeleteCharacter request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$delete, request, options: options); + } } abstract class CharactersServiceBase extends $grpc.Service { $core.String get $name => 'characters.Characters'; CharactersServiceBase() { - $addMethod($grpc.ServiceMethod<$0.WriteCharacter, $0.CharacterWritten>( + $addMethod($grpc.ServiceMethod<$0.CreateCharacter, $0.CharacterCreated>( + 'Create', + create_Pre, + false, + false, + ($core.List<$core.int> value) => $0.CreateCharacter.fromBuffer(value), + ($0.CharacterCreated value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.WriteRevision, $0.RevisionWritten>( 'WriteCharacterRevision', writeCharacterRevision_Pre, false, false, - ($core.List<$core.int> value) => $0.WriteCharacter.fromBuffer(value), - ($0.CharacterWritten value) => value.writeToBuffer())); + ($core.List<$core.int> value) => $0.WriteRevision.fromBuffer(value), + ($0.RevisionWritten value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.ReadRevision, $0.RevisionRead>( + 'ReadCharacterRevision', + readCharacterRevision_Pre, + false, + false, + ($core.List<$core.int> value) => $0.ReadRevision.fromBuffer(value), + ($0.RevisionRead value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.ReadLatestRevision, $0.RevisionRead>( + 'ReadLatestCharacterRevision', + readLatestCharacterRevision_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.ReadLatestRevision.fromBuffer(value), + ($0.RevisionRead value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.DeleteCharacter, $0.CharacterDeleted>( + 'Delete', + delete_Pre, + false, + false, + ($core.List<$core.int> value) => $0.DeleteCharacter.fromBuffer(value), + ($0.CharacterDeleted value) => value.writeToBuffer())); } - $async.Future<$0.CharacterWritten> writeCharacterRevision_Pre( - $grpc.ServiceCall call, $async.Future<$0.WriteCharacter> request) async { + $async.Future<$0.CharacterCreated> create_Pre( + $grpc.ServiceCall call, $async.Future<$0.CreateCharacter> request) async { + return create(call, await request); + } + + $async.Future<$0.RevisionWritten> writeCharacterRevision_Pre( + $grpc.ServiceCall call, $async.Future<$0.WriteRevision> request) async { return writeCharacterRevision(call, await request); } - $async.Future<$0.CharacterWritten> writeCharacterRevision( - $grpc.ServiceCall call, $0.WriteCharacter request); + $async.Future<$0.RevisionRead> readCharacterRevision_Pre( + $grpc.ServiceCall call, $async.Future<$0.ReadRevision> request) async { + return readCharacterRevision(call, await request); + } + + $async.Future<$0.RevisionRead> readLatestCharacterRevision_Pre( + $grpc.ServiceCall call, + $async.Future<$0.ReadLatestRevision> request) async { + return readLatestCharacterRevision(call, await request); + } + + $async.Future<$0.CharacterDeleted> delete_Pre( + $grpc.ServiceCall call, $async.Future<$0.DeleteCharacter> request) async { + return delete(call, await request); + } + + $async.Future<$0.CharacterCreated> create( + $grpc.ServiceCall call, $0.CreateCharacter request); + $async.Future<$0.RevisionWritten> writeCharacterRevision( + $grpc.ServiceCall call, $0.WriteRevision request); + $async.Future<$0.RevisionRead> readCharacterRevision( + $grpc.ServiceCall call, $0.ReadRevision request); + $async.Future<$0.RevisionRead> readLatestCharacterRevision( + $grpc.ServiceCall call, $0.ReadLatestRevision request); + $async.Future<$0.CharacterDeleted> delete( + $grpc.ServiceCall call, $0.DeleteCharacter request); } diff --git a/lib/proto/characters.pbjson.dart b/lib/proto/characters.pbjson.dart index f92f416..f7b486e 100644 --- a/lib/proto/characters.pbjson.dart +++ b/lib/proto/characters.pbjson.dart @@ -8,23 +8,93 @@ import 'dart:core' as $core; import 'dart:convert' as $convert; import 'dart:typed_data' as $typed_data; -@$core.Deprecated('Use writeCharacterDescriptor instead') -const WriteCharacter$json = const { - '1': 'WriteCharacter', +@$core.Deprecated('Use createCharacterDescriptor instead') +const CreateCharacter$json = const { + '1': 'CreateCharacter', +}; + +/// Descriptor for `CreateCharacter`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List createCharacterDescriptor = $convert.base64Decode('Cg9DcmVhdGVDaGFyYWN0ZXI='); +@$core.Deprecated('Use characterCreatedDescriptor instead') +const CharacterCreated$json = const { + '1': 'CharacterCreated', + '2': const [ + const {'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'}, + ], +}; + +/// Descriptor for `CharacterCreated`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List characterCreatedDescriptor = $convert.base64Decode('ChBDaGFyYWN0ZXJDcmVhdGVkEhIKBHV1aWQYASABKAlSBHV1aWQ='); +@$core.Deprecated('Use writeRevisionDescriptor instead') +const WriteRevision$json = const { + '1': 'WriteRevision', + '2': const [ + const {'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'}, + const {'1': 'character', '3': 2, '4': 1, '5': 11, '6': '.character.Character', '10': 'character'}, + const {'1': 'revision', '3': 3, '4': 1, '5': 4, '10': 'revision'}, + ], +}; + +/// Descriptor for `WriteRevision`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List writeRevisionDescriptor = $convert.base64Decode('Cg1Xcml0ZVJldmlzaW9uEhIKBHV1aWQYASABKAlSBHV1aWQSMgoJY2hhcmFjdGVyGAIgASgLMhQuY2hhcmFjdGVyLkNoYXJhY3RlclIJY2hhcmFjdGVyEhoKCHJldmlzaW9uGAMgASgEUghyZXZpc2lvbg=='); +@$core.Deprecated('Use revisionWrittenDescriptor instead') +const RevisionWritten$json = const { + '1': 'RevisionWritten', + '2': const [ + const {'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'}, + const {'1': 'revision', '3': 2, '4': 1, '5': 4, '10': 'revision'}, + ], +}; + +/// Descriptor for `RevisionWritten`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List revisionWrittenDescriptor = $convert.base64Decode('Cg9SZXZpc2lvbldyaXR0ZW4SEgoEdXVpZBgBIAEoCVIEdXVpZBIaCghyZXZpc2lvbhgCIAEoBFIIcmV2aXNpb24='); +@$core.Deprecated('Use readRevisionDescriptor instead') +const ReadRevision$json = const { + '1': 'ReadRevision', + '2': const [ + const {'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'}, + const {'1': 'revision', '3': 2, '4': 1, '5': 4, '10': 'revision'}, + ], +}; + +/// Descriptor for `ReadRevision`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List readRevisionDescriptor = $convert.base64Decode('CgxSZWFkUmV2aXNpb24SEgoEdXVpZBgBIAEoCVIEdXVpZBIaCghyZXZpc2lvbhgCIAEoBFIIcmV2aXNpb24='); +@$core.Deprecated('Use revisionReadDescriptor instead') +const RevisionRead$json = const { + '1': 'RevisionRead', '2': const [ - const {'1': 'character', '3': 1, '4': 1, '5': 11, '6': '.character.Character', '10': 'character'}, + const {'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'}, + const {'1': 'revision', '3': 2, '4': 1, '5': 4, '10': 'revision'}, + const {'1': 'character', '3': 3, '4': 1, '5': 11, '6': '.character.Character', '10': 'character'}, ], }; -/// Descriptor for `WriteCharacter`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List writeCharacterDescriptor = $convert.base64Decode('Cg5Xcml0ZUNoYXJhY3RlchIyCgljaGFyYWN0ZXIYASABKAsyFC5jaGFyYWN0ZXIuQ2hhcmFjdGVyUgljaGFyYWN0ZXI='); -@$core.Deprecated('Use characterWrittenDescriptor instead') -const CharacterWritten$json = const { - '1': 'CharacterWritten', +/// Descriptor for `RevisionRead`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List revisionReadDescriptor = $convert.base64Decode('CgxSZXZpc2lvblJlYWQSEgoEdXVpZBgBIAEoCVIEdXVpZBIaCghyZXZpc2lvbhgCIAEoBFIIcmV2aXNpb24SMgoJY2hhcmFjdGVyGAMgASgLMhQuY2hhcmFjdGVyLkNoYXJhY3RlclIJY2hhcmFjdGVy'); +@$core.Deprecated('Use readLatestRevisionDescriptor instead') +const ReadLatestRevision$json = const { + '1': 'ReadLatestRevision', '2': const [ const {'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'}, ], }; -/// Descriptor for `CharacterWritten`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List characterWrittenDescriptor = $convert.base64Decode('ChBDaGFyYWN0ZXJXcml0dGVuEhIKBHV1aWQYASABKAlSBHV1aWQ='); +/// Descriptor for `ReadLatestRevision`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List readLatestRevisionDescriptor = $convert.base64Decode('ChJSZWFkTGF0ZXN0UmV2aXNpb24SEgoEdXVpZBgBIAEoCVIEdXVpZA=='); +@$core.Deprecated('Use deleteCharacterDescriptor instead') +const DeleteCharacter$json = const { + '1': 'DeleteCharacter', + '2': const [ + const {'1': 'uuid', '3': 1, '4': 1, '5': 9, '10': 'uuid'}, + ], +}; + +/// Descriptor for `DeleteCharacter`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deleteCharacterDescriptor = $convert.base64Decode('Cg9EZWxldGVDaGFyYWN0ZXISEgoEdXVpZBgBIAEoCVIEdXVpZA=='); +@$core.Deprecated('Use characterDeletedDescriptor instead') +const CharacterDeleted$json = const { + '1': 'CharacterDeleted', +}; + +/// Descriptor for `CharacterDeleted`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List characterDeletedDescriptor = $convert.base64Decode('ChBDaGFyYWN0ZXJEZWxldGVk'); diff --git a/lib/proto/characters.pbserver.dart b/lib/proto/characters.pbserver.dart index 9b5bf31..a4de8dc 100644 --- a/lib/proto/characters.pbserver.dart +++ b/lib/proto/characters.pbserver.dart @@ -16,18 +16,30 @@ import 'characters.pbjson.dart'; export 'characters.pb.dart'; abstract class CharactersServiceBase extends $pb.GeneratedService { - $async.Future<$1.CharacterWritten> writeCharacterRevision($pb.ServerContext ctx, $1.WriteCharacter request); + $async.Future<$1.CharacterCreated> create($pb.ServerContext ctx, $1.CreateCharacter request); + $async.Future<$1.RevisionWritten> writeCharacterRevision($pb.ServerContext ctx, $1.WriteRevision request); + $async.Future<$1.RevisionRead> readCharacterRevision($pb.ServerContext ctx, $1.ReadRevision request); + $async.Future<$1.RevisionRead> readLatestCharacterRevision($pb.ServerContext ctx, $1.ReadLatestRevision request); + $async.Future<$1.CharacterDeleted> delete($pb.ServerContext ctx, $1.DeleteCharacter request); $pb.GeneratedMessage createRequest($core.String method) { switch (method) { - case 'WriteCharacterRevision': return $1.WriteCharacter(); + case 'Create': return $1.CreateCharacter(); + case 'WriteCharacterRevision': return $1.WriteRevision(); + case 'ReadCharacterRevision': return $1.ReadRevision(); + case 'ReadLatestCharacterRevision': return $1.ReadLatestRevision(); + case 'Delete': return $1.DeleteCharacter(); default: throw $core.ArgumentError('Unknown method: $method'); } } $async.Future<$pb.GeneratedMessage> handleCall($pb.ServerContext ctx, $core.String method, $pb.GeneratedMessage request) { switch (method) { - case 'WriteCharacterRevision': return this.writeCharacterRevision(ctx, request as $1.WriteCharacter); + case 'Create': return this.create(ctx, request as $1.CreateCharacter); + case 'WriteCharacterRevision': return this.writeCharacterRevision(ctx, request as $1.WriteRevision); + case 'ReadCharacterRevision': return this.readCharacterRevision(ctx, request as $1.ReadRevision); + case 'ReadLatestCharacterRevision': return this.readLatestCharacterRevision(ctx, request as $1.ReadLatestRevision); + case 'Delete': return this.delete(ctx, request as $1.DeleteCharacter); default: throw $core.ArgumentError('Unknown method: $method'); } } diff --git a/lib/state/providers/import.dart b/lib/state/providers/import.dart new file mode 100644 index 0000000..eba9219 --- /dev/null +++ b/lib/state/providers/import.dart @@ -0,0 +1,4 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final importObjectProvider = StateProvider((ref) => SharedObject()); diff --git a/lib/state/storage/api.dart b/lib/state/storage/api.dart index 1b40625..c2d7523 100644 --- a/lib/state/storage/api.dart +++ b/lib/state/storage/api.dart @@ -14,7 +14,7 @@ Future writeCharacterRevisionToAPI(Character character) async { try { final stub = CharactersClient(channel); var response = - await stub.writeCharacterRevision(WriteCharacter(character: character)); + await stub.writeCharacterRevision(WriteRevision(character: character)); log('Uploaded character: $response'); } catch (e) { log('Caught error: $e'); diff --git a/lib/views/abilities.dart b/lib/views/character_sheet/abilities.dart similarity index 95% rename from lib/views/abilities.dart rename to lib/views/character_sheet/abilities.dart index 887eb64..92b427e 100644 --- a/lib/views/abilities.dart +++ b/lib/views/character_sheet/abilities.dart @@ -5,8 +5,8 @@ import 'package:cypher_sheet/components/icons.dart'; import 'package:cypher_sheet/components/search.dart'; import 'package:cypher_sheet/proto/character.pb.dart'; import 'package:cypher_sheet/state/providers/abilities.dart'; -import 'package:cypher_sheet/views/dialogs/create_ability.dart'; -import 'package:cypher_sheet/views/equipment.dart'; +import 'package:cypher_sheet/views/character_sheet/equipment.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/create.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/ability.dart'; diff --git a/lib/views/cyphers.dart b/lib/views/character_sheet/cyphers.dart similarity index 98% rename from lib/views/cyphers.dart rename to lib/views/character_sheet/cyphers.dart index 4f7cc4e..3b2da4b 100644 --- a/lib/views/cyphers.dart +++ b/lib/views/character_sheet/cyphers.dart @@ -4,9 +4,9 @@ import 'package:cypher_sheet/components/separator.dart'; import 'package:cypher_sheet/proto/character.pb.dart'; import 'package:cypher_sheet/state/providers/character.dart'; import 'package:cypher_sheet/state/providers/cyphers.dart'; -import 'package:cypher_sheet/views/dialogs/create_artifact.dart'; -import 'package:cypher_sheet/views/dialogs/create_cypher.dart'; import 'package:cypher_sheet/views/dialogs/cypher_limit.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/create.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/create.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/appbar.dart' as app; diff --git a/lib/views/character_sheet/database.dart b/lib/views/character_sheet/database.dart new file mode 100644 index 0000000..cfabe44 --- /dev/null +++ b/lib/views/character_sheet/database.dart @@ -0,0 +1,59 @@ +import 'dart:io'; + +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/icon.dart'; +import 'package:cypher_sheet/components/icons.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:cypher_sheet/components/appbar.dart' as app; +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/components/text.dart'; + +class DatabaseView extends ConsumerWidget { + const DatabaseView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AppScrollView( + appBar: const app.AppBar( + child: AppText( + "Database", + align: TextAlign.left, + )), + slivers: [ + Row( + children: [ + const Spacer(), + SVGBoxLabeled( + icon: AppIcons.import, + label: "Import", + onTap: () async { + final obj = await importObjectFromPicker(); + if (obj == null) { + return; + } + // ignore: use_build_context_synchronously + if (!context.mounted) return; + showAppDialog(context, obj.importDialog()); + }, + ), + ], + ) + ], + ); + } + + Future importObjectFromPicker() async { + FilePickerResult? result = await FilePicker.platform.pickFiles(); + + if (result != null) { + File file = File(result.files.single.path!); + final obj = SharedObject.fromBuffer(file.readAsBytesSync()); + return obj; + } + return null; + } +} diff --git a/lib/views/equipment.dart b/lib/views/character_sheet/equipment.dart similarity index 99% rename from lib/views/equipment.dart rename to lib/views/character_sheet/equipment.dart index 37661d5..59de8d5 100644 --- a/lib/views/equipment.dart +++ b/lib/views/character_sheet/equipment.dart @@ -5,8 +5,8 @@ import 'package:cypher_sheet/extensions/item.dart'; import 'package:cypher_sheet/state/providers/character.dart'; import 'package:cypher_sheet/state/providers/inventories.dart'; import 'package:cypher_sheet/state/providers/items.dart'; -import 'package:cypher_sheet/views/dialogs/create_item.dart'; import 'package:cypher_sheet/views/dialogs/money.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/create.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/appbar.dart' as app; diff --git a/lib/views/notes.dart b/lib/views/character_sheet/notes.dart similarity index 97% rename from lib/views/notes.dart rename to lib/views/character_sheet/notes.dart index 65d04b7..49a4cbe 100644 --- a/lib/views/notes.dart +++ b/lib/views/character_sheet/notes.dart @@ -7,7 +7,7 @@ import 'package:cypher_sheet/components/search.dart'; import 'package:cypher_sheet/extensions/note.dart'; import 'package:cypher_sheet/proto/character.pb.dart'; import 'package:cypher_sheet/state/providers/notes.dart'; -import 'package:cypher_sheet/views/dialogs/create_note.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/create.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/appbar.dart' as app; diff --git a/lib/views/skills.dart b/lib/views/character_sheet/skills.dart similarity index 95% rename from lib/views/skills.dart rename to lib/views/character_sheet/skills.dart index 73b823e..f38a70b 100644 --- a/lib/views/skills.dart +++ b/lib/views/character_sheet/skills.dart @@ -2,8 +2,8 @@ import 'package:cypher_sheet/components/dialog.dart'; import 'package:cypher_sheet/components/search.dart'; import 'package:cypher_sheet/proto/character.pb.dart'; import 'package:cypher_sheet/state/providers/skills.dart'; -import 'package:cypher_sheet/views/dialogs/create_skill.dart'; -import 'package:cypher_sheet/views/equipment.dart'; +import 'package:cypher_sheet/views/character_sheet/equipment.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/create.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/appbar.dart' as app; diff --git a/lib/views/stats.dart b/lib/views/character_sheet/stats.dart similarity index 100% rename from lib/views/stats.dart rename to lib/views/character_sheet/stats.dart diff --git a/lib/views/character_sheet/view.dart b/lib/views/character_sheet/view.dart new file mode 100644 index 0000000..61c582a --- /dev/null +++ b/lib/views/character_sheet/view.dart @@ -0,0 +1,223 @@ +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/providers/import.dart'; +import 'package:cypher_sheet/views/character_sheet/abilities.dart'; +import 'package:cypher_sheet/views/character_sheet/cyphers.dart'; +import 'package:cypher_sheet/views/character_sheet/database.dart'; +import 'package:cypher_sheet/views/character_sheet/equipment.dart'; +import 'package:cypher_sheet/views/character_sheet/notes.dart'; +import 'package:cypher_sheet/views/character_sheet/skills.dart'; +import 'package:cypher_sheet/views/character_sheet/stats.dart'; +import 'package:cypher_sheet/views/scaffold.dart'; +import 'package:flutter/material.dart'; +import 'package:cypher_sheet/components/icon.dart'; +import 'package:cypher_sheet/components/icons.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class CharacterSheetView extends StatelessWidget { + const CharacterSheetView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + return false; + }, + child: const CharacterSheet( + views: characterSheetViews, + ), + ); + } +} + +const characterSheetViews = [ + CharacterSheetTab("Database", AppIcons.database, DatabaseView()), + CharacterSheetTab( + "Skills", + AppIcons.skills, + SkillsView(), + handlesImports: [ + SharedObject_Object.skill, + ], + ), + CharacterSheetTab( + "Abilities", + AppIcons.abilities, + AbilitiesView(), + handlesImports: [ + SharedObject_Object.ability, + ], + ), + CharacterSheetTab("Stats", AppIcons.stats, StatsView()), + CharacterSheetTab( + "Cyphers", + AppIcons.cypher, + CyphersView(), + handlesImports: [ + SharedObject_Object.cypher, + SharedObject_Object.artifact, + ], + ), + CharacterSheetTab( + "Equipment", + AppIcons.equipment, + EquipmentView(), + handlesImports: [ + SharedObject_Object.item, + ], + ), + CharacterSheetTab( + "Notes", + AppIcons.notes, + NotesView(), + handlesImports: [ + SharedObject_Object.note, + ], + ), +]; + +class CharacterSheet extends ConsumerStatefulWidget { + const CharacterSheet({super.key, required this.views}); + + final List views; + + @override + ConsumerState createState() => _CharacterSheetState(); +} + +class CharacterSheetTab { + final String name; + final AppIcons icon; + final Widget view; + final List handlesImports; + + const CharacterSheetTab(this.name, this.icon, this.view, + {this.handlesImports = const []}); +} + +class _CharacterSheetState extends ConsumerState + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + late final TabController _tabController; + late final ProviderSubscription _importSubscription; + @override + void initState() { + super.initState(); + + _tabController = TabController( + length: widget.views.length, + vsync: this, + initialIndex: (widget.views.length - 1) ~/ 2.0); + + _importSubscription = ref.listenManual( + importObjectProvider, + (previous, next) { + if (next.uuid.isNotEmpty) { + SchedulerBinding.instance.addPostFrameCallback( + (timeStamp) { + showImportDialog(next); + }, + ); + } + }, + ); + + final currentImport = _importSubscription.read(); + if (currentImport.uuid.isNotEmpty) { + SchedulerBinding.instance.addPostFrameCallback( + (timeStamp) { + showImportDialog(currentImport); + }, + ); + } + } + + void showImportDialog(SharedObject import) { + showAppDialog( + context, + import.importDialog( + onCancel: onImportClosed, + onSuccess: onImportSuccess(import), + ), + ); + } + + void onImportClosed() { + ref.read(importObjectProvider.notifier).state = SharedObject(); + closeDialog(context); + } + + Function() onImportSuccess(SharedObject import) => () { + _tabController.index = + findTabIndexForSharedObject(import.whichObject()); + ref.read(importObjectProvider.notifier).state = SharedObject(); + }; + + @override + Widget build(BuildContext context) { + super.build(context); + return AppScaffold( + body: Container( + constraints: const BoxConstraints(maxWidth: 500), + child: TabBarView( + controller: _tabController, + children: widget.views.map((view) => view.view).toList()), + ), + bottomNavigationBar: Container( + decoration: const BoxDecoration( + color: Colors.transparent, + boxShadow: [ + BoxShadow( + blurRadius: 40, + spreadRadius: 1, + color: Colors.black, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ConstrainedBox( + constraints: BoxConstraints.loose(const Size.fromHeight(62)), + child: FittedBox( + fit: BoxFit.contain, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: widget.views + .asMap() + .entries + .map( + (e) => ToolbarIconButton( + AppIcon( + e.value.icon, + size: 34, + ), + e.value.name, + () { + _tabController.index = e.key; + }, + ), + ) + .toList(), + ), + ), + ), + ), + ), + ); + } + + @override + bool get wantKeepAlive => true; + + int findTabIndexForSharedObject(SharedObject_Object obj) { + final foundIndex = characterSheetViews + .indexWhere((element) => element.handlesImports.contains(obj)); + if (foundIndex >= 0) { + return foundIndex; + } + // return the middle view if we can't find a matching one + return (characterSheetViews.length - 1) ~/ 2.0; + } +} diff --git a/lib/views/characters.dart b/lib/views/characters.dart index 12d6a0c..c0b8bfd 100644 --- a/lib/views/characters.dart +++ b/lib/views/characters.dart @@ -1,6 +1,8 @@ import 'package:cypher_sheet/extensions/metadata.dart'; +import 'package:cypher_sheet/main.dart'; import 'package:cypher_sheet/state/providers/character.dart'; import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; +import 'package:cypher_sheet/views/scaffold.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/appbar.dart' as app; @@ -20,65 +22,67 @@ class CharactersView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return AppScrollView( - appBar: app.AppBar( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const AppText( - "Characters", + return AppScaffold( + body: AppScrollView( + appBar: app.AppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const AppText( + "Characters", + align: TextAlign.left, + ), + AppBox( + onTap: () { + showAppDialog(context, const DevCharacterList()); + }, + flat: true, + padding: 4, + child: const AppIcon(AppIcons.devMode), + ), + ], + )), + slivers: [ + const SizedBox(height: 16.0), + AppText( + """ +Note: You are using a work-in-progress build of this app. +Features may not work as intended or your data may not be persisted correctly. +Please keep a paper backup of your character. + +You can report feedback to app-feedback@kwiesmueller.dev. + """, + maxLines: 10, + style: Theme.of(context).textTheme.labelLarge, align: TextAlign.left, + overflow: TextOverflow.ellipsis, ), + const SizedBox(height: 16.0), + ...ref.watch(characterListProvider).when( + loading: () => const [CircularProgressIndicator()], + error: (err, stack) => [Text("Error: $err")], + data: (characters) { + return characters.map((metadata) => Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: CharacterListItem( + metadata: metadata, + ), + )); + }), AppBox( onTap: () { - showAppDialog(context, const DevCharacterList()); + showAppDialog( + context, + const CreateCharacter(), + fullscreen: true, + ); }, flat: true, - padding: 4, - child: const AppIcon(AppIcons.devMode), + child: const AppText("Create Character"), ), ], - )), - slivers: [ - const SizedBox(height: 16.0), - AppText( - """ -Note: You are using a work-in-progress build of this app. -Features may not work as intended or your data may not be persisted correctly. -Please keep a paper backup of your character. - -You can report feedback to app-feedback@kwiesmueller.dev. - """, - maxLines: 10, - style: Theme.of(context).textTheme.labelLarge, - align: TextAlign.left, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 16.0), - ...ref.watch(characterListProvider).when( - loading: () => const [CircularProgressIndicator()], - error: (err, stack) => [Text("Error: $err")], - data: (characters) { - return characters.map((metadata) => Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: CharacterListItem( - metadata: metadata, - ), - )); - }), - AppBox( - onTap: () { - showAppDialog( - context, - const CreateCharacter(), - fullscreen: true, - ); - }, - flat: true, - child: const AppText("Create Character"), - ), - ], + ), ); } } @@ -98,11 +102,13 @@ class CharacterListItem extends ConsumerWidget { builder: (context, snapshot) { if (snapshot.hasData) { return AppBox( - color: Theme.of(context).colorScheme.surfaceTint, + color: Theme.of(context).colorScheme.surface, onTap: (() async { final character = await readLatestCharacterRevision(snapshot.data!.uuid); ref.read(characterProvider.notifier).load(character); + if (!context.mounted) return; + Navigator.of(context).pushReplacementNamed(routeCharacter); }), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/views/dialogs/create_ability.dart b/lib/views/dialogs/create_ability.dart deleted file mode 100644 index 92b14f9..0000000 --- a/lib/views/dialogs/create_ability.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'package:cypher_sheet/components/checkbox.dart'; -import 'package:cypher_sheet/components/icon.dart'; -import 'package:cypher_sheet/components/icons.dart'; -import 'package:cypher_sheet/components/scroll.dart'; -import 'package:cypher_sheet/components/selector.dart'; -import 'package:cypher_sheet/state/providers/character.dart'; -import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cypher_sheet/components/box.dart'; -import 'package:cypher_sheet/components/dialog.dart'; -import 'package:cypher_sheet/components/text.dart'; -import 'package:cypher_sheet/proto/character.pb.dart'; - -class CreateAbility extends ConsumerStatefulWidget { - const CreateAbility({ - super.key, - this.initialUUID = "", - this.initialName = "", - this.initialCost = "", - this.initialType = PoolType.intellect, - this.initialEnabler = false, - this.initialShortDescription = "", - this.initialDescription = "", - }); - - CreateAbility.fromState(Ability ability, {super.key}) - : initialUUID = ability.uuid, - initialName = ability.name, - initialCost = ability.cost, - initialType = ability.type, - initialEnabler = ability.enabler, - initialShortDescription = ability.shortDescription, - initialDescription = ability.description; - - final String initialUUID; - final String initialName; - final String initialCost; - final PoolType initialType; - final bool initialEnabler; - final String initialShortDescription; - final String initialDescription; - - @override - ConsumerState createState() => _CreateAbilityState(); -} - -class _CreateAbilityState extends ConsumerState { - late bool isNewAbility; - late TextEditingController name = TextEditingController(); - late TextEditingController cost = TextEditingController(); - late PoolType type; - late bool enabler; - late TextEditingController shortDescription = TextEditingController(); - late TextEditingController description = TextEditingController(); - - @override - void initState() { - super.initState(); - isNewAbility = widget.initialUUID.isEmpty; - name.text = widget.initialName; - cost.text = widget.initialCost; - type = widget.initialType; - enabler = widget.initialEnabler; - shortDescription.text = widget.initialShortDescription; - description.text = widget.initialDescription; - } - - @override - void dispose() { - name.dispose(); - cost.dispose(); - shortDescription.dispose(); - description.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppText( - "${isNewAbility ? "Create" : "Edit"} Ability", - align: TextAlign.left, - ), - if (!isNewAbility) - AppBox( - flat: true, - padding: 2, - onTap: () { - showConfirmDialog( - context, - AppText( - "Permanently delete\n${name.value.text}", - maxLines: 2, - style: Theme.of(context).textTheme.labelLarge, - ), () { - ref - .read(characterProvider.notifier) - .deleteAbility(widget.initialUUID); - closeDialog(context); - closeDialog(context); - }); - }, - child: const AppIcon( - AppIcons.deleteForever, - size: 24, - )), - ], - ), - ), - Expanded( - child: AppScrollView(customPadding: EdgeInsets.zero, slivers: [ - DialogTextBox( - controller: name, - label: "Name", - initialValue: name.value.text, - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: AppText( - "Type", - style: Theme.of(context).textTheme.labelLarge!.copyWith( - fontWeight: FontWeight.w600, - ), - align: TextAlign.left, - ), - ), - poolTypeSelectors(), - ], - ), - const SizedBox( - width: 0.0, - ), - Expanded( - child: DialogTextBox( - controller: cost, - label: "Cost", - initialValue: cost.value.text, - ), - ), - ], - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppCheckbox( - active: enabler, - onTap: () { - setState(() { - enabler = !enabler; - }); - }, - ), - const SizedBox(width: 16.0), - AppText( - "Enabler", - style: Theme.of(context).textTheme.labelLarge!.copyWith( - fontWeight: FontWeight.w600, - ), - align: TextAlign.left, - ), - ], - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: shortDescription, - label: "Short Description", - initialValue: shortDescription.text, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: description, - label: "Description", - initialValue: description.text, - multiLine: true, - ), - ])), - const SizedBox(height: 16.0), - AppBox( - onTap: (() { - final ability = Ability( - uuid: widget.initialUUID, - name: name.value.text, - cost: cost.value.text, - type: type, - enabler: enabler, - shortDescription: shortDescription.value.text, - description: description.value.text, - ); - if (isNewAbility) { - ref.read(characterProvider.notifier).addAbility(ability); - } else { - ref.read(characterProvider.notifier).updateAbility(ability); - } - closeDialog(context); - }), - color: Theme.of(context).colorScheme.primary, - child: AppText(isNewAbility ? "Create" : "Update"), - ), - ], - ); - } - - Widget poolTypeSelectors() { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...PoolType.values.map( - (selectorType) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: PoolTypeSelector( - activeType: type, - type: selectorType, - onSelect: (newType) { - setState(() { - type = newType; - }); - }, - ), - ); - }, - ), - ], - ); - } -} diff --git a/lib/views/dialogs/create_artifact.dart b/lib/views/dialogs/create_artifact.dart deleted file mode 100644 index 59e21bb..0000000 --- a/lib/views/dialogs/create_artifact.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'package:cypher_sheet/components/checkbox.dart'; -import 'package:cypher_sheet/components/icon.dart'; -import 'package:cypher_sheet/components/icons.dart'; -import 'package:cypher_sheet/components/scroll.dart'; -import 'package:cypher_sheet/state/providers/character.dart'; -import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cypher_sheet/components/box.dart'; -import 'package:cypher_sheet/components/dialog.dart'; -import 'package:cypher_sheet/components/text.dart'; -import 'package:cypher_sheet/proto/character.pb.dart'; - -class CreateArtifact extends ConsumerStatefulWidget { - const CreateArtifact({ - super.key, - this.uuid = "", - this.name = "", - this.level = "", - this.shortDescription = "", - this.effect = "", - this.active = false, - this.depletion = "", - this.form = "", - }); - - CreateArtifact.fromState(Artifact artifact, {super.key}) - : uuid = artifact.uuid, - name = artifact.name, - level = artifact.level, - shortDescription = artifact.shortDescription, - effect = artifact.effect, - active = artifact.active, - depletion = artifact.depletion, - form = artifact.form; - - final String uuid; - final String name; - final String level; - final String shortDescription; - final String effect; - final bool active; - final String depletion; - final String form; - - @override - ConsumerState createState() => _CreateAbilityState(); -} - -class _CreateAbilityState extends ConsumerState { - late bool isNew; - - late TextEditingController name = TextEditingController(); - late TextEditingController level = TextEditingController(); - late TextEditingController shortDescription = TextEditingController(); - late TextEditingController effect = TextEditingController(); - late bool active; - final TextEditingController depletion = TextEditingController(); - final TextEditingController form = TextEditingController(); - - @override - void initState() { - super.initState(); - - isNew = widget.uuid.isEmpty; - name.text = widget.name; - level.text = widget.level; - shortDescription.text = widget.shortDescription; - effect.text = widget.effect; - active = widget.active; - depletion.text = widget.depletion; - form.text = widget.form; - } - - @override - void dispose() { - name.dispose(); - level.dispose(); - shortDescription.dispose(); - effect.dispose(); - depletion.dispose(); - form.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppText( - "${isNew ? "Create" : "Edit"} Artifact", - align: TextAlign.left, - ), - if (!isNew) - AppBox( - flat: true, - padding: 2, - onTap: () { - showConfirmDialog( - context, - AppText( - "Permanently delete\n${name.value.text}", - maxLines: 2, - style: Theme.of(context).textTheme.labelLarge, - ), () { - ref - .read(characterProvider.notifier) - .deleteArtifact(widget.uuid); - closeDialog(context); - closeDialog(context); - }); - }, - child: const AppIcon( - AppIcons.deleteForever, - size: 24, - )), - ], - ), - ), - Expanded( - child: AppScrollView(customPadding: EdgeInsets.zero, slivers: [ - DialogTextBox( - controller: name, - label: "Name", - initialValue: name.value.text, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: shortDescription, - label: "Short Description", - initialValue: shortDescription.text, - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - fit: FlexFit.loose, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Container( - constraints: const BoxConstraints.tightFor(width: 64), - child: DialogTextBox( - controller: level, - label: "Level", - initialValue: level.value.text, - ), - ), - ), - ), - Expanded( - child: DialogTextBox( - controller: depletion, - label: "Depletion", - initialValue: depletion.value.text, - ), - ), - ], - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppCheckbox( - active: active, - onTap: () { - setState(() { - active = !active; - }); - }, - ), - const SizedBox(width: 16.0), - AppText( - "Active", - style: Theme.of(context).textTheme.labelLarge!.copyWith( - fontWeight: FontWeight.w600, - ), - align: TextAlign.left, - ), - ], - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: form, - label: "Form", - initialValue: form.text, - multiLine: true, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: effect, - label: "Effect", - initialValue: effect.text, - multiLine: true, - ), - ]), - ), - const SizedBox(height: 16.0), - AppBox( - onTap: (() { - final artifact = Artifact( - uuid: widget.uuid, - name: name.value.text, - level: level.value.text, - shortDescription: shortDescription.value.text, - effect: effect.value.text, - active: active, - depletion: depletion.value.text, - form: form.value.text, - ); - if (isNew) { - ref.read(characterProvider.notifier).addArtifact(artifact); - } else { - ref.read(characterProvider.notifier).updateArtifact(artifact); - } - closeDialog(context); - }), - color: Theme.of(context).colorScheme.primary, - child: AppText(isNew ? "Create" : "Update"), - ), - ], - ); - } -} diff --git a/lib/views/dialogs/create_cypher.dart b/lib/views/dialogs/create_cypher.dart deleted file mode 100644 index 317d8ff..0000000 --- a/lib/views/dialogs/create_cypher.dart +++ /dev/null @@ -1,262 +0,0 @@ -import 'package:cypher_sheet/components/checkbox.dart'; -import 'package:cypher_sheet/components/icon.dart'; -import 'package:cypher_sheet/components/icons.dart'; -import 'package:cypher_sheet/components/scroll.dart'; -import 'package:cypher_sheet/state/providers/character.dart'; -import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cypher_sheet/components/box.dart'; -import 'package:cypher_sheet/components/dialog.dart'; -import 'package:cypher_sheet/components/text.dart'; -import 'package:cypher_sheet/proto/character.pb.dart'; - -class CreateCypher extends ConsumerStatefulWidget { - const CreateCypher({ - super.key, - this.uuid = "", - this.name = "", - this.level = "", - this.shortDescription = "", - this.effect = "", - this.active = false, - this.depletion = "", - this.internal = "", - this.wearable = "", - this.usable = "", - }); - - CreateCypher.fromState(Cypher cypher, {super.key}) - : uuid = cypher.uuid, - name = cypher.name, - level = cypher.level, - shortDescription = cypher.shortDescription, - effect = cypher.effect, - active = cypher.active, - depletion = cypher.depletion, - internal = cypher.internal, - wearable = cypher.wearable, - usable = cypher.usable; - - final String uuid; - final String name; - final String level; - final String shortDescription; - final String effect; - final bool active; - final String depletion; - final String internal; - final String wearable; - final String usable; - - @override - ConsumerState createState() => _CreateAbilityState(); -} - -class _CreateAbilityState extends ConsumerState { - late bool isNew; - - late TextEditingController name = TextEditingController(); - late TextEditingController level = TextEditingController(); - late TextEditingController shortDescription = TextEditingController(); - late TextEditingController effect = TextEditingController(); - late bool active; - final TextEditingController depletion = TextEditingController(); - final TextEditingController internal = TextEditingController(); - final TextEditingController wearable = TextEditingController(); - final TextEditingController usable = TextEditingController(); - - @override - void initState() { - super.initState(); - - isNew = widget.uuid.isEmpty; - name.text = widget.name; - level.text = widget.level; - shortDescription.text = widget.shortDescription; - effect.text = widget.effect; - active = widget.active; - depletion.text = widget.depletion; - internal.text = widget.internal; - wearable.text = widget.wearable; - usable.text = widget.usable; - } - - @override - void dispose() { - name.dispose(); - level.dispose(); - shortDescription.dispose(); - effect.dispose(); - depletion.dispose(); - internal.dispose(); - wearable.dispose(); - usable.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppText( - "${isNew ? "Create" : "Edit"} Cypher", - align: TextAlign.left, - ), - if (!isNew) - AppBox( - flat: true, - padding: 2, - onTap: () { - showConfirmDialog( - context, - AppText( - "Permanently delete\n${name.value.text}", - maxLines: 2, - style: Theme.of(context).textTheme.labelLarge, - ), () { - ref - .read(characterProvider.notifier) - .deleteCypher(widget.uuid); - closeDialog(context); - closeDialog(context); - }); - }, - child: const AppIcon( - AppIcons.deleteForever, - size: 24, - )), - ], - ), - ), - Expanded( - child: AppScrollView(customPadding: EdgeInsets.zero, slivers: [ - DialogTextBox( - controller: name, - label: "Name", - initialValue: name.value.text, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: shortDescription, - label: "Short Description", - initialValue: shortDescription.text, - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - fit: FlexFit.loose, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Container( - constraints: const BoxConstraints.tightFor(width: 64), - child: DialogTextBox( - controller: level, - label: "Level", - initialValue: level.value.text, - ), - ), - ), - ), - Expanded( - child: DialogTextBox( - controller: depletion, - label: "Depletion", - initialValue: depletion.value.text, - ), - ), - ], - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppCheckbox( - active: active, - onTap: () { - setState(() { - active = !active; - }); - }, - ), - const SizedBox(width: 16.0), - AppText( - "Active", - style: Theme.of(context).textTheme.labelLarge!.copyWith( - fontWeight: FontWeight.w600, - ), - align: TextAlign.left, - ), - ], - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: internal, - label: "Internal", - initialValue: internal.text, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: wearable, - label: "Wearable", - initialValue: wearable.text, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: usable, - label: "Usable", - initialValue: usable.text, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: effect, - label: "Effect", - initialValue: effect.text, - multiLine: true, - ), - ]), - ), - const SizedBox(height: 16.0), - AppBox( - onTap: (() { - final cypher = Cypher( - uuid: widget.uuid, - name: name.value.text, - level: level.value.text, - shortDescription: shortDescription.value.text, - effect: effect.value.text, - active: active, - depletion: depletion.value.text, - internal: internal.value.text, - wearable: wearable.value.text, - usable: usable.value.text, - ); - if (isNew) { - ref.read(characterProvider.notifier).addCypher(cypher); - } else { - ref.read(characterProvider.notifier).updateCypher(cypher); - } - closeDialog(context); - }), - color: Theme.of(context).colorScheme.primary, - child: AppText(isNew ? "Create" : "Update"), - ), - ], - ); - } -} diff --git a/lib/views/dialogs/create_item.dart b/lib/views/dialogs/create_item.dart deleted file mode 100644 index a831987..0000000 --- a/lib/views/dialogs/create_item.dart +++ /dev/null @@ -1,340 +0,0 @@ -import 'package:cypher_sheet/components/equipment.dart'; -import 'package:cypher_sheet/components/icon.dart'; -import 'package:cypher_sheet/components/icons.dart'; -import 'package:cypher_sheet/components/label.dart'; -import 'package:cypher_sheet/components/scroll.dart'; -import 'package:cypher_sheet/extensions/item.dart'; -import 'package:cypher_sheet/state/filters/item_filter.dart'; -import 'package:cypher_sheet/state/providers/character.dart'; -import 'package:cypher_sheet/state/providers/inventories.dart'; -import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cypher_sheet/components/box.dart'; -import 'package:cypher_sheet/components/dialog.dart'; -import 'package:cypher_sheet/components/text.dart'; -import 'package:cypher_sheet/proto/character.pb.dart'; - -class CreateItem extends ConsumerStatefulWidget { - const CreateItem({ - super.key, - required this.path, - this.name = "", - this.shortDescription = "", - this.description = "", - this.types = const [], - this.amount = 1.0, - this.value = 0.0, - this.subItemType, - this.armor, - }); - - CreateItem.fromState(Item item, {super.key}) - : path = item.path, - name = item.name, - shortDescription = item.shortDescription, - description = item.description, - types = item.types, - amount = item.amount, - subItemType = item.hasSubItemType() ? item.subItemType : null, - value = item.value, - armor = item.hasArmor() ? item.armor : null; - - final ItemPath path; - final String name; - final String shortDescription; - final String description; - final List types; - final double amount; - final double value; - final ItemType? subItemType; - final int? armor; - - @override - ConsumerState createState() => _CreateAbilityState(); -} - -class _CreateAbilityState extends ConsumerState { - late bool isNew; - - late TextEditingController name = TextEditingController(); - late TextEditingController shortDescription = TextEditingController(); - late TextEditingController description = TextEditingController(); - late TextEditingController amount = TextEditingController(); - late TextEditingController value = TextEditingController(); - late ItemFilter types; - late ItemType? subItemType; - late TextEditingController armor = TextEditingController(); - late String inventory; - - @override - void initState() { - super.initState(); - - isNew = widget.path.self.isEmpty; - name.text = widget.name; - shortDescription.text = widget.shortDescription; - description.text = widget.description; - types = ItemFilter(activeTypes: widget.types); - amount.text = removeZeroDecimals(widget.amount).toString(); - value.text = removeZeroDecimals(widget.value).toString(); - subItemType = widget.subItemType; - armor.text = widget.armor != null ? widget.armor.toString() : ""; - inventory = widget.path.inventory; - } - - @override - void dispose() { - name.dispose(); - shortDescription.dispose(); - description.dispose(); - amount.dispose(); - value.dispose(); - armor.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppText( - "${isNew ? "Create" : "Edit"} Item", - align: TextAlign.left, - ), - if (!isNew) - AppBox( - flat: true, - padding: 2, - onTap: () { - showConfirmDialog( - context, - AppText( - "Permanently delete\n${name.value.text}", - maxLines: 2, - style: Theme.of(context).textTheme.labelLarge, - ), () { - ref - .read(characterProvider.notifier) - .deleteItem(widget.path.self); - closeDialog(context); - closeDialog(context); - }); - }, - child: const AppIcon( - AppIcons.deleteForever, - size: 24, - )), - ], - ), - ), - const AppLabel( - text: "Inventory", - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: SVGBox( - icon: AppIcons.self, - active: inventory == inventoryNameSelf, - onTap: () { - setState(() { - inventory = inventoryNameSelf; - }); - }, - ), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: SVGBox( - icon: AppIcons.backpack, - active: inventory == inventoryNameBackpack, - onTap: () { - setState(() { - inventory = inventoryNameBackpack; - }); - }, - ), - ), - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: SVGBox( - icon: AppIcons.home, - active: inventory == inventoryNameHome, - onTap: () { - setState(() { - inventory = inventoryNameHome; - }); - }, - ), - ), - ], - ), - const SizedBox(height: 16.0), - Expanded( - child: AppScrollView(customPadding: EdgeInsets.zero, slivers: [ - DialogTextBox( - controller: name, - label: "Name", - initialValue: name.value.text, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: shortDescription, - label: "Short Description", - initialValue: shortDescription.text, - ), - const SizedBox(height: 16.0), - Wrap( - runSpacing: 8.0, - spacing: 8.0, - children: ItemType.values - .map((e) => SVGBoxLabeled( - icon: e.toIcon(), - label: e.toLabel(), - active: types.isTypeActive(e), - iconSize: 26, - customLabelStyle: - Theme.of(context).textTheme.labelLarge, - onTap: () { - setState(() { - types = types.toggleFilter(e); - }); - }, - )) - .toList(growable: false), - ), - const SizedBox(height: 16.0), - if (types.isTypeActive(ItemType.armor)) - DialogTextBox( - controller: armor, - label: "Armor Bonus", - initialValue: armor.text, - ), - if (types.isTypeActive(ItemType.armor)) - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: DialogTextBox( - controller: amount, - label: "Amount / Count", - initialValue: amount.text, - ), - ), - ), - Expanded( - child: DialogTextBox( - controller: value, - label: "Value", - initialValue: value.text, - ), - ), - ], - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: description, - label: "Description", - initialValue: description.text, - multiLine: true, - ), - const SizedBox(height: 16.0), - const AppLabel(text: "Sub Item Type"), - Wrap( - runSpacing: 8.0, - spacing: 8.0, - children: [ - SVGBoxLabeled( - icon: AppIcons.none, - label: "None", - iconSize: 26, - customLabelStyle: Theme.of(context).textTheme.labelLarge, - onTap: () { - setState(() { - subItemType = null; - }); - }, - active: subItemType == null, - ), - ...ItemType.values - .map((e) => SVGBoxLabeled( - icon: e.toIcon(), - label: e.toLabel(), - active: subItemType == e, - iconSize: 26, - customLabelStyle: - Theme.of(context).textTheme.labelLarge, - onTap: () { - setState(() { - subItemType = e; - }); - }, - )) - .toList(growable: false) - ], - ), - ]), - ), - const SizedBox(height: 16.0), - AppBox( - onTap: (() { - final hasMoved = widget.path.inventory.isNotEmpty && - widget.path.inventory != inventory; - final path = ItemPath( - inventory: inventory.isEmpty ? widget.path.inventory : inventory, - parent: widget.path.parent, - self: widget.path.self, - ); - final item = Item( - path: path, - name: name.value.text, - shortDescription: shortDescription.value.text, - description: description.value.text, - types: types.activeTypes.isNotEmpty - ? types.activeTypes - : [ItemType.others], - amount: amount.value.text.isNotEmpty - ? double.parse(amount.value.text) - : 1, - value: value.value.text.isNotEmpty - ? double.parse(value.value.text) - : 0, - armor: armor.value.text.isNotEmpty - ? int.tryParse(armor.value.text) - : null, - subItemType: subItemType, - ); - if (isNew) { - ref.read(characterProvider.notifier).addItem(item); - } else if (hasMoved) { - ref.read(characterProvider.notifier).moveItem(item); - } else { - ref.read(characterProvider.notifier).updateItem(item); - } - closeDialog(context); - }), - color: Theme.of(context).colorScheme.primary, - child: AppText(isNew ? "Create" : "Update"), - ), - ], - ); - } -} diff --git a/lib/views/dialogs/create_note.dart b/lib/views/dialogs/create_note.dart deleted file mode 100644 index 5475d12..0000000 --- a/lib/views/dialogs/create_note.dart +++ /dev/null @@ -1,200 +0,0 @@ -import 'package:cypher_sheet/components/icon.dart'; -import 'package:cypher_sheet/components/icons.dart'; -import 'package:cypher_sheet/components/scroll.dart'; -import 'package:cypher_sheet/components/selector.dart'; -import 'package:cypher_sheet/state/providers/character.dart'; -import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cypher_sheet/components/box.dart'; -import 'package:cypher_sheet/components/dialog.dart'; -import 'package:cypher_sheet/components/text.dart'; -import 'package:cypher_sheet/proto/character.pb.dart'; - -class CreateNote extends ConsumerStatefulWidget { - const CreateNote({ - super.key, - this.uuid = "", - this.title = "", - this.type = NoteType.misc, - this.shortDescription = "", - this.text = "", - }); - - CreateNote.fromState(Note note, {super.key}) - : uuid = note.uuid, - title = note.title, - type = note.type, - shortDescription = note.shortDescription, - text = note.text; - - final String uuid; - final String title; - final NoteType type; - final String shortDescription; - final String text; - - @override - ConsumerState createState() => _CreateAbilityState(); -} - -class _CreateAbilityState extends ConsumerState { - late bool isNew; - - late TextEditingController title = TextEditingController(); - late TextEditingController shortDescription = TextEditingController(); - late NoteType type; - late TextEditingController text = TextEditingController(); - - @override - void initState() { - super.initState(); - - isNew = widget.uuid.isEmpty; - title.text = widget.title; - shortDescription.text = widget.shortDescription; - type = widget.type; - text.text = widget.text; - } - - @override - void dispose() { - title.dispose(); - shortDescription.dispose(); - text.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppText( - "${isNew ? "Create" : "Edit"} Note", - align: TextAlign.left, - ), - if (!isNew) - AppBox( - flat: true, - padding: 2, - onTap: () { - showConfirmDialog( - context, - AppText( - "Permanently delete\n${title.value.text}", - maxLines: 2, - style: Theme.of(context).textTheme.labelLarge, - ), () { - ref - .read(characterProvider.notifier) - .deleteNote(widget.uuid); - closeDialog(context); - closeDialog(context); - }); - }, - child: const AppIcon( - AppIcons.deleteForever, - size: 24, - )), - ], - ), - ), - Expanded( - child: AppScrollView(customPadding: EdgeInsets.zero, slivers: [ - DialogTextBox( - controller: title, - label: "Title", - initialValue: title.value.text, - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: shortDescription, - label: "Short Description", - initialValue: shortDescription.value.text, - ), - const SizedBox(height: 16.0), - Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: AppText( - "Type", - style: Theme.of(context).textTheme.labelLarge!.copyWith( - fontWeight: FontWeight.w600, - ), - align: TextAlign.left, - ), - ), - noteTypeSelectors(), - ], - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: text, - label: "Text", - initialValue: text.text, - multiLine: true, - ), - ]), - ), - const SizedBox(height: 16.0), - AppBox( - onTap: (() { - final note = Note( - uuid: widget.uuid, - title: title.value.text, - shortDescription: shortDescription.value.text, - type: type, - text: text.value.text, - ); - if (isNew) { - ref.read(characterProvider.notifier).addNote(note); - } else { - ref.read(characterProvider.notifier).updateNote(note); - } - closeDialog(context); - }), - color: Theme.of(context).colorScheme.primary, - child: AppText(isNew ? "Create" : "Update"), - ), - ], - ); - } - - Widget noteTypeSelectors() { - return Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - ...NoteType.values.map( - (selectorType) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: NoteTypeSelector( - activeType: type, - type: selectorType, - onSelect: (newType) { - setState(() { - type = newType; - }); - }, - ), - ); - }, - ), - ], - ); - } -} diff --git a/lib/views/dialogs/create_skill.dart b/lib/views/dialogs/create_skill.dart deleted file mode 100644 index 3a61821..0000000 --- a/lib/views/dialogs/create_skill.dart +++ /dev/null @@ -1,245 +0,0 @@ -import 'package:cypher_sheet/components/icon.dart'; -import 'package:cypher_sheet/components/icons.dart'; -import 'package:cypher_sheet/components/scroll.dart'; -import 'package:cypher_sheet/components/selector.dart'; -import 'package:cypher_sheet/state/providers/character.dart'; -import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:cypher_sheet/components/box.dart'; -import 'package:cypher_sheet/components/dialog.dart'; -import 'package:cypher_sheet/components/text.dart'; -import 'package:cypher_sheet/proto/character.pb.dart'; - -class CreateSkill extends ConsumerStatefulWidget { - const CreateSkill({ - super.key, - this.uuid = "", - this.name = "", - this.description = "", - this.type = PoolType.intellect, - this.level = SkillLevel.trained, - }); - - CreateSkill.fromState(Skill skill, {super.key}) - : uuid = skill.uuid, - name = skill.name, - description = skill.description, - type = skill.type, - level = skill.level; - - final String uuid; - final String name; - final String description; - final PoolType type; - final SkillLevel level; - - @override - ConsumerState createState() => _CreateAbilityState(); -} - -class _CreateAbilityState extends ConsumerState { - late bool isNew; - - late TextEditingController name = TextEditingController(); - late TextEditingController description = TextEditingController(); - late PoolType type; - late SkillLevel level; - - @override - void initState() { - super.initState(); - - isNew = widget.uuid.isEmpty; - name.text = widget.name; - description.text = widget.description; - type = widget.type; - level = widget.level; - } - - @override - void dispose() { - name.dispose(); - description.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AppText( - "${isNew ? "Create" : "Edit"} Skill", - align: TextAlign.left, - ), - if (!isNew) - AppBox( - flat: true, - padding: 2, - onTap: () { - showConfirmDialog( - context, - AppText( - "Permanently delete\n${name.value.text}", - maxLines: 2, - style: Theme.of(context).textTheme.labelLarge, - ), () { - ref - .read(characterProvider.notifier) - .deleteSkill(widget.uuid); - closeDialog(context); - closeDialog(context); - }); - }, - child: const AppIcon( - AppIcons.deleteForever, - size: 24, - )), - ], - ), - ), - Expanded( - child: AppScrollView(customPadding: EdgeInsets.zero, slivers: [ - DialogTextBox( - controller: name, - label: "Name", - initialValue: name.value.text, - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: AppText( - "Type", - style: Theme.of(context).textTheme.labelLarge!.copyWith( - fontWeight: FontWeight.w600, - ), - align: TextAlign.left, - ), - ), - poolTypeSelectors(), - ], - ), - const SizedBox(height: 16.0), - Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: AppText( - "Level", - style: Theme.of(context).textTheme.labelLarge!.copyWith( - fontWeight: FontWeight.w600, - ), - align: TextAlign.left, - ), - ), - skillLevelSelectors(), - ], - ), - ], - ), - const SizedBox(height: 16.0), - DialogTextBox( - controller: description, - label: "Description", - initialValue: description.text, - multiLine: true, - ), - ]), - ), - const SizedBox(height: 16.0), - AppBox( - onTap: (() { - final skill = Skill( - uuid: widget.uuid, - name: name.value.text, - description: description.value.text, - level: level, - type: type, - ); - if (isNew) { - ref.read(characterProvider.notifier).addSkill(skill); - } else { - ref.read(characterProvider.notifier).updateSkill(skill); - } - closeDialog(context); - }), - color: Theme.of(context).colorScheme.primary, - child: AppText(isNew ? "Create" : "Update"), - ), - ], - ); - } - - Widget poolTypeSelectors() { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...PoolType.values.map( - (selectorType) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: PoolTypeSelector( - activeType: type, - type: selectorType, - onSelect: (newType) { - setState(() { - type = newType; - }); - }, - ), - ); - }, - ), - ], - ); - } - - Widget skillLevelSelectors() { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...SkillLevel.values.map( - (selectorType) { - return Padding( - padding: const EdgeInsets.only(right: 8.0), - child: SkillLevelSelector( - activeLevel: level, - type: selectorType, - onSelect: (newLevel) { - setState(() { - level = newLevel; - }); - }, - ), - ); - }, - ), - ], - ); - } -} diff --git a/lib/views/dialogs/dev_character_list.dart b/lib/views/dialogs/dev_character_list.dart index e87af70..85d8180 100644 --- a/lib/views/dialogs/dev_character_list.dart +++ b/lib/views/dialogs/dev_character_list.dart @@ -1,5 +1,9 @@ +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; import 'package:cypher_sheet/state/providers/character.dart'; -import 'package:cypher_sheet/views/dialogs/share.dart'; +import 'package:cypher_sheet/state/providers/import.dart'; +import 'package:cypher_sheet/state/providers/inventories.dart'; +import 'package:cypher_sheet/views/dialogs/share_character.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/box.dart'; @@ -13,10 +17,8 @@ class DevCharacterList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + return AppScrollView( + slivers: [ Padding( padding: const EdgeInsets.only(bottom: 16.0), child: AppText( @@ -53,7 +55,101 @@ class DevCharacterList extends ConsumerWidget { }, child: const AppText("Show Licenses"), ), - const Spacer(), + const SizedBox(height: 28.0), + AppBox( + onTap: () { + closeDialog(context); + ref.read(importObjectProvider.notifier).state = SharedObject( + uuid: "test-uuid", + cypher: Cypher( + uuid: "test-uuid", + name: "Test Cypher", + active: false, + ), + ); + }, + child: const AppText("Import Test Cypher"), + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () { + closeDialog(context); + ref.read(importObjectProvider.notifier).state = SharedObject( + uuid: "test-uuid2", + artifact: Artifact( + uuid: "test-uuid2", + name: "Test Artifact", + active: true, + ), + ); + }, + child: const AppText("Import Test Artifact"), + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () { + closeDialog(context); + ref.read(importObjectProvider.notifier).state = SharedObject( + uuid: "test-uuid3", + item: Item( + path: ItemPath( + inventory: inventoryNameSelf, + parent: null, + self: "test-uuid3", + ), + name: "Test Item", + shortDescription: "Imported"), + ); + }, + child: const AppText("Import Test Item"), + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () { + closeDialog(context); + ref.read(importObjectProvider.notifier).state = SharedObject( + uuid: "test-uuid4", + ability: Ability( + uuid: "test-uuid4", + name: "Test Ability", + enabler: true, + ), + ); + }, + child: const AppText("Import Test Ability"), + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () { + closeDialog(context); + ref.read(importObjectProvider.notifier).state = SharedObject( + uuid: "test-uuid5", + skill: Skill( + uuid: "test-uuid5", + name: "Test Skill", + level: SkillLevel.inability, + ), + ); + }, + child: const AppText("Import Test Skill"), + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () { + closeDialog(context); + ref.read(importObjectProvider.notifier).state = SharedObject( + uuid: "test-uuid6", + note: Note( + uuid: "test-uuid6", + title: "Test Note", + shortDescription: "Foo", + type: NoteType.item, + ), + ); + }, + child: const AppText("Import Test Note"), + ), + const SizedBox(height: 28.0), AppBox( onTap: (() { closeDialog(context); diff --git a/lib/views/dialogs/edit_character_meta.dart b/lib/views/dialogs/edit_character_meta.dart index 6e71623..8756421 100644 --- a/lib/views/dialogs/edit_character_meta.dart +++ b/lib/views/dialogs/edit_character_meta.dart @@ -4,7 +4,7 @@ import 'package:cypher_sheet/extensions/metadata.dart'; import 'package:cypher_sheet/proto/character.pb.dart'; import 'package:cypher_sheet/state/providers/character.dart'; import 'package:cypher_sheet/state/storage/file.dart'; -import 'package:cypher_sheet/views/dialogs/share.dart'; +import 'package:cypher_sheet/views/dialogs/share_character.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/box.dart'; diff --git a/lib/views/dialogs/object/ability/create.dart b/lib/views/dialogs/object/ability/create.dart new file mode 100644 index 0000000..2649083 --- /dev/null +++ b/lib/views/dialogs/object/ability/create.dart @@ -0,0 +1,13 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/create.dart'; +import 'package:flutter/widgets.dart'; + +class CreateAbility extends StatelessWidget { + const CreateAbility({super.key}); + + @override + Widget build(BuildContext context) { + return CreateDialog(() => EditableAbility.empty()); + } +} diff --git a/lib/views/dialogs/object/ability/editable.dart b/lib/views/dialogs/object/ability/editable.dart new file mode 100644 index 0000000..feba291 --- /dev/null +++ b/lib/views/dialogs/object/ability/editable.dart @@ -0,0 +1,55 @@ +// TODO: generate this code +import 'package:cypher_sheet/extensions/editable.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/character.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/inputs.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; + +class EditableAbility extends GenericEditable { + factory EditableAbility.empty() { + return EditableAbility._(Ability.create()); + } + + factory EditableAbility.from(Ability original) { + return EditableAbility._(original); + } + + EditableAbility._(Ability original) + : type = original.hasType() ? original.type : PoolType.intellect, + super( + "Ability", + createNewEmpty: () => Ability.create(), + updateInState: (CharacterNotifier ref) => ref.updateAbility, + createInState: (CharacterNotifier ref) => ref.addAbility, + deleteInState: (CharacterNotifier ref) => ref.deleteAbility, + clearUuid: (Ability obj) => obj.clearUuid(), + setUuid: (Ability obj, String uuid) => obj.uuid = uuid, + originalUUID: original.uuid, + textFields: getEditableTextFieldsFrom(original), + boolFields: getEditableBoolFieldsFrom(original), + ); + + @override + Widget inputs(Function(Function()) setState) { + return AbilityEditInputs(this, setState); + } + + bool get enabler => boolFields["enabler"]!; + set enabler(bool to) => boolFields["enabler"] = to; + + TextEditingController get name => textFields["name"]!; + TextEditingController get cost => textFields["cost"]!; + TextEditingController get shortDescription => textFields["shortDescription"]!; + TextEditingController get description => textFields["description"]!; + + PoolType type; + + @override + void setOtherFields(Ability obj) { + obj.type = type; + } + + @override + String get objectName => name.value.text; +} diff --git a/lib/views/dialogs/object/ability/import.dart b/lib/views/dialogs/object/ability/import.dart new file mode 100644 index 0000000..f098237 --- /dev/null +++ b/lib/views/dialogs/object/ability/import.dart @@ -0,0 +1,26 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/import.dart'; +import 'package:flutter/widgets.dart'; + +class ImportAbility extends StatelessWidget { + const ImportAbility( + this.original, { + super.key, + this.onCancel, + this.onSuccess, + }); + + final Ability original; + final Function()? onCancel; + final Function()? onSuccess; + + @override + Widget build(BuildContext context) { + return ImportDialog( + () => EditableAbility.from(original), + onCancel: onCancel, + onSuccess: onSuccess, + ); + } +} diff --git a/lib/views/dialogs/object/ability/inputs.dart b/lib/views/dialogs/object/ability/inputs.dart new file mode 100644 index 0000000..3512c5a --- /dev/null +++ b/lib/views/dialogs/object/ability/inputs.dart @@ -0,0 +1,125 @@ +import 'package:cypher_sheet/components/checkbox.dart'; +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/components/selector.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/editable.dart'; +import 'package:flutter/material.dart'; +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; + +class AbilityEditInputs extends StatelessWidget { + const AbilityEditInputs(this.edit, this.setState, {super.key}); + + final EditableAbility edit; + final Function(Function()) setState; + + @override + Widget build(BuildContext context) { + return AppScrollView(customPadding: EdgeInsets.zero, slivers: [ + DialogTextBox( + controller: edit.name, + label: "Name", + initialValue: edit.name.value.text, + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: AppText( + "Type", + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontWeight: FontWeight.w600, + ), + align: TextAlign.left, + ), + ), + poolTypeSelectors(), + ], + ), + const SizedBox( + width: 0.0, + ), + Expanded( + child: DialogTextBox( + controller: edit.cost, + label: "Cost", + initialValue: edit.cost.value.text, + ), + ), + ], + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AppCheckbox( + active: edit.enabler, + onTap: () { + setState(() { + edit.enabler = !edit.enabler; + }); + }, + ), + const SizedBox(width: 16.0), + AppText( + "Enabler", + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontWeight: FontWeight.w600, + ), + align: TextAlign.left, + ), + ], + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.shortDescription, + label: "Short Description", + initialValue: edit.shortDescription.text, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.description, + label: "Description", + initialValue: edit.description.text, + multiLine: true, + ), + ]); + } + + Widget poolTypeSelectors() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...PoolType.values.map( + (selectorType) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: PoolTypeSelector( + activeType: edit.type, + type: selectorType, + onSelect: (newType) { + setState(() { + edit.type = newType; + }); + }, + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/views/dialogs/object/ability/update.dart b/lib/views/dialogs/object/ability/update.dart new file mode 100644 index 0000000..d7471ee --- /dev/null +++ b/lib/views/dialogs/object/ability/update.dart @@ -0,0 +1,16 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/update.dart'; +import 'package:flutter/widgets.dart'; + +class UpdateAbility extends StatelessWidget { + const UpdateAbility(this.original, {super.key}); + + final Ability original; + + @override + Widget build(BuildContext context) { + return UpdateDialog( + () => EditableAbility.from(original)); + } +} diff --git a/lib/views/dialogs/view_ability.dart b/lib/views/dialogs/object/ability/view.dart similarity index 89% rename from lib/views/dialogs/view_ability.dart rename to lib/views/dialogs/object/ability/view.dart index b72db5e..e91d8f2 100644 --- a/lib/views/dialogs/view_ability.dart +++ b/lib/views/dialogs/object/ability/view.dart @@ -3,9 +3,12 @@ import 'package:cypher_sheet/components/icon.dart'; import 'package:cypher_sheet/components/icons.dart'; import 'package:cypher_sheet/components/markdown.dart'; import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/extensions/ability.dart'; import 'package:cypher_sheet/extensions/pool.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; import 'package:cypher_sheet/state/providers/abilities.dart'; -import 'package:cypher_sheet/views/dialogs/create_ability.dart'; +import 'package:cypher_sheet/views/dialogs/object/ability/update.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/box.dart'; @@ -55,7 +58,7 @@ class ViewAbility extends ConsumerWidget { onTap: () { showAppDialog( context, - CreateAbility.fromState(ability), + UpdateAbility(ability), fullscreen: true, ); }, @@ -112,6 +115,11 @@ class ViewAbility extends ConsumerWidget { AppMarkdown(data: ability.description), ])), const SizedBox(height: 16.0), + ObjectActionButtons( + getShareable: () => ability.share().toFile(), + buildUpdateDialog: () => UpdateAbility(ability), + ), + const SizedBox(height: 16.0), AppBox( onTap: (() { closeDialog(context); diff --git a/lib/views/dialogs/object/artifact/create.dart b/lib/views/dialogs/object/artifact/create.dart new file mode 100644 index 0000000..31cfc89 --- /dev/null +++ b/lib/views/dialogs/object/artifact/create.dart @@ -0,0 +1,13 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/create.dart'; +import 'package:flutter/widgets.dart'; + +class CreateArtifact extends StatelessWidget { + const CreateArtifact({super.key}); + + @override + Widget build(BuildContext context) { + return CreateDialog(() => EditableArtifact.empty()); + } +} diff --git a/lib/views/dialogs/object/artifact/editable.dart b/lib/views/dialogs/object/artifact/editable.dart new file mode 100644 index 0000000..b34b24c --- /dev/null +++ b/lib/views/dialogs/object/artifact/editable.dart @@ -0,0 +1,49 @@ +// TODO: generate this code +import 'package:cypher_sheet/extensions/editable.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/character.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/inputs.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; + +class EditableArtifact extends GenericEditable { + factory EditableArtifact.empty() { + return EditableArtifact._(Artifact.create()); + } + + factory EditableArtifact.from(Artifact original) { + return EditableArtifact._(original); + } + + EditableArtifact._(Artifact original) + : super( + "Artifact", + createNewEmpty: () => Artifact.create(), + updateInState: (CharacterNotifier ref) => ref.updateArtifact, + createInState: (CharacterNotifier ref) => ref.addArtifact, + deleteInState: (CharacterNotifier ref) => ref.deleteArtifact, + clearUuid: (Artifact obj) => obj.clearUuid(), + setUuid: (Artifact obj, String uuid) => obj.uuid = uuid, + originalUUID: original.uuid, + textFields: getEditableTextFieldsFrom(original), + boolFields: getEditableBoolFieldsFrom(original), + ); + + @override + Widget inputs(Function(Function()) setState) { + return ArtifactEditInputs(this, setState); + } + + bool get active => boolFields["active"]!; + set active(bool to) => boolFields["active"] = to; + + TextEditingController get name => textFields["name"]!; + TextEditingController get level => textFields["level"]!; + TextEditingController get shortDescription => textFields["shortDescription"]!; + TextEditingController get effect => textFields["effect"]!; + TextEditingController get depletion => textFields["depletion"]!; + TextEditingController get form => textFields["form"]!; + + @override + String get objectName => name.value.text; +} diff --git a/lib/views/dialogs/object/artifact/import.dart b/lib/views/dialogs/object/artifact/import.dart new file mode 100644 index 0000000..df5c891 --- /dev/null +++ b/lib/views/dialogs/object/artifact/import.dart @@ -0,0 +1,26 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/import.dart'; +import 'package:flutter/widgets.dart'; + +class ImportArtifact extends StatelessWidget { + const ImportArtifact( + this.original, { + super.key, + this.onCancel, + this.onSuccess, + }); + + final Artifact original; + final Function()? onCancel; + final Function()? onSuccess; + + @override + Widget build(BuildContext context) { + return ImportDialog( + () => EditableArtifact.from(original), + onCancel: onCancel, + onSuccess: onSuccess, + ); + } +} diff --git a/lib/views/dialogs/object/artifact/inputs.dart b/lib/views/dialogs/object/artifact/inputs.dart new file mode 100644 index 0000000..9aa648a --- /dev/null +++ b/lib/views/dialogs/object/artifact/inputs.dart @@ -0,0 +1,97 @@ +import 'package:cypher_sheet/components/checkbox.dart'; +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/editable.dart'; +import 'package:flutter/material.dart'; +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; + +class ArtifactEditInputs extends StatelessWidget { + const ArtifactEditInputs(this.edit, this.setState, {super.key}); + + final EditableArtifact edit; + final Function(Function()) setState; + + @override + Widget build(BuildContext context) { + return AppScrollView(customPadding: EdgeInsets.zero, slivers: [ + DialogTextBox( + controller: edit.name, + label: "Name", + initialValue: edit.name.value.text, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.shortDescription, + label: "Short Description", + initialValue: edit.shortDescription.text, + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + fit: FlexFit.loose, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Container( + constraints: const BoxConstraints.tightFor(width: 64), + child: DialogTextBox( + controller: edit.level, + label: "Level", + initialValue: edit.level.value.text, + ), + ), + ), + ), + Expanded( + child: DialogTextBox( + controller: edit.depletion, + label: "Depletion", + initialValue: edit.depletion.value.text, + ), + ), + ], + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AppCheckbox( + active: edit.active, + onTap: () { + setState(() { + edit.active = !edit.active; + }); + }, + ), + const SizedBox(width: 16.0), + AppText( + "Active", + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontWeight: FontWeight.w600, + ), + align: TextAlign.left, + ), + ], + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.form, + label: "Form", + initialValue: edit.form.text, + multiLine: true, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.effect, + label: "Effect", + initialValue: edit.effect.text, + multiLine: true, + ), + ]); + } +} diff --git a/lib/views/dialogs/object/artifact/update.dart b/lib/views/dialogs/object/artifact/update.dart new file mode 100644 index 0000000..5bee1e4 --- /dev/null +++ b/lib/views/dialogs/object/artifact/update.dart @@ -0,0 +1,16 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/update.dart'; +import 'package:flutter/widgets.dart'; + +class UpdateArtifact extends StatelessWidget { + const UpdateArtifact(this.original, {super.key}); + + final Artifact original; + + @override + Widget build(BuildContext context) { + return UpdateDialog( + () => EditableArtifact.from(original)); + } +} diff --git a/lib/views/dialogs/view_artifact.dart b/lib/views/dialogs/object/artifact/view.dart similarity index 89% rename from lib/views/dialogs/view_artifact.dart rename to lib/views/dialogs/object/artifact/view.dart index 34985bd..56a2964 100644 --- a/lib/views/dialogs/view_artifact.dart +++ b/lib/views/dialogs/object/artifact/view.dart @@ -4,8 +4,11 @@ import 'package:cypher_sheet/components/icons.dart'; import 'package:cypher_sheet/components/markdown.dart'; import 'package:cypher_sheet/components/scroll.dart'; import 'package:cypher_sheet/components/label.dart'; +import 'package:cypher_sheet/extensions/artifact.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; import 'package:cypher_sheet/state/providers/cyphers.dart'; -import 'package:cypher_sheet/views/dialogs/create_artifact.dart'; +import 'package:cypher_sheet/views/dialogs/object/artifact/update.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/box.dart'; @@ -55,7 +58,7 @@ class ViewArtifact extends ConsumerWidget { onTap: () { showAppDialog( context, - CreateArtifact.fromState(artifact), + UpdateArtifact(artifact), fullscreen: true, ); }, @@ -114,6 +117,11 @@ class ViewArtifact extends ConsumerWidget { ]), ), const SizedBox(height: 16.0), + ObjectActionButtons( + getShareable: () => artifact.share().toFile(), + buildUpdateDialog: () => UpdateArtifact(artifact), + ), + const SizedBox(height: 16.0), AppBox( onTap: (() { closeDialog(context); diff --git a/lib/views/dialogs/object/base/create.dart b/lib/views/dialogs/object/base/create.dart new file mode 100644 index 0000000..a843e89 --- /dev/null +++ b/lib/views/dialogs/object/base/create.dart @@ -0,0 +1,31 @@ +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/edit.dart'; +import 'package:flutter/material.dart'; + +class BaseCreateView extends StatelessWidget { + const BaseCreateView({ + super.key, + this.action = "Create", + required this.type, + required this.inputs, + required this.onSubmit, + }); + + final String type; + final String action; + final Widget inputs; + final Function() onSubmit; + + @override + Widget build(BuildContext context) { + return BaseEditView( + action: action, + type: type, + inputs: inputs, + onSubmit: () { + onSubmit(); + closeDialog(context); + }, + ); + } +} diff --git a/lib/views/dialogs/object/base/edit.dart b/lib/views/dialogs/object/base/edit.dart new file mode 100644 index 0000000..d448563 --- /dev/null +++ b/lib/views/dialogs/object/base/edit.dart @@ -0,0 +1,63 @@ +import 'package:cypher_sheet/components/box.dart'; +import 'package:cypher_sheet/components/icon.dart'; +import 'package:cypher_sheet/components/icons.dart'; +import 'package:cypher_sheet/components/text.dart'; +import 'package:flutter/material.dart'; + +class BaseEditView extends StatelessWidget { + const BaseEditView({ + super.key, + required this.action, + required this.type, + required this.inputs, + this.onDelete, + required this.onSubmit, + }); + + final String type; + final String action; + final Widget inputs; + final Function()? onDelete; + final Function() onSubmit; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AppText( + "$action $type", + align: TextAlign.left, + ), + if (onDelete != null) + AppBox( + flat: true, + padding: 2, + onTap: onDelete, + child: const AppIcon( + AppIcons.deleteForever, + size: 24, + )), + ], + ), + ), + Expanded( + child: inputs, + ), + const SizedBox(height: 16.0), + AppBox( + onTap: onSubmit, + color: Theme.of(context).colorScheme.primary, + child: AppText(action), + ), + ], + ); + } +} diff --git a/lib/views/dialogs/object/base/import.dart b/lib/views/dialogs/object/base/import.dart new file mode 100644 index 0000000..bbd97a8 --- /dev/null +++ b/lib/views/dialogs/object/base/import.dart @@ -0,0 +1,49 @@ +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/edit.dart'; +import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; +import 'package:flutter/material.dart'; + +class BaseImportView extends StatelessWidget { + const BaseImportView({ + super.key, + this.action = "Import", + required this.type, + required this.inputs, + required this.onSubmit, + required this.onCancel, + required this.getName, + }); + + final String type; + final String action; + final Widget inputs; + final Function() onSubmit; + final Function() onCancel; + final String Function() getName; + + @override + Widget build(BuildContext context) { + return BaseEditView( + action: action, + type: type, + inputs: inputs, + onDelete: () { + showConfirmDialog( + context, + AppText( + "Abort\n${getName()}", + maxLines: 2, + style: Theme.of(context).textTheme.labelLarge, + ), () { + onCancel(); + closeDialog(context); + }); + }, + onSubmit: () { + onSubmit(); + closeDialog(context); + }, + ); + } +} diff --git a/lib/views/dialogs/object/base/update.dart b/lib/views/dialogs/object/base/update.dart new file mode 100644 index 0000000..3701728 --- /dev/null +++ b/lib/views/dialogs/object/base/update.dart @@ -0,0 +1,50 @@ +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/edit.dart'; +import 'package:cypher_sheet/views/dialogs/edit_character_meta.dart'; +import 'package:flutter/material.dart'; + +class BaseUpdateView extends StatelessWidget { + const BaseUpdateView({ + super.key, + this.action = "Update", + required this.type, + required this.inputs, + required this.onSubmit, + required this.onDelete, + required this.getName, + }); + + final String type; + final String action; + final Widget inputs; + final Function() onSubmit; + final Function() onDelete; + final String Function() getName; + + @override + Widget build(BuildContext context) { + return BaseEditView( + action: action, + type: type, + inputs: inputs, + onDelete: () { + showConfirmDialog( + context, + AppText( + "Permanently delete\n${getName()}", + maxLines: 2, + style: Theme.of(context).textTheme.labelLarge, + ), () { + onDelete(); + closeDialog(context); + closeDialog(context); + }); + }, + onSubmit: () { + onSubmit(); + closeDialog(context); + }, + ); + } +} diff --git a/lib/views/dialogs/object/base/view.dart b/lib/views/dialogs/object/base/view.dart new file mode 100644 index 0000000..a180321 --- /dev/null +++ b/lib/views/dialogs/object/base/view.dart @@ -0,0 +1,39 @@ +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/icon.dart'; +import 'package:cypher_sheet/components/icons.dart'; +import 'package:cypher_sheet/components/share.dart'; +import 'package:flutter/material.dart'; +import 'package:share_plus/share_plus.dart'; + +class ObjectActionButtons extends StatelessWidget { + const ObjectActionButtons({ + super.key, + required this.getShareable, + required this.buildUpdateDialog, + }); + + final Future Function()? getShareable; + final Widget Function() buildUpdateDialog; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (getShareable != null) ShareObjectButton(getShareable!), + const SizedBox(width: 16.0), + SVGBox( + padding: 12, + onTap: (() { + showAppDialog( + context, + buildUpdateDialog(), + fullscreen: true, + ); + }), + icon: AppIcons.edit, + ), + ], + ); + } +} diff --git a/lib/views/dialogs/object/cypher/create.dart b/lib/views/dialogs/object/cypher/create.dart new file mode 100644 index 0000000..884f694 --- /dev/null +++ b/lib/views/dialogs/object/cypher/create.dart @@ -0,0 +1,13 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/create.dart'; +import 'package:flutter/widgets.dart'; + +class CreateCypher extends StatelessWidget { + const CreateCypher({super.key}); + + @override + Widget build(BuildContext context) { + return CreateDialog(() => EditableCypher.empty()); + } +} diff --git a/lib/views/dialogs/object/cypher/editable.dart b/lib/views/dialogs/object/cypher/editable.dart new file mode 100644 index 0000000..b58189e --- /dev/null +++ b/lib/views/dialogs/object/cypher/editable.dart @@ -0,0 +1,59 @@ +// TODO: generate this code +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/character.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/inputs.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; + +class EditableCypher extends GenericEditable { + factory EditableCypher.empty() { + final original = Cypher.create(); + final edit = EditableCypher._(original); + edit.loadFields(original); + return edit; + } + + factory EditableCypher.from(Cypher original) { + final edit = EditableCypher._(original); + edit.loadFields(original); + return edit; + } + + EditableCypher._(Cypher original) + : super( + "Cypher", + createNewEmpty: () => Cypher.create(), + updateInState: (CharacterNotifier ref) => ref.updateCypher, + createInState: (CharacterNotifier ref) => ref.addCypher, + deleteInState: (CharacterNotifier ref) => ref.deleteCypher, + clearUuid: (Cypher obj) => obj.clearUuid(), + setUuid: (Cypher obj, String uuid) => obj.uuid = uuid, + originalUUID: original.uuid, + // This editable loads all fields at once as there are too many different kinds. + textFields: {}, + boolFields: {}, + doubleFields: {}, + intFields: {}, + ); + + @override + Widget inputs(Function(Function()) setState) { + return CypherEditInputs(this, setState); + } + + bool get active => boolFields["active"]!; + set active(bool to) => boolFields["active"] = to; + + TextEditingController get depletion => textFields["depletion"]!; + TextEditingController get effect => textFields["effect"]!; + TextEditingController get internal => textFields["internal"]!; + TextEditingController get level => textFields["level"]!; + TextEditingController get name => textFields["name"]!; + TextEditingController get shortDescription => textFields["shortDescription"]!; + TextEditingController get usable => textFields["usable"]!; + TextEditingController get uuid => textFields["uuid"]!; + TextEditingController get wearable => textFields["wearable"]!; + + @override + String get objectName => name.value.text; +} diff --git a/lib/views/dialogs/object/cypher/import.dart b/lib/views/dialogs/object/cypher/import.dart new file mode 100644 index 0000000..bf45cff --- /dev/null +++ b/lib/views/dialogs/object/cypher/import.dart @@ -0,0 +1,26 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/import.dart'; +import 'package:flutter/widgets.dart'; + +class ImportCypher extends StatelessWidget { + const ImportCypher( + this.original, { + super.key, + this.onCancel, + this.onSuccess, + }); + + final Cypher original; + final Function()? onCancel; + final Function()? onSuccess; + + @override + Widget build(BuildContext context) { + return ImportDialog( + () => EditableCypher.from(original), + onCancel: onCancel, + onSuccess: onSuccess, + ); + } +} diff --git a/lib/views/dialogs/object/cypher/inputs.dart b/lib/views/dialogs/object/cypher/inputs.dart new file mode 100644 index 0000000..db8603c --- /dev/null +++ b/lib/views/dialogs/object/cypher/inputs.dart @@ -0,0 +1,108 @@ +import 'package:cypher_sheet/components/checkbox.dart'; +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/editable.dart'; +import 'package:flutter/material.dart'; +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; + +class CypherEditInputs extends StatelessWidget { + const CypherEditInputs(this.edit, this.setState, {super.key}); + + final EditableCypher edit; + final Function(Function()) setState; + + @override + Widget build(BuildContext context) { + return AppScrollView(customPadding: EdgeInsets.zero, slivers: [ + DialogTextBox( + controller: edit.name, + label: "Name", + initialValue: edit.name.value.text, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.shortDescription, + label: "Short Description", + initialValue: edit.shortDescription.text, + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + fit: FlexFit.loose, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Container( + constraints: const BoxConstraints.tightFor(width: 64), + child: DialogTextBox( + controller: edit.level, + label: "Level", + initialValue: edit.level.value.text, + ), + ), + ), + ), + Expanded( + child: DialogTextBox( + controller: edit.depletion, + label: "Depletion", + initialValue: edit.depletion.value.text, + ), + ), + ], + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AppCheckbox( + active: edit.active, + onTap: () { + setState(() { + edit.active = !edit.active; + }); + }, + ), + const SizedBox(width: 16.0), + AppText( + "Active", + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontWeight: FontWeight.w600, + ), + align: TextAlign.left, + ), + ], + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.internal, + label: "Internal", + initialValue: edit.internal.text, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.wearable, + label: "Wearable", + initialValue: edit.wearable.text, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.usable, + label: "Usable", + initialValue: edit.usable.text, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.effect, + label: "Effect", + initialValue: edit.effect.text, + multiLine: true, + ), + ]); + } +} diff --git a/lib/views/dialogs/object/cypher/update.dart b/lib/views/dialogs/object/cypher/update.dart new file mode 100644 index 0000000..7065b28 --- /dev/null +++ b/lib/views/dialogs/object/cypher/update.dart @@ -0,0 +1,15 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/update.dart'; +import 'package:flutter/widgets.dart'; + +class UpdateCypher extends StatelessWidget { + const UpdateCypher(this.original, {super.key}); + + final Cypher original; + + @override + Widget build(BuildContext context) { + return UpdateDialog(() => EditableCypher.from(original)); + } +} diff --git a/lib/views/dialogs/view_cypher.dart b/lib/views/dialogs/object/cypher/view.dart similarity index 91% rename from lib/views/dialogs/view_cypher.dart rename to lib/views/dialogs/object/cypher/view.dart index 965156e..0515cf3 100644 --- a/lib/views/dialogs/view_cypher.dart +++ b/lib/views/dialogs/object/cypher/view.dart @@ -4,8 +4,11 @@ import 'package:cypher_sheet/components/icons.dart'; import 'package:cypher_sheet/components/markdown.dart'; import 'package:cypher_sheet/components/scroll.dart'; import 'package:cypher_sheet/components/label.dart'; +import 'package:cypher_sheet/extensions/cypher.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; import 'package:cypher_sheet/state/providers/cyphers.dart'; -import 'package:cypher_sheet/views/dialogs/create_cypher.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/view.dart'; +import 'package:cypher_sheet/views/dialogs/object/cypher/update.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/box.dart'; @@ -55,7 +58,7 @@ class ViewCypher extends ConsumerWidget { onTap: () { showAppDialog( context, - CreateCypher.fromState(cypher), + UpdateCypher(cypher), fullscreen: true, ); }, @@ -142,6 +145,11 @@ class ViewCypher extends ConsumerWidget { ]), ), const SizedBox(height: 16.0), + ObjectActionButtons( + getShareable: () => cypher.share().toFile(), + buildUpdateDialog: () => UpdateCypher(cypher), + ), + const SizedBox(height: 16.0), AppBox( onTap: (() { closeDialog(context); diff --git a/lib/views/dialogs/object/generic/create.dart b/lib/views/dialogs/object/generic/create.dart new file mode 100644 index 0000000..97a8ab5 --- /dev/null +++ b/lib/views/dialogs/object/generic/create.dart @@ -0,0 +1,25 @@ +import 'package:cypher_sheet/views/dialogs/object/base/create.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/dialog.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; + +class CreateDialog extends StatelessWidget { + const CreateDialog(this.editableCreator, {super.key}); + + final Editable Function() editableCreator; + + @override + Widget build(BuildContext context) { + return GenericEditDialog( + (edit, setState, ref) => BaseCreateView( + action: "Create", + type: edit.typeName, + inputs: edit.inputs(setState), + onSubmit: () { + edit.create(ref); + }, + ), + editableCreator, + ); + } +} diff --git a/lib/views/dialogs/object/generic/dialog.dart b/lib/views/dialogs/object/generic/dialog.dart new file mode 100644 index 0000000..262ddab --- /dev/null +++ b/lib/views/dialogs/object/generic/dialog.dart @@ -0,0 +1,39 @@ +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class GenericEditDialog extends ConsumerStatefulWidget { + const GenericEditDialog(this.viewBuilder, this.editableCreator, {super.key}); + + final Widget Function(Editable edit, + void Function(void Function()) setState, WidgetRef ref) viewBuilder; + final Editable Function() editableCreator; + + @override + ConsumerState createState() => + _GenericEditDialogState(); +} + +class _GenericEditDialogState + extends ConsumerState> { + late Editable edit; + + @override + void initState() { + super.initState(); + + edit = widget.editableCreator(); + } + + @override + void dispose() { + edit.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.viewBuilder(edit, setState, ref); + } +} diff --git a/lib/views/dialogs/object/generic/editable.dart b/lib/views/dialogs/object/generic/editable.dart new file mode 100644 index 0000000..a516711 --- /dev/null +++ b/lib/views/dialogs/object/generic/editable.dart @@ -0,0 +1,139 @@ +import 'package:cypher_sheet/extensions/editable.dart'; +import 'package:cypher_sheet/state/character.dart'; +import 'package:cypher_sheet/state/providers/character.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:protobuf/protobuf.dart'; + +abstract class Editable { + Widget inputs(Function(Function()) setState); + String get typeName; + String get objectName; + void create(WidgetRef ref); + void update(WidgetRef ref); + void delete(WidgetRef ref); + void import(WidgetRef ref); + void dispose(); +} + +// GenericEditable provides automatic handling of certain proto fields by +// converting them between an editable type and their proto representation. +// This way we only need to specify the field names/tags and map them to +// variables in dart. The process of creating a new object after editing is +// taken care of. +// Some field types can't be supported so the implementer can simply manage them +// manually and override setOtherFields to make sure they are set on the new +// object. +abstract class GenericEditable + implements Editable { + GenericEditable( + String typeName, { + required this.originalUUID, + required this.createNewEmpty, + required this.updateInState, + required this.createInState, + required this.deleteInState, + required this.clearUuid, + required this.setUuid, + required this.textFields, + required this.boolFields, + this.doubleFields = const {}, + this.intFields = const {}, + }) : _typeName = typeName; + + final String _typeName; + // createNewEmpty must create a GeneratedMessage that is not frozen. + // So one should use .create() not .getDefault(). + final T Function() createNewEmpty; + + final void Function(T obj) Function(CharacterNotifier ref) updateInState; + final void Function(T obj) Function(CharacterNotifier ref) createInState; + final void Function(String uuid) Function(CharacterNotifier ref) + deleteInState; + + final void Function(T obj) clearUuid; + final void Function(T obj, String uuid) setUuid; + + final String originalUUID; + + Map textFields; + Map doubleFields; + Map intFields; + Map boolFields; + + @override + void create(WidgetRef ref) { + createInState(ref.read(characterProvider.notifier))(finalize()); + } + + @override + void delete(WidgetRef ref) { + if (originalUUID.isEmpty) { + assert(originalUUID.isNotEmpty); + return; + } + deleteInState(ref.read(characterProvider.notifier))(originalUUID); + } + + @override + void dispose() { + if (!_isFinalized) { + textFields.forEach((key, value) { + value.dispose(); + }); + } + } + + @override + void import(WidgetRef ref) { + final obj = finalize(); + clearUuid(obj); + createInState(ref.read(characterProvider.notifier))(obj); + } + + // loadFields can be used instead of the constructor fields to load all + // currently supported fields into the Editable. + void loadFields(T from) { + mergeEditableFieldsFrom( + from, + strings: textFields, + bools: boolFields, + doubles: doubleFields, + ints: intFields, + ); + } + + @override + get typeName => _typeName; + + @override + void update(WidgetRef ref) { + final obj = finalize(); + if (originalUUID.isNotEmpty) { + setUuid(obj, originalUUID); + } + updateInState(ref.read(characterProvider.notifier))(obj); + } + + bool _isFinalized = false; + + // finalize turns the editable object back into a artifact. + // Must only be called once as it disposes any TextEditingControllers. + T finalize() { + assert(!_isFinalized); + _isFinalized = true; + + final obj = createNewEmpty(); + + textFields.forEach(textFieldsUpdater(obj)); + boolFields.forEach(boolFieldsUpdater(obj)); + doubleFields.forEach(doubleFieldsUpdater(obj)); + intFields.forEach(intFieldsUpdater(obj)); + + setOtherFields(obj); + + return obj; + } + + void setOtherFields(T obj) {} +} diff --git a/lib/views/dialogs/object/generic/import.dart b/lib/views/dialogs/object/generic/import.dart new file mode 100644 index 0000000..f1ac241 --- /dev/null +++ b/lib/views/dialogs/object/generic/import.dart @@ -0,0 +1,35 @@ +import 'package:cypher_sheet/views/dialogs/object/base/import.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/dialog.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; + +class ImportDialog extends StatelessWidget { + const ImportDialog( + this.editableCreator, { + super.key, + this.onCancel, + this.onSuccess, + }); + + final Editable Function() editableCreator; + final Function()? onCancel; + final Function()? onSuccess; + + @override + Widget build(BuildContext context) { + return GenericEditDialog( + (edit, setState, ref) => BaseImportView( + action: "Import", + type: edit.typeName, + inputs: edit.inputs(setState), + onSubmit: () { + edit.import(ref); + if (onSuccess != null) onSuccess!(); + }, + onCancel: onCancel ?? () {}, + getName: () => edit.objectName, + ), + editableCreator, + ); + } +} diff --git a/lib/views/dialogs/object/generic/update.dart b/lib/views/dialogs/object/generic/update.dart new file mode 100644 index 0000000..2f8a7d6 --- /dev/null +++ b/lib/views/dialogs/object/generic/update.dart @@ -0,0 +1,29 @@ +import 'package:cypher_sheet/views/dialogs/object/base/update.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/dialog.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; + +class UpdateDialog extends StatelessWidget { + const UpdateDialog(this.editableCreator, {super.key}); + + final Editable Function() editableCreator; + + @override + Widget build(BuildContext context) { + return GenericEditDialog( + (Editable edit, setState, ref) => BaseUpdateView( + action: "Update", + type: edit.typeName, + inputs: edit.inputs(setState), + onSubmit: () { + edit.update(ref); + }, + onDelete: () { + edit.delete(ref); + }, + getName: () => edit.objectName, + ), + editableCreator, + ); + } +} diff --git a/lib/views/dialogs/object/item/create.dart b/lib/views/dialogs/object/item/create.dart new file mode 100644 index 0000000..d7eae5e --- /dev/null +++ b/lib/views/dialogs/object/item/create.dart @@ -0,0 +1,18 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/create.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/editable.dart'; +import 'package:flutter/widgets.dart'; + +class CreateItem extends StatelessWidget { + const CreateItem({ + super.key, + required this.path, + }); + + final ItemPath path; + + @override + Widget build(BuildContext context) { + return CreateDialog(() => EditableItem.empty(path)); + } +} diff --git a/lib/views/dialogs/object/item/editable.dart b/lib/views/dialogs/object/item/editable.dart new file mode 100644 index 0000000..ee195c7 --- /dev/null +++ b/lib/views/dialogs/object/item/editable.dart @@ -0,0 +1,104 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/character.dart'; +import 'package:cypher_sheet/state/filters/item_filter.dart'; +import 'package:cypher_sheet/state/providers/character.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/inputs.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class EditableItem extends GenericEditable { + factory EditableItem.empty(ItemPath path) { + final original = Item.create(); + final edit = EditableItem._(path, original); + edit.loadFields(original); + return edit; + } + + factory EditableItem.from(Item original) { + final edit = EditableItem._(original.path, original); + edit.loadFields(original); + return edit; + } + + // We keep track of the path we started at to know if the item was moved. + final ItemPath originalPath; + final ItemPath path; + + EditableItem._(ItemPath path, Item original) + : path = ItemPath( + inventory: path.inventory, + parent: path.parent, + self: path.self, + ), + originalPath = path, + types = ItemFilter(activeTypes: original.types), + subItemType = original.hasSubItemType() ? original.subItemType : null, + super( + "Item", + createNewEmpty: () => Item.create(), + updateInState: (CharacterNotifier ref) => ref.updateItem, + createInState: (CharacterNotifier ref) => ref.addItem, + deleteInState: (CharacterNotifier ref) => ref.deleteItem, + clearUuid: (Item obj) => obj.path.clearSelf(), + setUuid: (Item obj, String uuid) => obj.path.self = uuid, + textFields: {}, + boolFields: {}, + doubleFields: {}, + intFields: {}, + originalUUID: path.self, + ); + + @override + Widget inputs(Function(Function()) setState) { + return ItemEditInputs(this, setState); + } + + TextEditingController get name => textFields["name"]!; + TextEditingController get shortDescription => textFields["shortDescription"]!; + TextEditingController get description => textFields["description"]!; + TextEditingController get amount => doubleFields["amount"]!; + TextEditingController get value => doubleFields["value"]!; + TextEditingController get armor => intFields["armor"]!; + + ItemFilter types; + ItemType? subItemType; + + @override + void setOtherFields(Item obj) { + obj.types.clear(); + obj.types.addAll( + types.activeTypes.isNotEmpty ? types.activeTypes : [ItemType.others]); + + if (subItemType != null) { + obj.subItemType = subItemType!; + } else { + obj.clearSubItemType(); + } + + obj.path = path; + + if (!obj.hasAmount()) { + obj.amount = 1; + } + } + + // Override the update handler to make sure inventory items are moved correctly. + @override + void update(WidgetRef ref) { + final obj = finalize(); + if (originalUUID.isNotEmpty) { + setUuid(obj, originalUUID); + } + final hasMoved = + path.inventory.isNotEmpty && path.inventory != originalPath.inventory; + if (hasMoved) { + ref.read(characterProvider.notifier).moveItem(obj); + } else { + updateInState(ref.read(characterProvider.notifier))(obj); + } + } + + @override + String get objectName => name.value.text; +} diff --git a/lib/views/dialogs/object/item/import.dart b/lib/views/dialogs/object/item/import.dart new file mode 100644 index 0000000..e341f33 --- /dev/null +++ b/lib/views/dialogs/object/item/import.dart @@ -0,0 +1,26 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/import.dart'; +import 'package:flutter/widgets.dart'; + +class ImportItem extends StatelessWidget { + const ImportItem( + this.original, { + super.key, + this.onCancel, + this.onSuccess, + }); + + final Item original; + final Function()? onCancel; + final Function()? onSuccess; + + @override + Widget build(BuildContext context) { + return ImportDialog( + () => EditableItem.from(original), + onCancel: onCancel, + onSuccess: onSuccess, + ); + } +} diff --git a/lib/views/dialogs/object/item/inputs.dart b/lib/views/dialogs/object/item/inputs.dart new file mode 100644 index 0000000..2ebd14d --- /dev/null +++ b/lib/views/dialogs/object/item/inputs.dart @@ -0,0 +1,171 @@ +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/icon.dart'; +import 'package:cypher_sheet/components/icons.dart'; +import 'package:cypher_sheet/components/label.dart'; +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/extensions/item.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/providers/inventories.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/editable.dart'; +import 'package:flutter/material.dart'; + +class ItemEditInputs extends StatelessWidget { + const ItemEditInputs(this.edit, this.setState, {super.key}); + + final EditableItem edit; + final Function(Function()) setState; + + // TODO: figure out how to do the custom header for inventory selection etc. + + @override + Widget build(BuildContext context) { + return AppScrollView(customPadding: EdgeInsets.zero, slivers: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: SVGBox( + icon: AppIcons.self, + active: edit.path.inventory == inventoryNameSelf, + onTap: () { + setState(() { + edit.path.inventory = inventoryNameSelf; + }); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: SVGBox( + icon: AppIcons.backpack, + active: edit.path.inventory == inventoryNameBackpack, + onTap: () { + setState(() { + edit.path.inventory = inventoryNameBackpack; + }); + }, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: SVGBox( + icon: AppIcons.home, + active: edit.path.inventory == inventoryNameHome, + onTap: () { + setState(() { + edit.path.inventory = inventoryNameHome; + }); + }, + ), + ), + ], + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.name, + label: "Name", + initialValue: edit.name.value.text, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.shortDescription, + label: "Short Description", + initialValue: edit.shortDescription.text, + ), + const SizedBox(height: 16.0), + Wrap( + runSpacing: 8.0, + spacing: 8.0, + children: ItemType.values + .map((e) => SVGBoxLabeled( + icon: e.toIcon(), + label: e.toLabel(), + active: edit.types.isTypeActive(e), + iconSize: 26, + customLabelStyle: Theme.of(context).textTheme.labelLarge, + onTap: () { + setState(() { + edit.types = edit.types.toggleFilter(e); + }); + }, + )) + .toList(growable: false), + ), + const SizedBox(height: 16.0), + if (edit.types.isTypeActive(ItemType.armor)) + DialogTextBox( + controller: edit.armor, + label: "Armor Bonus", + initialValue: edit.armor.text, + ), + if (edit.types.isTypeActive(ItemType.armor)) const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: DialogTextBox( + controller: edit.amount, + label: "Amount / Count", + initialValue: edit.amount.text, + ), + ), + ), + Expanded( + child: DialogTextBox( + controller: edit.value, + label: "Value", + initialValue: edit.value.text, + ), + ), + ], + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.description, + label: "Description", + initialValue: edit.description.text, + multiLine: true, + ), + const SizedBox(height: 16.0), + const AppLabel(text: "Sub Item Type"), + Wrap( + runSpacing: 8.0, + spacing: 8.0, + children: [ + SVGBoxLabeled( + icon: AppIcons.none, + label: "None", + iconSize: 26, + customLabelStyle: Theme.of(context).textTheme.labelLarge, + onTap: () { + setState(() { + edit.subItemType = null; + }); + }, + active: edit.subItemType == null, + ), + ...ItemType.values + .map((e) => SVGBoxLabeled( + icon: e.toIcon(), + label: e.toLabel(), + active: edit.subItemType == e, + iconSize: 26, + customLabelStyle: Theme.of(context).textTheme.labelLarge, + onTap: () { + setState(() { + edit.subItemType = e; + }); + }, + )) + .toList(growable: false) + ], + ), + ]); + } +} diff --git a/lib/views/dialogs/object/item/update.dart b/lib/views/dialogs/object/item/update.dart new file mode 100644 index 0000000..2d31198 --- /dev/null +++ b/lib/views/dialogs/object/item/update.dart @@ -0,0 +1,15 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/update.dart'; +import 'package:flutter/widgets.dart'; + +class UpdateItem extends StatelessWidget { + const UpdateItem(this.original, {super.key}); + + final Item original; + + @override + Widget build(BuildContext context) { + return UpdateDialog(() => EditableItem.from(original)); + } +} diff --git a/lib/views/dialogs/view_item.dart b/lib/views/dialogs/object/item/view.dart similarity index 81% rename from lib/views/dialogs/view_item.dart rename to lib/views/dialogs/object/item/view.dart index 86ccfc7..81ea0f8 100644 --- a/lib/views/dialogs/view_item.dart +++ b/lib/views/dialogs/object/item/view.dart @@ -3,8 +3,11 @@ import 'package:cypher_sheet/components/icons.dart'; import 'package:cypher_sheet/components/markdown.dart'; import 'package:cypher_sheet/components/scroll.dart'; import 'package:cypher_sheet/components/label.dart'; +import 'package:cypher_sheet/extensions/item.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; import 'package:cypher_sheet/state/providers/items.dart'; -import 'package:cypher_sheet/views/dialogs/create_item.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/view.dart'; +import 'package:cypher_sheet/views/dialogs/object/item/update.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/box.dart'; @@ -42,7 +45,7 @@ class ViewItem extends ConsumerWidget { onTap: () { showAppDialog( context, - CreateItem.fromState(item), + UpdateItem(item), fullscreen: true, ); }, @@ -73,6 +76,14 @@ class ViewItem extends ConsumerWidget { ]), ), const SizedBox(height: 16.0), + ObjectActionButtons( + // Don't allow sharing of subItems for now until we are able to import + // them. + getShareable: + item.path.parent.isEmpty ? () => item.share().toFile() : null, + buildUpdateDialog: () => UpdateItem(item), + ), + const SizedBox(height: 16.0), AppBox( onTap: (() { closeDialog(context); diff --git a/lib/views/dialogs/object/note/create.dart b/lib/views/dialogs/object/note/create.dart new file mode 100644 index 0000000..cea8d3f --- /dev/null +++ b/lib/views/dialogs/object/note/create.dart @@ -0,0 +1,13 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/create.dart'; +import 'package:flutter/widgets.dart'; + +class CreateNote extends StatelessWidget { + const CreateNote({super.key}); + + @override + Widget build(BuildContext context) { + return CreateDialog(() => EditableNote.empty()); + } +} diff --git a/lib/views/dialogs/object/note/editable.dart b/lib/views/dialogs/object/note/editable.dart new file mode 100644 index 0000000..ab0dea2 --- /dev/null +++ b/lib/views/dialogs/object/note/editable.dart @@ -0,0 +1,51 @@ +// TODO: generate this code +import 'package:cypher_sheet/extensions/editable.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/character.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/inputs.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; + +class EditableNote extends GenericEditable { + factory EditableNote.empty() { + return EditableNote._(Note.create()); + } + + factory EditableNote.from(Note original) { + return EditableNote._(original); + } + + EditableNote._(Note original) + : type = original.hasType() ? original.type : NoteType.misc, + super( + "Note", + createNewEmpty: () => Note.create(), + updateInState: (CharacterNotifier ref) => ref.updateNote, + createInState: (CharacterNotifier ref) => ref.addNote, + deleteInState: (CharacterNotifier ref) => ref.deleteNote, + clearUuid: (Note obj) => obj.clearUuid(), + setUuid: (Note obj, String uuid) => obj.uuid = uuid, + originalUUID: original.uuid, + textFields: getEditableTextFieldsFrom(original), + boolFields: {}, + ); + + @override + Widget inputs(Function(Function()) setState) { + return NoteEditInputs(this, setState); + } + + TextEditingController get title => textFields["title"]!; + TextEditingController get shortDescription => textFields["shortDescription"]!; + TextEditingController get text => textFields["text"]!; + + NoteType type; + + @override + void setOtherFields(Note obj) { + obj.type = type; + } + + @override + String get objectName => title.value.text; +} diff --git a/lib/views/dialogs/object/note/import.dart b/lib/views/dialogs/object/note/import.dart new file mode 100644 index 0000000..f75b4a3 --- /dev/null +++ b/lib/views/dialogs/object/note/import.dart @@ -0,0 +1,26 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/import.dart'; +import 'package:flutter/widgets.dart'; + +class ImportNote extends StatelessWidget { + const ImportNote( + this.original, { + super.key, + this.onCancel, + this.onSuccess, + }); + + final Note original; + final Function()? onCancel; + final Function()? onSuccess; + + @override + Widget build(BuildContext context) { + return ImportDialog( + () => EditableNote.from(original), + onCancel: onCancel, + onSuccess: onSuccess, + ); + } +} diff --git a/lib/views/dialogs/object/note/inputs.dart b/lib/views/dialogs/object/note/inputs.dart new file mode 100644 index 0000000..f058b08 --- /dev/null +++ b/lib/views/dialogs/object/note/inputs.dart @@ -0,0 +1,82 @@ +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/components/selector.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/editable.dart'; +import 'package:flutter/material.dart'; +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; + +class NoteEditInputs extends StatelessWidget { + const NoteEditInputs(this.edit, this.setState, {super.key}); + + final EditableNote edit; + final Function(Function()) setState; + + @override + Widget build(BuildContext context) { + return AppScrollView(customPadding: EdgeInsets.zero, slivers: [ + DialogTextBox( + controller: edit.title, + label: "Title", + initialValue: edit.title.value.text, + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.shortDescription, + label: "Short Description", + initialValue: edit.shortDescription.value.text, + ), + const SizedBox(height: 16.0), + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: AppText( + "Type", + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontWeight: FontWeight.w600, + ), + align: TextAlign.left, + ), + ), + noteTypeSelectors(), + ], + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.text, + label: "Text", + initialValue: edit.text.text, + multiLine: true, + ), + ]); + } + + Widget noteTypeSelectors() { + return Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + ...NoteType.values.map( + (selectorType) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: NoteTypeSelector( + activeType: edit.type, + type: selectorType, + onSelect: (newType) { + setState(() { + edit.type = newType; + }); + }, + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/views/dialogs/object/note/update.dart b/lib/views/dialogs/object/note/update.dart new file mode 100644 index 0000000..ef8d8df --- /dev/null +++ b/lib/views/dialogs/object/note/update.dart @@ -0,0 +1,15 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/update.dart'; +import 'package:flutter/widgets.dart'; + +class UpdateNote extends StatelessWidget { + const UpdateNote(this.original, {super.key}); + + final Note original; + + @override + Widget build(BuildContext context) { + return UpdateDialog(() => EditableNote.from(original)); + } +} diff --git a/lib/views/dialogs/view_note.dart b/lib/views/dialogs/object/note/view.dart similarity index 87% rename from lib/views/dialogs/view_note.dart rename to lib/views/dialogs/object/note/view.dart index 106bc75..2743126 100644 --- a/lib/views/dialogs/view_note.dart +++ b/lib/views/dialogs/object/note/view.dart @@ -4,8 +4,10 @@ import 'package:cypher_sheet/components/markdown.dart'; import 'package:cypher_sheet/components/scroll.dart'; import 'package:cypher_sheet/components/label.dart'; import 'package:cypher_sheet/extensions/note.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; import 'package:cypher_sheet/state/providers/notes.dart'; -import 'package:cypher_sheet/views/dialogs/create_note.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/view.dart'; +import 'package:cypher_sheet/views/dialogs/object/note/update.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/box.dart'; @@ -43,7 +45,7 @@ class ViewNote extends ConsumerWidget { onTap: () { showAppDialog( context, - CreateNote.fromState(note), + UpdateNote(note), fullscreen: true, ); }, @@ -80,6 +82,11 @@ class ViewNote extends ConsumerWidget { ]), ), const SizedBox(height: 16.0), + ObjectActionButtons( + getShareable: () => note.share().toFile(), + buildUpdateDialog: () => UpdateNote(note), + ), + const SizedBox(height: 16.0), AppBox( onTap: (() { closeDialog(context); diff --git a/lib/views/dialogs/object/skill/create.dart b/lib/views/dialogs/object/skill/create.dart new file mode 100644 index 0000000..476862e --- /dev/null +++ b/lib/views/dialogs/object/skill/create.dart @@ -0,0 +1,13 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/create.dart'; +import 'package:flutter/widgets.dart'; + +class CreateSkill extends StatelessWidget { + const CreateSkill({super.key}); + + @override + Widget build(BuildContext context) { + return CreateDialog(() => EditableSkill.empty()); + } +} diff --git a/lib/views/dialogs/object/skill/editable.dart b/lib/views/dialogs/object/skill/editable.dart new file mode 100644 index 0000000..a88607f --- /dev/null +++ b/lib/views/dialogs/object/skill/editable.dart @@ -0,0 +1,53 @@ +// TODO: generate this code +import 'package:cypher_sheet/extensions/editable.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/character.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/inputs.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/editable.dart'; +import 'package:flutter/widgets.dart'; + +class EditableSkill extends GenericEditable { + factory EditableSkill.empty() { + return EditableSkill._(Skill.create()); + } + + factory EditableSkill.from(Skill original) { + return EditableSkill._(original); + } + + EditableSkill._(Skill original) + : type = original.hasType() ? original.type : PoolType.intellect, + level = original.hasLevel() ? original.level : SkillLevel.trained, + super( + "Skill", + createNewEmpty: () => Skill.create(), + updateInState: (CharacterNotifier ref) => ref.updateSkill, + createInState: (CharacterNotifier ref) => ref.addSkill, + deleteInState: (CharacterNotifier ref) => ref.deleteSkill, + clearUuid: (Skill obj) => obj.clearUuid(), + setUuid: (Skill obj, String uuid) => obj.uuid = uuid, + originalUUID: original.uuid, + textFields: getEditableTextFieldsFrom(original), + boolFields: {}, + ); + + @override + Widget inputs(Function(Function()) setState) { + return SkillEditInputs(this, setState); + } + + TextEditingController get name => textFields["name"]!; + TextEditingController get description => textFields["description"]!; + + PoolType type; + SkillLevel level; + + @override + void setOtherFields(Skill obj) { + obj.type = type; + obj.level = level; + } + + @override + String get objectName => name.value.text; +} diff --git a/lib/views/dialogs/object/skill/import.dart b/lib/views/dialogs/object/skill/import.dart new file mode 100644 index 0000000..45c1fe0 --- /dev/null +++ b/lib/views/dialogs/object/skill/import.dart @@ -0,0 +1,26 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/import.dart'; +import 'package:flutter/widgets.dart'; + +class ImportSkill extends StatelessWidget { + const ImportSkill( + this.original, { + super.key, + this.onCancel, + this.onSuccess, + }); + + final Skill original; + final Function()? onCancel; + final Function()? onSuccess; + + @override + Widget build(BuildContext context) { + return ImportDialog( + () => EditableSkill.from(original), + onCancel: onCancel, + onSuccess: onSuccess, + ); + } +} diff --git a/lib/views/dialogs/object/skill/inputs.dart b/lib/views/dialogs/object/skill/inputs.dart new file mode 100644 index 0000000..98b0033 --- /dev/null +++ b/lib/views/dialogs/object/skill/inputs.dart @@ -0,0 +1,128 @@ +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/components/selector.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/editable.dart'; +import 'package:flutter/material.dart'; +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; + +class SkillEditInputs extends StatelessWidget { + const SkillEditInputs(this.edit, this.setState, {super.key}); + + final EditableSkill edit; + final Function(Function()) setState; + + @override + Widget build(BuildContext context) { + return AppScrollView(customPadding: EdgeInsets.zero, slivers: [ + DialogTextBox( + controller: edit.name, + label: "Name", + initialValue: edit.name.value.text, + ), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: AppText( + "Type", + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontWeight: FontWeight.w600, + ), + align: TextAlign.left, + ), + ), + poolTypeSelectors(), + ], + ), + const SizedBox(height: 16.0), + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: AppText( + "Level", + style: Theme.of(context).textTheme.labelLarge!.copyWith( + fontWeight: FontWeight.w600, + ), + align: TextAlign.left, + ), + ), + skillLevelSelectors(), + ], + ), + ], + ), + const SizedBox(height: 16.0), + DialogTextBox( + controller: edit.description, + label: "Description", + initialValue: edit.description.text, + multiLine: true, + ), + ]); + } + + Widget poolTypeSelectors() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...PoolType.values.map( + (selectorType) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: PoolTypeSelector( + activeType: edit.type, + type: selectorType, + onSelect: (newType) { + setState(() { + edit.type = newType; + }); + }, + ), + ); + }, + ), + ], + ); + } + + Widget skillLevelSelectors() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...SkillLevel.values.map( + (selectorType) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: SkillLevelSelector( + activeLevel: edit.level, + type: selectorType, + onSelect: (newLevel) { + setState(() { + edit.level = newLevel; + }); + }, + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/views/dialogs/object/skill/update.dart b/lib/views/dialogs/object/skill/update.dart new file mode 100644 index 0000000..d99c302 --- /dev/null +++ b/lib/views/dialogs/object/skill/update.dart @@ -0,0 +1,15 @@ +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/editable.dart'; +import 'package:cypher_sheet/views/dialogs/object/generic/update.dart'; +import 'package:flutter/widgets.dart'; + +class UpdateSkill extends StatelessWidget { + const UpdateSkill(this.original, {super.key}); + + final Skill original; + + @override + Widget build(BuildContext context) { + return UpdateDialog(() => EditableSkill.from(original)); + } +} diff --git a/lib/views/dialogs/view_skill.dart b/lib/views/dialogs/object/skill/view.dart similarity index 87% rename from lib/views/dialogs/view_skill.dart rename to lib/views/dialogs/object/skill/view.dart index 4d7f888..2ccb8c0 100644 --- a/lib/views/dialogs/view_skill.dart +++ b/lib/views/dialogs/object/skill/view.dart @@ -4,9 +4,11 @@ import 'package:cypher_sheet/components/markdown.dart'; import 'package:cypher_sheet/components/scroll.dart'; import 'package:cypher_sheet/components/label.dart'; import 'package:cypher_sheet/extensions/pool.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; import 'package:cypher_sheet/extensions/skill.dart'; import 'package:cypher_sheet/state/providers/skills.dart'; -import 'package:cypher_sheet/views/dialogs/create_skill.dart'; +import 'package:cypher_sheet/views/dialogs/object/base/view.dart'; +import 'package:cypher_sheet/views/dialogs/object/skill/update.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cypher_sheet/components/box.dart'; @@ -44,7 +46,7 @@ class ViewSkill extends ConsumerWidget { onTap: () { showAppDialog( context, - CreateSkill.fromState(skill), + UpdateSkill(skill), fullscreen: true, ); }, @@ -79,6 +81,11 @@ class ViewSkill extends ConsumerWidget { ]), ), const SizedBox(height: 16.0), + ObjectActionButtons( + getShareable: () => skill.share().toFile(), + buildUpdateDialog: () => UpdateSkill(skill), + ), + const SizedBox(height: 16.0), AppBox( onTap: (() { closeDialog(context); diff --git a/lib/views/dialogs/share.dart b/lib/views/dialogs/share.dart index 040b8b0..1d6f7cf 100644 --- a/lib/views/dialogs/share.dart +++ b/lib/views/dialogs/share.dart @@ -1,22 +1,16 @@ -import 'dart:developer'; -import 'dart:io'; - import 'package:cypher_sheet/components/box.dart'; -import 'package:cypher_sheet/components/dialog.dart'; import 'package:cypher_sheet/components/text.dart'; +import 'package:cypher_sheet/extensions/shared_object.dart'; import 'package:cypher_sheet/proto/character.pb.dart'; -import 'package:cypher_sheet/state/providers/character.dart'; -import 'package:cypher_sheet/state/storage/api.dart'; -import 'package:cypher_sheet/state/storage/file.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:share_plus/share_plus.dart'; -class ShareCharacter extends ConsumerWidget { - const ShareCharacter({super.key, required this.uuid}); +class ShareObject extends ConsumerWidget { + const ShareObject({super.key, required this.obj, this.deleteAfterwards}); - final String uuid; + final SharedObject obj; + final Function()? deleteAfterwards; @override Widget build(BuildContext context, WidgetRef ref) { @@ -25,84 +19,22 @@ class ShareCharacter extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AppText( - "Share Character", + "Share ${obj.printableType()}", style: Theme.of(context).textTheme.bodyLarge, align: TextAlign.left, ), const SizedBox(height: 28.0), AppBox( onTap: () async { - final revision = await readLatestCharacterRevision(uuid); - final revisionRaw = revision.writeToBuffer(); - Share.shareXFiles([XFile.fromData(revisionRaw)]); - }, - child: const AppText( - "Share Latest Revision", - maxLines: 2, - ), - ), - const SizedBox(height: 28.0), - AppBox( - onTap: () async { - final revision = await readLatestCharacterRevision(uuid); - final revisionRaw = revision.writeToBuffer(); - log(revisionRaw.toString()); - log(revision.toDebugString()); + Share.shareXFiles([ + await obj.toFile(), + ]); }, - child: const AppText( - "Log Latest Revision", + child: AppText( + "Share ${obj.printableType()}", maxLines: 2, ), ), - const SizedBox(height: 28.0), - AppBox( - onTap: () async { - final revision = await readLatestCharacterRevision(uuid); - await writeCharacterRevisionToAPI(revision); - }, - child: const AppText( - "Upload Latest Revision", - maxLines: 2, - ), - ), - ], - ); - } -} - -class ImportCharacter extends ConsumerWidget { - const ImportCharacter({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - AppText( - "Import Character", - style: Theme.of(context).textTheme.bodyLarge, - align: TextAlign.left, - ), - const SizedBox(height: 28.0), - AppBox( - onTap: () async { - FilePickerResult? result = await FilePicker.platform.pickFiles(); - - if (result != null) { - File file = File(result.files.single.path!); - final character = Character.fromBuffer(file.readAsBytesSync()); - final newRevision = await writeLatestCharacterRevision(character); - log("loaded new revision $newRevision from file"); - ref.invalidate(characterListProvider); - // ignore: use_build_context_synchronously - if (!context.mounted) return; - closeDialog(context); - closeDialog(context); - } - }, - child: const AppText("Import Revision File"), - ), ], ); } diff --git a/lib/views/dialogs/share_character.dart b/lib/views/dialogs/share_character.dart new file mode 100644 index 0000000..040b8b0 --- /dev/null +++ b/lib/views/dialogs/share_character.dart @@ -0,0 +1,109 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:cypher_sheet/components/box.dart'; +import 'package:cypher_sheet/components/dialog.dart'; +import 'package:cypher_sheet/components/text.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/providers/character.dart'; +import 'package:cypher_sheet/state/storage/api.dart'; +import 'package:cypher_sheet/state/storage/file.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:share_plus/share_plus.dart'; + +class ShareCharacter extends ConsumerWidget { + const ShareCharacter({super.key, required this.uuid}); + + final String uuid; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppText( + "Share Character", + style: Theme.of(context).textTheme.bodyLarge, + align: TextAlign.left, + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () async { + final revision = await readLatestCharacterRevision(uuid); + final revisionRaw = revision.writeToBuffer(); + Share.shareXFiles([XFile.fromData(revisionRaw)]); + }, + child: const AppText( + "Share Latest Revision", + maxLines: 2, + ), + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () async { + final revision = await readLatestCharacterRevision(uuid); + final revisionRaw = revision.writeToBuffer(); + log(revisionRaw.toString()); + log(revision.toDebugString()); + }, + child: const AppText( + "Log Latest Revision", + maxLines: 2, + ), + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () async { + final revision = await readLatestCharacterRevision(uuid); + await writeCharacterRevisionToAPI(revision); + }, + child: const AppText( + "Upload Latest Revision", + maxLines: 2, + ), + ), + ], + ); + } +} + +class ImportCharacter extends ConsumerWidget { + const ImportCharacter({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppText( + "Import Character", + style: Theme.of(context).textTheme.bodyLarge, + align: TextAlign.left, + ), + const SizedBox(height: 28.0), + AppBox( + onTap: () async { + FilePickerResult? result = await FilePicker.platform.pickFiles(); + + if (result != null) { + File file = File(result.files.single.path!); + final character = Character.fromBuffer(file.readAsBytesSync()); + final newRevision = await writeLatestCharacterRevision(character); + log("loaded new revision $newRevision from file"); + ref.invalidate(characterListProvider); + // ignore: use_build_context_synchronously + if (!context.mounted) return; + closeDialog(context); + closeDialog(context); + } + }, + child: const AppText("Import Revision File"), + ), + ], + ); + } +} diff --git a/lib/views/dialogs/xp.dart b/lib/views/dialogs/xp.dart index 2ec11f5..d5c3c04 100644 --- a/lib/views/dialogs/xp.dart +++ b/lib/views/dialogs/xp.dart @@ -1,4 +1,3 @@ -import 'package:cypher_sheet/components/number.dart'; import 'package:cypher_sheet/components/scroll.dart'; import 'package:cypher_sheet/state/providers/character.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/import_character_selection.dart b/lib/views/import_character_selection.dart new file mode 100644 index 0000000..552e33e --- /dev/null +++ b/lib/views/import_character_selection.dart @@ -0,0 +1,114 @@ +import 'package:cypher_sheet/main.dart'; +import 'package:cypher_sheet/state/providers/character.dart'; +import 'package:cypher_sheet/state/providers/import.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:cypher_sheet/components/appbar.dart' as app; +import 'package:cypher_sheet/components/box.dart'; +import 'package:cypher_sheet/components/scroll.dart'; +import 'package:cypher_sheet/components/text.dart'; +import 'package:cypher_sheet/proto/character.pb.dart'; +import 'package:cypher_sheet/state/storage/file.dart'; + +class ImportCharacterSelectionView extends ConsumerWidget { + const ImportCharacterSelectionView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return WillPopScope( + onWillPop: () async { + ref.read(importObjectProvider.notifier).state = SharedObject(); + return false; + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: AppScrollView( + appBar: const app.AppBar( + child: AppText( + "Select Character for Import", + align: TextAlign.left, + ), + ), + slivers: [ + const SizedBox(height: 16.0), + ...ref.watch(characterListProvider).when( + loading: () => const [CircularProgressIndicator()], + error: (err, stack) => [Text("Error: $err")], + data: (characters) { + return characters.map((metadata) => Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: SimpleCharacterListItem( + metadata: metadata, + ), + )); + }), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: AppBox( + onTap: (() { + ref.read(importObjectProvider.notifier).state = SharedObject(); + }), + child: const AppText("Abort"), + ), + ), + ], + ), + ); + } +} + +class SimpleCharacterListItem extends ConsumerWidget { + const SimpleCharacterListItem({ + super.key, + required this.metadata, + }); + + final Future metadata; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FutureBuilder( + future: metadata, + builder: (context, snapshot) { + if (snapshot.hasData) { + return AppBox( + color: Theme.of(context).colorScheme.surfaceTint, + onTap: (() async { + final character = + await readLatestCharacterRevision(snapshot.data!.uuid); + ref.read(characterProvider.notifier).load(character); + if (!context.mounted) return; + Navigator.of(context).pushReplacementNamed(routeCharacter); + }), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: AppText( + snapshot.data!.name, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ], + ), + ); + } + if (snapshot.hasError) { + return AppBox( + child: AppText( + snapshot.error.toString(), + style: Theme.of(context).textTheme.bodySmall, + ), + ); + } + return const CircularProgressIndicator(); + }, + ); + } +} diff --git a/lib/views/scaffold.dart b/lib/views/scaffold.dart new file mode 100644 index 0000000..2439aeb --- /dev/null +++ b/lib/views/scaffold.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +class AppScaffold extends StatelessWidget { + const AppScaffold({super.key, required this.body, this.bottomNavigationBar}); + + final Widget body; + final Widget? bottomNavigationBar; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: body, + extendBody: true, + primary: true, + resizeToAvoidBottomInset: true, + bottomNavigationBar: bottomNavigationBar, + ); + } +} + +class ToolbarIconButton extends StatelessWidget { + const ToolbarIconButton(this.icon, this.text, this.onPressed, {super.key}); + + final Widget icon; + final String text; + final void Function()? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: FittedBox( + fit: BoxFit.contain, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: onPressed, + icon: icon, + ), + Text( + text, + style: Theme.of(context).textTheme.labelSmall, + maxLines: 1, + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/view.dart b/lib/views/view.dart deleted file mode 100644 index db47443..0000000 --- a/lib/views/view.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:cypher_sheet/components/icon.dart'; -import 'package:cypher_sheet/components/icons.dart'; - -class AppView extends StatefulWidget { - const AppView({super.key, required this.views}); - - final List views; - - @override - State createState() => _AppViewState(); -} - -class ViewConfig { - final String name; - final AppIcons icon; - final Widget view; - - ViewConfig(this.name, this.icon, this.view); -} - -class _AppViewState extends State - with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - late final TabController _tabController; - @override - void initState() { - _tabController = TabController( - length: widget.views.length, - vsync: this, - initialIndex: (widget.views.length - 1) ~/ 2.0); - super.initState(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold( - body: Container( - constraints: const BoxConstraints(maxWidth: 500), - child: TabBarView( - controller: _tabController, - children: widget.views.map((view) => view.view).toList()), - ), - extendBody: true, - primary: true, - resizeToAvoidBottomInset: true, - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Colors.transparent, - boxShadow: [ - BoxShadow( - blurRadius: 40, - spreadRadius: 1, - color: Colors.black, - ), - ], - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ConstrainedBox( - constraints: BoxConstraints.loose(const Size.fromHeight(62)), - child: FittedBox( - fit: BoxFit.contain, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: widget.views - .asMap() - .entries - .map( - (e) => ToolbarIconButton( - AppIcon( - e.value.icon, - size: 34, - ), - e.value.name, - () { - _tabController.index = e.key; - }, - ), - ) - .toList(), - ), - ), - ), - ), - ), - ); - } - - @override - bool get wantKeepAlive => true; -} - -class ToolbarIconButton extends StatelessWidget { - const ToolbarIconButton(this.icon, this.text, this.onPressed, {super.key}); - - final Widget icon; - final String text; - final void Function()? onPressed; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4.0), - child: FittedBox( - fit: BoxFit.contain, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: onPressed, - icon: icon, - ), - Text( - text, - style: Theme.of(context).textTheme.labelSmall, - maxLines: 1, - ), - ], - ), - ), - ); - } -} diff --git a/proto b/proto index 8894673..46d39a4 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 88946732f664ff0130bf0790b7097ddddb1a974d +Subproject commit 46d39a4d4e30155d7a3952044407c32dbaf57064 diff --git a/pubspec.lock b/pubspec.lock index ef521a2..565b383 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -632,6 +632,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + receive_sharing_intent: + dependency: "direct main" + description: + name: receive_sharing_intent + sha256: "912bebb551bce75a14098891fd750305b30d53eba0d61cc70cd9973be9866e8d" + url: "https://pub.dev" + source: hosted + version: "1.4.5" riverpod: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d64fbd6..4a71395 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: cypher_sheet description: A little fun project for a Cypher Character Sheet built in Flutter publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 1.0.15+4 +version: 1.1.0+5 environment: sdk: ">=2.18.1 <3.0.0" @@ -24,6 +24,7 @@ dependencies: url_launcher: ^6.1.10 equatable: ^2.0.5 grpc: ^3.1.0 + receive_sharing_intent: ^1.4.5 dev_dependencies: flutter_test: