diff --git a/api/lib/src/event/client.dart b/api/lib/src/event/client.dart index f10c3780..80f54ea5 100644 --- a/api/lib/src/event/client.dart +++ b/api/lib/src/event/client.dart @@ -42,7 +42,7 @@ final class ShuffleCellRequest extends ClientWorldEvent @MappableClass() final class PacksChangeRequest extends ClientWorldEvent with PacksChangeRequestMappable { - final Set packs; + final List packs; PacksChangeRequest(this.packs); } diff --git a/api/lib/src/event/event.mapper.dart b/api/lib/src/event/event.mapper.dart index 04c8a47e..78385b35 100644 --- a/api/lib/src/event/event.mapper.dart +++ b/api/lib/src/event/event.mapper.dart @@ -1422,8 +1422,8 @@ class PacksChangeRequestMapper extends SubClassMapperBase { @override final String id = 'PacksChangeRequest'; - static Set _$packs(PacksChangeRequest v) => v.packs; - static const Field> _f$packs = + static List _$packs(PacksChangeRequest v) => v.packs; + static const Field> _f$packs = Field('packs', _$packs); @override @@ -1498,8 +1498,9 @@ extension PacksChangeRequestValueCopy<$R, $Out> abstract class PacksChangeRequestCopyWith<$R, $In extends PacksChangeRequest, $Out> implements ClientWorldEventCopyWith<$R, $In, $Out> { + ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get packs; @override - $R call({Set? packs}); + $R call({List? packs}); PacksChangeRequestCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t); } @@ -1513,7 +1514,11 @@ class _PacksChangeRequestCopyWithImpl<$R, $Out> late final ClassMapperBase $mapper = PacksChangeRequestMapper.ensureInitialized(); @override - $R call({Set? packs}) => + ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get packs => + ListCopyWith($value.packs, (v, t) => ObjectCopyWith(v, $identity, t), + (v) => call(packs: v)); + @override + $R call({List? packs}) => $apply(FieldCopyWithData({if (packs != null) #packs: packs})); @override PacksChangeRequest $make(CopyWithData data) => diff --git a/api/lib/src/event/process/client.dart b/api/lib/src/event/process/client.dart index 768879ec..448ecb3a 100644 --- a/api/lib/src/event/process/client.dart +++ b/api/lib/src/event/process/client.dart @@ -46,7 +46,7 @@ bool isValidClientEvent( table: state.table, info: state.info, id: channel, - packsSignature: assetManager.createSignature(state.info.packs), + packsSignature: assetManager.createSignature(state.info.packs.toSet()), teamMembers: state.teamMembers, ), channel ?? kAnyChannel, diff --git a/api/lib/src/models/info.dart b/api/lib/src/models/info.dart index 48781206..e4bab11f 100644 --- a/api/lib/src/models/info.dart +++ b/api/lib/src/models/info.dart @@ -7,11 +7,11 @@ part 'info.mapper.dart'; @MappableClass() class GameInfo with GameInfoMappable { final Map teams; - final Set packs; + final List packs; const GameInfo({ this.teams = const {}, - this.packs = const {}, + this.packs = const [], }); } diff --git a/api/lib/src/models/info.mapper.dart b/api/lib/src/models/info.mapper.dart index 32322ab2..f3844eae 100644 --- a/api/lib/src/models/info.mapper.dart +++ b/api/lib/src/models/info.mapper.dart @@ -106,9 +106,9 @@ class GameInfoMapper extends ClassMapperBase { static Map _$teams(GameInfo v) => v.teams; static const Field> _f$teams = Field('teams', _$teams, opt: true, def: const {}); - static Set _$packs(GameInfo v) => v.packs; - static const Field> _f$packs = - Field('packs', _$packs, opt: true, def: const {}); + static List _$packs(GameInfo v) => v.packs; + static const Field> _f$packs = + Field('packs', _$packs, opt: true, def: const []); @override final MappableFields fields = const { @@ -171,7 +171,8 @@ abstract class GameInfoCopyWith<$R, $In extends GameInfo, $Out> implements ClassCopyWith<$R, $In, $Out> { MapCopyWith<$R, String, GameTeam, GameTeamCopyWith<$R, GameTeam, GameTeam>> get teams; - $R call({Map? teams, Set? packs}); + ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get packs; + $R call({Map? teams, List? packs}); GameInfoCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); } @@ -188,7 +189,11 @@ class _GameInfoCopyWithImpl<$R, $Out> get teams => MapCopyWith( $value.teams, (v, t) => v.copyWith.$chain(t), (v) => call(teams: v)); @override - $R call({Map? teams, Set? packs}) => + ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get packs => + ListCopyWith($value.packs, (v, t) => ObjectCopyWith(v, $identity, t), + (v) => call(packs: v)); + @override + $R call({Map? teams, List? packs}) => $apply(FieldCopyWithData({ if (teams != null) #teams: teams, if (packs != null) #packs: packs diff --git a/app/lib/board/hand/deck.dart b/app/lib/board/hand/deck.dart index 2f0d24d6..3c644744 100644 --- a/app/lib/board/hand/deck.dart +++ b/app/lib/board/hand/deck.dart @@ -30,12 +30,16 @@ class DeckDefinitionHandItem extends HandItem> { @override void moveItem(HandItemDropZone zone) { if (zone is! GameCell) return; + final location = zone.toDefinition(); bloc.process(ObjectsSpawned( - zone.toDefinition(), + location, item.item.figures .map((e) => GameObject( asset: ItemLocation(item.namespace, e.name), variation: e.variation)) .toList())); + if (bloc.state.switchCellOnMove) { + bloc.process(CellSwitched(location)); + } } } diff --git a/app/lib/board/hand/figure.dart b/app/lib/board/hand/figure.dart index 61733ca5..78e9450a 100644 --- a/app/lib/board/hand/figure.dart +++ b/app/lib/board/hand/figure.dart @@ -1,4 +1,5 @@ import 'package:flame/widgets.dart'; +import 'package:quokka/bloc/world/local.dart'; import 'package:quokka/bloc/world/state.dart'; import 'package:quokka/board/cell.dart'; import 'package:quokka/board/hand/item.dart'; @@ -26,10 +27,14 @@ class FigureDefinitionHandItem @override void moveItem(HandItemDropZone zone) { if (zone is! GameCell) return; - bloc.process(ObjectsSpawned(zone.toDefinition(), [ + final location = zone.toDefinition(); + bloc.process(ObjectsSpawned(location, [ GameObject( asset: ItemLocation(item.$1.namespace, item.$1.id), variation: item.$2) ])); + if (bloc.state.switchCellOnMove) { + bloc.process(CellSwitched(location)); + } } } diff --git a/app/lib/board/hand/view.dart b/app/lib/board/hand/view.dart index bc68a508..af563516 100644 --- a/app/lib/board/hand/view.dart +++ b/app/lib/board/hand/view.dart @@ -50,6 +50,9 @@ class GameHand extends CustomPainterComponent HandItemDropZone { final _scrollView = ScrollViewComponent(direction: Axis.horizontal); + /// Should hand be redrawn + bool _isDirty = true; + GameHand() : super(anchor: Anchor.topLeft, painter: GameHandCustomPainter()); @override @@ -59,10 +62,20 @@ class GameHand extends CustomPainterComponent } @override - void onInitialState(ClientWorldState state) => _buildHand(state); + void update(double dt) { + if (_isDirty) { + _isDirty = false; + if (isMounted) { + _buildHand(bloc.state); + } + } + } + + @override + void onInitialState(ClientWorldState state) => _isDirty = true; @override - void onNewState(ClientWorldState state) => _buildHand(state); + void onNewState(ClientWorldState state) => _isDirty = true; @override void onParentResize(Vector2 maxSize) { diff --git a/app/lib/helpers/secondary.dart b/app/lib/helpers/secondary.dart index 7c7ad71a..20bf3fc8 100644 --- a/app/lib/helpers/secondary.dart +++ b/app/lib/helpers/secondary.dart @@ -1,13 +1,14 @@ import 'package:flame/events.dart'; import 'package:flame/game.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; mixin SecondaryTapCallbacks { bool onSecondaryTapUp(TapUpInfo info) => false; } mixin DetailsTapCallbacks on SecondaryTapCallbacks, DoubleTapCallbacks { - Vector2 _position = Vector2.zero(); + Vector2? _position; @override @mustCallSuper @@ -19,12 +20,18 @@ mixin DetailsTapCallbacks on SecondaryTapCallbacks, DoubleTapCallbacks { @override @mustCallSuper void onDoubleTapDown(DoubleTapDownEvent event) { - _position = event.devicePosition; + _position = event.deviceKind == PointerDeviceKind.touch + ? event.devicePosition + : null; } @override @mustCallSuper - void onDoubleTapUp(DoubleTapEvent event) => onContextMenu(_position); + void onDoubleTapUp(DoubleTapEvent event) { + final position = _position; + if (position == null) return; + onContextMenu(position); + } void onContextMenu(Vector2 position); } diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index f1c00c40..7f107c1d 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -139,5 +139,7 @@ "export": "Export", "editInfo": "Edit information", "invalidPacks": "The server requested packs that doesn't exist on this client", - "switchCellOnMove": "Switch cell on move" + "switchCellOnMove": "Switch cell on move", + "addPack": "Add pack", + "removePack": "Remove pack" } diff --git a/app/lib/pages/home/create.dart b/app/lib/pages/home/create.dart index b47d247a..c10bec4c 100644 --- a/app/lib/pages/home/create.dart +++ b/app/lib/pages/home/create.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -19,7 +20,7 @@ class CreateDialog extends StatefulWidget { class _CreateDialogState extends State with TickerProviderStateMixin { - //late final TabController _tabController, _customTabController; + late final TabController _tabController, _customTabController; final PageController _pageController = PageController(keepPage: true); final GlobalKey _pageKey = GlobalKey(); final TextEditingController _nameController = TextEditingController(), @@ -27,8 +28,10 @@ class _CreateDialogState extends State late final TypedKeyFileSystem _templateSystem, _worldSystem; late final QuokkaFileSystem _fileSystem; late Stream>> _templatesStream; + late final Future>> _packsFuture; String? _selectedTemplate; + List? _selectedPacks; bool _infoView = false; @@ -38,23 +41,24 @@ class _CreateDialogState extends State _fileSystem = context.read(); _worldSystem = _fileSystem.worldSystem; _templateSystem = _fileSystem.templateSystem; - _templatesStream = ValueConnectableStream(_loadPacks()).autoConnect(); - //_tabController = TabController(length: 2, vsync: this); - //_customTabController = TabController(length: 2, vsync: this); + _templatesStream = ValueConnectableStream(_loadTemplates()).autoConnect(); + _packsFuture = _fileSystem.getPacks(); + _tabController = TabController(length: 2, vsync: this); + _customTabController = TabController(length: 2, vsync: this); } - void _reloadTemplates() => setState(() => - _templatesStream = ValueConnectableStream(_loadPacks()).autoConnect()); + void _reloadTemplates() => setState(() => _templatesStream = + ValueConnectableStream(_loadTemplates()).autoConnect()); - Stream>> _loadPacks() async* { + Stream>> _loadTemplates() async* { await _templateSystem.initialize(); yield* _templateSystem.fetchFiles(); } @override void dispose() { - //_tabController.dispose(); - //_customTabController.dispose(); + _tabController.dispose(); + _customTabController.dispose(); _pageController.dispose(); super.dispose(); } @@ -62,80 +66,29 @@ class _CreateDialogState extends State @override Widget build(BuildContext context) { final isMobile = MediaQuery.sizeOf(context).width < LeapBreakpoints.medium; - final selections = /*Column( + final selections = Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TabBar( controller: _tabController, indicatorSize: TabBarIndicatorSize.tab, tabs: [ - HorizontalTab( - icon: const PhosphorIcon(PhosphorIconsLight.folder), - label: Text(AppLocalizations.of(context).installed), - ), HorizontalTab( icon: const PhosphorIcon(PhosphorIconsLight.globe), label: Text(AppLocalizations.of(context).custom), ), + HorizontalTab( + icon: const PhosphorIcon(PhosphorIconsLight.folder), + label: Text(AppLocalizations.of(context).templates), + ), ], ), const SizedBox(height: 16), Expanded( child: TabBarView( controller: _tabController, - children: [*/ - Material( - type: MaterialType.transparency, - child: StreamBuilder( - stream: _templatesStream, - builder: (context, snapshot) { - final templates = snapshot.data; - if (templates == null) { - return const Center(child: CircularProgressIndicator()); - } - return ListView.builder( - itemCount: templates.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - return ListTile( - title: Text(AppLocalizations.of(context).blank), - selected: _selectedTemplate == null, - onTap: () => setState(() => _selectedTemplate = null), - ); - } - index--; - final entry = templates[index]; - final name = entry.pathWithoutLeadingSlash; - return ListTile( - title: Text(name), - trailing: MenuAnchor( - builder: defaultMenuButton(), - menuChildren: [ - MenuItemButton( - leadingIcon: const Icon(PhosphorIconsLight.export), - child: Text(AppLocalizations.of(context).export), - onPressed: () => - exportData(context, entry.data!, entry.fileName), - ), - MenuItemButton( - leadingIcon: const Icon(PhosphorIconsLight.trash), - child: Text(AppLocalizations.of(context).delete), - onPressed: () async { - await _templateSystem.deleteFile(entry.path); - _reloadTemplates(); - }, - ) - ], - ), - selected: _selectedTemplate == name, - onTap: () => setState(() => _selectedTemplate = entry.fileName), - ); - }, - ); - }, - ), - ) - /*Column(children: [ + children: [ + Column(children: [ TabBar.secondary( tabs: [ HorizontalTab( @@ -157,29 +110,21 @@ class _CreateDialogState extends State child: TabBarView( controller: _customTabController, children: [ - ListView.builder( - itemCount: 30, - shrinkWrap: true, - itemBuilder: (context, index) { - return CheckboxListTile( - title: Text('Custom ${index + 1}'), - value: false, - onChanged: (bool? value) {}, - ); - }, + _CustomCreateView( + packsFuture: _packsFuture, + selectedPacksId: _selectedPacks, + onPacksSelected: (value) => + setState(() => _selectedPacks = value), ), ListView( children: [ ListTile( title: Text(AppLocalizations.of(context).background), - subtitle: const Text('Not set'), + subtitle: + Text(AppLocalizations.of(context).comingSoon), onTap: () => Navigator.of(context).pop(), ), - const ListTile( - title: Text('Rules'), - subtitle: Text('Coming soon'), - ), ], ) ], @@ -187,12 +132,59 @@ class _CreateDialogState extends State ), ), ]), + Material( + type: MaterialType.transparency, + child: StreamBuilder( + stream: _templatesStream, + builder: (context, snapshot) { + final templates = snapshot.data; + if (templates == null) { + return const Center(child: CircularProgressIndicator()); + } + return ListView.builder( + itemCount: templates.length, + itemBuilder: (context, index) { + final entry = templates[index]; + final name = entry.pathWithoutLeadingSlash; + return ListTile( + title: Text(name), + trailing: MenuAnchor( + builder: defaultMenuButton(), + menuChildren: [ + MenuItemButton( + leadingIcon: + const Icon(PhosphorIconsLight.export), + child: + Text(AppLocalizations.of(context).export), + onPressed: () => exportData( + context, entry.data!, entry.fileName), + ), + MenuItemButton( + leadingIcon: + const Icon(PhosphorIconsLight.trash), + child: + Text(AppLocalizations.of(context).delete), + onPressed: () async { + await _templateSystem.deleteFile(entry.path); + _reloadTemplates(); + }, + ) + ], + ), + selected: _selectedTemplate == name, + onTap: () => setState( + () => _selectedTemplate = entry.fileName), + ); + }, + ); + }, + ), + ), ], ), ), ], - )*/ - ; + ); final details = ListView( children: [ Text(AppLocalizations.of(context).details, @@ -288,10 +280,16 @@ class _CreateDialogState extends State onPressed: () async { final name = _nameController.text; final description = _descriptionController.text; - var template = _selectedTemplate == null - ? null - : await _templateSystem.getFile(_selectedTemplate!); - template ??= QuokkaData.empty(); + var template = + _selectedTemplate == null || _tabController.index == 0 + ? null + : await _templateSystem.getFile(_selectedTemplate!); + template ??= QuokkaData.empty().setInfo( + GameInfo( + packs: _selectedPacks ?? + (await _packsFuture).map((e) => e.path).toList(), + ), + ); template = template.setFileMetadata( FileMetadata( name: name, @@ -313,3 +311,108 @@ class _CreateDialogState extends State ); } } + +class _CustomCreateView extends StatelessWidget { + final Future>> packsFuture; + final List? selectedPacksId; + final void Function(List) onPacksSelected; + + const _CustomCreateView({ + required this.packsFuture, + required this.selectedPacksId, + required this.onPacksSelected, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder>>( + future: packsFuture, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + final allPacks = snapshot.data!; + final addedPacks = selectedPacksId + ?.map((e) => + allPacks.firstWhereOrNull((element) => element.path == e)) + .nonNulls + .toList() ?? + allPacks; + final notAdded = allPacks + .where((e) => !(selectedPacksId?.contains(e.path) ?? true)) + .toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ReorderableListView.builder( + itemCount: addedPacks.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final pack = addedPacks[index]; + return ListTile( + title: Text(pack.data?.getMetadata()?.name ?? + AppLocalizations.of(context).unnamed), + subtitle: Text(pack.pathWithoutLeadingSlash), + key: ObjectKey(pack.path), + leading: IconButton.outlined( + icon: const Icon(PhosphorIconsLight.minus), + onPressed: () { + final newSelected = addedPacks + .map((e) => e.path) + .where((e) => e != pack.path) + .toList(); + onPacksSelected(newSelected); + }, + ), + ); + }, + onReorder: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final newSelected = addedPacks.map((e) => e.path).toList(); + final item = newSelected.removeAt(oldIndex); + newSelected.insert(newIndex, item); + onPacksSelected(newSelected); + }, + ), + ), + SizedBox( + height: 42, + child: ElevatedButton.icon( + icon: const Icon(PhosphorIconsLight.plus), + label: Text(AppLocalizations.of(context).addPack), + onPressed: notAdded.isEmpty + ? null + : () { + showLeapBottomSheet( + context: context, + titleBuilder: (context) => + Text(AppLocalizations.of(context).addPack), + childrenBuilder: (context) => notAdded.map((e) { + return ListTile( + title: Text(e.data!.getMetadata()?.name ?? + AppLocalizations.of(context).unnamed), + subtitle: Text(e.pathWithoutLeadingSlash), + onTap: () { + Navigator.of(context).pop(); + onPacksSelected([ + ...?selectedPacksId, + e.path, + ]); + }, + ); + }).toList(), + ); + }, + ), + ), + ], + ); + }); + } +} diff --git a/app/lib/pages/home/packs.dart b/app/lib/pages/home/packs.dart index ad2c0872..729e66d7 100644 --- a/app/lib/pages/home/packs.dart +++ b/app/lib/pages/home/packs.dart @@ -210,63 +210,22 @@ class _PacksDialogState extends State .contains(query) ?? entry.fileName.toLowerCase().contains(query)) .toList(); - - var worldPacks = const >[]; final bloc = widget.bloc; - if (bloc != null) { - worldPacks = bloc.assetManager.packs.toList(); - } - return TabBarView( controller: _tabController, children: [ - if (isWorldLoaded) - BlocBuilder( + if (bloc != null) + _WorldPacksView( bloc: bloc, - buildWhen: (previous, current) => - previous.info.packs != current.info.packs, - builder: (context, state) => ListView.builder( - itemCount: worldPacks.length, - itemBuilder: (context, index) { - final entry = worldPacks[index]; - final id = entry.key; - final data = entry.value; - final metadata = data.getMetadata(); - return CheckboxListTile( - title: Text(metadata?.name ?? - AppLocalizations.of(context).unnamed), - subtitle: Text(id), - value: state.info.packs.contains(id), - onChanged: (value) { - final packs = - Set.from(state.info.packs); - if (value ?? false) { - packs.add(id); - } else { - packs.remove(id); - } - bloc?.process(PacksChangeRequest(packs)); - }, - ); - }, - ), ), - ListView.builder( - itemCount: filtered.length, - itemBuilder: (context, index) { - final pack = packs[index]; - final key = pack.pathWithoutLeadingSlash; - final data = pack.data!; - final metadata = data.getMetadata(); - return ListTile( - title: Text(metadata?.name ?? - AppLocalizations.of(context).unnamed), - subtitle: Text(key), - selected: _selectedPack?.$1 == data && - (!isMobile || _isMobileOpen), - onTap: () => selectPack(data, key, true), - ); - }, + _InstalledPacksView( + filtered: filtered, + packs: packs, + selectedPack: _selectedPack, + isMobile: isMobile, + isMobileOpen: _isMobileOpen, + bloc: bloc, + selectPack: selectPack, ), Center( child: @@ -371,3 +330,124 @@ class _PacksDialogState extends State ); } } + +class _InstalledPacksView extends StatelessWidget { + final List> filtered; + final List> packs; + final (QuokkaData, String, bool)? selectedPack; + final void Function(QuokkaData, String, bool) selectPack; + final bool isMobile; + final bool isMobileOpen; + final WorldBloc? bloc; + + const _InstalledPacksView({ + required this.filtered, + required this.packs, + required this.selectedPack, + required this.selectPack, + required this.isMobile, + required this.isMobileOpen, + required this.bloc, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: filtered.length, + itemBuilder: (context, index) { + final pack = packs[index]; + final key = pack.pathWithoutLeadingSlash; + final data = pack.data!; + final metadata = data.getMetadata(); + return ListTile( + title: Text(metadata?.name ?? AppLocalizations.of(context).unnamed), + subtitle: Text(key), + selected: selectedPack?.$1 == data && (!isMobile || isMobileOpen), + onTap: () => selectPack(data, key, true), + leading: bloc != null + ? BlocBuilder( + bloc: bloc, + buildWhen: (previous, current) => + previous.info.packs != current.info.packs, + builder: (context, state) { + return IconButton.outlined( + icon: const Icon(PhosphorIconsLight.plus), + tooltip: AppLocalizations.of(context).addPack, + onPressed: state.info.packs.contains(key) + ? null + : () { + final packs = [ + ...bloc!.state.info.packs, + key, + ]; + bloc!.process( + PacksChangeRequest(packs), + ); + }, + ); + }) + : null, + ); + }, + ); + } +} + +class _WorldPacksView extends StatelessWidget { + const _WorldPacksView({ + required this.bloc, + }); + + final WorldBloc bloc; + + @override + Widget build(BuildContext context) { + final loadedPacks = bloc.assetManager.packs.toList(); + return BlocBuilder( + bloc: bloc, + buildWhen: (previous, current) => + previous.info.packs != current.info.packs, + builder: (context, state) { + final worldPacks = loadedPacks + .where((entry) => state.info.packs.contains(entry.key)) + .toList(); + return ReorderableListView.builder( + itemCount: worldPacks.length, + itemBuilder: (context, index) { + final entry = worldPacks[index]; + final id = entry.key; + final data = entry.value; + final metadata = data.getMetadata(); + return ListTile( + key: ValueKey(id), + title: + Text(metadata?.name ?? AppLocalizations.of(context).unnamed), + subtitle: Text(id), + leading: IconButton.outlined( + icon: const Icon(PhosphorIconsLight.minus), + tooltip: AppLocalizations.of(context).removePack, + onPressed: () { + final packs = List.from(state.info.packs)..remove(id); + bloc.process( + PacksChangeRequest(packs), + ); + }, + ), + ); + }, + onReorder: (int oldIndex, int newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final packs = List.from(state.info.packs); + final pack = packs.removeAt(oldIndex); + packs.insert(newIndex, pack); + bloc.process( + PacksChangeRequest(packs), + ); + }, + ); + }, + ); + } +} diff --git a/app/pubspec.lock b/app/pubspec.lock index 81931e1e..aed655c9 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -957,10 +957,10 @@ packages: dependency: transitive description: name: sembast - sha256: "481e0a4199015e0050ee4b42d59d51731b1fb324a1eea5c24557fa72335790b0" + sha256: "99f9d53ecdc75db1630797a8b9b2f2050ee5377e49f60f5f6e788431db04224d" url: "https://pub.dev" source: hosted - version: "3.7.3+2" + version: "3.7.3+3" share_plus: dependency: transitive description: