diff --git a/.fvmrc b/.fvmrc index c300356c..084b2bc5 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "stable" -} \ No newline at end of file + "flutter": "3.41.9" +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml index e5484675..8772fc19 100644 --- a/.github/workflows/firebase-hosting-merge.yml +++ b/.github/workflows/firebase-hosting-merge.yml @@ -30,8 +30,6 @@ jobs: run: | curl -fsSL https://fvm.app/install.sh | bash echo "/home/runner/fvm/bin" >> $GITHUB_PATH - export PATH="/home/runner/fvm/bin:$PATH" - fvm use --force - uses: kuhnroyal/flutter-fvm-config-action@v2 id: fvm-config-action diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index dc239a44..ec059b42 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -29,8 +29,6 @@ jobs: run: | curl -fsSL https://fvm.app/install.sh | bash echo "/home/runner/fvm/bin" >> $GITHUB_PATH - export PATH="/home/runner/fvm/bin:$PATH" - fvm use --force - uses: kuhnroyal/flutter-fvm-config-action@v2 id: fvm-config-action diff --git a/.vscode/settings.json b/.vscode/settings.json index 472c2ebb..764520ec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,24 +8,5 @@ "**/.fvm/versions": true, "**/.fvm/flutter_sdk": true }, - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#fbed80", - "activityBar.background": "#fbed80", - "activityBar.foreground": "#15202b", - "activityBar.inactiveForeground": "#15202b99", - "activityBarBadge.background": "#06b9a5", - "activityBarBadge.foreground": "#15202b", - "commandCenter.border": "#15202b99", - "sash.hoverBorder": "#fbed80", - "statusBar.background": "#f9e64f", - "statusBar.foreground": "#15202b", - "statusBarItem.hoverBackground": "#f7df1e", - "statusBarItem.remoteBackground": "#f9e64f", - "statusBarItem.remoteForeground": "#15202b", - "titleBar.activeBackground": "#f9e64f", - "titleBar.activeForeground": "#15202b", - "titleBar.inactiveBackground": "#f9e64f99", - "titleBar.inactiveForeground": "#15202b99" - }, "peacock.color": "#f9e64f" } \ No newline at end of file diff --git a/demo/pubspec.yaml b/demo/pubspec.yaml index 6e7b5830..fd9f8960 100644 --- a/demo/pubspec.yaml +++ b/demo/pubspec.yaml @@ -8,14 +8,14 @@ environment: dependencies: flutter: sdk: flutter - google_fonts: ^6.3.2 + google_fonts: ^8.1.0 mesh: ^0.4.3 - mix: ^2.0.0-rc.0 + mix: ^2.0.3 superdeck_core: ^1.0.0 superdeck: ^1.0.0 signals_flutter: ^6.0.4 naked_ui: ^0.2.0-beta.7 - remix: ^0.1.0-beta.1 + remix: ^0.2.0 dev_dependencies: flutter_test: sdk: flutter diff --git a/melos.yaml b/melos.yaml index daa12c84..24dd5332 100644 --- a/melos.yaml +++ b/melos.yaml @@ -12,7 +12,7 @@ command: flutter: ">=3.38.1" dependencies: collection: ^1.18.0 - mix: ^2.0.0-rc.0 + mix: ^2.0.3 ack: 1.0.0-beta.9 # publish: # hooks: diff --git a/packages/builder/lib/src/parsers/markdown_parser.dart b/packages/builder/lib/src/parsers/markdown_parser.dart index f5016ae6..8de9efdb 100644 --- a/packages/builder/lib/src/parsers/markdown_parser.dart +++ b/packages/builder/lib/src/parsers/markdown_parser.dart @@ -38,6 +38,10 @@ class MarkdownParser { static final _yamlKeyPattern = RegExp(r'^[A-Za-z_][\w-]*\s*:'); + /// Leading characters that mark a line as markdown body (heading, directive, + /// blockquote, image/link) and therefore rule out YAML frontmatter. + static const _markdownLeadChars = {'#', '@', '>', '!'}; + /// Splits the entire markdown into slides. /// /// A slide is bounded by `---` separator lines. A slide may begin with an @@ -54,35 +58,35 @@ class MarkdownParser { final slides = []; final buffer = StringBuffer(); + void flush() { + final pending = buffer.toString().trim(); + if (pending.isNotEmpty) slides.add(pending); + buffer.clear(); + } + var i = 0; while (i < lines.length) { - if (separators.contains(i)) { - final pending = buffer.toString().trim(); - if (pending.isNotEmpty) { - slides.add(pending); - buffer.clear(); - } - - final closeIdx = _findFrontmatterClose(lines, i, separators); - if (closeIdx != null) { - for (var j = i; j <= closeIdx; j++) { - buffer.writeln(lines[j]); - } - i = closeIdx + 1; - continue; - } + if (!separators.contains(i)) { + buffer.writeln(lines[i]); + i++; + continue; + } + flush(); + final closeIdx = _findFrontmatterClose(lines, i, separators); + if (closeIdx == null) { i++; continue; } - buffer.writeln(lines[i]); - i++; + // Consume the frontmatter block (open `---`, YAML body, close `---`). + for (var j = i; j <= closeIdx; j++) { + buffer.writeln(lines[j]); + } + i = closeIdx + 1; } - final tail = buffer.toString().trim(); - if (tail.isNotEmpty) slides.add(tail); - + flush(); return slides; } @@ -112,46 +116,31 @@ class MarkdownParser { } /// If [openIdx] opens a YAML frontmatter block, returns the index of the - /// closing `---`. Returns null when the next separator is too far away or - /// the lines between look like markdown content rather than YAML. + /// closing `---`. Returns null when no closing `---` is found, or when the + /// lines between look like markdown content rather than YAML. static int? _findFrontmatterClose( List lines, int openIdx, Set separators, ) { - int? closeIdx; + var hasContent = false; + var hasYamlMarker = false; for (var j = openIdx + 1; j < lines.length; j++) { if (separators.contains(j)) { - closeIdx = j; - break; - } - final trimmed = lines[j].trimLeft(); - if (trimmed.isEmpty) continue; - // Distinctive markdown body indicators rule out frontmatter. - final firstChar = trimmed[0]; - if (firstChar == '#' || - firstChar == '@' || - firstChar == '>' || - firstChar == '!') { - return null; + // An empty pair (`---\n---`) is a valid (empty) frontmatter block. + // Otherwise require at least one YAML-shaped line. + return (!hasContent || hasYamlMarker) ? j : null; } - } - - if (closeIdx == null) return null; - - var hasContent = false; - var hasYamlMarker = false; - for (var j = openIdx + 1; j < closeIdx; j++) { final trimmed = lines[j].trim(); if (trimmed.isEmpty) continue; + // Distinctive markdown body indicators rule out frontmatter. + if (_markdownLeadChars.contains(trimmed[0])) return null; hasContent = true; if (_yamlKeyPattern.hasMatch(trimmed) || trimmed.startsWith('- ')) { hasYamlMarker = true; - break; } } - - if (!hasContent || hasYamlMarker) return closeIdx; + // Reached EOF without a closing `---`. return null; } diff --git a/packages/builder/lib/superdeck_builder.dart b/packages/builder/lib/superdeck_builder.dart index adb35a4c..bdd4e5da 100644 --- a/packages/builder/lib/superdeck_builder.dart +++ b/packages/builder/lib/superdeck_builder.dart @@ -5,3 +5,6 @@ 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'; diff --git a/packages/playground/.gitignore b/packages/playground/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/packages/playground/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/playground/.metadata b/packages/playground/.metadata new file mode 100644 index 00000000..bc29b989 --- /dev/null +++ b/packages/playground/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + - platform: macos + create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa + - platform: web + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/playground/CONTEXT.md b/packages/playground/CONTEXT.md new file mode 100644 index 00000000..c7d1c09a --- /dev/null +++ b/packages/playground/CONTEXT.md @@ -0,0 +1,48 @@ +# Playground Editor + +The live Markdown editor in the SuperDeck playground app: a three-panel screen +where edits to Markdown re-render slide previews in real time. + +## Language + +**Deck**: +The ordered set of slides parsed from the editor's Markdown buffer. +_Avoid_: presentation, document + +**Slide**: +One unit of deck content, delimited in Markdown by `---` separators. +_Avoid_: page, card + +**Active Slide**: +The slide the editor caret currently sits in; its index is owned solely by `EditorState.activeSlideIndex`. +_Avoid_: current slide, selected slide + +**Thumbnail**: +A cached PNG render of a slide, shown in the Preview Sidebar for every slide except the Active Slide. +_Avoid_: preview image, snapshot + +**Preview Sidebar**: +The left panel that lists every slide — the Active Slide rendered live, the rest as Thumbnails. +_Avoid_: slide list, filmstrip + +**Thumbnail Refresher**: +The module that regenerates Thumbnails when the deck first loads and whenever the Active Slide changes. +_Avoid_: thumbnail service (that is superdeck's capture pipeline), debouncer + +## Relationships + +- A **Deck** contains one or more **Slides** +- Exactly one **Slide** is the **Active Slide** at any time +- Every **Slide** except the **Active Slide** appears in the **Preview Sidebar** as a **Thumbnail** +- The **Thumbnail Refresher** regenerates **Thumbnails** when the deck first loads and when the **Active Slide** changes + +## Example dialogue + +> **Dev:** "When the caret moves from slide 2 to slide 3, what regenerates?" +> **Domain expert:** "Slide 3 becomes the Active Slide and renders live. Slide 2 is no longer active, so the Thumbnail Refresher captures a fresh Thumbnail for it — its content has settled." +> **Dev:** "And while I'm typing inside slide 3?" +> **Domain expert:** "Nothing regenerates. Slide 3 is the Active Slide; the Preview Sidebar already shows it live, so its Thumbnail can wait until you leave it." + +## Flagged ambiguities + +- "current slide" was used for both the **Active Slide** (editor caret) and `DeckController.presentation.currentIndex` (presentation-mode routing) — resolved: these are distinct concepts; **Active Slide** refers only to the editor. diff --git a/packages/playground/README.md b/packages/playground/README.md new file mode 100644 index 00000000..083a0f92 --- /dev/null +++ b/packages/playground/README.md @@ -0,0 +1,37 @@ +# SuperDeck Studio (playground) + +> This package is currently named `playground` but is evolving toward **SuperDeck Studio** — a rename is planned in a future PR. + +SuperDeck Studio is a standalone editor and presentation environment built on the SuperDeck framework. It lets users author Markdown slides in a rich text editor, see a live preview as they type, and present directly from the app — all without the CLI pipeline or external file setup. + +## How it differs from `demo/` + +The root-level `demo/` app exists to test the CLI and custom extension points (styles, templates, widgets). Studio is the product: a self-contained SuperDeck environment for authoring and presenting slides using the base framework implementation. + +## Current Features + +| Feature | Description | +|---------|-------------| +| **Rich text editor** | Markdown editing with syntax highlighting for headers, `---` separators, and `@block` directives. | +| **Live preview** | Slide thumbnails update in real time as you type. | +| **Presentation mode** | Full-screen takeover route with keyboard navigation (arrows, space, escape). | +| **Theme support** | Follows the system light/dark theme automatically. | + +## Planned Features + +- Visual controls in the customization sidebar for adjusting slide style, layout, and content options +- Text scaling for previews + +## Running + +From the repository root: + +```bash +cd packages/playground +fvm flutter run -d macos # macOS desktop +fvm flutter run -d chrome # Web +``` + +## Note + +This package is not published (`publish_to: none`). It depends on local `superdeck`, `superdeck_core`, and `superdeck_builder` packages from the monorepo. diff --git a/packages/playground/analysis_options.yaml b/packages/playground/analysis_options.yaml new file mode 100644 index 00000000..f3d426c5 --- /dev/null +++ b/packages/playground/analysis_options.yaml @@ -0,0 +1,2 @@ +include: package:flutter_lints/flutter.yaml +extends: ../../shared_analysis_options.yaml diff --git a/packages/playground/devtools_options.yaml b/packages/playground/devtools_options.yaml new file mode 100644 index 00000000..fa0b357c --- /dev/null +++ b/packages/playground/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/packages/playground/lib/features/editor/customization_sidebar.dart b/packages/playground/lib/features/editor/customization_sidebar.dart new file mode 100644 index 00000000..23254d64 --- /dev/null +++ b/packages/playground/lib/features/editor/customization_sidebar.dart @@ -0,0 +1,436 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hero_ui/hero_ui.dart'; +import 'package:provider/provider.dart'; +import 'package:remix/remix.dart'; +import 'package:signals_flutter/signals_flutter.dart'; + +import '../../stores/deck_customization_store.dart'; + +class CustomizationSidebar extends StatefulWidget { + const CustomizationSidebar({super.key}); + + @override + State createState() => _CustomizationSidebarState(); +} + +class _CustomizationSidebarState extends State { + late final RemixAccordionController _accordionController; + + @override + void initState() { + super.initState(); + _accordionController = RemixAccordionController(); + } + + @override + void dispose() { + _accordionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Box( + style: BoxStyler().width(280).marginAll(16).clipBehavior(.none), + child: ColumnBox( + style: FlexBoxStyler().crossAxisAlignment(.stretch).clipBehavior(.none), + children: [ + const _Toolbar(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: const HeroDivider(), + ), + Expanded( + child: SingleChildScrollView( + clipBehavior: Clip.none, + child: ColumnBox( + style: FlexBoxStyler() + .crossAxisAlignment(.stretch) + .clipBehavior(.none), + children: [ + const _BackgroundSection(), + const SizedBox(height: 16), + const HeroDivider(), + const SizedBox(height: 16), + RemixAccordionGroup( + controller: _accordionController, + child: ColumnBox( + style: FlexBoxStyler() + .crossAxisAlignment(.stretch) + .spacing(16), + children: const [ + _LevelAccordion( + level: TextLevel.h1, + title: 'Heading 1', + ), + HeroDivider(), + _LevelAccordion( + level: TextLevel.h2, + title: 'Heading 2', + ), + HeroDivider(), + _LevelAccordion( + level: TextLevel.h3, + title: 'Heading 3', + ), + HeroDivider(), + _LevelAccordion(level: TextLevel.p, title: 'Paragraph'), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _LevelAccordion extends StatelessWidget { + const _LevelAccordion({required this.level, required this.title}); + + final TextLevel level; + final String title; + + @override + Widget build(BuildContext context) { + return RemixAccordion( + value: level, + title: title, + trailingIcon: CupertinoIcons.plus, + style: RemixAccordionStyle() + .titleColor($foreground()) + .titleStyle($labelMedium.mix()) + .trailingIconColor($muted()) + .trailingIconSize(18) + .content(.clipBehavior(.none).padding(.bottom(8).left(1))), + child: Padding( + padding: const EdgeInsets.only(top: 8), + child: _LevelControls(level: level), + ), + ); + } +} + +class _BackgroundSection extends StatelessWidget { + const _BackgroundSection(); + + @override + Widget build(BuildContext context) { + final store = context.read(); + return ColumnBox( + style: FlexBoxStyler().spacing(8).crossAxisAlignment(.start), + children: [ + const _SectionLabel('Background'), + Watch((context) { + return _SwatchRow( + swatches: playgroundBackgroundSwatches, + selected: store.background.value, + onSelected: (color) => store.background.value = color, + ); + }), + ], + ); + } +} + +class _LevelControls extends StatelessWidget { + const _LevelControls({required this.level}); + + final TextLevel level; + + @override + Widget build(BuildContext context) { + final store = context.read(); + final signals = store.level(level); + + return ColumnBox( + style: FlexBoxStyler().spacing(16).crossAxisAlignment(.stretch), + children: [ + Watch((context) { + return _SwatchRow( + swatches: playgroundTextSwatches, + selected: signals.color.value, + onSelected: (color) => signals.color.value = color, + ); + }), + _FontSizeField(level: level), + _FontWeightSlider(level: level), + _FontFamilySelect(level: level), + ], + ); + } +} + +class _SwatchRow extends StatelessWidget { + const _SwatchRow({ + required this.swatches, + required this.selected, + required this.onSelected, + }); + + final List swatches; + final Color selected; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + return RowBox( + style: FlexBoxStyler().spacing(8), + children: [ + for (final swatch in swatches) + Pressable( + onPress: () => onSelected(swatch.color), + child: Box( + style: BoxStyler() + .width(28) + .height(28) + .color(swatch.color) + .borderRounded(999) + .shadowOnly( + color: swatch.color == selected ? $accent() : $background(), + offset: Offset(0, 0), + blurRadius: 0, + spreadRadius: 2, + ) + .borderAll( + color: swatch.color == selected ? $background() : $border(), + width: swatch.color == selected ? 2 : 1, + ), + ), + ), + ], + ); + } +} + +class _FontSizeField extends StatefulWidget { + const _FontSizeField({required this.level}); + + final TextLevel level; + + @override + State<_FontSizeField> createState() => _FontSizeFieldState(); +} + +class _FontSizeFieldState extends State<_FontSizeField> { + static const _minSize = 8; + static const _maxSize = 128; + + late final TextEditingController _controller; + late final FocusNode _focusNode; + late final EffectCleanup _externalCleanup; + + @override + void initState() { + super.initState(); + final store = context.read(); + final signal = store.level(widget.level).size; + _controller = TextEditingController(text: signal.peek().toInt().toString()); + _focusNode = FocusNode(); + _focusNode.addListener(_handleFocusChange); + // Pull external mutations (e.g., future reset action) into the field. + _externalCleanup = effect(() { + final value = signal.value; + if (_focusNode.hasFocus) return; + final expected = value.toInt().toString(); + if (_controller.text != expected) _controller.text = expected; + }); + } + + @override + void dispose() { + _externalCleanup(); + _focusNode.removeListener(_handleFocusChange); + _focusNode.dispose(); + _controller.dispose(); + super.dispose(); + } + + void _handleFocusChange() { + if (!_focusNode.hasFocus) _commit(); + } + + /// Parses the field, clamps to [_minSize, _maxSize], and either writes to the + /// signal or rewrites the field with the last known good value. + void _commit() { + final store = context.read(); + final signal = store.level(widget.level).size; + final parsed = int.tryParse(_controller.text.trim()); + if (parsed == null) { + _controller.text = signal.peek().toInt().toString(); + return; + } + final clamped = parsed.clamp(_minSize, _maxSize); + signal.value = clamped.toDouble(); + _controller.text = clamped.toString(); + } + + @override + Widget build(BuildContext context) { + return ColumnBox( + style: FlexBoxStyler().spacing(4).crossAxisAlignment(.start), + children: [ + const _ControlLabel('Size'), + HeroTextField( + fullWidth: true, + controller: _controller, + focusNode: _focusNode, + keyboardType: const TextInputType.numberWithOptions(decimal: false), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onSubmitted: (_) => _commit(), + ), + ], + ); + } +} +// ), +// ], +// ); +// } +// } + +class _FontWeightSlider extends StatelessWidget { + const _FontWeightSlider({required this.level}); + + final TextLevel level; + + @override + Widget build(BuildContext context) { + final store = context.read(); + final signal = store.level(level).weight; + + return Watch((context) { + final weight = signal.value; + return ColumnBox( + style: FlexBoxStyler().spacing(2).crossAxisAlignment(.start), + children: [ + RowBox( + style: FlexBoxStyler().mainAxisAlignment(.spaceBetween), + children: [ + const _ControlLabel('Weight'), + StyledText( + weight.toString(), + style: TextStyler() + .color($foreground()) + .style($labelSmall.mix()), + ), + ], + ), + HeroSlider( + min: 100, + max: 900, + snapDivisions: 8, + showOutput: false, + value: weight.toDouble(), + onChanged: (value) => signal.value = (value / 100).round() * 100, + ), + ], + ); + }); + } +} + +class _FontFamilySelect extends StatelessWidget { + const _FontFamilySelect({required this.level}); + + final TextLevel level; + + @override + Widget build(BuildContext context) { + final store = context.read(); + final signal = store.level(level).family; + final items = [ + for (final family in playgroundFontFamilies) + HeroSelectItem(value: family, label: family), + ]; + + return ColumnBox( + style: FlexBoxStyler().spacing(4).crossAxisAlignment(.start), + children: [ + const _ControlLabel('Family'), + Watch((context) { + return HeroSelect( + fullWidth: true, + placeholder: 'Font', + items: items, + icon: CupertinoIcons.textformat, + selectedValue: signal.value, + style: .new().trigger(.new().spacing(12)), + onChanged: (value) { + if (value != null) signal.value = value; + }, + ); + }), + ], + ); + } +} + +class _SectionLabel extends StatelessWidget { + const _SectionLabel(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + return StyledText( + text, + style: TextStyler() + .color($foreground()) + .style($labelMedium.mix()) + .wrap(.padding(.vertical(4))), + ); + } +} + +class _ControlLabel extends StatelessWidget { + const _ControlLabel(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + return StyledText( + text, + style: TextStyler().color($muted()).style($labelSmall.mix()), + ); + } +} + +class _Toolbar extends StatelessWidget { + const _Toolbar(); + + @override + Widget build(BuildContext context) { + return RowBox( + style: FlexBoxStyler().spacing(8), + children: [ + Spacer(), + SizedBox( + width: 48, + child: HeroIconButton( + icon: CupertinoIcons.share, + size: .lg, + variant: .secondary, + onPressed: () {}, + ), + ), + SizedBox( + width: 48, + child: HeroIconButton( + size: .lg, + icon: CupertinoIcons.play, + onPressed: () { + Navigator.of(context).pushNamed('/present'); + }, + ), + ), + ], + ); + } +} diff --git a/packages/playground/lib/features/editor/editor_page.dart b/packages/playground/lib/features/editor/editor_page.dart new file mode 100644 index 00000000..94ee4281 --- /dev/null +++ b/packages/playground/lib/features/editor/editor_page.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:hero_ui/hero_ui.dart'; +import 'package:mix/mix.dart'; + +import 'customization_sidebar.dart'; +import 'preview_sidebar.dart'; +import 'text_editor.dart'; +import 'thumbnail_refresher.dart'; + +class EditorPage extends StatelessWidget { + const EditorPage({super.key}); + + @override + Widget build(BuildContext context) { + return ThumbnailRefresher( + child: Scaffold( + backgroundColor: $background.resolve(context), + body: Box( + style: BoxStyler().color($background()), + child: RowBox( + children: [ + PreviewSidebar(), + Expanded(child: TextEditor()), + CustomizationSidebar(), + ], + ), + ), + ), + ); + } +} diff --git a/packages/playground/lib/features/editor/preview_sidebar.dart b/packages/playground/lib/features/editor/preview_sidebar.dart new file mode 100644 index 00000000..ef75f212 --- /dev/null +++ b/packages/playground/lib/features/editor/preview_sidebar.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:hero_ui/hero_ui.dart'; +import 'package:mix/mix.dart'; +import 'package:provider/provider.dart'; +import 'package:signals_flutter/signals_flutter.dart'; +import 'package:superdeck/superdeck.dart'; + +import '../../stores/editor_state.dart'; + +class PreviewSidebar extends StatelessWidget { + const PreviewSidebar({super.key}); + + @override + Widget build(BuildContext context) { + return HeroMode( + enabled: false, + child: StackBox( + style: StackBoxStyler().width(218).marginAll(16), + children: [ + ColumnBox( + style: FlexBoxStyler().spacing(24), + children: [Expanded(child: SlidesPreviewList())], + ), + ], + ), + ); + } +} + +class SlidesPreviewList extends StatelessWidget { + const SlidesPreviewList({super.key}); + + @override + Widget build(BuildContext context) { + final controller = context.read(); + final editorState = context.read(); + + return Watch((context) { + final slides = controller.slides.value; + final activeIndex = editorState.activeSlideIndex.value; + + if (slides.isEmpty) { + return Center( + child: StyledText( + 'No slides', + style: TextStyler().style(.color($muted())), + ), + ); + } + + return ScrollConfiguration( + behavior: ScrollBehavior().copyWith(scrollbars: false), + child: ListView.builder( + clipBehavior: .none, + itemCount: slides.length, + itemBuilder: (context, index) { + final isActive = index == activeIndex; + return Padding( + padding: const .only(bottom: 24), + child: _PreviewItem( + index: index, + configuration: slides[index], + isActive: isActive, + onTap: () { + editorState.activeSlideIndex.value = index; + }, + ), + ); + }, + ), + ); + }); + } +} + +class _PreviewItem extends StatelessWidget { + const _PreviewItem({ + required this.index, + required this.configuration, + this.isActive = false, + this.onTap, + }); + + final int index; + final SlideConfiguration configuration; + final bool isActive; + final VoidCallback? onTap; + + Widget _buildSlideRender() { + return FittedBox( + fit: .cover, + alignment: .topLeft, + child: SlideRenderView( + configuration.copyWith(style: configuration.style), + ), + ); + } + + @override + Widget build(BuildContext context) { + final controller = context.read(); + + return GestureDetector( + onTap: onTap, + child: HeroCard( + variant: .tertiary, + child: Stack( + alignment: .bottomRight, + children: [ + Box( + style: BoxStyler().wrap(.aspectRatio(16 / 9)), + child: _SlidePreview( + active: isActive, + controller: controller, + configuration: configuration, + fallback: _buildSlideRender, + ), + ), + Box( + style: BoxStyler() + .marginAll(8) + .padding(.horizontal(10).vertical(2)) + .color(isActive ? $accent() : $overlay()) + .borderRounded(12) + .textStyle( + .color( + isActive ? $accentForeground() : $surfaceForeground(), + ).fontSize(12), + ), + child: StyledText('${index + 1}'), + ), + ], + ), + ), + ); + } +} + +class _SlidePreview extends StatelessWidget { + const _SlidePreview({ + required this.controller, + required this.configuration, + required this.active, + required this.fallback, + }); + + final DeckController controller; + final SlideConfiguration configuration; + final bool active; + final Widget Function() fallback; + + @override + Widget build(BuildContext context) { + return Watch((context) { + final thumbnail = controller.presentation.getThumbnail(configuration.key); + + final status = thumbnail?.status.value; + final isThumbnailReady = status == AsyncFileStatus.done && !active; + + return isThumbnailReady + ? KeyedSubtree( + key: const ValueKey('thumbnail'), + child: thumbnail!.build(context), + ) + : KeyedSubtree(key: const ValueKey('fallback'), child: fallback()); + }); + } +} diff --git a/packages/playground/lib/features/editor/text_editor.dart b/packages/playground/lib/features/editor/text_editor.dart new file mode 100644 index 00000000..bb3fa07b --- /dev/null +++ b/packages/playground/lib/features/editor/text_editor.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hero_ui/hero_ui.dart'; +import 'package:provider/provider.dart'; + +import 'package:super_editor/super_editor.dart'; + +import '../../stores/editor_state.dart'; +import '../../utils/edit_reaction.dart'; +import '../../utils/memory_deck_loader.dart'; + +class TextEditor extends StatefulWidget { + const TextEditor({super.key, this.onChanged, this.onInit}); + + final ValueChanged? onChanged; + final VoidCallback? onInit; + + @override + State createState() => _TextEditorState(); +} + +class _TextEditorState extends State { + late final MutableDocument _document; + late final MutableDocumentComposer _composer; + late final Editor _editor; + late final void Function() _unsubscribeActiveIndex; + bool _isUpdatingFromCursor = false; + + @override + void initState() { + super.initState(); + + _document = MutableDocument( + nodes: [ + ParagraphNode(id: Editor.createNodeId(), text: AttributedText('---\n')), + ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText('# Title'), + ), + ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText('## Subtitle\n'), + ), + ParagraphNode(id: Editor.createNodeId(), text: AttributedText('---\n')), + ], + ); + + _document.addListener(_onDocumentChanged); + + _composer = MutableDocumentComposer(); + _composer.selectionNotifier.addListener(_onSelectionChanged); + _editor = Editor( + editables: {Editor.documentKey: _document, Editor.composerKey: _composer}, + requestHandlers: List.from(defaultRequestHandlers), + reactionPipeline: [ + UpdateComposerTextStylesReaction(), + SeparatorColorReaction(), + HeaderHighlightReaction(), + BlockHighlightReaction(), + ], + ); + + context.read().updateMarkdown(_extractText()); + + final editorState = context.read(); + _unsubscribeActiveIndex = editorState.activeSlideIndex.subscribe((index) { + if (_isUpdatingFromCursor) return; + _scrollToSlide(index); + }); + } + + void _onDocumentChanged(DocumentChangeLog changeLog) { + final text = _extractText(); + context.read().updateMarkdown(text); + } + + void _onSelectionChanged() { + final selection = _composer.selection; + + if (selection == null) return; + + final caretNodeId = selection.extent.nodeId; + var slideIndex = 0; + + for (final node in _document) { + if (node.id == caretNodeId) break; + if (node is TextNode && node.text.toPlainText().trim() == '---') { + slideIndex++; + } + } + + final editorState = context.read(); + // The first --- is the frontmatter separator, so slide 0 content + // appears after the first ---. Subtract 1 to convert separator count + // to 0-based slide index. + final adjustedIndex = (slideIndex - 1).clamp(0, slideIndex); + if (editorState.activeSlideIndex.value != adjustedIndex) { + _isUpdatingFromCursor = true; + editorState.activeSlideIndex.value = adjustedIndex; + _isUpdatingFromCursor = false; + } + } + + void _scrollToSlide(int targetIndex) { + // Find the node that starts the target slide's content. + // Slide N starts after the (N+1)th --- separator. + var separatorCount = 0; + String? targetNodeId; + + for (final node in _document) { + if (node is TextNode && node.text.toPlainText().trim() == '---') { + separatorCount++; + if (separatorCount == targetIndex + 1) { + // The next node after this separator is the slide's content. + // For now, place caret at the separator itself — the content + // node may not exist yet if the slide is empty. + final nodeIndex = _document.getNodeIndexById(node.id); + if (nodeIndex + 1 < _document.length) { + targetNodeId = _document.getNodeAt(nodeIndex + 1)!.id; + } else { + targetNodeId = node.id; + } + break; + } + } + } + + // For slide 0, target the first content node after the first --- + if (targetIndex == 0 && targetNodeId == null) { + for (final node in _document) { + if (node is TextNode && node.text.toPlainText().trim() == '---') { + final nodeIndex = _document.getNodeIndexById(node.id); + if (nodeIndex + 1 < _document.length) { + targetNodeId = _document.getNodeAt(nodeIndex + 1)!.id; + } + break; + } + } + } + + if (targetNodeId == null) return; + + _editor.execute([ + ChangeSelectionRequest( + DocumentSelection.collapsed( + position: DocumentPosition( + nodeId: targetNodeId, + nodePosition: const TextNodePosition(offset: 0), + ), + ), + SelectionChangeType.placeCaret, + SelectionReason.userInteraction, + ), + ]); + } + + String _extractText() { + final buffer = StringBuffer(); + var first = true; + for (final node in _document) { + if (!first) buffer.write('\n'); + first = false; + if (node is TextNode) { + buffer.write(node.text.toPlainText()); + } + } + return buffer.toString(); + } + + @override + void dispose() { + _unsubscribeActiveIndex(); + _composer.selectionNotifier.removeListener(_onSelectionChanged); + _document.removeListener(_onDocumentChanged); + _editor.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: HeroCard( + child: SuperEditor( + editor: _editor, + keyboardActions: [...defaultKeyboardActions], + documentOverlayBuilders: [ + DefaultCaretOverlayBuilder( + caretStyle: CaretStyle(color: $accent.resolve(context)), + ), + ], + stylesheet: Stylesheet( + rules: [ + StyleRule(BlockSelector.all, (doc, docNode) { + return { + Styles.textStyle: TextStyle( + color: $muted.resolve(context), + fontSize: 16, + height: 1.4, + fontFamily: GoogleFonts.googleSansCode().fontFamily, + ), + }; + }), + ], + inlineTextStyler: (attributions, textStyle) { + var style = const TextStyle(fontSize: 16).merge(textStyle); + + for (final attribution in attributions) { + switch (attribution) { + case separatorAttribution: + style = style.copyWith( + color: $separatorTertiary.resolve(context), + ); + break; + case headerAttribution: + style = style.copyWith( + fontWeight: FontWeight.bold, + color: $foreground.resolve(context), + ); + break; + case blockAttribution: + style = style.copyWith(color: $danger.resolve(context)); + break; + case blockKeyAttribution: + style = style.copyWith(color: $warning.resolve(context)); + break; + case blockValueAttribution: + style = style.copyWith(color: $foreground.resolve(context)); + break; + } + } + return style; + }, + documentPadding: const .all(32), + ), + ), + ), + ); + } +} diff --git a/packages/playground/lib/features/editor/thumbnail_refresher.dart b/packages/playground/lib/features/editor/thumbnail_refresher.dart new file mode 100644 index 00000000..8be28841 --- /dev/null +++ b/packages/playground/lib/features/editor/thumbnail_refresher.dart @@ -0,0 +1,119 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:signals_flutter/signals_flutter.dart'; +import 'package:superdeck/superdeck.dart'; + +import '../../stores/editor_state.dart'; + +/// Signature for the action that regenerates slide thumbnails. +/// +/// When [force] is true the existing cache entries are dropped and every +/// slide is re-captured, even if its content-hash key is unchanged. +typedef ThumbnailRegenerate = + void Function( + BuildContext context, + List slides, { + bool force, + }); + +/// Keeps the preview sidebar's thumbnails in sync with the deck. +/// +/// Mounted in the editor subtree, it regenerates thumbnails once when the +/// deck first loads, again whenever the active slide changes, and again on +/// any deck-wide style change. +/// +/// Typing inside a slide regenerates nothing — the active slide renders live +/// in the sidebar, so its thumbnail can wait until the caret leaves it. Slide +/// keys are content hashes, so [DeckController] captures only the slides +/// whose content actually changed since the last pass. +/// +/// A deck-wide style change is invisible to those content-hash keys: the +/// customization store writes a fresh [DeckOptions] but every `thumbnailKey` +/// stays the same, so a plain pass would reuse the stale capture. The options +/// effect therefore force-regenerates every thumbnail — active slide +/// included, so it is correct the instant the caret leaves it. +class ThumbnailRefresher extends StatefulWidget { + const ThumbnailRefresher({required this.child, this.regenerate, super.key}); + + final Widget child; + + /// Overrides the regeneration action. Defaults to + /// `DeckController.presentation.generateThumbnails`. + @visibleForTesting + final ThumbnailRegenerate? regenerate; + + @override + State createState() => _ThumbnailRefresherState(); +} + +class _ThumbnailRefresherState extends State { + late final DeckController _controller; + late final EditorState _editorState; + late final ThumbnailRegenerate _regenerate; + late final EffectCleanup _initialCleanup; + late final EffectCleanup _activeSlideCleanup; + late final EffectCleanup _optionsCleanup; + + bool _initialDone = false; + bool _generateScheduled = false; + bool _forcePending = false; + + @override + void initState() { + super.initState(); + _controller = context.read(); + _editorState = context.read(); + _regenerate = widget.regenerate ?? _generateViaController; + + // Initial generation: fire once, as soon as the deck has slides. + _initialCleanup = effect(() { + final slides = _controller.slides.value; + if (slides.isEmpty || _initialDone) return; + _initialDone = true; + _scheduleGenerate(); + }); + + // Regeneration: the active slide changing means the slide just left has + // settled and is no longer rendered live — refresh thumbnails then. + _activeSlideCleanup = effect(() { + _editorState.activeSlideIndex.value; // subscribe + if (!_initialDone) return; + _scheduleGenerate(); + }); + } + + void _generateViaController( + BuildContext context, + List slides, { + bool force = false, + }) { + _controller.presentation.generateThumbnails(context, slides, force: force); + } + + /// Coalesces triggers into a single post-frame regeneration, so the capture + /// context is laid out and overlapping triggers do not stack. A pending + /// [force] request wins over a coalesced plain one. + void _scheduleGenerate({bool force = false}) { + if (force) _forcePending = true; + if (_generateScheduled) return; + _generateScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _generateScheduled = false; + final shouldForce = _forcePending; + _forcePending = false; + if (!mounted) return; + _regenerate(context, _controller.slides.value, force: shouldForce); + }); + } + + @override + void dispose() { + _initialCleanup(); + _activeSlideCleanup(); + _optionsCleanup(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.child; +} diff --git a/packages/playground/lib/features/presentation/presentation_page.dart b/packages/playground/lib/features/presentation/presentation_page.dart new file mode 100644 index 00000000..83de7ae1 --- /dev/null +++ b/packages/playground/lib/features/presentation/presentation_page.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hero_ui/hero_ui.dart'; +import 'package:mix/mix.dart'; +import 'package:provider/provider.dart'; +import 'package:signals_flutter/signals_flutter.dart'; +import 'package:superdeck/superdeck.dart'; + +class PresentationPage extends StatefulWidget { + const PresentationPage({super.key}); + + @override + State createState() => _PresentationPageState(); +} + +class _PresentationPageState extends State { + static const _transitionDuration = Duration(milliseconds: 400); + + // Each slide is rendered as its own route in this inner navigator. Slide + // changes go through [Navigator.pushReplacement] so the [HeroController] + // below observes a route transition and flies matching `Hero` widgets + // between slides. A plain `setState` swap stays on one route, so the hero + // controller never runs and hero animations never play. + final GlobalKey _navigatorKey = GlobalKey(); + final HeroController _heroController = HeroController(); + final FocusNode _focusNode = FocusNode(); + + int _slideIndex = 0; + + @override + void dispose() { + _focusNode.dispose(); + _heroController.dispose(); + super.dispose(); + } + + void _goNext() { + final slides = context.read().slides.value; + if (_slideIndex < slides.length - 1) { + _goToSlide(_slideIndex + 1); + } + } + + void _goPrevious() { + if (_slideIndex > 0) { + _goToSlide(_slideIndex - 1); + } + } + + /// Replaces the current slide route with the route for [index]. The route + /// transition is what drives hero flights between the two slides. + void _goToSlide(int index) { + _slideIndex = index; + _navigatorKey.currentState?.pushReplacement(_buildSlideRoute(index)); + } + + Route _buildSlideRoute(int index) { + return PageRouteBuilder( + settings: RouteSettings(name: 'slide-$index'), + transitionDuration: _transitionDuration, + reverseTransitionDuration: _transitionDuration, + pageBuilder: (context, animation, secondaryAnimation) => + _SlideStage(index: index), + transitionsBuilder: (context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child), + ); + } + + @override + Widget build(BuildContext context) { + final controller = context.read(); + + return Scaffold( + backgroundColor: $background.resolve(context), + body: KeyboardListener( + focusNode: _focusNode, + autofocus: true, + onKeyEvent: (event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.arrowRight || + event.logicalKey == LogicalKeyboardKey.space) { + _goNext(); + } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + _goPrevious(); + } else if (event.logicalKey == LogicalKeyboardKey.escape) { + Navigator.of(context).pop(); + } + } + }, + child: Watch((context) { + final slides = controller.slides.value; + if (slides.isEmpty) { + return const _EmptyState(); + } + + return GestureDetector( + onTap: _goNext, + child: Navigator( + key: _navigatorKey, + observers: [_heroController], + onGenerateRoute: (_) => + _buildSlideRoute(_slideIndex.clamp(0, slides.length - 1)), + ), + ); + }), + ), + ); + } +} + +/// Renders a single slide, scaled to fit, as the content of one slide route. +class _SlideStage extends StatelessWidget { + const _SlideStage({required this.index}); + + final int index; + + @override + Widget build(BuildContext context) { + final slides = context.read().slides.value; + final slide = slides[index.clamp(0, slides.length - 1)]; + + return Center( + child: SizedBox.expand( + child: FittedBox(fit: .fitWidth, child: SlideRenderView(slide)), + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + const _EmptyState(); + + @override + Widget build(BuildContext context) { + final style = TextStyler().style(.color($foreground()).fontSize(24)); + + return Center(child: style('No slides')); + } +} diff --git a/packages/playground/lib/main.dart b/packages/playground/lib/main.dart new file mode 100644 index 00000000..e088ec70 --- /dev/null +++ b/packages/playground/lib/main.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:hero_ui/hero_ui.dart'; +import 'package:playground/stores/deck_customization_store.dart'; +import 'package:playground/stores/editor_state.dart'; +import 'package:playground/utils/memory_asset_cache_store.dart'; +import 'package:playground/utils/memory_deck_loader.dart'; +import 'package:playground/features/presentation/presentation_page.dart'; +import 'package:playground/utils/takeover_route.dart'; +import 'package:playground/features/editor/editor_page.dart'; +import 'package:provider/provider.dart'; +import 'package:signals_flutter/signals_flutter.dart'; +import 'package:superdeck/superdeck.dart'; + +void main() { + SignalsObserver.instance = null; + runApp(const PlaygroundApp()); +} + +class PlaygroundApp extends StatefulWidget { + const PlaygroundApp({super.key}); + + @override + State createState() => _PlaygroundAppState(); +} + +class _PlaygroundAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Superdeck', + builder: (context, child) { + return _Theme(child: _Providers(child: child!)); + }, + debugShowCheckedModeBanner: false, + initialRoute: '/', + onGenerateRoute: (settings) { + switch (settings.name) { + case '/present': + return TakeoverRoute( + settings: settings, + builder: (_) => const PresentationPage(), + ); + default: + return MaterialPageRoute( + settings: settings, + builder: (_) => const EditorPage(), + ); + } + }, + ); + } +} + +class _Providers extends StatefulWidget { + const _Providers({required this.child}); + + final Widget child; + @override + State<_Providers> createState() => _ProvidersState(); +} + +class _ProvidersState extends State<_Providers> { + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + Provider(create: (_) => MemoryDeckLoader()), + Provider( + create: (context) => DeckController( + deckLoader: context.read(), + options: .new(), + assetCacheStore: MemoryAssetCacheStore(), + ), + ), + Provider( + create: (context) => + DeckCustomizationStore(context.read()), + dispose: (_, store) => store.dispose(), + ), + Provider( + create: (_) => EditorState(), + dispose: (_, state) => state.dispose(), + ), + ], + child: widget.child, + ); + } +} + +class _Theme extends StatelessWidget { + const _Theme({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return HeroTheme( + data: MediaQuery.of(context).platformBrightness == Brightness.dark + ? .dark() + : .light(), + child: child, + ); + } +} diff --git a/packages/playground/lib/stores/deck_customization_store.dart b/packages/playground/lib/stores/deck_customization_store.dart new file mode 100644 index 00000000..133ccacd --- /dev/null +++ b/packages/playground/lib/stores/deck_customization_store.dart @@ -0,0 +1,221 @@ +import 'package:flutter/widgets.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hero_ui/hero_ui.dart'; +import 'package:mix/mix.dart'; +import 'package:signals_flutter/signals_flutter.dart'; +import 'package:superdeck/superdeck.dart'; + +/// Curated font families surfaced in the playground's customization sidebar. +const playgroundFontFamilies = [ + 'Inter', + 'Roboto', + 'Playfair Display', + 'Source Serif Pro', + 'JetBrains Mono', + 'Space Grotesk', + 'Lora', + 'DM Sans', +]; + +/// Background swatches shown in the customization sidebar. +/// +/// Token swatches resolve through the active [HeroTheme]; the last two are +/// fixed white/black for decks targeting a known display environment. +final playgroundBackgroundSwatches = [ + PlaygroundSwatch(label: 'Background', color: $background()), + PlaygroundSwatch(label: 'Surface', color: $surface()), + PlaygroundSwatch(label: 'Muted', color: $muted()), + PlaygroundSwatch(label: 'Accent', color: $accent()), + const PlaygroundSwatch(label: 'White', color: Color(0xFFFFFFFF)), + const PlaygroundSwatch(label: 'Black', color: Color(0xFF000000)), +]; + +/// Text swatches mirror background swatches; the default heading color uses +/// `$foreground` so headings remain legible on the seeded background. +final playgroundTextSwatches = [ + PlaygroundSwatch(label: 'Foreground', color: $foreground()), + PlaygroundSwatch(label: 'Muted', color: $muted()), + PlaygroundSwatch(label: 'Accent', color: $accent()), + PlaygroundSwatch(label: 'Surface', color: $surface()), + const PlaygroundSwatch(label: 'White', color: Color(0xFFFFFFFF)), + const PlaygroundSwatch(label: 'Black', color: Color(0xFF000000)), +]; + +@immutable +class PlaygroundSwatch { + const PlaygroundSwatch({required this.label, required this.color}); + + final String label; + final Color color; +} + +/// Per-heading typography state. Each [TextLevel] owns four independent +/// signals; the store derives a [TextStyler] from them on every change. +class TextLevelSignals { + TextLevelSignals({ + required Color color, + required double size, + required int weight, + required String family, + }) : color = signal(color), + size = signal(size), + weight = signal(weight), + family = signal(family); + + final Signal color; + final Signal size; + final Signal weight; + final Signal family; + + void dispose() { + color.dispose(); + size.dispose(); + weight.dispose(); + family.dispose(); + } +} + +enum TextLevel { h1, h2, h3, p } + +/// Owns the playground's deck-wide customization state and pushes a fresh +/// [DeckOptions] into the [DeckController] on every change. +/// +/// All state is in-memory; reloading the page resets to the seeded defaults. +class DeckCustomizationStore { + DeckCustomizationStore(this._controller) { + levels = { + TextLevel.h1: TextLevelSignals( + color: $foreground(), + size: 40, + weight: 700, + family: 'Inter', + ), + TextLevel.h2: TextLevelSignals( + color: $foreground(), + size: 32, + weight: 600, + family: 'Inter', + ), + TextLevel.h3: TextLevelSignals( + color: $foreground(), + size: 24, + weight: 600, + family: 'Inter', + ), + TextLevel.p: TextLevelSignals( + color: $foreground(), + size: 18, + weight: 400, + family: 'Inter', + ), + }; + + _slideStyle = computed(() { + return SlideStyle( + h1: _stylerFor(TextLevel.h1), + h2: _stylerFor(TextLevel.h2), + h3: _stylerFor(TextLevel.h3), + p: _stylerFor(TextLevel.p), + ); + }); + + _effectCleanup = effect(() { + final style = _slideStyle.value; + final bg = background.value; + _controller.options.value = DeckOptions( + baseStyle: style, + parts: SlideParts(background: Box(style: BoxStyler().color(bg))), + ); + }); + } + + final DeckController _controller; + + final Signal background = signal($background()); + + late final Map levels; + + late final ReadonlySignal _slideStyle; + late final EffectCleanup _effectCleanup; + + TextLevelSignals level(TextLevel level) => levels[level]!; + + TextStyler _stylerFor(TextLevel level) { + final signals = levels[level]!; + return _buildTextStyler( + color: signals.color.value, + size: signals.size.value, + weight: signals.weight.value, + family: signals.family.value, + ); + } + + TextStyler _buildTextStyler({ + required Color color, + required double size, + required int weight, + required String family, + }) { + final fontWeight = _fontWeightFor(weight); + final resolvedFamily = _resolveFamily(family, fontWeight); + return TextStyler().style( + TextStyleMix( + fontSize: size, + fontWeight: fontWeight, + color: color, + fontFamily: resolvedFamily.fontFamily, + fontFamilyFallback: resolvedFamily.fontFamilyFallback, + ), + ); + } + + /// Resolves `family` through `google_fonts`, swallowing missing-weight + /// errors so a stray slider tick never crashes the editor. + TextStyle _resolveFamily(String family, FontWeight weight) { + if (!GoogleFonts.config.allowRuntimeFetching) { + return const TextStyle(); + } + try { + return GoogleFonts.getFont(family, fontWeight: weight); + } catch (_) { + try { + return GoogleFonts.getFont(family); + } catch (_) { + return const TextStyle(); + } + } + } + + FontWeight _fontWeightFor(int value) { + switch (value) { + case 100: + return FontWeight.w100; + case 200: + return FontWeight.w200; + case 300: + return FontWeight.w300; + case 400: + return FontWeight.w400; + case 500: + return FontWeight.w500; + case 600: + return FontWeight.w600; + case 700: + return FontWeight.w700; + case 800: + return FontWeight.w800; + case 900: + return FontWeight.w900; + } + return FontWeight.w400; + } + + void dispose() { + _effectCleanup(); + background.dispose(); + for (final signals in levels.values) { + signals.dispose(); + } + _slideStyle.dispose(); + } +} diff --git a/packages/playground/lib/stores/editor_state.dart b/packages/playground/lib/stores/editor_state.dart new file mode 100644 index 00000000..e12c17fd --- /dev/null +++ b/packages/playground/lib/stores/editor_state.dart @@ -0,0 +1,14 @@ +import 'package:signals_flutter/signals_flutter.dart'; + +/// Editor-local navigation state: which slide the caret is currently in. +/// +/// This is the single source of truth for the *active slide* in the editor, +/// distinct from `DeckController.presentation.currentIndex`, which drives +/// presentation-mode routing. +class EditorState { + final activeSlideIndex = signal(0); + + void dispose() { + activeSlideIndex.dispose(); + } +} diff --git a/packages/playground/lib/stores/slide_configuration_store.dart b/packages/playground/lib/stores/slide_configuration_store.dart new file mode 100644 index 00000000..e2c0db21 --- /dev/null +++ b/packages/playground/lib/stores/slide_configuration_store.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart'; +import 'package:superdeck/superdeck.dart'; + +class SlideConfigurationStore extends ChangeNotifier { + SlideConfigurationStore(DeckController controller) { + _slides = List.unmodifiable(controller.slides.value); + _unsubscribe = controller.slides.subscribe((value) { + _slides = List.unmodifiable(value); + notifyListeners(); + }); + } + + late List _slides; + late final void Function() _unsubscribe; + int _activeSlideIndex = 0; + + List get slides => _slides; + + int get activeSlideIndex => _activeSlideIndex; + + set activeSlideIndex(int value) { + final index = value - 1; + + if (_activeSlideIndex == index) return; + _activeSlideIndex = index; + notifyListeners(); + } + + @override + void dispose() { + _unsubscribe.call(); + super.dispose(); + } +} diff --git a/packages/playground/lib/utils/edit_reaction.dart b/packages/playground/lib/utils/edit_reaction.dart new file mode 100644 index 00000000..6d105beb --- /dev/null +++ b/packages/playground/lib/utils/edit_reaction.dart @@ -0,0 +1,213 @@ +import 'package:super_editor/super_editor.dart'; + +const separatorAttribution = NamedAttribution('separator'); +const headerAttribution = NamedAttribution('header'); +const blockAttribution = NamedAttribution('block'); +const blockKeyAttribution = NamedAttribution('block-key'); +const blockValueAttribution = NamedAttribution('block-value'); + +/// Toggles a set of [attributions] across the full range of every [TextNode] +/// whose plain text matches [matches]. +abstract class TextNodeAttributionReaction extends EditReaction { + Set get attributions; + + bool matches(String text); + + @override + void modifyContent( + EditContext editorContext, + RequestDispatcher requestDispatcher, + List changeList, + ) { + final document = editorContext.document; + + for (final node in document) { + if (node is! TextNode) continue; + + final text = node.text.toPlainText(); + if (text.isEmpty) continue; + + final nodeRange = DocumentRange( + start: DocumentPosition( + nodeId: node.id, + nodePosition: const TextNodePosition(offset: 0), + ), + end: DocumentPosition( + nodeId: node.id, + nodePosition: TextNodePosition(offset: text.length), + ), + ); + + requestDispatcher.execute([ + RemoveTextAttributionsRequest( + documentRange: nodeRange, + attributions: attributions, + ), + ]); + + if (matches(text)) { + requestDispatcher.execute([ + AddTextAttributionsRequest( + documentRange: nodeRange, + attributions: attributions, + ), + ]); + } + } + } +} + +class SeparatorColorReaction extends TextNodeAttributionReaction { + @override + Set get attributions => {separatorAttribution}; + + @override + bool matches(String text) => text.trim() == '---'; +} + +final _headerPattern = RegExp(r'^#{1,6} '); + +class HeaderHighlightReaction extends TextNodeAttributionReaction { + @override + Set get attributions => {headerAttribution}; + + @override + bool matches(String text) => _headerPattern.hasMatch(text.trimLeft()); +} + +final _blockPattern = RegExp(r'@block\s*\{([^}]*)\}', dotAll: true); +final _blockKeyValuePattern = RegExp( + r'(\w[\w-]*\s*:)\s*([^,]*?)(?=\s*(?:,|$))', + dotAll: true, +); + +const _blockKeyword = '@block'; + +/// Highlights `@block { key: value }` directives within each text node: +/// the `@block` keyword and braces use [blockAttribution], keys use +/// [blockKeyAttribution], and values use [blockValueAttribution]. +class BlockHighlightReaction extends EditReaction { + @override + void modifyContent( + EditContext editorContext, + RequestDispatcher requestDispatcher, + List changeList, + ) { + final document = editorContext.document; + final attributions = { + blockAttribution, + blockKeyAttribution, + blockValueAttribution, + }; + + for (final node in document) { + if (node is! TextNode) continue; + + final text = node.text.toPlainText(); + if (text.isEmpty) continue; + + requestDispatcher.execute([ + RemoveTextAttributionsRequest( + documentRange: DocumentRange( + start: DocumentPosition( + nodeId: node.id, + nodePosition: const TextNodePosition(offset: 0), + ), + end: DocumentPosition( + nodeId: node.id, + nodePosition: TextNodePosition(offset: text.length), + ), + ), + attributions: attributions, + ), + ]); + + for (final match in _blockPattern.allMatches(text)) { + _highlightDirective(node.id, text, match, requestDispatcher); + } + } + } + + void _addAttribution( + String nodeId, + int start, + int end, + Attribution attribution, + RequestDispatcher requestDispatcher, + ) { + if (end <= start) return; + requestDispatcher.execute([ + AddTextAttributionsRequest( + documentRange: DocumentRange( + start: DocumentPosition( + nodeId: nodeId, + nodePosition: TextNodePosition(offset: start), + ), + end: DocumentPosition( + nodeId: nodeId, + nodePosition: TextNodePosition(offset: end), + ), + ), + attributions: {attribution}, + ), + ]); + } + + void _highlightDirective( + String nodeId, + String text, + RegExpMatch match, + RequestDispatcher requestDispatcher, + ) { + final keywordStart = match.start; + final braceOpen = text.indexOf('{', keywordStart); + final braceClose = match.end - 1; + + _addAttribution( + nodeId, + keywordStart, + keywordStart + _blockKeyword.length, + blockAttribution, + requestDispatcher, + ); + _addAttribution( + nodeId, + braceOpen, + braceOpen + 1, + blockAttribution, + requestDispatcher, + ); + _addAttribution( + nodeId, + braceClose, + braceClose + 1, + blockAttribution, + requestDispatcher, + ); + + final innerStart = braceOpen + 1; + final inner = text.substring(innerStart, braceClose); + for (final kv in _blockKeyValuePattern.allMatches(inner)) { + final keyStart = innerStart + kv.start; + final keyEnd = keyStart + kv.group(1)!.length; + _addAttribution( + nodeId, + keyStart, + keyEnd, + blockKeyAttribution, + requestDispatcher, + ); + + final value = kv.group(2)!; + if (value.isEmpty) continue; + final valueStart = innerStart + kv.start + kv.group(0)!.indexOf(value, kv.group(1)!.length); + _addAttribution( + nodeId, + valueStart, + valueStart + value.length, + blockValueAttribution, + requestDispatcher, + ); + } + } +} diff --git a/packages/playground/lib/utils/memory_asset_cache_store.dart b/packages/playground/lib/utils/memory_asset_cache_store.dart new file mode 100644 index 00000000..b2db9a79 --- /dev/null +++ b/packages/playground/lib/utils/memory_asset_cache_store.dart @@ -0,0 +1,30 @@ +import 'dart:typed_data'; + +import 'package:superdeck_core/superdeck_core.dart'; + +class MemoryAssetCacheStore implements AssetCacheStore { + final Map _cache = {}; + + @override + Future resolve(String assetKey) async { + final key = AssetCacheStore.validateAssetKey(assetKey); + final bytes = _cache[key]; + if (bytes == null || bytes.isEmpty) return null; + return Uri.dataFromBytes(bytes, mimeType: 'image/png'); + } + + @override + Future write(String assetKey, List bytes) async { + final key = AssetCacheStore.validateAssetKey(assetKey); + final data = Uint8List.fromList(bytes); + if (data.isEmpty) return null; + _cache[key] = data; + return Uri.dataFromBytes(data, mimeType: 'image/png'); + } + + @override + Future delete(String assetKey) async { + final key = AssetCacheStore.validateAssetKey(assetKey); + _cache.remove(key); + } +} diff --git a/packages/playground/lib/utils/memory_deck_loader.dart b/packages/playground/lib/utils/memory_deck_loader.dart new file mode 100644 index 00000000..13c4e763 --- /dev/null +++ b/packages/playground/lib/utils/memory_deck_loader.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:superdeck_builder/superdeck_builder.dart'; +import 'package:superdeck_core/superdeck_core.dart'; + +/// A [DeckLoader] that parses markdown in-memory for live preview. +class MemoryDeckLoader extends DeckLoader { + final _controller = StreamController.broadcast(); + bool _disposed = false; + + @override + Stream load() => _controller.stream; + + /// Parses the given markdown and emits a [SlidesLoadedEvent]. + void updateMarkdown(String markdown) { + if (_disposed) return; + + try { + final rawSlides = const MarkdownParser().parse(markdown); + final slides = [ + for (final raw in rawSlides) + Slide( + key: raw.key, + options: .parse(raw.frontmatter), + sections: const SectionParser().parse(raw.content), + comments: const CommentParser().parse(raw.content), + ), + ]; + _controller.add(SlidesLoadedEvent(slides)); + } catch (e) { + _controller.add(SlidesErrorEvent('$e', error: e)); + } + } + + @override + Future reload() async {} + + @override + Future dispose() async { + _disposed = true; + await _controller.close(); + } +} diff --git a/packages/playground/lib/utils/takeover_route.dart b/packages/playground/lib/utils/takeover_route.dart new file mode 100644 index 00000000..aacc030d --- /dev/null +++ b/packages/playground/lib/utils/takeover_route.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class TakeoverRoute extends PageRoute { + TakeoverRoute({ + required this.builder, + this.transitionDuration = const Duration(milliseconds: 300), + this.reverseTransitionDuration = const Duration(milliseconds: 300), + super.settings, + }); + + final WidgetBuilder builder; + + @override + final Duration transitionDuration; + + @override + final Duration reverseTransitionDuration; + + @override + bool get opaque => true; + + @override + bool get maintainState => false; + + @override + Color? get barrierColor => null; + + @override + String? get barrierLabel => null; + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return builder(context); + } + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition(opacity: animation, child: child); + } +} diff --git a/packages/playground/macos/.gitignore b/packages/playground/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/packages/playground/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/playground/macos/Flutter/Flutter-Debug.xcconfig b/packages/playground/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/packages/playground/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/playground/macos/Flutter/Flutter-Release.xcconfig b/packages/playground/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/packages/playground/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/playground/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/playground/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..84a48528 --- /dev/null +++ b/packages/playground/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import screen_retriever_macos +import sqflite_darwin +import url_launcher_macos +import webview_flutter_wkwebview +import window_manager + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/packages/playground/macos/Podfile b/packages/playground/macos/Podfile new file mode 100644 index 00000000..ff5ddb3b --- /dev/null +++ b/packages/playground/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/playground/macos/Podfile.lock b/packages/playground/macos/Podfile.lock new file mode 100644 index 00000000..b21588bf --- /dev/null +++ b/packages/playground/macos/Podfile.lock @@ -0,0 +1,48 @@ +PODS: + - FlutterMacOS (1.0.0) + - screen_retriever_macos (0.0.1): + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + webview_flutter_wkwebview: + :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + +SPEC CHECKSUMS: + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/packages/playground/macos/Runner.xcodeproj/project.pbxproj b/packages/playground/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..560721fe --- /dev/null +++ b/packages/playground/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 7C09BF2FF8AA7DC3AA87B934 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13C11E7A0C7D289B8711255A /* Pods_Runner.framework */; }; + A1B3F9994F5F7AEF4E145DB1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A0E2F9E7B3F9C615A771CE8 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 10B5D095BC65774991CA8609 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 13C11E7A0C7D289B8711255A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 193C6B94091A41C1F69580CA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 2F6C76FB008536F72CF171ED /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* playground.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = playground.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5A0E2F9E7B3F9C615A771CE8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7C9554502C199CA533DF6680 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + B488B841613D088BD70DDB64 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + BA780508A5DF6AD077DB453C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A1B3F9994F5F7AEF4E145DB1 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C09BF2FF8AA7DC3AA87B934 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 86B05520F6D2E2E98D1027CA /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* playground.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 86B05520F6D2E2E98D1027CA /* Pods */ = { + isa = PBXGroup; + children = ( + 10B5D095BC65774991CA8609 /* Pods-Runner.debug.xcconfig */, + B488B841613D088BD70DDB64 /* Pods-Runner.release.xcconfig */, + 193C6B94091A41C1F69580CA /* Pods-Runner.profile.xcconfig */, + BA780508A5DF6AD077DB453C /* Pods-RunnerTests.debug.xcconfig */, + 7C9554502C199CA533DF6680 /* Pods-RunnerTests.release.xcconfig */, + 2F6C76FB008536F72CF171ED /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 13C11E7A0C7D289B8711255A /* Pods_Runner.framework */, + 5A0E2F9E7B3F9C615A771CE8 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 9F5E019185C37FC3D9E04B5D /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 8F2351FD941B3A40988149F5 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 63837BC782A09802B3C66508 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* playground.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 63837BC782A09802B3C66508 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8F2351FD941B3A40988149F5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9F5E019185C37FC3D9E04B5D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BA780508A5DF6AD077DB453C /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.playground.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/playground.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/playground"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7C9554502C199CA533DF6680 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.playground.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/playground.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/playground"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2F6C76FB008536F72CF171ED /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.playground.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/playground.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/playground"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/playground/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/playground/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/playground/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/playground/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/playground/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e9cfaddd --- /dev/null +++ b/packages/playground/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/playground/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/playground/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/playground/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/playground/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/playground/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/playground/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/playground/macos/Runner/AppDelegate.swift b/packages/playground/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/packages/playground/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/packages/playground/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/playground/macos/Runner/Base.lproj/MainMenu.xib b/packages/playground/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/packages/playground/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/playground/macos/Runner/Configs/AppInfo.xcconfig b/packages/playground/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..0cf60e19 --- /dev/null +++ b/packages/playground/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = playground + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.playground + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/packages/playground/macos/Runner/Configs/Debug.xcconfig b/packages/playground/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/packages/playground/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/playground/macos/Runner/Configs/Release.xcconfig b/packages/playground/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/packages/playground/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/playground/macos/Runner/Configs/Warnings.xcconfig b/packages/playground/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/packages/playground/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/playground/macos/Runner/DebugProfile.entitlements b/packages/playground/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..dddb8a30 --- /dev/null +++ b/packages/playground/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/playground/macos/Runner/Info.plist b/packages/playground/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/packages/playground/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/playground/macos/Runner/MainFlutterWindow.swift b/packages/playground/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/packages/playground/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/playground/macos/Runner/Release.entitlements b/packages/playground/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/packages/playground/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/playground/macos/RunnerTests/RunnerTests.swift b/packages/playground/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/packages/playground/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/playground/pubspec.yaml b/packages/playground/pubspec.yaml new file mode 100644 index 00000000..dd6b7fb3 --- /dev/null +++ b/packages/playground/pubspec.yaml @@ -0,0 +1,35 @@ +name: playground +description: A playground app for experimenting with Mix, Remix, and Hero UI. +publish_to: none + +environment: + sdk: ">=3.11.3 <4.0.0" + flutter: ">=3.38.1" + +dependencies: + flutter: + sdk: flutter + mix: ^2.0.3 + remix: ^0.2.0 + super_editor: 0.3.0-dev.49 + provider: ^6.1.2 + signals_flutter: ^6.2.0 + google_fonts: ^8.1.0 + superdeck: + path: ../superdeck + superdeck_core: + path: ../core + superdeck_builder: + path: ../builder + hero_ui: + git: + url: https://github.com/tilucasoli/hero_ui + ref: v0.0.5 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true diff --git a/packages/playground/test/features/editor/thumbnail_refresher_test.dart b/packages/playground/test/features/editor/thumbnail_refresher_test.dart new file mode 100644 index 00000000..d92b69e9 --- /dev/null +++ b/packages/playground/test/features/editor/thumbnail_refresher_test.dart @@ -0,0 +1,131 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:playground/features/editor/thumbnail_refresher.dart'; +import 'package:playground/stores/editor_state.dart'; +import 'package:playground/utils/memory_deck_loader.dart'; +import 'package:provider/provider.dart'; +import 'package:superdeck/superdeck.dart'; + +const _twoSlides = '# Slide one\n\n---\n\n# Slide two'; + +void main() { + // Keep deck building offline: never fetch fonts over the network in tests. + setUpAll(() => GoogleFonts.config.allowRuntimeFetching = false); + + late MemoryDeckLoader loader; + late DeckController controller; + late EditorState editorState; + late List forceCalls; + + setUp(() { + loader = MemoryDeckLoader(); + controller = DeckController(deckLoader: loader, options: DeckOptions()); + editorState = EditorState(); + forceCalls = []; + }); + + tearDown(() async { + controller.dispose(); + editorState.dispose(); + await loader.dispose(); + }); + + // Records the `force` flag of every regeneration the refresher requests. + void record( + BuildContext context, + List slides, { + bool force = false, + }) { + forceCalls.add(force); + } + + Future pumpRefresher(WidgetTester tester) { + return tester.pumpWidget( + MultiProvider( + providers: [ + Provider.value(value: controller), + Provider.value(value: editorState), + ], + child: ThumbnailRefresher( + regenerate: record, + child: const SizedBox(), + ), + ), + ); + } + + // Runs the refresher's pending post-frame regeneration. `addPostFrameCallback` + // never schedules a frame, so an isolated test must request one itself. + Future settle(WidgetTester tester) async { + tester.binding.scheduleFrame(); + await tester.pump(); + } + + // Delivers the SlidesLoadedEvent, then runs the initial regeneration. + Future loadDeck(WidgetTester tester) async { + loader.updateMarkdown(_twoSlides); + await tester.pump(); // deliver the event; the effect schedules a pass + await settle(tester); + } + + testWidgets('does not regenerate before the deck has slides', ( + tester, + ) async { + await pumpRefresher(tester); + await settle(tester); + + expect(forceCalls, isEmpty); + }); + + testWidgets('initial deck load regenerates without force', (tester) async { + await pumpRefresher(tester); + await loadDeck(tester); + + expect(forceCalls, [false]); + }); + + testWidgets('a style change force-regenerates every thumbnail', ( + tester, + ) async { + await pumpRefresher(tester); + await loadDeck(tester); + expect(forceCalls, [false]); + + controller.options.value = DeckOptions(debug: true); + await settle(tester); + + expect(forceCalls, [false, true]); + }); + + testWidgets('style changes within one frame coalesce into one pass', ( + tester, + ) async { + await pumpRefresher(tester); + await loadDeck(tester); + expect(forceCalls, [false]); + + // Three option writes before the next frame collapse to a single pass. + controller.options.value = DeckOptions(debug: true); + controller.options.value = DeckOptions(debug: false); + controller.options.value = DeckOptions(debug: true); + await settle(tester); + + expect(forceCalls, [false, true]); + }); + + testWidgets('a style change before the deck loads is ignored', ( + tester, + ) async { + await pumpRefresher(tester); + + // No slides yet: the options effect must not request a pass. + controller.options.value = DeckOptions(debug: true); + await settle(tester); + expect(forceCalls, isEmpty); + + // The deck then loads and captures once with the current options. + await loadDeck(tester); + expect(forceCalls, [false]); + }); +} diff --git a/packages/playground/web/favicon.png b/packages/playground/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/packages/playground/web/favicon.png differ diff --git a/packages/playground/web/icons/Icon-192.png b/packages/playground/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/packages/playground/web/icons/Icon-192.png differ diff --git a/packages/playground/web/icons/Icon-512.png b/packages/playground/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/packages/playground/web/icons/Icon-512.png differ diff --git a/packages/playground/web/icons/Icon-maskable-192.png b/packages/playground/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/packages/playground/web/icons/Icon-maskable-192.png differ diff --git a/packages/playground/web/icons/Icon-maskable-512.png b/packages/playground/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/packages/playground/web/icons/Icon-maskable-512.png differ diff --git a/packages/playground/web/index.html b/packages/playground/web/index.html new file mode 100644 index 00000000..74c61af7 --- /dev/null +++ b/packages/playground/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + playground + + + + + + + diff --git a/packages/playground/web/manifest.json b/packages/playground/web/manifest.json new file mode 100644 index 00000000..3274aace --- /dev/null +++ b/packages/playground/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "playground", + "short_name": "playground", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/superdeck/lib/src/capture/slide_capture_service.dart b/packages/superdeck/lib/src/capture/slide_capture_service.dart index 51d27fa4..a401b696 100644 --- a/packages/superdeck/lib/src/capture/slide_capture_service.dart +++ b/packages/superdeck/lib/src/capture/slide_capture_service.dart @@ -6,8 +6,9 @@ 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'; import '../utils/constants.dart'; import '../deck/slide_configuration.dart'; @@ -117,6 +118,7 @@ class SlideCaptureService { RenderConfig config, ) async { try { + final mixScope = MixScope.maybeOf(config.context); final child = InheritedTheme.captureAll( config.context, MediaQuery( @@ -124,7 +126,15 @@ class SlideCaptureService { child: MaterialApp( theme: Theme.of(config.context), debugShowCheckedModeBanner: false, - home: Scaffold(body: widget), + home: Scaffold( + body: MixScope( + tokens: { + ...?mixScope?.tokens, + ...SDColors.colorMap, + }, + child: widget, + ), + ), ), ), ); diff --git a/packages/superdeck/lib/src/rendering/slides/slide_parts.dart b/packages/superdeck/lib/src/rendering/slides/slide_parts.dart index a8266ac7..163398fe 100644 --- a/packages/superdeck/lib/src/rendering/slides/slide_parts.dart +++ b/packages/superdeck/lib/src/rendering/slides/slide_parts.dart @@ -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; } diff --git a/packages/superdeck/lib/src/rendering/slides/slide_render_view.dart b/packages/superdeck/lib/src/rendering/slides/slide_render_view.dart index cd98d38c..c2979110 100644 --- a/packages/superdeck/lib/src/rendering/slides/slide_render_view.dart +++ b/packages/superdeck/lib/src/rendering/slides/slide_render_view.dart @@ -14,8 +14,11 @@ class SlideRenderView extends StatelessWidget { @override Widget build(BuildContext context) { - return MixScope( + final oldMixScope = MixScope.maybeOf(context); + + return MixScope.inherit( colors: SDColors.colorMap, + tokens: {...oldMixScope?.tokens ?? {}}, child: InheritedData( data: configuration, child: SlideView(configuration), diff --git a/packages/superdeck/lib/src/ui/widgets/button.dart b/packages/superdeck/lib/src/ui/widgets/button.dart index e25db507..584f1db5 100644 --- a/packages/superdeck/lib/src/ui/widgets/button.dart +++ b/packages/superdeck/lib/src/ui/widgets/button.dart @@ -20,7 +20,7 @@ class SDButton extends StatelessWidget { onPressed: onPressed, style: _style, label: label, - icon: icon, + leadingIcon: icon, ); } diff --git a/packages/superdeck/lib/superdeck.dart b/packages/superdeck/lib/superdeck.dart index e2c85726..c29c9bfd 100644 --- a/packages/superdeck/lib/superdeck.dart +++ b/packages/superdeck/lib/superdeck.dart @@ -22,6 +22,7 @@ export 'src/styling/components/slide.dart'; // UI export 'src/ui/superdeck_app.dart'; +export 'src/ui/widgets/loading_indicator.dart'; // Presentation export 'src/deck/deck_controller.dart'; diff --git a/packages/superdeck/pubspec.yaml b/packages/superdeck/pubspec.yaml index 9206ab2f..dd6fd046 100644 --- a/packages/superdeck/pubspec.yaml +++ b/packages/superdeck/pubspec.yaml @@ -21,15 +21,15 @@ dependencies: scrollable_positioned_list: ^0.3.8 go_router: ^14.2.7 path_provider: ^2.1.4 - mix: ^2.0.0-rc.0 - remix: ^0.1.0-beta.2 + mix: ^2.0.3 + remix: ^0.2.0 flutter_markdown_plus: ^1.0.5 superdeck_core: ^1.0.0 markdown: ^7.3.0 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 diff --git a/pubspec.lock b/pubspec.lock index e94cb138..99372d71 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -282,5 +282,4 @@ packages: source: hosted version: "2.2.2" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.38.1" + dart: ">=3.11.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index f07d3148..1141ef98 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,8 +2,7 @@ name: superdeck_workspace publish_to: none environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.1" + sdk: ">=3.11.0 <4.0.0" dependencies: melos: ^6.3.3