Merge: Ctrl+O shortcut and bulk copy-slides-to-another-deck
Some checks failed
CI / Analyze & test (push) Has been cancelled
Some checks failed
CI / Analyze & test (push) Has been cancelled
This commit is contained in:
commit
fce4278b9e
3 changed files with 118 additions and 0 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;
|
||||
|
||||
static const _imageExtensions = {
|
||||
|
|
@ -370,6 +376,9 @@ class _AppShellState extends ConsumerState<AppShell> with WindowListener {
|
|||
const SingleActivator(LogicalKeyboardKey.keyS, control: 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(
|
||||
autofocus: true,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import '../../models/slide.dart';
|
|||
import '../../state/deck_provider.dart';
|
||||
import '../../state/editor_provider.dart';
|
||||
import '../../state/settings_provider.dart';
|
||||
import '../../state/tabs_provider.dart';
|
||||
import '../../services/image_service.dart';
|
||||
import '../../services/slide_rasterizer.dart';
|
||||
import '../../state/slide_clipboard_provider.dart';
|
||||
|
|
@ -186,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.
|
||||
void _deleteSelection() {
|
||||
final deck = ref.read(deckProvider).deck;
|
||||
|
|
@ -497,6 +570,7 @@ class _SlideListPanelState extends ConsumerState<SlideListPanel> {
|
|||
const SizedBox(height: 6),
|
||||
_BulkActionBar(
|
||||
count: editor.selection.length,
|
||||
onCopyToDeck: _copySelectionToOtherDeck,
|
||||
onDelete: _deleteSelection,
|
||||
onSkip: () => notifier.setSkippedForSlides(
|
||||
editor.selection,
|
||||
|
|
@ -779,6 +853,7 @@ class _SkipBanner extends StatelessWidget {
|
|||
|
||||
class _BulkActionBar extends StatelessWidget {
|
||||
final int count;
|
||||
final VoidCallback onCopyToDeck;
|
||||
final VoidCallback onDelete;
|
||||
final VoidCallback onSkip;
|
||||
final VoidCallback onShow;
|
||||
|
|
@ -786,6 +861,7 @@ class _BulkActionBar extends StatelessWidget {
|
|||
|
||||
const _BulkActionBar({
|
||||
required this.count,
|
||||
required this.onCopyToDeck,
|
||||
required this.onDelete,
|
||||
required this.onSkip,
|
||||
required this.onShow,
|
||||
|
|
@ -813,6 +889,11 @@ class _BulkActionBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
_BulkIcon(
|
||||
icon: Icons.drive_file_move_outline,
|
||||
tooltip: 'Kopiëren naar ander deck',
|
||||
onTap: onCopyToDeck,
|
||||
),
|
||||
_BulkIcon(
|
||||
icon: Icons.visibility_off_outlined,
|
||||
tooltip: 'Overslaan bij presenteren/exporteren',
|
||||
|
|
|
|||
|
|
@ -61,6 +61,34 @@ void main() {
|
|||
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', () {
|
||||
final n = _notifier()..newDeck('D');
|
||||
n.addSlide(SlideType.bullets); // 1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue