Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
2 changes: 0 additions & 2 deletions demo/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
import FlutterMacOS
import Foundation

import path_provider_foundation
import screen_retriever_macos
import sqflite_darwin
import webview_flutter_wkwebview
import window_manager

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
Expand Down
9 changes: 1 addition & 8 deletions demo/macos/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
PODS:
- FlutterMacOS (1.0.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- screen_retriever_macos (0.0.1):
- FlutterMacOS
- sqflite_darwin (0.0.4):
Expand All @@ -16,7 +13,6 @@ PODS:

DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`)
Expand All @@ -25,8 +21,6 @@ DEPENDENCIES:
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
screen_retriever_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
sqflite_darwin:
Expand All @@ -38,10 +32,9 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c

PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3
Expand Down
2 changes: 1 addition & 1 deletion demo/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ environment:
dependencies:
flutter:
sdk: flutter
google_fonts: ^6.3.2
google_fonts: ^8.1.0
mesh: ^0.4.3
mix: ^2.0.3
superdeck_core: ^1.0.0
Expand Down
1 change: 1 addition & 0 deletions demo/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
)

set(PLUGIN_BUNDLED_LIBRARIES)
Expand Down
4 changes: 4 additions & 0 deletions packages/builder/lib/superdeck_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export 'package:superdeck_core/superdeck_core.dart' show DeckFormatException;

export 'src/build/build_event.dart';
export 'src/build/deck_builder.dart';

export 'src/parsers/comment_parser.dart';
export 'src/parsers/markdown_parser.dart';
export 'src/parsers/section_parser.dart';
11 changes: 10 additions & 1 deletion packages/superdeck/lib/src/capture/slide_capture_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show MaterialApp, Scaffold, Theme;
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:mix/mix.dart';
import '../ui/tokens/colors.dart';
import '../ui/widgets/provider.dart';

import '../rendering/slides/slide_view.dart';
Expand Down Expand Up @@ -117,14 +119,21 @@ class SlideCaptureService {
RenderConfig config,
) async {
try {
final mixScope = MixScope.maybeOf(config.context);
final child = InheritedTheme.captureAll(
config.context,
MediaQuery(
data: MediaQuery.of(config.context),
child: MaterialApp(
theme: Theme.of(config.context),
debugShowCheckedModeBanner: false,
home: Scaffold(body: widget),

home: Scaffold(
body: MixScope(
tokens: {...?mixScope?.tokens, ...SDColors.colorMap},
child: widget,
),
),
),
),
);
Expand Down
16 changes: 16 additions & 0 deletions packages/superdeck/lib/src/deck/deck_presentation_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,22 @@ final class DeckPresentationState {
return _thumbnails.value[slideKey];
}

/// Deletes every cached thumbnail (in-memory and persistent).
///
/// After this completes, [getThumbnail] returns `null` for every slide
/// until [generateThumbnails] is called again.
Future<void> deleteAllThumbnails() async {
if (_disposed) return;
await _thumbnailService.deleteAllThumbnails(
slides: _slides.value,
cache: _thumbnails.value,
onCacheUpdate: (updated) {
if (_disposed) return;
_thumbnails.value = updated;
},
);
}

void dispose() {
_disposed = true;
_indexClampEffect?.call();
Expand Down
10 changes: 4 additions & 6 deletions packages/superdeck/lib/src/rendering/slides/slide_parts.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import 'package:flutter/widgets.dart';
import 'background.dart';
import 'footer.dart';
import 'header.dart';

class SlideParts {
const SlideParts({
this.header = const HeaderPart(),
this.footer = const FooterPart(),
this.header,
this.footer,
this.background = const BackgroundPart(),
});

final PreferredSizeWidget header;
final PreferredSizeWidget footer;
final PreferredSizeWidget? header;
final PreferredSizeWidget? footer;
final Widget background;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class SlideRenderView extends StatelessWidget {

@override
Widget build(BuildContext context) {
return MixScope(
return MixScope.inherit(
colors: SDColors.colorMap,
child: InheritedData(
data: configuration,
Expand Down
25 changes: 25 additions & 0 deletions packages/superdeck/lib/src/thumbnails/thumbnail_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,31 @@ class ThumbnailService {
onCacheUpdate(updatedCache);
}

/// Deletes every cached thumbnail and clears the in-memory cache.
///
/// Disposes every [AsyncThumbnail] in [cache] and removes the corresponding
/// entry from the asset cache store. Keys to delete are taken from both the
/// disposed [AsyncThumbnail]s (so orphan entries for removed slides are
/// still cleaned) and the current [slides] list. After completion,
/// [onCacheUpdate] is invoked with an empty map.
Future<void> deleteAllThumbnails({
required List<SlideConfiguration> slides,
required Map<String, AsyncThumbnail> cache,
required void Function(Map<String, AsyncThumbnail>) onCacheUpdate,
}) async {
final keysToDelete = <String>{
for (final thumbnail in cache.values) thumbnail.thumbnailKey,
for (final slide in slides) slide.thumbnailKey,
};

for (final thumbnail in cache.values) {
thumbnail.dispose();
}
onCacheUpdate(<String, AsyncThumbnail>{});

await Future.wait(keysToDelete.map(_cacheStore.delete));
}

/// Generates a single thumbnail for a slide.
///
/// Resolve order:
Expand Down
16 changes: 14 additions & 2 deletions packages/superdeck/lib/src/ui/widgets/hero_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,22 @@ Widget buildElementHero<T>({
final to = HeroElement.of<T>(toHeroContext);
final from = HeroElement.maybeOf<T>(fromHeroContext) ?? to;

// The shuttle is built inside the Navigator's Overlay, which sits
// outside the route's Material and therefore exposes a different
// DefaultTextStyle than the slide. Text properties an element spec
// leaves unset (letterSpacing, leadingDistribution, ...) would
// otherwise resolve against the Overlay's bare default and make the
// text change size the instant the Hero hands off to/from the real
// widget. Re-apply the source slide's DefaultTextStyle so the
// shuttle resolves identically to the widget it stands in for.
final slideTextStyle = DefaultTextStyle.of(fromHeroContext).style;

return AnimatedBuilder(
animation: animation,
builder: (context, _) =>
buildFlight(context, from, to, animation.value),
builder: (context, _) => DefaultTextStyle.merge(
style: slideTextStyle,
child: buildFlight(context, from, to, animation.value),
),
);
},
);
Expand Down
2 changes: 1 addition & 1 deletion packages/superdeck/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dependencies:
web: ^1.1.0
webview_flutter: ^4.10.0
webview_flutter_web: ^0.2.3
google_fonts: ^6.3.2
google_fonts: ^8.1.0
meta: ^1.16.0
qr_flutter: ^4.1.0
signals: ^6.2.0
Expand Down
60 changes: 60 additions & 0 deletions packages/superdeck/test/src/deck/deck_presentation_state_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class _RecordingThumbnailService extends ThumbnailService {
_RecordingThumbnailService() : super(cacheStore: NoopAssetCacheStore());

int callCount = 0;
int deleteAllCallCount = 0;
final List<Set<String>> receivedCacheKeys = <Set<String>>[];
final Map<String, _TrackableAsyncThumbnail> trackedThumbnails =
<String, _TrackableAsyncThumbnail>{};
Expand Down Expand Up @@ -61,6 +62,19 @@ class _RecordingThumbnailService extends ThumbnailService {

onCacheUpdate(updatedCache);
}

@override
Future<void> deleteAllThumbnails({
required List<SlideConfiguration> slides,
required Map<String, AsyncThumbnail> cache,
required void Function(Map<String, AsyncThumbnail>) onCacheUpdate,
}) async {
deleteAllCallCount++;
for (final thumbnail in cache.values) {
thumbnail.dispose();
}
onCacheUpdate(<String, AsyncThumbnail>{});
}
}

Future<BuildContext> _pumpContext(WidgetTester tester) async {
Expand Down Expand Up @@ -100,6 +114,52 @@ void main() {
expect(service.receivedCacheKeys.last, equals({'slide-0'}));
});

testWidgets('deleteAllThumbnails disposes cache and clears getThumbnail', (
tester,
) async {
final slides = signal<List<SlideConfiguration>>(createTestSlides(2));
addTearDown(slides.dispose);

final service = _RecordingThumbnailService();
final state = DeckPresentationState(
thumbnailService: service,
slides: slides,
transitionDuration: Duration.zero,
);
addTearDown(state.dispose);

final context = await _pumpContext(tester);
state.generateThumbnails(context, slides.value);
final slide0 = service.trackedThumbnails['slide-0']!;
final slide1 = service.trackedThumbnails['slide-1']!;

await state.deleteAllThumbnails();

expect(service.deleteAllCallCount, 1);
expect(state.getThumbnail('slide-0'), isNull);
expect(state.getThumbnail('slide-1'), isNull);
expect(slide0.disposed, isTrue);
expect(slide1.disposed, isTrue);
});

testWidgets('deleteAllThumbnails is a no-op after dispose', (tester) async {
final slides = signal<List<SlideConfiguration>>(createTestSlides(1));
addTearDown(slides.dispose);

final service = _RecordingThumbnailService();
final state = DeckPresentationState(
thumbnailService: service,
slides: slides,
transitionDuration: Duration.zero,
);

state.dispose();
await state.deleteAllThumbnails();

expect(service.deleteAllCallCount, 0);
expect(tester.takeException(), isNull);
});

testWidgets('dispose tears down thumbnail cache and blocks later updates', (
tester,
) async {
Expand Down
64 changes: 64 additions & 0 deletions packages/superdeck/test/src/thumbnails/thumbnail_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,70 @@ void main() {
);
});

test('deleteAllThumbnails clears cache and store entries', () async {
final store = _FakeAssetCacheStore();
final capture = FakeSlideCaptureService(Uint8List.fromList([1, 2, 3]));
final service = ThumbnailService(
cacheStore: store,
slideCaptureService: capture,
);
final introThumbnail = AsyncThumbnail(
thumbnailKey: _thumbnailKey('intro'),
generator: (ctx, {required force}) async => null,
);
final agendaThumbnail = AsyncThumbnail(
thumbnailKey: _thumbnailKey('agenda'),
generator: (ctx, {required force}) async => null,
);

Map<String, AsyncThumbnail>? updatedCache;
await service.deleteAllThumbnails(
slides: [_createSlide('intro'), _createSlide('agenda')],
cache: {'intro': introThumbnail, 'agenda': agendaThumbnail},
onCacheUpdate: (cache) {
updatedCache = cache;
},
);

expect(updatedCache, isNotNull);
expect(updatedCache, isEmpty);
expect(
store.callOrder,
containsAll([
'delete:${_thumbnailKey('intro')}',
'delete:${_thumbnailKey('agenda')}',
]),
);
});

test('deleteAllThumbnails also removes orphan keys from cache', () async {
final store = _FakeAssetCacheStore();
final capture = FakeSlideCaptureService(Uint8List.fromList([1, 2, 3]));
final service = ThumbnailService(
cacheStore: store,
slideCaptureService: capture,
);
// 'orphan' is in the cache but no longer in the slides list.
final orphan = AsyncThumbnail(
thumbnailKey: _thumbnailKey('orphan'),
generator: (ctx, {required force}) async => null,
);

await service.deleteAllThumbnails(
slides: [_createSlide('intro')],
cache: {'orphan': orphan},
onCacheUpdate: (_) {},
);

expect(
store.callOrder,
containsAll([
'delete:${_thumbnailKey('orphan')}',
'delete:${_thumbnailKey('intro')}',
]),
);
});

testWidgets('replaces cached async thumbnail when thumbnail key changes', (
tester,
) async {
Expand Down
Loading
Loading