Add Ctrl/Cmd+O open shortcut and bulk copy-slides-to-another-deck
- Bind Ctrl/Cmd+O app-wide to the open-presentation dialog (was unbound). - Add a "copy to another deck" bulk action to the slide list: with multiple slides selected, pick a target open tab; the slides are appended there as fresh copies, leaving the source deck untouched. Multi-select, bulk delete and bulk skip/show already existed. Test: cross-deck copy keeps the source intact and assigns fresh ids. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e63679978b
commit
01cc1c2ecd
3 changed files with 119 additions and 2 deletions
|
|
@ -299,6 +299,12 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open een presentatie via de zoek-/kies-dialoog. App-breed zodat Ctrl/Cmd+O
|
||||||
|
/// altijd werkt, ongeacht waar de focus zit.
|
||||||
|
void _openActive() {
|
||||||
|
_openWithSearch(context, ref, ref.read(settingsProvider).homeDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
bool _dragging = false;
|
bool _dragging = false;
|
||||||
|
|
||||||
static const _imageExtensions = {
|
static const _imageExtensions = {
|
||||||
|
|
@ -370,6 +376,9 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
||||||
const SingleActivator(LogicalKeyboardKey.keyS, control: true):
|
const SingleActivator(LogicalKeyboardKey.keyS, control: true):
|
||||||
_saveActive,
|
_saveActive,
|
||||||
const SingleActivator(LogicalKeyboardKey.keyS, meta: true): _saveActive,
|
const SingleActivator(LogicalKeyboardKey.keyS, meta: true): _saveActive,
|
||||||
|
const SingleActivator(LogicalKeyboardKey.keyO, control: true):
|
||||||
|
_openActive,
|
||||||
|
const SingleActivator(LogicalKeyboardKey.keyO, meta: true): _openActive,
|
||||||
},
|
},
|
||||||
child: FocusScope(
|
child: FocusScope(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '../../models/slide.dart';
|
||||||
import '../../state/deck_provider.dart';
|
import '../../state/deck_provider.dart';
|
||||||
import '../../state/editor_provider.dart';
|
import '../../state/editor_provider.dart';
|
||||||
import '../../state/settings_provider.dart';
|
import '../../state/settings_provider.dart';
|
||||||
|
import '../../state/tabs_provider.dart';
|
||||||
import '../../services/image_service.dart';
|
import '../../services/image_service.dart';
|
||||||
import '../../services/slide_rasterizer.dart';
|
import '../../services/slide_rasterizer.dart';
|
||||||
import '../../state/slide_clipboard_provider.dart';
|
import '../../state/slide_clipboard_provider.dart';
|
||||||
|
|
@ -174,8 +175,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ok =
|
final ok =
|
||||||
bytes != null &&
|
bytes != null && await ImageService().copyImageBytesToClipboard(bytes);
|
||||||
await ImageService().copyImageBytesToClipboard(bytes);
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
messenger.hideCurrentSnackBar();
|
messenger.hideCurrentSnackBar();
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
|
|
@ -187,6 +187,78 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// De geselecteerde slides, op volgorde van positie in het deck.
|
||||||
|
List<Slide> _selectedSlides(Deck deck) {
|
||||||
|
final indices = ref.read(editorProvider).selection.toList()..sort();
|
||||||
|
return [
|
||||||
|
for (final i in indices)
|
||||||
|
if (i >= 0 && i < deck.slides.length) deck.slides[i],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kopieer de geselecteerde slides (bulk) naar een ander open deck. Toont een
|
||||||
|
/// keuzelijst van de overige open tabbladen; de slides worden achteraan dat
|
||||||
|
/// deck toegevoegd (met nieuwe id's, zodat het kopieën zijn).
|
||||||
|
Future<void> _copySelectionToOtherDeck() async {
|
||||||
|
final deck = ref.read(deckProvider).deck;
|
||||||
|
if (deck == null) return;
|
||||||
|
final slides = _selectedSlides(deck);
|
||||||
|
if (slides.isEmpty) return;
|
||||||
|
|
||||||
|
final tabs = ref.read(tabsProvider);
|
||||||
|
final currentId = tabs.current?.id;
|
||||||
|
final targets = tabs.tabs
|
||||||
|
.where((t) => t.id != currentId && t.isOpen)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
if (targets.isEmpty) {
|
||||||
|
messenger.showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Geen ander deck open. Open eerst een ander tabblad.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final target = await showDialog<TabInfo>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => SimpleDialog(
|
||||||
|
title: Text(
|
||||||
|
slides.length == 1
|
||||||
|
? '1 slide kopiëren naar…'
|
||||||
|
: '${slides.length} slides kopiëren naar…',
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
for (final t in targets)
|
||||||
|
SimpleDialogOption(
|
||||||
|
onPressed: () => Navigator.pop(ctx, t),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.slideshow_outlined, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(child: Text(t.label)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (target == null || !mounted) return;
|
||||||
|
|
||||||
|
final at = target.deckNotifier.insertSlides(slides);
|
||||||
|
if (!mounted) return;
|
||||||
|
messenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
at >= 0
|
||||||
|
? '${slides.length} slide(s) gekopieerd naar “${target.label}”.'
|
||||||
|
: 'Kopiëren mislukt.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Verwijder alle geselecteerde slides (bulk). Houdt minstens één over.
|
/// Verwijder alle geselecteerde slides (bulk). Houdt minstens één over.
|
||||||
void _deleteSelection() {
|
void _deleteSelection() {
|
||||||
final deck = ref.read(deckProvider).deck;
|
final deck = ref.read(deckProvider).deck;
|
||||||
|
|
@ -498,6 +570,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_BulkActionBar(
|
_BulkActionBar(
|
||||||
count: editor.selection.length,
|
count: editor.selection.length,
|
||||||
|
onCopyToDeck: _copySelectionToOtherDeck,
|
||||||
onDelete: _deleteSelection,
|
onDelete: _deleteSelection,
|
||||||
onSkip: () => notifier.setSkippedForSlides(
|
onSkip: () => notifier.setSkippedForSlides(
|
||||||
editor.selection,
|
editor.selection,
|
||||||
|
|
@ -780,6 +853,7 @@ class _SkipBanner extends StatelessWidget {
|
||||||
|
|
||||||
class _BulkActionBar extends StatelessWidget {
|
class _BulkActionBar extends StatelessWidget {
|
||||||
final int count;
|
final int count;
|
||||||
|
final VoidCallback onCopyToDeck;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
final VoidCallback onSkip;
|
final VoidCallback onSkip;
|
||||||
final VoidCallback onShow;
|
final VoidCallback onShow;
|
||||||
|
|
@ -787,6 +861,7 @@ class _BulkActionBar extends StatelessWidget {
|
||||||
|
|
||||||
const _BulkActionBar({
|
const _BulkActionBar({
|
||||||
required this.count,
|
required this.count,
|
||||||
|
required this.onCopyToDeck,
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
required this.onSkip,
|
required this.onSkip,
|
||||||
required this.onShow,
|
required this.onShow,
|
||||||
|
|
@ -814,6 +889,11 @@ class _BulkActionBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_BulkIcon(
|
||||||
|
icon: Icons.drive_file_move_outline,
|
||||||
|
tooltip: 'Kopiëren naar ander deck',
|
||||||
|
onTap: onCopyToDeck,
|
||||||
|
),
|
||||||
_BulkIcon(
|
_BulkIcon(
|
||||||
icon: Icons.visibility_off_outlined,
|
icon: Icons.visibility_off_outlined,
|
||||||
tooltip: 'Overslaan bij presenteren/exporteren',
|
tooltip: 'Overslaan bij presenteren/exporteren',
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,34 @@ void main() {
|
||||||
expect(n.state.deck!.slides[1].id, isNot(source.id));
|
expect(n.state.deck!.slides[1].id, isNot(source.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('copying a selection into another deck leaves the source intact', () {
|
||||||
|
// Source deck with three slides; "select" indices 0 and 2.
|
||||||
|
final source = _notifier()..newDeck('Bron');
|
||||||
|
source.addSlide(SlideType.bullets); // 1
|
||||||
|
source.addSlide(SlideType.image); // 2
|
||||||
|
final picked = [source.state.deck!.slides[0], source.state.deck!.slides[2]];
|
||||||
|
final sourceIdsBefore = source.state.deck!.slides.map((s) => s.id).toList();
|
||||||
|
|
||||||
|
// Target deck receives the copies (appended at the end).
|
||||||
|
final target = _notifier()..newDeck('Doel');
|
||||||
|
final at = target.insertSlides(picked);
|
||||||
|
|
||||||
|
expect(at, 1); // appended after the single title slide
|
||||||
|
expect(target.state.deck!.slides, hasLength(3));
|
||||||
|
expect(target.state.deck!.slides[1].type, SlideType.title);
|
||||||
|
expect(target.state.deck!.slides[2].type, SlideType.image);
|
||||||
|
// Copies must have fresh ids, not the source ids.
|
||||||
|
expect(
|
||||||
|
target.state.deck!.slides[1].id,
|
||||||
|
isNot(anyOf(picked.map((s) => s.id))),
|
||||||
|
);
|
||||||
|
// The source deck is untouched.
|
||||||
|
expect(
|
||||||
|
source.state.deck!.slides.map((s) => s.id).toList(),
|
||||||
|
sourceIdsBefore,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('reorderSlides moves a slide to a new position', () {
|
test('reorderSlides moves a slide to a new position', () {
|
||||||
final n = _notifier()..newDeck('D');
|
final n = _notifier()..newDeck('D');
|
||||||
n.addSlide(SlideType.bullets); // 1
|
n.addSlide(SlideType.bullets); // 1
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue