diff --git a/api/lib/src/event/event.mapper.dart b/api/lib/src/event/event.mapper.dart index 9b69c38..4b291dc 100644 --- a/api/lib/src/event/event.mapper.dart +++ b/api/lib/src/event/event.mapper.dart @@ -1797,6 +1797,8 @@ class HybridWorldEventMapper extends SubClassMapperBase { CellItemsClearedMapper.ensureInitialized(); TableRenamedMapper.ensureInitialized(); TableRemovedMapper.ensureInitialized(); + NoteChangedMapper.ensureInitialized(); + NoteRemovedMapper.ensureInitialized(); } return _instance!; } @@ -3260,6 +3262,234 @@ class _TableRemovedCopyWithImpl<$R, $Out> _TableRemovedCopyWithImpl($value, $cast, t); } +class NoteChangedMapper extends SubClassMapperBase { + NoteChangedMapper._(); + + static NoteChangedMapper? _instance; + static NoteChangedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = NoteChangedMapper._()); + HybridWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + } + return _instance!; + } + + @override + final String id = 'NoteChanged'; + + static String _$name(NoteChanged v) => v.name; + static const Field _f$name = Field('name', _$name); + static String _$content(NoteChanged v) => v.content; + static const Field _f$content = + Field('content', _$content); + + @override + final MappableFields fields = const { + #name: _f$name, + #content: _f$content, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'NoteChanged'; + @override + late final ClassMapperBase superMapper = + HybridWorldEventMapper.ensureInitialized(); + + static NoteChanged _instantiate(DecodingData data) { + return NoteChanged(data.dec(_f$name), data.dec(_f$content)); + } + + @override + final Function instantiate = _instantiate; + + static NoteChanged fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static NoteChanged fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin NoteChangedMappable { + String toJson() { + return NoteChangedMapper.ensureInitialized() + .encodeJson(this as NoteChanged); + } + + Map toMap() { + return NoteChangedMapper.ensureInitialized() + .encodeMap(this as NoteChanged); + } + + NoteChangedCopyWith get copyWith => + _NoteChangedCopyWithImpl(this as NoteChanged, $identity, $identity); + @override + String toString() { + return NoteChangedMapper.ensureInitialized() + .stringifyValue(this as NoteChanged); + } + + @override + bool operator ==(Object other) { + return NoteChangedMapper.ensureInitialized() + .equalsValue(this as NoteChanged, other); + } + + @override + int get hashCode { + return NoteChangedMapper.ensureInitialized().hashValue(this as NoteChanged); + } +} + +extension NoteChangedValueCopy<$R, $Out> + on ObjectCopyWith<$R, NoteChanged, $Out> { + NoteChangedCopyWith<$R, NoteChanged, $Out> get $asNoteChanged => + $base.as((v, t, t2) => _NoteChangedCopyWithImpl(v, t, t2)); +} + +abstract class NoteChangedCopyWith<$R, $In extends NoteChanged, $Out> + implements HybridWorldEventCopyWith<$R, $In, $Out> { + @override + $R call({String? name, String? content}); + NoteChangedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _NoteChangedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, NoteChanged, $Out> + implements NoteChangedCopyWith<$R, NoteChanged, $Out> { + _NoteChangedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + NoteChangedMapper.ensureInitialized(); + @override + $R call({String? name, String? content}) => $apply(FieldCopyWithData( + {if (name != null) #name: name, if (content != null) #content: content})); + @override + NoteChanged $make(CopyWithData data) => NoteChanged( + data.get(#name, or: $value.name), data.get(#content, or: $value.content)); + + @override + NoteChangedCopyWith<$R2, NoteChanged, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _NoteChangedCopyWithImpl($value, $cast, t); +} + +class NoteRemovedMapper extends SubClassMapperBase { + NoteRemovedMapper._(); + + static NoteRemovedMapper? _instance; + static NoteRemovedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = NoteRemovedMapper._()); + HybridWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + } + return _instance!; + } + + @override + final String id = 'NoteRemoved'; + + static String _$name(NoteRemoved v) => v.name; + static const Field _f$name = Field('name', _$name); + + @override + final MappableFields fields = const { + #name: _f$name, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'NoteRemoved'; + @override + late final ClassMapperBase superMapper = + HybridWorldEventMapper.ensureInitialized(); + + static NoteRemoved _instantiate(DecodingData data) { + return NoteRemoved(data.dec(_f$name)); + } + + @override + final Function instantiate = _instantiate; + + static NoteRemoved fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static NoteRemoved fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin NoteRemovedMappable { + String toJson() { + return NoteRemovedMapper.ensureInitialized() + .encodeJson(this as NoteRemoved); + } + + Map toMap() { + return NoteRemovedMapper.ensureInitialized() + .encodeMap(this as NoteRemoved); + } + + NoteRemovedCopyWith get copyWith => + _NoteRemovedCopyWithImpl(this as NoteRemoved, $identity, $identity); + @override + String toString() { + return NoteRemovedMapper.ensureInitialized() + .stringifyValue(this as NoteRemoved); + } + + @override + bool operator ==(Object other) { + return NoteRemovedMapper.ensureInitialized() + .equalsValue(this as NoteRemoved, other); + } + + @override + int get hashCode { + return NoteRemovedMapper.ensureInitialized().hashValue(this as NoteRemoved); + } +} + +extension NoteRemovedValueCopy<$R, $Out> + on ObjectCopyWith<$R, NoteRemoved, $Out> { + NoteRemovedCopyWith<$R, NoteRemoved, $Out> get $asNoteRemoved => + $base.as((v, t, t2) => _NoteRemovedCopyWithImpl(v, t, t2)); +} + +abstract class NoteRemovedCopyWith<$R, $In extends NoteRemoved, $Out> + implements HybridWorldEventCopyWith<$R, $In, $Out> { + @override + $R call({String? name}); + NoteRemovedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _NoteRemovedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, NoteRemoved, $Out> + implements NoteRemovedCopyWith<$R, NoteRemoved, $Out> { + _NoteRemovedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + NoteRemovedMapper.ensureInitialized(); + @override + $R call({String? name}) => + $apply(FieldCopyWithData({if (name != null) #name: name})); + @override + NoteRemoved $make(CopyWithData data) => + NoteRemoved(data.get(#name, or: $value.name)); + + @override + NoteRemovedCopyWith<$R2, NoteRemoved, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _NoteRemovedCopyWithImpl($value, $cast, t); +} + class LocalWorldEventMapper extends SubClassMapperBase { LocalWorldEventMapper._(); diff --git a/api/lib/src/event/hybrid.dart b/api/lib/src/event/hybrid.dart index d8f7961..5f1f10a 100644 --- a/api/lib/src/event/hybrid.dart +++ b/api/lib/src/event/hybrid.dart @@ -98,3 +98,17 @@ final class TableRemoved extends HybridWorldEvent with TableRemovedMappable { TableRemoved(this.name); } + +@MappableClass() +final class NoteChanged extends HybridWorldEvent with NoteChangedMappable { + final String name, content; + + NoteChanged(this.name, this.content); +} + +@MappableClass() +final class NoteRemoved extends HybridWorldEvent with NoteRemovedMappable { + final String name; + + NoteRemoved(this.name); +} diff --git a/api/lib/src/event/process/server.dart b/api/lib/src/event/process/server.dart index 4765b46..a24c62c 100644 --- a/api/lib/src/event/process/server.dart +++ b/api/lib/src/event/process/server.dart @@ -233,5 +233,10 @@ WorldState? processServerEvent( return state.copyWith( tableName: state.tableName == event.name ? '' : state.tableName, data: state.data.removeTable(event.name)); + case NoteChanged(): + return state.copyWith( + data: state.data.setNote(event.name, event.content)); + case NoteRemoved(): + return state.copyWith(data: state.data.removeNote(event.name)); } } diff --git a/api/lib/src/models/data.dart b/api/lib/src/models/data.dart index dfc59c0..c4a2a0a 100644 --- a/api/lib/src/models/data.dart +++ b/api/lib/src/models/data.dart @@ -23,6 +23,7 @@ const kPackBackgroundsPath = 'backgrounds'; const kGameTablePath = 'tables'; const kGameTeamPath = 'teams.json'; +const kGameNotesPath = 'notes'; class QuokkaData extends ArchiveData { QuokkaData(super.archive, {super.state}); @@ -52,6 +53,21 @@ class QuokkaData extends ArchiveData { Iterable getTables() => getAssets(kGameTablePath, true); + String? getNote(String name) { + final data = getAsset('$kGameNotesPath/$name.md'); + if (data == null) return null; + return utf8.decode(data); + } + + QuokkaData setNote(String name, String content) => setAsset( + '$kGameNotesPath/$name.md', + utf8.encode(content), + ); + + QuokkaData removeNote(String name) => removeAsset('$kGameNotesPath/$name.md'); + + Iterable getNotes() => getAssets(kGameNotesPath, true); + FileMetadata? getMetadata() { final data = getAsset(kPackMetadataPath); if (data == null) { diff --git a/app/lib/bloc/world/bloc.dart b/app/lib/bloc/world/bloc.dart index bf0c026..2c2cd41 100644 --- a/app/lib/bloc/world/bloc.dart +++ b/app/lib/bloc/world/bloc.dart @@ -86,6 +86,9 @@ class WorldBloc extends Bloc { data: state.data.setTable(state.table, state.tableName), )); }); + on((event, emit) { + emit(state.copyWith(drawerView: event.view)); + }); } Future save() async { diff --git a/app/lib/bloc/world/local.dart b/app/lib/bloc/world/local.dart index b48c5d5..ff4e4b5 100644 --- a/app/lib/bloc/world/local.dart +++ b/app/lib/bloc/world/local.dart @@ -1,5 +1,6 @@ import 'package:dart_mappable/dart_mappable.dart'; import 'package:flutter/material.dart'; +import 'package:quokka/bloc/world/state.dart'; import 'package:quokka_api/quokka_api.dart'; part 'local.mapper.dart'; @@ -49,3 +50,11 @@ final class TableSwitched extends LocalWorldEvent with TableSwitchedMappable { TableSwitched([this.name = '']); } + +@MappableClass() +final class DrawerViewChanged extends LocalWorldEvent + with DrawerViewChangedMappable { + final DrawerView view; + + DrawerViewChanged(this.view); +} diff --git a/app/lib/bloc/world/local.mapper.dart b/app/lib/bloc/world/local.mapper.dart index 6353d55..53ee334 100644 --- a/app/lib/bloc/world/local.mapper.dart +++ b/app/lib/bloc/world/local.mapper.dart @@ -608,3 +608,122 @@ class _TableSwitchedCopyWithImpl<$R, $Out> Then<$Out2, $R2> t) => _TableSwitchedCopyWithImpl($value, $cast, t); } + +class DrawerViewChangedMapper extends SubClassMapperBase { + DrawerViewChangedMapper._(); + + static DrawerViewChangedMapper? _instance; + static DrawerViewChangedMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = DrawerViewChangedMapper._()); + LocalWorldEventMapper.ensureInitialized().addSubMapper(_instance!); + DrawerViewMapper.ensureInitialized(); + } + return _instance!; + } + + @override + final String id = 'DrawerViewChanged'; + + static DrawerView _$view(DrawerViewChanged v) => v.view; + static const Field _f$view = + Field('view', _$view); + + @override + final MappableFields fields = const { + #view: _f$view, + }; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'DrawerViewChanged'; + @override + late final ClassMapperBase superMapper = + LocalWorldEventMapper.ensureInitialized(); + + static DrawerViewChanged _instantiate(DecodingData data) { + return DrawerViewChanged(data.dec(_f$view)); + } + + @override + final Function instantiate = _instantiate; + + static DrawerViewChanged fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static DrawerViewChanged fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin DrawerViewChangedMappable { + String toJson() { + return DrawerViewChangedMapper.ensureInitialized() + .encodeJson(this as DrawerViewChanged); + } + + Map toMap() { + return DrawerViewChangedMapper.ensureInitialized() + .encodeMap(this as DrawerViewChanged); + } + + DrawerViewChangedCopyWith + get copyWith => _DrawerViewChangedCopyWithImpl( + this as DrawerViewChanged, $identity, $identity); + @override + String toString() { + return DrawerViewChangedMapper.ensureInitialized() + .stringifyValue(this as DrawerViewChanged); + } + + @override + bool operator ==(Object other) { + return DrawerViewChangedMapper.ensureInitialized() + .equalsValue(this as DrawerViewChanged, other); + } + + @override + int get hashCode { + return DrawerViewChangedMapper.ensureInitialized() + .hashValue(this as DrawerViewChanged); + } +} + +extension DrawerViewChangedValueCopy<$R, $Out> + on ObjectCopyWith<$R, DrawerViewChanged, $Out> { + DrawerViewChangedCopyWith<$R, DrawerViewChanged, $Out> + get $asDrawerViewChanged => + $base.as((v, t, t2) => _DrawerViewChangedCopyWithImpl(v, t, t2)); +} + +abstract class DrawerViewChangedCopyWith<$R, $In extends DrawerViewChanged, + $Out> implements LocalWorldEventCopyWith<$R, $In, $Out> { + @override + $R call({DrawerView? view}); + DrawerViewChangedCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _DrawerViewChangedCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, DrawerViewChanged, $Out> + implements DrawerViewChangedCopyWith<$R, DrawerViewChanged, $Out> { + _DrawerViewChangedCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + DrawerViewChangedMapper.ensureInitialized(); + @override + $R call({DrawerView? view}) => + $apply(FieldCopyWithData({if (view != null) #view: view})); + @override + DrawerViewChanged $make(CopyWithData data) => + DrawerViewChanged(data.get(#view, or: $value.view)); + + @override + DrawerViewChangedCopyWith<$R2, DrawerViewChanged, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _DrawerViewChangedCopyWithImpl($value, $cast, t); +} diff --git a/app/lib/bloc/world/state.dart b/app/lib/bloc/world/state.dart index 387729d..52e5b6c 100644 --- a/app/lib/bloc/world/state.dart +++ b/app/lib/bloc/world/state.dart @@ -14,6 +14,12 @@ enum WorldOperationMode { boards, } +@MappableEnum() +enum DrawerView { + chat, + notes, +} + @MappableClass() final class ClientWorldState extends WorldState with ClientWorldStateMappable { final MultiplayerCubit multiplayer; @@ -22,6 +28,7 @@ final class ClientWorldState extends WorldState with ClientWorldStateMappable { final VectorDefinition? selectedCell; final ItemLocation? selectedDeck; final bool showHand, switchCellOnMove; + final DrawerView drawerView; const ClientWorldState({ required this.multiplayer, @@ -40,6 +47,7 @@ final class ClientWorldState extends WorldState with ClientWorldStateMappable { super.teamMembers, super.messages, required super.data, + this.drawerView = DrawerView.chat, }); QuokkaFileSystem get fileSystem => assetManager.fileSystem; diff --git a/app/lib/bloc/world/state.mapper.dart b/app/lib/bloc/world/state.mapper.dart index a3600b5..e2754fb 100644 --- a/app/lib/bloc/world/state.mapper.dart +++ b/app/lib/bloc/world/state.mapper.dart @@ -52,6 +52,52 @@ extension WorldOperationModeMapperExtension on WorldOperationMode { } } +class DrawerViewMapper extends EnumMapper { + DrawerViewMapper._(); + + static DrawerViewMapper? _instance; + static DrawerViewMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = DrawerViewMapper._()); + } + return _instance!; + } + + static DrawerView fromValue(dynamic value) { + ensureInitialized(); + return MapperContainer.globals.fromValue(value); + } + + @override + DrawerView decode(dynamic value) { + switch (value) { + case 'chat': + return DrawerView.chat; + case 'notes': + return DrawerView.notes; + default: + throw MapperException.unknownEnumValue(value); + } + } + + @override + dynamic encode(DrawerView self) { + switch (self) { + case DrawerView.chat: + return 'chat'; + case DrawerView.notes: + return 'notes'; + } + } +} + +extension DrawerViewMapperExtension on DrawerView { + String toValue() { + DrawerViewMapper.ensureInitialized(); + return MapperContainer.globals.toValue(this) as String; + } +} + class ClientWorldStateMapper extends ClassMapperBase { ClientWorldStateMapper._(); @@ -66,6 +112,7 @@ class ClientWorldStateMapper extends ClassMapperBase { VectorDefinitionMapper.ensureInitialized(); ItemLocationMapper.ensureInitialized(); ChatMessageMapper.ensureInitialized(); + DrawerViewMapper.ensureInitialized(); } return _instance!; } @@ -122,6 +169,9 @@ class ClientWorldStateMapper extends ClassMapperBase { static QuokkaData _$data(ClientWorldState v) => v.data; static const Field _f$data = Field('data', _$data); + static DrawerView _$drawerView(ClientWorldState v) => v.drawerView; + static const Field _f$drawerView = + Field('drawerView', _$drawerView, opt: true, def: DrawerView.chat); @override final MappableFields fields = const { @@ -141,6 +191,7 @@ class ClientWorldStateMapper extends ClassMapperBase { #teamMembers: _f$teamMembers, #messages: _f$messages, #data: _f$data, + #drawerView: _f$drawerView, }; static ClientWorldState _instantiate(DecodingData data) { @@ -160,7 +211,8 @@ class ClientWorldStateMapper extends ClassMapperBase { id: data.dec(_f$id), teamMembers: data.dec(_f$teamMembers), messages: data.dec(_f$messages), - data: data.dec(_f$data)); + data: data.dec(_f$data), + drawerView: data.dec(_f$drawerView)); } @override @@ -249,7 +301,8 @@ abstract class ClientWorldStateCopyWith<$R, $In extends ClientWorldState, $Out> int? id, Map>? teamMembers, List? messages, - QuokkaData? data}); + QuokkaData? data, + DrawerView? drawerView}); ClientWorldStateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t); } @@ -306,7 +359,8 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> int? id, Map>? teamMembers, List? messages, - QuokkaData? data}) => + QuokkaData? data, + DrawerView? drawerView}) => $apply(FieldCopyWithData({ if (multiplayer != null) #multiplayer: multiplayer, if (colorScheme != null) #colorScheme: colorScheme, @@ -323,7 +377,8 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> if (id != null) #id: id, if (teamMembers != null) #teamMembers: teamMembers, if (messages != null) #messages: messages, - if (data != null) #data: data + if (data != null) #data: data, + if (drawerView != null) #drawerView: drawerView })); @override ClientWorldState $make(CopyWithData data) => ClientWorldState( @@ -343,7 +398,8 @@ class _ClientWorldStateCopyWithImpl<$R, $Out> id: data.get(#id, or: $value.id), teamMembers: data.get(#teamMembers, or: $value.teamMembers), messages: data.get(#messages, or: $value.messages), - data: data.get(#data, or: $value.data)); + data: data.get(#data, or: $value.data), + drawerView: data.get(#drawerView, or: $value.drawerView)); @override ClientWorldStateCopyWith<$R2, ClientWorldState, $Out2> $chain<$R2, $Out2>( diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index b55fa27..390a5f4 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -158,5 +158,16 @@ } }, "open": "Open", - "rename": "Rename" + "rename": "Rename", + "notes": "Notes", + "deleteNote": "Delete note", + "deleteNoteMessage": "Are you sure you want to delete {name}? This action cannot be undone.", + "@deleteNoteMessage": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "content": "Content" } diff --git a/app/lib/pages/game/drawer.dart b/app/lib/pages/game/drawer.dart index 9a2e1ac..a851511 100644 --- a/app/lib/pages/game/drawer.dart +++ b/app/lib/pages/game/drawer.dart @@ -295,6 +295,19 @@ class GameDrawer extends StatelessWidget { leading: const Icon(PhosphorIconsLight.chat), title: Text(AppLocalizations.of(context).chat), onTap: () { + context + .read() + .process(DrawerViewChanged(DrawerView.chat)); + Scaffold.of(context).openEndDrawer(); + }, + ), + ListTile( + leading: const Icon(PhosphorIconsLight.file), + title: Text(AppLocalizations.of(context).notes), + onTap: () { + context + .read() + .process(DrawerViewChanged(DrawerView.notes)); Scaffold.of(context).openEndDrawer(); }, ), diff --git a/app/lib/pages/game/note.dart b/app/lib/pages/game/note.dart new file mode 100644 index 0000000..29ec37f --- /dev/null +++ b/app/lib/pages/game/note.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:quokka/bloc/world/bloc.dart'; +import 'package:quokka_api/quokka_api.dart'; + +class GameNoteDialog extends StatefulWidget { + final String? note; + + const GameNoteDialog({ + super.key, + this.note, + }); + + @override + State createState() => _GameNoteDialogState(); +} + +class _GameNoteDialogState extends State { + late final WorldBloc _bloc; + bool _editing = true; + final TextEditingController _nameController = TextEditingController(), + _contentController = TextEditingController(); + + @override + void initState() { + super.initState(); + _bloc = context.read(); + final note = widget.note; + if (note != null) { + _editing = false; + _nameController.text = note; + _contentController.text = _bloc.state.data.getNote(note) ?? ''; + } + } + + @override + void dispose() { + super.dispose(); + _bloc.process(NoteChanged(_nameController.text, _contentController.text)); + _nameController.dispose(); + _contentController.dispose(); + } + + @override + Widget build(BuildContext context) { + return ResponsiveAlertDialog( + title: widget.note == null + ? TextFormField( + controller: _nameController, + style: Theme.of(context).textTheme.headlineSmall, + decoration: InputDecoration( + hintText: AppLocalizations.of(context).name, + filled: true, + ), + ) + : Text(widget.note!), + headerActions: [ + IconButton( + icon: const Icon(PhosphorIconsLight.pencil), + tooltip: _editing + ? AppLocalizations.of(context).exitEditMode + : AppLocalizations.of(context).enterEditMode, + isSelected: _editing, + selectedIcon: const Icon(PhosphorIconsLight.monitor), + onPressed: () { + setState(() { + _editing = !_editing; + }); + }, + ), + ], + leading: IconButton.outlined( + icon: const Icon(PhosphorIconsLight.x), + onPressed: () => Navigator.of(context).pop(), + ), + constraints: const BoxConstraints( + maxWidth: LeapBreakpoints.medium, maxHeight: 800), + content: _editing + ? TextFormField( + minLines: 5, + maxLines: 50, + controller: _contentController, + decoration: InputDecoration( + hintText: AppLocalizations.of(context).content, + border: const OutlineInputBorder(), + ), + ) + : ListenableBuilder( + listenable: _contentController, + builder: (context, _) => Markdown( + extensionSet: md.ExtensionSet( + md.ExtensionSet.gitHubFlavored.blockSyntaxes, + [ + md.EmojiSyntax(), + ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes + ], + ), + data: _contentController.text)), + ); + } +} diff --git a/app/lib/pages/game/notes.dart b/app/lib/pages/game/notes.dart new file mode 100644 index 0000000..e1a1925 --- /dev/null +++ b/app/lib/pages/game/notes.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:quokka/bloc/world/bloc.dart'; +import 'package:quokka/pages/game/note.dart'; +import 'package:quokka_api/quokka_api.dart'; + +class GameNotesDrawer extends StatefulWidget { + const GameNotesDrawer({super.key}); + + @override + State createState() => _GameNotesDrawerState(); +} + +class _GameNotesDrawerState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Drawer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Header( + leading: IconButton.outlined( + icon: const Icon(PhosphorIconsLight.x), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text(AppLocalizations.of(context).notes), + ), + Flexible( + child: BlocBuilder( + buildWhen: (previous, current) => previous.data != current.data, + builder: (context, state) { + final notes = state.data.getNotes().toList(); + return ListView.builder( + itemCount: notes.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(notes[index]), + onTap: () { + final bloc = context.read(); + showDialog( + context: context, + builder: (context) => BlocProvider.value( + value: bloc, + child: GameNoteDialog(note: notes[index])), + ); + }, + trailing: IconButton( + icon: const Icon(PhosphorIconsLight.trash), + onPressed: () async { + final bloc = context.read(); + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: + Text(AppLocalizations.of(context).deleteNote), + content: Text(AppLocalizations.of(context) + .deleteNoteMessage(notes[index])), + actions: [ + TextButton( + onPressed: () => + Navigator.of(context).pop(false), + child: + Text(AppLocalizations.of(context).cancel), + ), + TextButton( + onPressed: () => + Navigator.of(context).pop(true), + child: + Text(AppLocalizations.of(context).delete), + ), + ], + ), + ); + if (!(result ?? false)) return; + bloc.process( + NoteRemoved(notes[index]), + ); + }, + ), + ); + }, + ); + }, + ), + ), + const SizedBox(height: 8), + SizedBox( + height: 48, + child: ElevatedButton.icon( + icon: const Icon(PhosphorIconsLight.plus), + label: Text(AppLocalizations.of(context).create), + onPressed: () { + final bloc = context.read(); + showDialog( + context: context, + builder: (context) => BlocProvider.value( + value: bloc, child: const GameNoteDialog()), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/pages/game/page.dart b/app/lib/pages/game/page.dart index 0d838f3..2487906 100644 --- a/app/lib/pages/game/page.dart +++ b/app/lib/pages/game/page.dart @@ -13,6 +13,7 @@ import 'package:quokka/bloc/settings.dart'; import 'package:quokka/board/game.dart'; import 'package:quokka/pages/game/chat.dart'; import 'package:quokka/pages/game/drawer.dart'; +import 'package:quokka/pages/game/notes.dart'; import 'package:quokka/pages/home/background.dart'; import 'package:quokka/services/file_system.dart'; import 'package:quokka_api/quokka_api.dart'; @@ -165,7 +166,14 @@ class _GamePageState extends State { ], ), drawer: const GameDrawer(), - endDrawer: const GameChatDrawer(), + endDrawer: BlocBuilder( + buildWhen: (previous, current) => + previous.drawerView != current.drawerView, + builder: (context, state) => switch (state.drawerView) { + DrawerView.chat => const GameChatDrawer(), + DrawerView.notes => const GameNotesDrawer(), + }, + ), body: BlocListener( listenWhen: (previous, current) => previous.messages.length != current.messages.length, diff --git a/app/pubspec.lock b/app/pubspec.lock index 28b9c4b..96862dd 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -439,6 +439,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.5" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: a23c41ee57573e62fc2190a1f36a0480c4d90bde3a8a8d7126e5d5992fb53fb7 + url: "https://pub.dev" + source: hosted + version: "0.7.3+1" flutter_secure_storage: dependency: transitive description: @@ -687,6 +695,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.2-main.4" + markdown: + dependency: "direct main" + description: + name: markdown + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + url: "https://pub.dev" + source: hosted + version: "7.2.2" matcher: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index d07789f..0075ef9 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -87,6 +87,8 @@ dependencies: # Serialization dart_mappable: ^4.2.2 archive: ^3.4.10 + flutter_markdown: ^0.7.3+1 + markdown: ^7.2.2 dependency_overrides: flutter_secure_storage_web: git: diff --git a/docs/package.json b/docs/package.json index 57081d2..ae00904 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,7 +12,7 @@ "dependencies": { "@astrojs/check": "^0.9.3", "@astrojs/react": "^3.6.2", - "@astrojs/starlight": "^0.26.4", + "@astrojs/starlight": "^0.27.0", "@phosphor-icons/react": "^2.1.7", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 8489725..7666bd8 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^3.6.2 version: 3.6.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@5.4.3(sass@1.78.0)) '@astrojs/starlight': - specifier: ^0.26.4 - version: 0.26.4(astro@4.15.4(rollup@4.21.2)(sass@1.78.0)(typescript@5.5.4)) + specifier: ^0.27.0 + version: 0.27.0(astro@4.15.4(rollup@4.21.2)(sass@1.78.0)(typescript@5.5.4)) '@phosphor-icons/react': specifier: ^2.1.7 version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -107,8 +107,8 @@ packages: '@astrojs/sitemap@3.1.6': resolution: {integrity: sha512-1Qp2NvAzVImqA6y+LubKi1DVhve/hXXgFvB0szxiipzh7BvtuKe4oJJ9dXSqaubaTkt4nMa6dv6RCCAYeB6xaQ==} - '@astrojs/starlight@0.26.4': - resolution: {integrity: sha512-ks+GAYkYGZxuCjAJR88HFafY4/K73PtkbYniGaptmdB0yDJY/HwJ/s1vIuig3j63oq9otQfuZFByxWsb4x1urg==} + '@astrojs/starlight@0.27.0': + resolution: {integrity: sha512-W06VHc4VVohKE6g1CytSF64WQ97nv/hlHHf2+vKGh/+I9nDdom/M7RXqkyLx7FBlLCbgsSvLYZUkh8cQ/mMOzQ==} peerDependencies: astro: ^4.8.6 @@ -1027,8 +1027,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.17: - resolution: {integrity: sha512-Q6Q+04tjC2KJ8qsSOSgovvhWcv5t+SmpH6/YfAWmhpE5/r+zw6KQy1/yWVFFNyEBvy68twTTXr2d5eLfCq7QIw==} + electron-to-chromium@1.5.18: + resolution: {integrity: sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ==} emmet@2.4.7: resolution: {integrity: sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA==} @@ -2391,7 +2391,7 @@ snapshots: stream-replace-string: 2.0.0 zod: 3.23.8 - '@astrojs/starlight@0.26.4(astro@4.15.4(rollup@4.21.2)(sass@1.78.0)(typescript@5.5.4))': + '@astrojs/starlight@0.27.0(astro@4.15.4(rollup@4.21.2)(sass@1.78.0)(typescript@5.5.4))': dependencies: '@astrojs/mdx': 3.1.5(astro@4.15.4(rollup@4.21.2)(sass@1.78.0)(typescript@5.5.4)) '@astrojs/sitemap': 3.1.6 @@ -3234,7 +3234,7 @@ snapshots: browserslist@4.23.3: dependencies: caniuse-lite: 1.0.30001658 - electron-to-chromium: 1.5.17 + electron-to-chromium: 1.5.18 node-releases: 2.0.18 update-browserslist-db: 1.1.0(browserslist@4.23.3) @@ -3360,7 +3360,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.17: {} + electron-to-chromium@1.5.18: {} emmet@2.4.7: dependencies: