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..aa69c61 100644
--- a/lib/proto/characters.pb.dart
+++ b/lib/proto/characters.pb.dart
@@ -5,97 +5,269 @@
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
+import 'dart:async' as $async;
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;
+import 'character.pb.dart' as $0;
-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({
- $1.Character? character,
+ 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<$0.Character>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'character', subBuilder: $0.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,
+ $0.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 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 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')
- WriteCharacter clone() => WriteCharacter()..mergeFromMessage(this);
+ 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')
- WriteCharacter copyWith(void Function(WriteCharacter) updates) => super.copyWith((message) => updates(message as WriteCharacter)) as WriteCharacter; // ignore: deprecated_member_use
+ 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 WriteCharacter create() => WriteCharacter._();
- WriteCharacter createEmptyInstance() => create();
- static $pb.PbList createRepeated() => $pb.PbList();
+ static WriteRevision create() => WriteRevision._();
+ WriteRevision createEmptyInstance() => create();
+ static $pb.PbList createRepeated() => $pb.PbList();
@$core.pragma('dart2js:noInline')
- static WriteCharacter getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create);
- static WriteCharacter? _defaultInstance;
+ static WriteRevision getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create);
+ static WriteRevision? _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)
+ $0.Character get character => $_getN(1);
+ @$pb.TagNumber(2)
+ set character($0.Character v) { setField(2, v); }
+ @$pb.TagNumber(2)
+ $core.bool hasCharacter() => $_has(1);
+ @$pb.TagNumber(2)
+ void clearCharacter() => clearField(2);
+ @$pb.TagNumber(2)
+ $0.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)
- 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);
+
+ @$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 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 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
;
- CharacterWritten._() : super();
- factory CharacterWritten({
+ 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 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 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')
- CharacterWritten clone() => CharacterWritten()..mergeFromMessage(this);
+ 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')
- CharacterWritten copyWith(void Function(CharacterWritten) updates) => super.copyWith((message) => updates(message as CharacterWritten)) as CharacterWritten; // ignore: deprecated_member_use
+ 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 CharacterWritten create() => CharacterWritten._();
- CharacterWritten createEmptyInstance() => create();
- static $pb.PbList createRepeated() => $pb.PbList();
+ static ReadRevision create() => ReadRevision._();
+ ReadRevision createEmptyInstance() => create();
+ static $pb.PbList createRepeated() => $pb.PbList();
@$core.pragma('dart2js:noInline')
- static CharacterWritten getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create);
- static CharacterWritten? _defaultInstance;
+ static ReadRevision getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create);
+ static ReadRevision? _defaultInstance;
@$pb.TagNumber(1)
$core.String get uuid => $_getSZ(0);
@@ -105,5 +277,240 @@ class CharacterWritten extends $pb.GeneratedMessage {
$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<$0.Character>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'character', subBuilder: $0.Character.create)
+ ..hasRequiredFields = false
+ ;
+
+ RevisionRead._() : super();
+ factory RevisionRead({
+ $core.String? uuid,
+ $fixnum.Int64? revision,
+ $0.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 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')
+ 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')
+ 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 RevisionRead create() => RevisionRead._();
+ RevisionRead createEmptyInstance() => create();
+ static $pb.PbList createRepeated() => $pb.PbList();
+ @$core.pragma('dart2js:noInline')
+ static RevisionRead getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create);
+ static RevisionRead? _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);
+
+ @$pb.TagNumber(3)
+ $0.Character get character => $_getN(2);
+ @$pb.TagNumber(3)
+ set character($0.Character v) { setField(3, v); }
+ @$pb.TagNumber(3)
+ $core.bool hasCharacter() => $_has(2);
+ @$pb.TagNumber(3)
+ void clearCharacter() => clearField(3);
+ @$pb.TagNumber(3)
+ $0.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)
+ $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 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
+ ;
+
+ DeleteCharacter._() : super();
+ factory DeleteCharacter({
+ $core.String? uuid,
+ }) {
+ final _result = create();
+ if (uuid != null) {
+ _result.uuid = uuid;
+ }
+ return _result;
+ }
+ 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')
+ 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')
+ 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 DeleteCharacter create() => DeleteCharacter._();
+ DeleteCharacter createEmptyInstance() => create();
+ static $pb.PbList createRepeated() => $pb.PbList();
+ @$core.pragma('dart2js:noInline')
+ static DeleteCharacter getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create);
+ static DeleteCharacter? _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 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;
+}
+
+class CharactersApi {
+ $pb.RpcClient _client;
+ CharactersApi(this._client);
+
+ $async.Future create_($pb.ClientContext? ctx, CreateCharacter request) {
+ var emptyResponse = CharacterCreated();
+ return _client.invoke(ctx, 'Characters', 'Create', request, emptyResponse);
+ }
+ $async.Future writeCharacterRevision($pb.ClientContext? ctx, WriteRevision request) {
+ var emptyResponse = RevisionWritten();
+ return _client.invoke(ctx, 'Characters', 'WriteCharacterRevision', request, emptyResponse);
+ }
+ $async.Future readCharacterRevision($pb.ClientContext? ctx, ReadRevision request) {
+ var emptyResponse = RevisionRead();
+ return _client.invoke(ctx, 'Characters', 'ReadCharacterRevision', request, emptyResponse);
+ }
+ $async.Future readLatestCharacterRevision($pb.ClientContext? ctx, ReadLatestRevision request) {
+ var emptyResponse = RevisionRead();
+ return _client.invoke(ctx, 'Characters', 'ReadLatestCharacterRevision', request, emptyResponse);
+ }
+ $async.Future delete($pb.ClientContext? ctx, DeleteCharacter request) {
+ var emptyResponse = CharacterDeleted();
+ return _client.invoke(ctx, 'Characters', 'Delete', request, emptyResponse);
+ }
}
diff --git a/lib/proto/characters.pbjson.dart b/lib/proto/characters.pbjson.dart
index f92f416..2eca8d0 100644
--- a/lib/proto/characters.pbjson.dart
+++ b/lib/proto/characters.pbjson.dart
@@ -8,23 +8,137 @@
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',
+import 'character.pbjson.dart' as $0;
+
+@$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': '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': '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 `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 `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': '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 `CharacterWritten`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List characterWrittenDescriptor = $convert.base64Decode('ChBDaGFyYWN0ZXJXcml0dGVuEhIKBHV1aWQYASABKAlSBHV1aWQ=');
+/// 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 `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');
+const $core.Map<$core.String, $core.dynamic> CharactersServiceBase$json = const {
+ '1': 'Characters',
+ '2': const [
+ const {'1': 'Create', '2': '.characters.CreateCharacter', '3': '.characters.CharacterCreated'},
+ const {'1': 'WriteCharacterRevision', '2': '.characters.WriteRevision', '3': '.characters.RevisionWritten'},
+ const {'1': 'ReadCharacterRevision', '2': '.characters.ReadRevision', '3': '.characters.RevisionRead'},
+ const {'1': 'ReadLatestCharacterRevision', '2': '.characters.ReadLatestRevision', '3': '.characters.RevisionRead'},
+ const {'1': 'Delete', '2': '.characters.DeleteCharacter', '3': '.characters.CharacterDeleted'},
+ ],
+};
+
+@$core.Deprecated('Use charactersServiceDescriptor instead')
+const $core.Map<$core.String, $core.Map<$core.String, $core.dynamic>> CharactersServiceBase$messageJson = const {
+ '.characters.CreateCharacter': CreateCharacter$json,
+ '.characters.CharacterCreated': CharacterCreated$json,
+ '.characters.WriteRevision': WriteRevision$json,
+ '.character.Character': $0.Character$json,
+ '.character.Progress': $0.Progress$json,
+ '.character.Advancements': $0.Advancements$json,
+ '.character.Stats': $0.Stats$json,
+ '.character.Stat': $0.Stat$json,
+ '.character.Recovery': $0.Recovery$json,
+ '.character.Damage': $0.Damage$json,
+ '.character.Skill': $0.Skill$json,
+ '.character.Ability': $0.Ability$json,
+ '.character.Cypher': $0.Cypher$json,
+ '.character.Artifact': $0.Artifact$json,
+ '.character.Inventory': $0.Inventory$json,
+ '.character.CharacterColor': $0.CharacterColor$json,
+ '.character.Item': $0.Item$json,
+ '.character.ItemPath': $0.ItemPath$json,
+ '.character.Note': $0.Note$json,
+ '.characters.RevisionWritten': RevisionWritten$json,
+ '.characters.ReadRevision': ReadRevision$json,
+ '.characters.RevisionRead': RevisionRead$json,
+ '.characters.ReadLatestRevision': ReadLatestRevision$json,
+ '.characters.DeleteCharacter': DeleteCharacter$json,
+ '.characters.CharacterDeleted': CharacterDeleted$json,
+};
+
+/// Descriptor for `Characters`. Decode as a `google.protobuf.ServiceDescriptorProto`.
+final $typed_data.Uint8List charactersServiceDescriptor = $convert.base64Decode('CgpDaGFyYWN0ZXJzEkMKBkNyZWF0ZRIbLmNoYXJhY3RlcnMuQ3JlYXRlQ2hhcmFjdGVyGhwuY2hhcmFjdGVycy5DaGFyYWN0ZXJDcmVhdGVkElAKFldyaXRlQ2hhcmFjdGVyUmV2aXNpb24SGS5jaGFyYWN0ZXJzLldyaXRlUmV2aXNpb24aGy5jaGFyYWN0ZXJzLlJldmlzaW9uV3JpdHRlbhJLChVSZWFkQ2hhcmFjdGVyUmV2aXNpb24SGC5jaGFyYWN0ZXJzLlJlYWRSZXZpc2lvbhoYLmNoYXJhY3RlcnMuUmV2aXNpb25SZWFkElcKG1JlYWRMYXRlc3RDaGFyYWN0ZXJSZXZpc2lvbhIeLmNoYXJhY3RlcnMuUmVhZExhdGVzdFJldmlzaW9uGhguY2hhcmFjdGVycy5SZXZpc2lvblJlYWQSQwoGRGVsZXRlEhsuY2hhcmFjdGVycy5EZWxldGVDaGFyYWN0ZXIaHC5jaGFyYWN0ZXJzLkNoYXJhY3RlckRlbGV0ZWQ=');
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/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..628322b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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: