diff --git a/.gitignore b/.gitignore index 1821d0e..e26e637 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + # Miscellaneous *.class *.log @@ -21,18 +25,28 @@ migrate_working_dir/ # is commented out by default. #.vscode/ +# Ignoring native folders of the example as they can be re-generated easily using: +# flutter create --platforms=android,ios,web,windows,macos . +**/example/android/ +**/example/ios/ +**/example/web/ +**/example/windows/ +**/example/macos/ +**/example/linux/ + # Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock **/doc/api/ .dart_tool/ -.packages -build/ .flutter-plugins .flutter-plugins-dependencies - +.packages +.pub-cache/ +.pub/ +build/ # FVM Version Cache .fvm/ .firebase +# Node file for website +node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index bd44e97..dc7d15c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ ### 0.0.3 -- Cleaned up dependencies -- Updated example code -- Improved logging +* Cleaned up dependencies +* Updated example code +* Improved logging +* Fixed and improved mermaid generation ## 0.0.2 diff --git a/analysis_options.yaml b/analysis_options.yaml index 46141a9..c2bb38b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -16,6 +16,7 @@ analyzer: linter: rules: public_member_api_docs: false + always_use_package_imports: false prefer_relative_imports: true library_private_types_in_public_api: false diff --git a/example/assets/assets.json b/example/assets/assets.json new file mode 100644 index 0000000..4f02b2b --- /dev/null +++ b/example/assets/assets.json @@ -0,0 +1,359 @@ +[ + { + "hash": "475860768", + "width": 1080.0, + "height": 720.0, + "local_path": "assets/images/sd_cached_475860768.jpeg", + "type": "cached" + }, + { + "hash": "968081805", + "width": 1080.0, + "height": 1080.0, + "local_path": "assets/images/sd_cached_968081805.gif", + "type": "cached" + }, + { + "hash": "580039713", + "width": 1080.0, + "height": 500.0, + "local_path": "assets/images/sd_cached_580039713.jpeg", + "type": "cached" + }, + { + "hash": "165689209", + "width": 270.0, + "height": 480.0, + "local_path": "assets/images/sd_cached_165689209.gif", + "type": "cached" + }, + { + "hash": "151971805", + "width": 300.0, + "height": 200.0, + "local_path": "assets/images/sd_cached_151971805.jpeg", + "type": "cached" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "hash": "372574455", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_372574455.png", + "type": "thumbnail" + }, + { + "hash": "531847541", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_531847541.png", + "type": "thumbnail" + }, + { + "hash": "231105015", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_231105015.png", + "type": "thumbnail" + }, + { + "hash": "39806272", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_39806272.png", + "type": "thumbnail" + }, + { + "hash": "825215970", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_825215970.png", + "type": "thumbnail" + }, + { + "hash": "126316725", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_126316725.png", + "type": "thumbnail" + }, + { + "hash": "745277359", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_745277359.png", + "type": "thumbnail" + }, + { + "hash": "232865622", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_232865622.png", + "type": "thumbnail" + }, + { + "hash": "890549629", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_890549629.png", + "type": "thumbnail" + }, + { + "hash": "836619553", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_836619553.png", + "type": "thumbnail" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "hash": "883364285", + "width": 1280.0, + "height": 720.0, + "local_path": "assets/images/sd_thumb_883364285.png", + "type": "thumbnail" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + }, + { + "width": 600.0, + "height": 860.0, + "local_path": "assets/images/sd_generated_7177249.png", + "hash": "7177249", + "type": "generated" + } +] diff --git a/example/assets/config.json b/example/assets/config.json index ea398a4..be4e9f2 100644 --- a/example/assets/config.json +++ b/example/assets/config.json @@ -1,9 +1 @@ -{ - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "cache_remote_assets": false -} \ No newline at end of file +{"transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"cache_remote_assets":false} \ No newline at end of file diff --git a/example/assets/images/sd_cached_151971805.jpeg b/example/assets/images/sd_cached_151971805.jpeg new file mode 100644 index 0000000..4f8860d Binary files /dev/null and b/example/assets/images/sd_cached_151971805.jpeg differ diff --git a/example/assets/images/sd_cached_165689209.gif b/example/assets/images/sd_cached_165689209.gif new file mode 100644 index 0000000..87935e8 Binary files /dev/null and b/example/assets/images/sd_cached_165689209.gif differ diff --git a/example/assets/images/sd_cached_475860768.jpeg b/example/assets/images/sd_cached_475860768.jpeg new file mode 100644 index 0000000..957117d Binary files /dev/null and b/example/assets/images/sd_cached_475860768.jpeg differ diff --git a/example/assets/images/sd_cached_580039713.jpeg b/example/assets/images/sd_cached_580039713.jpeg new file mode 100644 index 0000000..1a87f11 Binary files /dev/null and b/example/assets/images/sd_cached_580039713.jpeg differ diff --git a/example/assets/images/sd_cached_968081805.gif b/example/assets/images/sd_cached_968081805.gif new file mode 100644 index 0000000..aaabb82 Binary files /dev/null and b/example/assets/images/sd_cached_968081805.gif differ diff --git a/example/assets/images/sd_asset_7177249.png b/example/assets/images/sd_generated_7177249.png similarity index 100% rename from example/assets/images/sd_asset_7177249.png rename to example/assets/images/sd_generated_7177249.png diff --git a/example/assets/images/sd_thumb_126316725.png b/example/assets/images/sd_thumb_126316725.png new file mode 100644 index 0000000..6da5af0 Binary files /dev/null and b/example/assets/images/sd_thumb_126316725.png differ diff --git a/example/assets/images/sd_thumb_231105015.png b/example/assets/images/sd_thumb_231105015.png new file mode 100644 index 0000000..5114837 Binary files /dev/null and b/example/assets/images/sd_thumb_231105015.png differ diff --git a/example/assets/images/sd_thumb_232865622.png b/example/assets/images/sd_thumb_232865622.png new file mode 100644 index 0000000..7726393 Binary files /dev/null and b/example/assets/images/sd_thumb_232865622.png differ diff --git a/example/assets/images/sd_thumb_372574455.png b/example/assets/images/sd_thumb_372574455.png new file mode 100644 index 0000000..9d9c5c4 Binary files /dev/null and b/example/assets/images/sd_thumb_372574455.png differ diff --git a/example/assets/images/sd_thumb_39806272.png b/example/assets/images/sd_thumb_39806272.png new file mode 100644 index 0000000..e9f5785 Binary files /dev/null and b/example/assets/images/sd_thumb_39806272.png differ diff --git a/example/assets/images/sd_thumb_531847541.png b/example/assets/images/sd_thumb_531847541.png new file mode 100644 index 0000000..7e12329 Binary files /dev/null and b/example/assets/images/sd_thumb_531847541.png differ diff --git a/example/assets/images/sd_thumb_745277359.png b/example/assets/images/sd_thumb_745277359.png new file mode 100644 index 0000000..7d69611 Binary files /dev/null and b/example/assets/images/sd_thumb_745277359.png differ diff --git a/example/assets/images/sd_thumb_825215970.png b/example/assets/images/sd_thumb_825215970.png new file mode 100644 index 0000000..0d76bd4 Binary files /dev/null and b/example/assets/images/sd_thumb_825215970.png differ diff --git a/example/assets/images/sd_thumb_836619553.png b/example/assets/images/sd_thumb_836619553.png new file mode 100644 index 0000000..8f91337 Binary files /dev/null and b/example/assets/images/sd_thumb_836619553.png differ diff --git a/example/assets/images/sd_thumb_883364285.png b/example/assets/images/sd_thumb_883364285.png new file mode 100644 index 0000000..d3d9b80 Binary files /dev/null and b/example/assets/images/sd_thumb_883364285.png differ diff --git a/example/assets/images/sd_thumb_890549629.png b/example/assets/images/sd_thumb_890549629.png new file mode 100644 index 0000000..a0edff2 Binary files /dev/null and b/example/assets/images/sd_thumb_890549629.png differ diff --git a/example/assets/slides.json b/example/assets/slides.json index 5e7da0b..728f014 100644 --- a/example/assets/slides.json +++ b/example/assets/slides.json @@ -1,256 +1 @@ -[ - { - "background": "https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXE3N3hlcXl0OXR3eXJjOGJ3MGtvcmMwMWY4eDZqMHFvd3lxaHA4ZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/YpwVvng323pw45tYwv/giphy.gif", - "content": { - "flex": 1, - "alignment": "center_left" - }, - "style": "announcement", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "raw": "\nstyle: announcement\n\nbackground: https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXE3N3hlcXl0OXR3eXJjOGJ3MGtvcmMwMWY4eDZqMHFvd3lxaHA4ZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/YpwVvng323pw45tYwv/giphy.gif\ncontent:\n alignment: center_left\n\n", - "data": "# SUPERDECK \n ## RELEASED! 🎉\n\n #### Create beautiful Flutter\n #### slides with Markdown", - "type": "Slide" - }, - { - "background": "https://source.unsplash.com/person-discussing-while-standing-in-front-of-a-large-screen-in-front-of-people-inside-dim-lighted-room-bzdhc5b3Bxs", - "style": "cover", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "::left::\n \n## Making your Flutter presentations easier than ever.\n\n::right::\n#### For Flutter developers. Made with ❤️ Flutter.", - "sections": { - "left": { - "flex": 2, - "alignment": "center_left" - }, - "right": { - "flex": 1, - "alignment": "bottom_right" - } - }, - "raw": "\nstyle: cover\nlayout: two_column\nbackground: https://source.unsplash.com/person-discussing-while-standing-in-front-of-a-large-screen-in-front-of-people-inside-dim-lighted-room-bzdhc5b3Bxs\nsections:\n left:\n flex: 2\n alignment: center_left\n right:\n alignment: bottom_right\n flex: 1\n", - "type": "Slide", - "layout": "two_column" - }, - { - "style": "quote", - "content": { - "flex": 1, - "alignment": "bottom_right" - }, - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "> Create your Flutter presentations faster and easier than ever.\n> You can quote me on that.\n> ### Leo Farias", - "options": { - "src": "https://source.unsplash.com/people-watching-concert-during-night-time-blgOFmPIlr0", - "fit": "cover", - "flex": 1, - "position": "right" - }, - "raw": "\nstyle: quote\nlayout: image\noptions:\n src: https://source.unsplash.com/people-watching-concert-during-night-time-blgOFmPIlr0\n fit: cover\ncontent:\n alignment: bottom_right\n", - "type": "Slide", - "layout": "image" - }, - { - "background": "https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGt1MnQ5N2k3cXVma24wb3V5cThlZ3ExY2NvY3czcmozang0bGQ1ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XzWd8acQ37byKR4tmd/giphy.gif", - "style": "cover", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "raw": "\nbackground: https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGt1MnQ5N2k3cXVma24wb3V5cThlZ3ExY2NvY3czcmozang0bGQ1ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XzWd8acQ37byKR4tmd/giphy.gif\nstyle: cover\n", - "data": "# Complex layouts", - "type": "Slide" - }, - { - "style": "show_sections", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "# Image Layout\n\nCreate beautiful slides with images that fit your content.\n\n##### Options\n```yaml\noptions:\n src: https//www.url.com/image.jpg\n fit: cover\n position: left\n flex: 1\n```\n\n> Define position fit and flex options for the image.", - "options": { - "src": "https://source.unsplash.com/random/900×700/?waves", - "fit": "cover", - "flex": 1, - "position": "left" - }, - "raw": "\nlayout: image\nstyle: show_sections\noptions:\n src: https://source.unsplash.com/random/900×700/?waves\n fit: cover\n position: left\n flex: 1\n", - "type": "Slide", - "layout": "image" - }, - { - "style": "show_sections", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "::left::\n\n# Two Column\n\nThis is a two-column layout. You can use it to compare two different concepts or ideas.\n\n::right::\n\n### Section Options\n\nEasily customize the content of each section to suit your needs.\n\nUse front matter to define the layout of each section\n\n\n```yaml\nsections:\n left:\n flex: 2\n right:\n alignment: bottom_left\n```", - "sections": { - "left": { - "flex": 2, - "alignment": "center_left" - }, - "right": { - "flex": 1, - "alignment": "bottom_left" - } - }, - "raw": "\nlayout: two_column\nstyle: show_sections\nsections:\n left:\n flex: 2\n right:\n alignment: bottom_left\n", - "type": "Slide", - "layout": "two_column" - }, - { - "content": { - "flex": 2, - "alignment": "center" - }, - "style": "show_sections", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "# Two Column + Header\n\n\n::left::\n\n### Left Section\nEasily customize the content of each section to suit your needs.\n\nUse front matter to define the layout of each section\n::right::\n\n#### Section Options\n\n```yaml\nsections:\n left:\n alignment: bottom_right\n flex: 2\n right:\n alignment: bottom_left\n header:\n alignment: bottom_left\n```", - "sections": { - "left": { - "flex": 2, - "alignment": "center_left" - }, - "right": { - "flex": 1, - "alignment": "bottom_left" - }, - "header": { - "flex": 1, - "alignment": "bottom_left" - } - }, - "raw": "\nlayout: two_column_header\ncontent:\n alignment: center\n flex: 2\nsections:\n left:\n flex: 2\n right:\n alignment: bottom_left\n header:\n alignment: bottom_left\nstyle: show_sections\n", - "type": "Slide", - "layout": "two_column_header" - }, - { - "content": { - "flex": 1, - "alignment": "center" - }, - "style": "rad", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "# Mix\n\nIntegration with Mix gives you complete control over all styling elements in your slides with a simple and intuitive API.\n\n::right::\n\n```dart\nVariantAttribute get radStyle {\n return const SlideVariant('rad')(\n $.h1.textStyle.as(GoogleFonts.poppins()),\n $.h1.textStyle.fontSize(140),\n $.code.decoration.border.all(\n color: Colors.red,\n width: 3,\n ),\n $.code.decoration(\n color: Colors.black54,\n ),\n $.code.padding.all(40),\n\n $.outerContainer.margin.all(60),\n\n $.innerContainer.borderRadius(25),\n $.innerContainer.shadow(\n blurRadius: 0,\n spreadRadius: 10,\n color: Colors.red.withOpacity(1),\n ),\n $.innerContainer.gradient.radial(\n stops: [0.0, 1.0],\n radius: 0.7,\n colors: [Colors.purple, Colors.deepPurple],\n ),\n\n // Events\n onMouseHover((event) {\n final position = event.position;\n final dx = position.x * 10;\n final dy = position.y * 10;\n\n return Style(\n $.innerContainer.transform(_transformMatrix(position)),\n $.innerContainer.shadow.offset(dx, dy),\n $.innerContainer.gradient.radial(\n center: position,\n ),\n );\n }),\n\n (onPressed | onLongPressed)(\n $.innerContainer.shadow(\n blurRadius: 5,\n spreadRadius: 1,\n offset: Offset.zero,\n color: Colors.purpleAccent,\n ),\n $.innerContainer.border.all(color: Colors.white, width: 1),\n $.innerContainer.gradient.radial\n .colors([Colors.purpleAccent, Colors.purpleAccent]),\n ),\n );\n}\n```", - "sections": { - "left": null, - "right": { - "flex": 2, - "alignment": "bottom_left" - } - }, - "raw": "\nstyle: rad\nlayout: two_column\ncontent:\n alignment: center\nsections:\n left:\n right:\n alignment: bottom_left\n flex: 2\n", - "type": "Slide", - "layout": "two_column" - }, - { - "background": "https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExeGswdWJvY2oxazJoY3g2Y2poNHBvZXlpYmd5YTg0Z2g0ODRrbng4MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/oB6KlAvOuaLtxYy8l4/giphy.gif", - "style": "cover", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "raw": "\nstyle: cover\nbackground: https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExeGswdWJvY2oxazJoY3g2Y2poNHBvZXlpYmd5YTg0Z2g0ODRrbng4MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/oB6KlAvOuaLtxYy8l4/giphy.gif\n", - "data": "# Markdown support", - "type": "Slide" - }, - { - "content": { - "flex": 4, - "alignment": "center_left" - }, - "style": "show_sections", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "::left::\n\n\n**Bold Text**\n\n*Italic Text*\n\n~~Strikethrough~~\n\n`Inline Code`\n\n[Link here](https://github.com/leoafarias/superdeck)\n\n::right::\n\nLists\n\n1. Ordered list item 1\n2. Ordered list item 2\n\n- Unordered list item 1\n- Unordered list item 2\n\nQuotes\n\n> If you want to go fast, go alone. \n> If you want to go far, go together.\n> ### African Proverb", - "sections": {}, - "raw": "\nstyle: show_sections\nlayout: two_column\nsections:\ncontent:\n flex: 4\n", - "type": "Slide", - "layout": "two_column" - }, - { - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "::left::\n\n\nCode\n```dart\nint factorial(int n) {\n return n == 0 ? 1 : n * factorial(n - 1);\n}\n```\n\nTasks\n- [ ] Item 1\n- [x] Item 2\n\nSubtasks\n\n- [x] Item 1\n - [ ] Subitem 1\n\n::right::\n\nImages\n![Unsplash Image](https://source.unsplash.com/random/300x200/?landscape)\n\n\nTable\n\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1A | Cell 1B |\n| Cell 2A | Cell 2B |\n\nDivider\n\n___", - "sections": {}, - "raw": "\nlayout: two_column\n", - "type": "Slide", - "layout": "two_column" - }, - { - "title": "Mermaid example", - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "::left::\n\n![Mermaid Diagram](assets/images/sd_asset_7177249.png)\n \n\n::right::\n\n## Mermaid Support\n\nSuperdeck allows you to use Mermaid diagrams in your slides. It automatically converts the code into a visual representation.", - "sections": {}, - "raw": "\ntitle: \"Mermaid example\"\nlayout: two_column\n", - "type": "Slide", - "layout": "two_column" - }, - { - "options": { - "name": "demo", - "args": { - "text": "Hello, Superdeck!", - "height": 200.0, - "width": 300.0 - }, - "flex": 1, - "position": "right" - }, - "transition": { - "type": "fade_in", - "duration": 0, - "delay": 0, - "curve": "ease" - }, - "data": "## Showcase your widgets", - "raw": "\nlayout: widget\noptions:\n name: demo\n args:\n text: Hello, Superdeck!\n height: 200.0\n width: 300.0\n", - "type": "Slide", - "layout": "widget" - } -] \ No newline at end of file +[{"style":"quote","content":{"flex":1,"alignment":"bottom_right"},"transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"> Create your Flutter presentations faster and easier than ever.\n> You can quote me on that.\n> ### Leo Farias","options":{"src":"https://source.unsplash.com/people-watching-concert-during-night-time-blgOFmPIlr0","fit":"cover","flex":1,"position":"right"},"raw":"---\nstyle: quote\nlayout: image\noptions:\n src: https://source.unsplash.com/people-watching-concert-during-night-time-blgOFmPIlr0\n fit: cover\ncontent:\n alignment: bottom_right\n---\n\n\n> Create your Flutter presentations faster and easier than ever.\n> You can quote me on that.\n> ### Leo Farias","type":"Slide","layout":"image"},{"background":"https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGt1MnQ5N2k3cXVma24wb3V5cThlZ3ExY2NvY3czcmozang0bGQ1ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XzWd8acQ37byKR4tmd/giphy.gif","style":"cover","transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"raw":"---\nbackground: https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGt1MnQ5N2k3cXVma24wb3V5cThlZ3ExY2NvY3czcmozang0bGQ1ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XzWd8acQ37byKR4tmd/giphy.gif\nstyle: cover\n---\n\n# Complex layouts","data":"# Complex layouts","type":"Slide"},{"style":"show_sections","transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"# Image Layout\n\nCreate beautiful slides with images that fit your content.\n\n##### Options\n```yaml\noptions:\n src: https//www.url.com/image.jpg\n fit: cover\n position: left\n flex: 1\n```\n\n> Define position fit and flex options for the image.","options":{"src":"https://source.unsplash.com/random/900×700/?waves","fit":"cover","flex":1,"position":"left"},"raw":"---\nlayout: image\nstyle: show_sections\noptions:\n src: https://source.unsplash.com/random/900×700/?waves\n fit: cover\n position: left\n flex: 1\n---\n\n# Image Layout\n\nCreate beautiful slides with images that fit your content.\n\n##### Options\n```yaml\noptions:\n src: https//www.url.com/image.jpg\n fit: cover\n position: left\n flex: 1\n```\n\n> Define position fit and flex options for the image.","type":"Slide","layout":"image"},{"style":"show_sections","transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"::left::\n\n# Two Column\n\nThis is a two-column layout. You can use it to compare two different concepts or ideas.\n\n::right::\n\n### Section Options\n\nEasily customize the content of each section to suit your needs.\n\nUse front matter to define the layout of each section\n\n\n```yaml\nsections:\n left:\n flex: 2\n right:\n alignment: bottom_left\n```","sections":{"left":{"flex":2,"alignment":"center_left"},"right":{"flex":1,"alignment":"bottom_left"}},"raw":"---\nlayout: two_column\nstyle: show_sections\nsections:\n left:\n flex: 2\n right:\n alignment: bottom_left\n---\n\n::left::\n\n# Two Column\n\nThis is a two-column layout. You can use it to compare two different concepts or ideas.\n\n::right::\n\n### Section Options\n\nEasily customize the content of each section to suit your needs.\n\nUse front matter to define the layout of each section\n\n\n```yaml\nsections:\n left:\n flex: 2\n right:\n alignment: bottom_left\n```","type":"Slide","layout":"two_column"},{"content":{"flex":2,"alignment":"center"},"style":"show_sections","transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"# Two Column + Header\n\n\n::left::\n\n### Left Section\nEasily customize the content of each section to suit your needs.\n\nUse front matter to define the layout of each section\n::right::\n\n#### Section Options\n\n```yaml\nsections:\n left:\n alignment: bottom_right\n flex: 2\n right:\n alignment: bottom_left\n header:\n alignment: bottom_left\n```","sections":{"left":{"flex":2,"alignment":"center_left"},"right":{"flex":1,"alignment":"bottom_left"},"header":{"flex":1,"alignment":"bottom_left"}},"raw":"---\nlayout: two_column_header\ncontent:\n alignment: center\n flex: 2\nsections:\n left:\n flex: 2\n right:\n alignment: bottom_left\n header:\n alignment: bottom_left\nstyle: show_sections\n---\n\n# Two Column + Header\n\n\n::left::\n\n### Left Section\nEasily customize the content of each section to suit your needs.\n\nUse front matter to define the layout of each section\n::right::\n\n#### Section Options\n\n```yaml\nsections:\n left:\n alignment: bottom_right\n flex: 2\n right:\n alignment: bottom_left\n header:\n alignment: bottom_left\n```","type":"Slide","layout":"two_column_header"},{"content":{"flex":1,"alignment":"center"},"style":"rad","transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"# Mix\n\nIntegration with Mix gives you complete control over all styling elements in your slides with a simple and intuitive API.\n\n::right::\n\n```dart\nVariantAttribute get radStyle {\n return const SlideVariant('rad')(\n $.h1.textStyle.as(GoogleFonts.poppins()),\n $.h1.textStyle.fontSize(140),\n $.code.decoration.border.all(\n color: Colors.red,\n width: 3,\n ),\n $.code.decoration(\n color: Colors.black54,\n ),\n $.code.padding.all(40),\n\n $.outerContainer.margin.all(60),\n\n $.innerContainer.borderRadius(25),\n $.innerContainer.shadow(\n blurRadius: 0,\n spreadRadius: 10,\n color: Colors.red.withOpacity(1),\n ),\n $.innerContainer.gradient.radial(\n stops: [0.0, 1.0],\n radius: 0.7,\n colors: [Colors.purple, Colors.deepPurple],\n ),\n\n // Events\n onMouseHover((event) {\n final position = event.position;\n final dx = position.x * 10;\n final dy = position.y * 10;\n\n return Style(\n $.innerContainer.transform(_transformMatrix(position)),\n $.innerContainer.shadow.offset(dx, dy),\n $.innerContainer.gradient.radial(\n center: position,\n ),\n );\n }),\n\n (onPressed | onLongPressed)(\n $.innerContainer.shadow(\n blurRadius: 5,\n spreadRadius: 1,\n offset: Offset.zero,\n color: Colors.purpleAccent,\n ),\n $.innerContainer.border.all(color: Colors.white, width: 1),\n $.innerContainer.gradient.radial\n .colors([Colors.purpleAccent, Colors.purpleAccent]),\n ),\n );\n}\n```","sections":{"left":null,"right":{"flex":2,"alignment":"bottom_left"}},"raw":"---\nstyle: rad\nlayout: two_column\ncontent:\n alignment: center\nsections:\n left:\n right:\n alignment: bottom_left\n flex: 2\n---\n\n# Mix\n\nIntegration with Mix gives you complete control over all styling elements in your slides with a simple and intuitive API.\n\n::right::\n\n```dart\nVariantAttribute get radStyle {\n return const SlideVariant('rad')(\n $.h1.textStyle.as(GoogleFonts.poppins()),\n $.h1.textStyle.fontSize(140),\n $.code.decoration.border.all(\n color: Colors.red,\n width: 3,\n ),\n $.code.decoration(\n color: Colors.black54,\n ),\n $.code.padding.all(40),\n\n $.outerContainer.margin.all(60),\n\n $.innerContainer.borderRadius(25),\n $.innerContainer.shadow(\n blurRadius: 0,\n spreadRadius: 10,\n color: Colors.red.withOpacity(1),\n ),\n $.innerContainer.gradient.radial(\n stops: [0.0, 1.0],\n radius: 0.7,\n colors: [Colors.purple, Colors.deepPurple],\n ),\n\n // Events\n onMouseHover((event) {\n final position = event.position;\n final dx = position.x * 10;\n final dy = position.y * 10;\n\n return Style(\n $.innerContainer.transform(_transformMatrix(position)),\n $.innerContainer.shadow.offset(dx, dy),\n $.innerContainer.gradient.radial(\n center: position,\n ),\n );\n }),\n\n (onPressed | onLongPressed)(\n $.innerContainer.shadow(\n blurRadius: 5,\n spreadRadius: 1,\n offset: Offset.zero,\n color: Colors.purpleAccent,\n ),\n $.innerContainer.border.all(color: Colors.white, width: 1),\n $.innerContainer.gradient.radial\n .colors([Colors.purpleAccent, Colors.purpleAccent]),\n ),\n );\n}\n```","type":"Slide","layout":"two_column"},{"background":"https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExeGswdWJvY2oxazJoY3g2Y2poNHBvZXlpYmd5YTg0Z2g0ODRrbng4MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/oB6KlAvOuaLtxYy8l4/giphy.gif","style":"cover","transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"raw":"---\nstyle: cover\nbackground: https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExeGswdWJvY2oxazJoY3g2Y2poNHBvZXlpYmd5YTg0Z2g0ODRrbng4MyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/oB6KlAvOuaLtxYy8l4/giphy.gif\n---\n\n# Markdown support","data":"# Markdown support","type":"Slide"},{"content":{"flex":4,"alignment":"center_left"},"style":"show_sections","transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"::left::\n\n\n**Bold Text**\n\n*Italic Text*\n\n~~Strikethrough~~\n\n`Inline Code`\n\n[Link here](https://github.com/leoafarias/superdeck)\n\n::right::\n\nLists\n\n1. Ordered list item 1\n2. Ordered list item 2\n\n- Unordered list item 1\n- Unordered list item 2\n\nQuotes\n\n> If you want to go fast, go alone. \n> If you want to go far, go together.\n> ### African Proverb","sections":{},"raw":"---\nstyle: show_sections\nlayout: two_column\nsections:\ncontent:\n flex: 4\n---\n\n::left::\n\n\n**Bold Text**\n\n*Italic Text*\n\n~~Strikethrough~~\n\n`Inline Code`\n\n[Link here](https://github.com/leoafarias/superdeck)\n\n::right::\n\nLists\n\n1. Ordered list item 1\n2. Ordered list item 2\n\n- Unordered list item 1\n- Unordered list item 2\n\nQuotes\n\n> If you want to go fast, go alone. \n> If you want to go far, go together.\n> ### African Proverb","type":"Slide","layout":"two_column"},{"transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"::left::\n\n\nCode\n```dart\nint factorial(int n) {\n return n == 0 ? 1 : n * factorial(n - 1);\n}\n```\n\nTasks\n- [ ] Item 1\n- [x] Item 2\n\nSubtasks\n\n- [x] Item 1\n - [ ] Subitem 1\n\n::right::\n\nImages\n![Unsplash Image](https://source.unsplash.com/random/300x200/?landscape)\n\n\nTable\n\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1A | Cell 1B |\n| Cell 2A | Cell 2B |\n\nDivider\n\n___","sections":{},"raw":"---\nlayout: two_column\n---\n\n::left::\n\n\nCode\n```dart\nint factorial(int n) {\n return n == 0 ? 1 : n * factorial(n - 1);\n}\n```\n\nTasks\n- [ ] Item 1\n- [x] Item 2\n\nSubtasks\n\n- [x] Item 1\n - [ ] Subitem 1\n\n::right::\n\nImages\n![Unsplash Image](https://source.unsplash.com/random/300x200/?landscape)\n\n\nTable\n\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1A | Cell 1B |\n| Cell 2A | Cell 2B |\n\nDivider\n\n___","type":"Slide","layout":"two_column"},{"title":"Mermaid example","transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"::left::\n\n![Mermaid Diagram](assets/images/sd_generated_7177249.png)\n \n\n::right::\n\n## Mermaid Support\n\nSuperdeck allows you to use Mermaid diagrams in your slides. It automatically converts the code into a visual representation.","sections":{},"raw":"---\ntitle: \"Mermaid example\"\nlayout: two_column\n---\n\n::left::\n\n```mermaid\nflowchart TD\n A[This is crazy] -->|Get money| B(Go shopping)\n B --> C{Let me car}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[fa:fa-car Car]\n```\n \n\n::right::\n\n## Mermaid Support\n\nSuperdeck allows you to use Mermaid diagrams in your slides. It automatically converts the code into a visual representation.","type":"Slide","layout":"two_column"},{"options":{"name":"demo","args":{"text":"Hello, Superdeck!","height":200.0,"width":300.0},"flex":1,"position":"right"},"transition":{"type":"fade_in","duration":0,"delay":0,"curve":"ease"},"data":"## Showcase your widgets","raw":"---\nlayout: widget\noptions:\n name: demo\n args:\n text: Hello, Superdeck!\n height: 200.0\n width: 300.0\n---\n\n## Showcase your widgets\n","type":"Slide","layout":"widget"}] \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 72bef5d..1635572 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:superdeck/models/options_model.dart'; import 'package:superdeck/superdeck.dart'; import 'src/style.dart'; import 'src/widget/mix_demo.dart'; void main() async { + await SuperDeckApp.initialize(); runApp( Builder(builder: (context) { return MaterialApp( diff --git a/example/lib/src/widget/mix_demo.dart b/example/lib/src/widget/mix_demo.dart index fad32bf..5470795 100644 --- a/example/lib/src/widget/mix_demo.dart +++ b/example/lib/src/widget/mix_demo.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'package:superdeck/models/options_model.dart'; import 'package:superdeck/schema/schema.dart'; import 'package:superdeck/superdeck.dart'; diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 9c9935a..0370eb9 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,7 +7,6 @@ import Foundation import path_provider_foundation import screen_retriever -import shared_preferences_foundation import sqflite import url_launcher_macos import window_manager @@ -15,7 +14,6 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index b927950..07031a0 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -5,9 +5,6 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - sqflite (0.0.3): - Flutter - FlutterMacOS @@ -20,7 +17,6 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) @@ -32,8 +28,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin url_launcher_macos: @@ -45,7 +39,6 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 diff --git a/example/pubspec.lock b/example/pubspec.lock index 6a4093d..0311369 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -161,22 +161,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.7" - device_frame: - dependency: transitive - description: - name: device_frame - sha256: afe76182aec178d171953d9b4a50a43c57c7cf3c77d8b09a48bf30c8fa04dd9d - url: "https://pub.dev" - source: hosted - version: "1.1.0" - device_preview: - dependency: transitive - description: - name: device_preview - sha256: "2f097bf31b929e15e6756dbe0ec1bcb63952ab9ed51c25dc5a2c722d2b21fdaf" - url: "https://pub.dev" - source: hosted - version: "1.1.0" fake_async: dependency: transitive description: @@ -238,11 +222,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -261,14 +240,6 @@ packages: description: flutter source: sdk version: "0.0.0" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d - url: "https://pub.dev" - source: hosted - version: "2.4.1" go_router: dependency: transitive description: @@ -317,14 +288,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.7" - intl: - dependency: transitive - description: - name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" - url: "https://pub.dev" - source: hosted - version: "0.18.1" js: dependency: transitive description: @@ -333,14 +296,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.1" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" leak_tracker: dependency: transitive description: @@ -429,14 +384,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0-beta.12" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" octo_image: dependency: transitive description: @@ -549,14 +496,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.8.0" - provider: - dependency: transitive - description: - name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c - url: "https://pub.dev" - source: hosted - version: "6.1.2" qr: dependency: transitive description: @@ -565,14 +504,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" - re_highlight: - dependency: transitive - description: - name: re_highlight - sha256: "6c4ac3f76f939fb7ca9df013df98526634e17d8f7460e028bd23a035870024f2" - url: "https://pub.dev" - source: hosted - version: "0.0.3" recase: dependency: transitive description: @@ -605,62 +536,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 - url: "https://pub.dev" - source: hosted - version: "2.2.3" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" - url: "https://pub.dev" - source: hosted - version: "2.3.5" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" - url: "https://pub.dev" - source: hosted - version: "2.3.2" signals: dependency: transitive description: diff --git a/example/slides.md b/example/slides.md index dabc238..391af07 100644 --- a/example/slides.md +++ b/example/slides.md @@ -1,41 +1,3 @@ ---- -style: announcement - -background: https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbXE3N3hlcXl0OXR3eXJjOGJ3MGtvcmMwMWY4eDZqMHFvd3lxaHA4ZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/YpwVvng323pw45tYwv/giphy.gif -content: - alignment: center_left - ---- - - - # SUPERDECK - ## RELEASED! 🎉 - - #### Create beautiful Flutter - #### slides with Markdown - - - ---- -style: cover -layout: two_column -background: https://source.unsplash.com/person-discussing-while-standing-in-front-of-a-large-screen-in-front-of-people-inside-dim-lighted-room-bzdhc5b3Bxs -sections: - left: - flex: 2 - alignment: center_left - right: - alignment: bottom_right - flex: 1 ---- - -::left:: - -## Making your Flutter presentations easier than ever. - -::right:: -#### For Flutter developers. Made with ❤️ Flutter. - --- style: quote layout: image @@ -51,7 +13,6 @@ content: > You can quote me on that. > ### Leo Farias - --- background: https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGt1MnQ5N2k3cXVma24wb3V5cThlZ3ExY2NvY3czcmozang0bGQ1ZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/XzWd8acQ37byKR4tmd/giphy.gif style: cover diff --git a/lib/components/atoms/image_widget.dart b/lib/components/atoms/image_widget.dart index ad82dba..14ed334 100644 --- a/lib/components/atoms/image_widget.dart +++ b/lib/components/atoms/image_widget.dart @@ -1,33 +1,81 @@ import 'dart:io'; +import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:mix/mix.dart'; +import 'package:signals/signals_flutter.dart'; import '../../helpers/constants.dart'; +import '../../superdeck.dart'; class CachedImage extends StatelessWidget { - final ImageSpec spec; final String url; + final BoxFit? fit; + final ImageSpec spec; + final Alignment? alignment; + final Size size; const CachedImage({ - required this.spec, required this.url, + this.fit = BoxFit.cover, + this.alignment = Alignment.center, + this.spec = const ImageSpec.empty(), + required this.size, super.key, }); @override Widget build(BuildContext context) { - final imageProvider = getImageProvider(url); + final assets = SuperDeckProvider.instance.assets.watch(context); + + final asset = + assets.firstWhereOrNull((a) => a.hash == url.hashCode.toString()); + + final (:width, :height) = _calculateImageSize(size, asset); + + final imageProvider = + ResizeImage.resizeIfNeeded(width, height, getImageProvider(url, size)); return AnimatedMixedImage( image: imageProvider, - spec: spec, + spec: spec.copyWith( + fit: fit, + alignment: alignment, + ), ); } } -ImageProvider getImageProvider(String url) { +({int? width, int? height}) _calculateImageSize(Size size, SlideAsset? asset) { + int? cacheWidth; + int? cacheHeight; + // check if height or asset is larger + if (asset != null) { + final portrait = asset.height > asset.width; + + // cache the smallest dimension of the image + // So set the other dimension to null + if (portrait) { + cacheHeight = min(size.height, asset.height).toInt(); + } else { + cacheWidth = min(size.width, asset.width).toInt(); + } + } else { + // If no asset is available, set both cacheWidth and cacheHeight + final ifHeightIsBigger = size.height > size.width; + +// cache the smallest + if (ifHeightIsBigger) { + cacheWidth = size.width.toInt(); + } else { + cacheHeight = size.height.toInt(); + } + } + + return (width: cacheWidth, height: cacheHeight); +} + +ImageProvider getImageProvider(String url, Size canvasSize) { ImageProvider provider; // check if its a local path or a network path @@ -41,5 +89,6 @@ ImageProvider getImageProvider(String url) { provider = AssetImage(url); } } + return provider; } diff --git a/lib/components/atoms/markdown_viewer.dart b/lib/components/atoms/markdown_viewer.dart index d4757b6..afa3ff6 100644 --- a/lib/components/atoms/markdown_viewer.dart +++ b/lib/components/atoms/markdown_viewer.dart @@ -104,8 +104,9 @@ Widget _imageBuilder( final constraints = calculateConstraints(size, spec.contentContainer); return ConstrainedBox( constraints: constraints, - child: AnimatedMixedImage( - image: getImageProvider(uri.toString()), + child: CachedImage( + url: uri.toString(), + size: constraints.biggest, spec: imageSpec.copyWith( width: info.width ?? imageSpec.width, height: info.height ?? imageSpec.height, diff --git a/lib/components/atoms/slide_thumbnail.dart b/lib/components/atoms/slide_thumbnail.dart index e23b762..8e32c1e 100644 --- a/lib/components/atoms/slide_thumbnail.dart +++ b/lib/components/atoms/slide_thumbnail.dart @@ -1,10 +1,12 @@ -import 'package:flutter/foundation.dart'; +import 'dart:developer'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:signals/signals_flutter.dart'; import '../../helpers/constants.dart'; -import '../../helpers/slide_to_image.dart'; -import '../../models/slide_model.dart'; +import '../../services/assets_service.dart'; +import '../../services/image_generation_service.dart'; import '../../superdeck.dart'; import '../molecules/scaled_app.dart'; import 'slide_view.dart'; @@ -48,13 +50,8 @@ class SlideThumbnail extends StatefulWidget { } class _SlideThumbnailState extends State { - late final imageGenerator = ImageGenerationService.instance; - late final imageCache = ImageCacheService( - slide: widget.slide, - quality: ExportQuality.low, - cacheKey: widget.index.toString(), - ); - late final quality = ExportQuality.low; + late final imageGenerator = ImageGenerationService(context); + final quality = SnapshotQuality.good; late final asyncState = signal>(AsyncState.loading()); @@ -64,6 +61,12 @@ class _SlideThumbnailState extends State { _getLocalAsset().then((value) => getThumbnail()); } + @override + void dispose() { + imageGenerator.dispose(); + super.dispose(); + } + @override void didUpdateWidget(SlideThumbnail oldWidget) { if (oldWidget.index != widget.index || oldWidget.slide != widget.slide) { @@ -83,10 +86,11 @@ class _SlideThumbnailState extends State { Future _getLocalAsset() async { try { asyncState.value = AsyncState.loading(); - final asset = await imageCache.get(); + final asset = await assetService.loadThumbnailAsset(widget.slide.hash); if (asset != null) { - asyncState.value = AsyncState.data(asset); + final data = await assetService.loadBytes(asset.localPath); + asyncState.value = AsyncState.data(data); } } on Exception catch (e) { asyncState.value = AsyncState.error(e); @@ -94,27 +98,41 @@ class _SlideThumbnailState extends State { } Future _generateThumbnail() async { + final asset = await assetService.loadThumbnailAsset(widget.slide.hash); + + if (asset != null) { + await _getLocalAsset(); + return; + } try { - const delay = Durations.short2; while (_isGenerating) { - await Future.delayed(delay); + await Future.delayed(const Duration(milliseconds: 1)); } + if (!mounted) { + log('Context is unmounted'); + return; + } + + Uint8List image; + _isGenerating = true; - await Future.delayed(delay * 3); - final data = await imageGenerator.generate( - // ignore: use_build_context_synchronously - context: context, + image = await imageGenerator.generate( quality: quality, slide: widget.slide, ); - await imageCache.set(data); + await assetService.saveThumbnailAsset( + hash: widget.slide.hash, + data: image, + ); - asyncState.value = AsyncState.data(data); + asyncState.value = AsyncState.data(image); } on Exception catch (e) { asyncState.value = AsyncState.error(e); + log('Error generating thumbnail: $e'); + return; } finally { _isGenerating = false; } @@ -122,41 +140,44 @@ class _SlideThumbnailState extends State { @override Widget build(BuildContext context) { - final selectedColor = widget.selected ? Colors.blue : Colors.transparent; + return LayoutBuilder(builder: (context, constraints) { + final selectedColor = widget.selected ? Colors.blue : Colors.transparent; - final result = asyncState.watch(context); - - final child = result.map( - data: (data) => Image.memory( - data, - gaplessPlayback: true, - ), - loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, - error: (error, _) { - return const Center( - child: Text('Error loading image'), - ); - }, - ); + final result = asyncState.watch(context); - return GestureDetector( - onTap: widget.onTap, - child: PreviewBox( - style: Style( - box.border.all.color(selectedColor), + final child = result.map( + data: (data) => Image.memory( + data, + gaplessPlayback: true, + cacheWidth: constraints.maxWidth.toInt(), ), - child: AbsorbPointer( - child: AspectRatio( - aspectRatio: kAspectRatio, - child: child, + loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, + error: (error, _) { + return const Center( + child: Text('Error loading image'), + ); + }, + ); + + return GestureDetector( + onTap: widget.onTap, + child: PreviewBox( + style: Style( + box.border.all.color(selectedColor), + ), + child: AbsorbPointer( + child: AspectRatio( + aspectRatio: kAspectRatio, + child: child, + ), ), ), - ), - ); + ); + }); } } diff --git a/lib/components/atoms/slide_view.dart b/lib/components/atoms/slide_view.dart index ccc4b81..dd00b99 100644 --- a/lib/components/atoms/slide_view.dart +++ b/lib/components/atoms/slide_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:signals/signals_flutter.dart'; +import '../../helpers/constants.dart'; import '../../helpers/layout_builder.dart'; import '../../helpers/measure_size.dart'; -import '../../models/slide_model.dart'; import '../../providers/slide_provider.dart'; import '../../superdeck.dart'; import 'image_widget.dart'; @@ -26,13 +26,22 @@ class SlideView extends StatelessWidget { @override Widget build(BuildContext context) { - final superdeck = SuperDeckProvider.instance; + final provider = SuperDeckProvider.instance; final slide = this.slide; final variant = slide.styleVariant; - final style = superdeck.style.watch(context); + final style = provider.style.watch(context); final variantStyle = style.applyVariant(variant); + final backgroundWidget = slide.background != null + ? CachedImage( + url: slide.background!, + fit: BoxFit.cover, + size: kResolution, + alignment: Alignment.center, + ) + : const SizedBox(); + return TransitionWidget( key: ValueKey(slide.transition), transition: slide.transition, @@ -46,40 +55,41 @@ class SlideView extends StatelessWidget { return AnimatedMixedBox( spec: spec.outerContainer, duration: Durations.medium1, - child: AnimatedMixedBox( - spec: _buildInnerContainerSpec( - slide: slide, - spec: spec.innerContainer, - context: context, - ), - duration: const Duration(milliseconds: 300), - child: SlideProvider( - slide: slide, - spec: spec, - examples: superdeck.examples.watch(context), - isSnapshot: _isSnapshot, - child: SlideConstraints( - (_) { - if (slide is SimpleSlide) { - return SimpleSlideBuilder(config: slide); - } else if (slide is WidgetSlide) { - return WidgetSlideBuilder(config: slide); - } else if (slide is ImageSlide) { - return ImageSlideBuilder(config: slide); - } else if (slide is TwoColumnSlide) { - return TwoColumnSlideBuilder(config: slide); - } else if (slide is TwoColumnHeaderSlide) { - return TwoColumnHeaderSlideBuilder(config: slide); - } else if (slide is InvalidSlide) { - return InvalidSlideBuilder(config: slide); - } else { - throw UnimplementedError( - 'Slide config not implemented', - ); - } - }, + child: Stack( + children: [ + Positioned.fill(child: backgroundWidget), + AnimatedMixedBox( + spec: spec.innerContainer, + duration: const Duration(milliseconds: 300), + child: SlideProvider( + slide: slide, + spec: spec, + examples: provider.examples.watch(context), + isSnapshot: _isSnapshot, + child: SlideConstraints( + (_) { + if (slide is SimpleSlide) { + return SimpleSlideBuilder(config: slide); + } else if (slide is WidgetSlide) { + return WidgetSlideBuilder(config: slide); + } else if (slide is ImageSlide) { + return ImageSlideBuilder(config: slide); + } else if (slide is TwoColumnSlide) { + return TwoColumnSlideBuilder(config: slide); + } else if (slide is TwoColumnHeaderSlide) { + return TwoColumnHeaderSlideBuilder(config: slide); + } else if (slide is InvalidSlide) { + return InvalidSlideBuilder(config: slide); + } else { + throw UnimplementedError( + 'Slide config not implemented', + ); + } + }, + ), + ), ), - ), + ], ), ); }); @@ -113,63 +123,3 @@ class SlideConstraintsProvider extends InheritedWidget { return oldWidget.constraints != constraints; } } - -BoxSpec _buildInnerContainerSpec({ - required Slide slide, - required BoxSpec spec, - required BuildContext context, -}) { - final background = slide.background; - if (background == null) { - return spec; - } - final uri = Uri.tryParse(background); - - if (uri == null) { - return spec; - } - - final decoration = spec.decoration; - - final imageProvider = getImageProvider(background); - - if (decoration is BoxDecoration) { - final innerContainerSpecImage = decoration.image; - final mix = MixProvider.of(context); - final backgroundDecorationDto = DecorationImageDto( - image: imageProvider, - fit: BoxFit.cover, - alignment: Alignment.center, - ); - - if (innerContainerSpecImage == null) { - return spec.copyWith( - decoration: decoration.copyWith( - image: backgroundDecorationDto.resolve(mix), - ), - ); - } - - final decorationDto = DecorationImageDto.from(innerContainerSpecImage); - - final newDecoration = backgroundDecorationDto - .merge(decorationDto) - .merge(DecorationImageDto(image: imageProvider)); - - return spec.copyWith( - decoration: decoration.copyWith( - image: newDecoration.resolve(mix), - ), - ); - } - - return spec.copyWith( - decoration: BoxDecoration( - image: DecorationImage( - image: imageProvider, - fit: BoxFit.cover, - alignment: Alignment.center, - ), - ), - ); -} diff --git a/lib/components/molecules/exception_widget.dart b/lib/components/molecules/exception_widget.dart index 49d1e84..49c57ae 100644 --- a/lib/components/molecules/exception_widget.dart +++ b/lib/components/molecules/exception_widget.dart @@ -1,24 +1,26 @@ import 'package:flutter/material.dart'; class ExceptionWidget extends StatelessWidget { - final Exception exception; - const ExceptionWidget(this.exception, {super.key, required this.onRetry}); + final Object? error; + const ExceptionWidget(this.error, {super.key, required this.onRetry}); final void Function() onRetry; @override Widget build(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('An error occurred'), - Text(exception.toString()), - const SizedBox(height: 20), - ElevatedButton( - onPressed: onRetry, - child: const Text('Retry'), - ), - ], + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('An error occurred'), + Text(error.toString()), + const SizedBox(height: 20), + ElevatedButton( + onPressed: onRetry, + child: const Text('Retry'), + ), + ], + ), ), ); } diff --git a/lib/components/molecules/slide_thumbnail_list.dart b/lib/components/molecules/slide_thumbnail_list.dart index 4a2261c..c26c50c 100644 --- a/lib/components/molecules/slide_thumbnail_list.dart +++ b/lib/components/molecules/slide_thumbnail_list.dart @@ -1,9 +1,10 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:signals/signals_flutter.dart'; import '../../models/slide_model.dart'; -import '../../superdeck.dart'; +import '../../providers/deck_provider.dart'; import '../atoms/slide_thumbnail.dart'; class SlideThumbnailList extends StatefulWidget { @@ -87,7 +88,7 @@ class _SlideThumbnailListState extends State { final slide = widget.slides[idx]; SuperDeckProvider.instance.style.watch(context); - return SlideThumbnailDynamic( + return SlideThumbnail( index: idx, selected: idx == widget.currentSlide, onTap: () => goToPage(idx), diff --git a/lib/components/superdeck_app.dart b/lib/components/superdeck_app.dart index 8992b35..a30cc92 100644 --- a/lib/components/superdeck_app.dart +++ b/lib/components/superdeck_app.dart @@ -9,12 +9,12 @@ import 'package:signals/signals_flutter.dart'; import 'package:window_manager/window_manager.dart'; import '../../helpers/syntax_highlighter.dart'; -import '../../models/options_model.dart'; import '../../superdeck.dart'; import '../helpers/constants.dart'; import '../helpers/theme.dart'; import '../screens/export_screen.dart'; import '../screens/home_screen.dart'; +import '../services/assets_service.dart'; import 'molecules/exception_widget.dart'; class SuperDeckApp extends StatefulWidget { @@ -27,21 +27,41 @@ class SuperDeckApp extends StatefulWidget { final Style? style; final List examples; + static bool isInitialized = false; + + static Future initialize() async { + // Return if its initialized + if (SuperDeckApp.isInitialized) return; + + WidgetsFlutterBinding.ensureInitialized(); + + SignalsObserver.instance = null; + + await Future.wait([ + initLocalStorage(), + SyntaxHighlight.initialize(), + AssetService.initialize(), + _initializeWindowManager(), + ]); + + SuperDeckApp.isInitialized = true; + } + @override // ignore: library_private_types_in_public_api _SuperDeckAppState createState() => _SuperDeckAppState(); } class _SuperDeckAppState extends State { - final superdeck = SuperDeckProvider.instance; - late final initialize = futureSignal(_initialize); + final _provider = SuperDeckProvider.instance; + late final _initializing = futureSignal(_initializeDependencies); @override void didUpdateWidget(SuperDeckApp oldWidget) { super.didUpdateWidget(oldWidget); if (widget.style != oldWidget.style || widget.examples != oldWidget.examples) { - superdeck.update( + _provider.update( style: widget.style, examples: widget.examples, ); @@ -51,27 +71,19 @@ class _SuperDeckAppState extends State { @override void dispose() { super.dispose(); - initialize.dispose(); + _initializing.dispose(); } - Future _initialize() async { - WidgetsFlutterBinding.ensureInitialized(); - SignalsObserver.instance = null; - - await Future.wait([ - initLocalStorage(), - SyntaxHighlight.initialize(), - _initializeWindowManager(), - ]); - - await superdeck.initialize( + Future _initializeDependencies() async { + await SuperDeckApp.initialize(); + await _provider.initialize( style: widget.style, examples: widget.examples, ); } void onRetry() { - superdeck.initialize( + _provider.initialize( style: widget.style, examples: widget.examples, ); @@ -79,40 +91,40 @@ class _SuperDeckAppState extends State { @override Widget build(BuildContext context) { - final initializing = initialize.watch(context); - - if (initializing.isLoading) { - return const Center( - child: CircularProgressIndicator(), + Widget renderLoading() { + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), ); } - final result = superdeck.data.watch(context); - - return result.map( - data: (data) { - return MixTheme( - data: MixThemeData.withMaterial(), - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - title: 'Superdeck', - theme: theme, - routerConfig: _router, - ), - ); - }, - loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, - error: (error) { - log(error.toString()); - return ExceptionWidget( - error, - onRetry: onRetry, - ); - }, + return MixTheme( + data: MixThemeData.withMaterial(), + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + title: 'Superdeck', + theme: theme, + routerConfig: _router, + builder: (context, child) { + final initializing = _initializing.watch(context); + + return initializing.map( + data: (data) => child!, + loading: () => Scaffold( + body: renderLoading(), + ), + error: (error) { + log(error.toString()); + + return ExceptionWidget( + error, + onRetry: onRetry, + ); + }, + ); + }, + ), ); } } diff --git a/lib/helpers/config.dart b/lib/helpers/config.dart index 25b7bc5..3e1dcc0 100644 --- a/lib/helpers/config.dart +++ b/lib/helpers/config.dart @@ -2,10 +2,12 @@ import 'dart:io'; import 'package:path/path.dart'; -final kConfig = SuperDeckConfig(); +const sdConfig = SDConfig.instance; -class SuperDeckConfig { - SuperDeckConfig(); +class SDConfig { + const SDConfig._(); + + static const instance = SDConfig._(); String get _assetsDirName => 'assets'; @@ -20,10 +22,10 @@ class SuperDeckConfig { ({ File slides, File config, - }) get references { - return ( - slides: File(join(_assetsDirName, 'slides.json')), - config: File(join(_assetsDirName, 'config.json')), - ); - } + File assets, + }) get references => ( + slides: File(join(_assetsDirName, 'slides.json')), + config: File(join(_assetsDirName, 'config.json')), + assets: File(join(_assetsDirName, 'assets.json')), + ); } diff --git a/lib/helpers/layout_builder.dart b/lib/helpers/layout_builder.dart index abfa97f..ed67e22 100644 --- a/lib/helpers/layout_builder.dart +++ b/lib/helpers/layout_builder.dart @@ -3,10 +3,9 @@ import 'package:flutter/material.dart'; import '../components/atoms/image_widget.dart'; import '../components/molecules/code_preview.dart'; import '../components/molecules/slide_content.dart'; -import '../models/options_model.dart'; -import '../models/slide_model.dart'; import '../providers/slide_provider.dart'; import '../superdeck.dart'; +import 'constants.dart'; import 'measure_size.dart'; abstract class SlideBuilder extends StatelessWidget { @@ -147,13 +146,34 @@ class ImageSlideBuilder extends SplitSlideBuilder { final src = config.options.src; final boxFit = config.options.fit?.toBoxFit() ?? spec.image.fit; + // THis slide breaks in half and I want to calculate the size based on if its in top or bottom + // or left or right. Also there is a property called flex which is how much of the slide it takes + // so I can use that to calculate the size of the canvas + final firstHalf = + config.contentOptions?.flex ?? const ContentOptions().flex; + final imageHalf = config.options.flex; + +// available size const width = 1280.0; +//const height = 720.0; + + double width; + double height; + const availableSize = kResolution; + if (config.options.position.isHorizontal()) { + width = availableSize.width * firstHalf / (firstHalf + imageHalf); + height = availableSize.height; + } else { + width = availableSize.width; + height = availableSize.height * firstHalf / (firstHalf + imageHalf); + } + final side = Container( height: spec.image.height, width: spec.image.width, alignment: spec.image.alignment, decoration: BoxDecoration( image: DecorationImage( - image: getImageProvider(src), + image: getImageProvider(src, Size(width, height)), centerSlice: spec.image.centerSlice, repeat: spec.image.repeat ?? ImageRepeat.noRepeat, filterQuality: spec.image.filterQuality ?? FilterQuality.low, diff --git a/lib/helpers/loader.dart b/lib/helpers/loader.dart index 6126286..7ac6394 100644 --- a/lib/helpers/loader.dart +++ b/lib/helpers/loader.dart @@ -1,85 +1,75 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'package:flutter/services.dart'; import 'package:watcher/watcher.dart'; -import '../models/options_model.dart'; -import '../models/slide_model.dart'; +import '../services/assets_service.dart'; import 'config.dart'; import 'constants.dart'; import 'markdown_processor.dart'; class SlidesLoader { // Constructor that accepts the text input for parsing. - const SlidesLoader._(); + SlidesLoader._(); - static const _pipeline = Pipeline( - markdown: [ - FrontMatterProcessor(), - MermaidProcessor(), - ImageMarkdownProcessor(), - ], - postMarkdown: [ - StoreLocalReferencesProcessor(), - ], - ); + static final instance = SlidesLoader._(); - static Future loadString(String path) { - if (kCanRunProcess) { - return File(path).readAsString(); - } else { - return rootBundle.loadString(path); - } - } + static final List> _subscriptions = []; - static Future generate() async { - final markdownFile = kConfig.slidesMarkdownFile; + Future generate( + [File? markdownFile, File? configFile]) async { + markdownFile ??= sdConfig.slidesMarkdownFile; + configFile ??= sdConfig.projectConfigFile; - if (!await markdownFile.exists()) { - throw Exception('Slides markdown file not found'); - } + final assets = await AssetService.instance.loadAssets(); + final config = await AssetService.instance.loadProjectConfig(configFile); + + final pipeline = Pipeline( + config: config, + assets: assets, + processors: [ + const ImageCachingTask(), + const MermaidConverterTask(), + ], + ); + final data = await pipeline.run(markdownFile); + + await AssetService.instance.saveReferences(data); + + return data; + } - if (!await kConfig.assetsImageDir.exists()) { - await kConfig.assetsImageDir.create(recursive: true); + static void _unsubscribe() { + for (var subscription in _subscriptions) { + subscription.cancel(); } - await _pipeline.run(await markdownFile.readAsString()); + _subscriptions.clear(); } - static List> listen({ + void listen({ required void Function() onChange, }) { - return [ - kConfig.slidesMarkdownFile, - kConfig.projectConfigFile, + _unsubscribe(); + _subscriptions.addAll([ + sdConfig.slidesMarkdownFile, + sdConfig.projectConfigFile, ].map((file) { - return FileWatcher(file.path).events.listen((event) async { - if (event.type == ChangeType.MODIFY) { - onChange(); - } - }); - }).toList(); + return FileWatcher(file.path).events.listen( + (event) { + if (event.type == ChangeType.MODIFY) { + onChange(); + } + }, + ); + }).toList()); } - static Future loadFromStorage() async { - final slidesJson = - await SlidesLoader.loadString(kConfig.references.slides.path); - - final configJson = - await SlidesLoader.loadString(kConfig.references.config.path); + Future loadFromStorage() async { + if (kCanRunProcess) { + return await generate(); + } - return ( - slides: _parseFromJson(slidesJson), - config: ProjectConfig.fromJson(configJson) - ); + return await AssetService.instance.loadReferences(); } } - -List _parseFromJson(String slidesJson) { - final slides = jsonDecode(slidesJson) as List; - final slideMap = List.castFrom>(slides); - - return slideMap.map(Slide.fromMap).toList(); -} diff --git a/lib/helpers/markdown_processor.dart b/lib/helpers/markdown_processor.dart index eae6bcd..2cfcdca 100644 --- a/lib/helpers/markdown_processor.dart +++ b/lib/helpers/markdown_processor.dart @@ -1,188 +1,156 @@ import 'dart:async'; -import 'dart:developer'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; -import '../models/asset_model.dart'; -import '../models/options_model.dart'; -import '../models/slide_model.dart'; import '../schema/schema.dart'; +import '../services/assets_service.dart'; +import '../services/mermaid_service.dart'; +import '../superdeck.dart'; import 'config.dart'; import 'deep_merge.dart'; import 'utils.dart'; final _mermaidBlockRegex = RegExp(r'```mermaid([\s\S]*?)```'); -typedef DeckData = ({ +typedef DeckReferenceDto = ({ List slides, ProjectConfig config, + List assets, }); -final emptyDeckData = ( - slides: [], - assets: [], +final DeckReferenceDto emptyDeckData = ( + slides: [], + assets: [], config: const ProjectConfig.empty(), ); -typedef ProcessData = ({ - String content, - Map options, +typedef TaskDto = ({ + Slide slide, + List assets, ProjectConfig config, }); class Pipeline { - final List markdown; - final List postMarkdown; - - static List assetsLoaded = []; - - String getFileName(String path) { - return p.basename(path); - } + final List processors; + final ProjectConfig config; + final List assets; + + Pipeline({ + required this.processors, + required this.assets, + required this.config, + }); - static Future cleanAssets() async { + static Future cleanAssets(List assetsUsed) async { // Gothrough the assets directory and check if the asset is not in assetsSaved // delete it - final assetsDir = kConfig.assetsImageDir; + final assetsDir = sdConfig.assetsImageDir; if (!await assetsDir.exists()) return; + final assetPaths = assetsUsed.map((e) => e.localPath).toSet(); + await for (final entity in assetsDir.list()) { if (entity is File) { final fileName = p.basename(entity.path); - if (fileName.startsWith(SlideAsset.assetPrefix)) { - if (!assetsLoaded.contains(entity.path)) { + if (fileName.startsWith(SlideAsset.cachedPrefix)) { + if (!assetPaths.contains(entity.path)) { await entity.delete(); } } } } - - assetsLoaded.clear(); } - const Pipeline({ - required this.markdown, - required this.postMarkdown, - }); - - static Future getAsset(String fileName) async { - final assetFile = SlideAsset.buildFile(fileName); + static Future cleanThumbnails(List slides) async { + final assetsDir = sdConfig.assetsImageDir; - final asset = await SlideAsset.maybeLoad(assetFile); - if (asset != null) { - Pipeline.assetsLoaded.add(assetFile.path); - } - return asset; - } - - static Future saveAsset( - String fileName, { - required List data, - }) async { - final imageFile = SlideAsset.buildFile(fileName); - - await imageFile.writeAsBytes(data); - final asset = await SlideAsset.load(imageFile); - Pipeline.assetsLoaded.add(imageFile.path); - - return asset; - } - - Future run(String contents) async { - final slidesRaw = _splitSlides(contents.trim()); + if (!await assetsDir.exists()) return; - final config = await _loadProjectConfig(); + final assets = []; - final slides = []; + await for (final entity in assetsDir.list()) { + if (entity is File) { + final fileName = p.basename(entity.path); + if (!fileName.startsWith(SlideAsset.thumbnailPrefix)) { + continue; + } - for (final raw in slidesRaw) { - slides.add(await runEach(raw, config)); + assets.add(entity); + } } - final data = ( - slides: slides, - config: config, - ); + for (final asset in assets) { + final slide = slides.firstWhereOrNull((e) => asset.path.contains(e.hash)); - final result = await _runPostMarkdown(data); + if (slide == null) { + await asset.delete(); + } + } + } - await cleanAssets(); - return result; + Future onFinish(DeckReferenceDto data) async { + await cleanAssets(data.assets); + await cleanThumbnails(data.slides); } - Future _runPostMarkdown(DeckData data) async { - var updatedData = data; + Future onInit() async { + final assetDir = sdConfig.assetsImageDir; + if (!await assetDir.exists()) { + await assetDir.create(recursive: true); + } + } - for (final processor in postMarkdown) { - updatedData = await processor.run(updatedData); + Future run(File markdownFile) async { + await onInit(); + if (!await markdownFile.exists()) { + throw Exception('File not found'); } - return updatedData; - } + final presentationRaw = await markdownFile.readAsString(); + + final parser = SlideParser(config); - Future runEach(String slideContents, ProjectConfig config) async { - ProcessData result = (content: slideContents, options: {}, config: config); + final slides = parser.run(presentationRaw); + final results = []; - for (final processor in markdown) { - result = await processor.run(result); + for (var slide in slides) { + TaskDto result = (slide: slide, assets: assets, config: config); + for (var task in processors) { + result = await task.run(result); + } + results.add(result); } - final (:content, :options, config: _) = result; + final slideResults = results.map((e) => e.slide).toList(); + final assetResults = results.expand((e) => e.assets).toList(); - return _parseSlideFromMap({ - ...options, - 'layout': options['layout'] ?? 'simple', - 'data': content, - }); + final result = ( + slides: slideResults, + assets: assetResults, + config: config, + ); + await onFinish(result); + + return result; } +} - Future _loadProjectConfig() async { - final file = kConfig.projectConfigFile; +abstract class Task { + const Task(); - if (!await file.exists()) { - return const ProjectConfig.empty(); - } + FutureOr run( + TaskDto data, + ); +} - final configContents = await file.readAsString(); - return ProjectConfig.fromYaml(configContents); - } +class SlideParser { + final ProjectConfig config; - Future _parseSlideFromMap(Map slideMap) async { - final layout = slideMap['layout'] as String?; - - const config = 'config'; - try { - switch (layout) { - case LayoutType.simple: - case null: - SimpleSlide.schema.validateOrThrow(config, slideMap); - return SimpleSlide.fromMap(slideMap); - case LayoutType.image: - ImageSlide.schema.validateOrThrow(config, slideMap); - return ImageSlide.fromMap(slideMap); - case LayoutType.widget: - WidgetSlide.schema.validateOrThrow(config, slideMap); - return WidgetSlide.fromMap(slideMap); - case LayoutType.twoColumn: - TwoColumnSlide.schema.validateOrThrow(config, slideMap); - return TwoColumnSlide.fromMap(slideMap); - case LayoutType.twoColumnHeader: - TwoColumnHeaderSlide.schema.validateOrThrow(config, slideMap); - return TwoColumnHeaderSlide.fromMap(slideMap); - default: - return InvalidSlide.invalidTemplate(layout); - } - } on SchemaValidationException catch (e) { - return InvalidSlide.schemaError(e.result); - } on Exception catch (e) { - return InvalidSlide.exception(e); - } catch (e) { - return InvalidSlide.message('# Unknown Error \n $e'); - } - } + SlideParser(this.config); + + final _frontMatterRegex = RegExp(r'---([\s\S]*?)---'); List _splitSlides(String content) { final lines = content.split('\n'); @@ -220,328 +188,190 @@ class Pipeline { return slides; } -} - -abstract class PostMarkdownProcessor { - const PostMarkdownProcessor(); - - FutureOr run(DeckData data); -} -class StoreLocalReferencesProcessor extends PostMarkdownProcessor { - const StoreLocalReferencesProcessor(); - - @override - Future run(DeckData data) async { - final (:slides, :config) = data; - - await saveSlideJson(slides); - await saveConfig(config); - final assetsImageDir = kConfig.assetsImageDir; - - final files = await assetsImageDir.list().where((e) => e is File).toList(); - - for (var file in files) { - final fileName = p.basename(file.path); - - if (!fileName.startsWith('sd_slide_')) { - continue; - } - - // filename example sd_asset_1_thumb.png - // get the number after sd_asset_ and before _thumb.png - final regex = RegExp(r'sd_slide_(\d+)_thumb.png'); - final match = regex.firstMatch(fileName); - if (match != null) { - final slideIndex = int.parse(match.group(1)!); - - if (slideIndex > slides.length) { - await file.delete(); - } - } - - // check for generated assets - // check if file starts witih sd_asset_ - } - - return data; - } + List run(String contents) { + final slidesRaw = _splitSlides(contents.trim()); - Future saveConfig(Config config) async { - try { - final configJson = kConfig.references.config; - if (!await configJson.exists()) { - await configJson.create(recursive: true); - } + final slides = []; - await configJson.writeAsString(prettyJson(config.toMap())); - } catch (e) { - log('Error while saving config: $e'); - rethrow; + for (final slideRaw in slidesRaw) { + slides.add(_runEach(slideRaw)); } - } - - Future saveSlideJson(List slides) async { - try { - final slidesJson = kConfig.references.slides; - - final map = slides.map((e) => e.toMap()).toList(); - if (!await slidesJson.exists()) { - await slidesJson.create(recursive: true); - } - - // Write a json file with a list of slide - await slidesJson.writeAsString(prettyJson(map)); - } catch (e) { - log('Error while saving slides json: $e'); - rethrow; - } + return slides; } -} - -abstract class MarkdownProcessor { - const MarkdownProcessor(); - - FutureOr run(ProcessData data); -} -final _frontMatterRegex = RegExp(r'---([\s\S]*?)---'); - -class FrontMatterProcessor extends MarkdownProcessor { - const FrontMatterProcessor(); - - @override - ProcessData run(ProcessData data) { - final frontMatter = - _frontMatterRegex.firstMatch(data.content)?.group(1) ?? ''; + Slide _runEach(String slideRaw) { + final frontMatter = _frontMatterRegex.firstMatch(slideRaw)?.group(1) ?? ''; final options = converYamlToMap(frontMatter); - final content = data.content - .substring(_frontMatterRegex.matchAsPrefix(data.content)?.end ?? 0) + final content = slideRaw + .substring(_frontMatterRegex.matchAsPrefix(slideRaw)?.end ?? 0) .trim(); // Set default layout options['layout'] = options['layout'] ?? 'simple'; - options['raw'] = frontMatter; + // Raw front matter + options['raw'] = slideRaw; + + // Slide contents + options['data'] = content; final mergedOptions = deepMerge( - data.config.toSlideMap(), + config.toSlideMap(), options, ); - return ( - content: content, - options: mergedOptions, - config: data.config, - ); + return _buildSlide(mergedOptions); } } -class ImageMarkdownProcessor extends MarkdownProcessor { - const ImageMarkdownProcessor(); +class ImageCachingTask extends Task { + const ImageCachingTask(); @override - Future run(ProcessData data) async { + Future run(data) async { + final slide = data.slide; + final assets = []; + var content = slide.data; // Do not cache remot edata if cacheRemoteAssets is false - if (!data.config.cacheRemoteAssets) { - return data; - } + // Get any url of images that are in the markdown // Save it the local path on the device // and replace the url with the local path final imageRegex = RegExp(r'!\[.*?\]\((.*?)\)'); - var content = data.content; - var options = {...data.options}; + final matches = imageRegex.allMatches(content); - final matches = imageRegex.allMatches(data.content); + Future saveAsset(String assetUri) async { + final cachedAsset = await AssetService.instance.loadCachedAsset(assetUri); - for (final Match match in matches) { - final imageUrl = match.group(1); - if (imageUrl == null) continue; - - final asset = await cacheRemoteAsset(imageUrl); - - if (asset != null) { - final imageMarkdown = '![Image](${asset.relativePath})'; - content = content.replaceFirst(match.group(0)!, imageMarkdown); + if (cachedAsset != null) { + assets.add(cachedAsset); + return; } - } - // Check also if image is on background: or src: in front matter - // and replace the url with the local path, frontmatter is now data.options Map - var background = options['background']; - - if (background != null && background is String) { - final asset = await cacheRemoteAsset(background); - - if (asset != null) { - background = asset.relativePath; - options['background'] = background; - } + assets.add(await AssetService.instance.saveCachedAsset(assetUri)); } - var imageSource = options['options']?['src']; - - if (imageSource != null && imageSource is String) { - final asset = await cacheRemoteAsset(imageSource); + for (final Match match in matches) { + final assetUri = match.group(1); + if (assetUri == null) continue; - if (asset != null) { - imageSource = asset.relativePath; - options['options']['src'] = imageSource; - } + await saveAsset(assetUri); } - return ( - content: content, - options: options, - config: data.config, - ); - } + final background = slide.background; - Future cacheRemoteAsset(String url) async { - if (!url.startsWith('http')) { - return null; + if (background != null) { + await saveAsset(background); } - var ext = p.extension(url).replaceFirst('.', ''); - - // Check if url has extension and is an image - if (!SlideAsset.allowedExtensions.contains(ext)) { - return null; + if (slide is ImageSlide) { + final imageSource = slide.options.src; + await saveAsset(imageSource); } - final client = HttpClient(); - final request = await client.getUrl(Uri.parse(url)); - final response = await request.close(); - final data = await consolidateHttpClientResponseBytes(response); - - final contentType = response.headers.contentType; - // Default to .jpg if no extension is found - final extension = contentType?.subType ?? 'jpg'; - - final fileName = '${url.hashCode}.$extension'; - - return Pipeline.saveAsset( - fileName, - data: data, - ); + return (slide: slide, assets: assets, config: data.config); } } -typedef Replacement = ({int start, int end, String markdown}); - -class MermaidProcessor extends MarkdownProcessor { - const MermaidProcessor(); +class MermaidConverterTask extends Task { + const MermaidConverterTask(); @override - Future run(ProcessData data) async { - final replacements = []; + FutureOr run(data) async { + final slide = data.slide; - final matches = _mermaidBlockRegex.allMatches(data.content); + final matches = _mermaidBlockRegex.allMatches(slide.data); if (matches.isEmpty) return data; + final replacements = + <({int start, int end, String markdown, GeneratedAsset asset})>[]; for (final Match match in matches) { final mermaidSyntax = match.group(1); if (mermaidSyntax == null) continue; - // Process the mermaid syntax to generate an image file - final asset = await generateAndSaveMermaidImage(mermaidSyntax); + final mermaidImageHash = mermaidSyntax.hashCode.toString(); - if (asset == null) continue; + var asset = await assetService.loadGeneratedAsset(mermaidImageHash); + // Check if image already exists - final markdown = '![Mermaid Diagram](${asset.relativePath})'; + if (asset == null) { + // Process the mermaid syntax to generate an image file + final imageData = await mermaidService.generateImage(mermaidSyntax); + + if (imageData != null) { + asset = await assetService.saveGeneratedAsset( + hash: mermaidImageHash, + data: imageData, + ); + } + } + + if (asset == null) continue; - // Collect replacement information replacements.add(( start: match.start, end: match.end, - markdown: markdown, + markdown: '![Mermaid Diagram](${asset.relativePath})', + asset: asset, )); } - var content = data.content; + var replacedData = slide.data; + final assets = []; // Apply replacements in reverse order for (var replacement in replacements.reversed) { - final (:start, :end, :markdown) = replacement; + final (:start, :end, :markdown, :asset) = replacement; - content = content.replaceRange(start, end, markdown); + replacedData = replacedData.replaceRange(start, end, markdown); + assets.add(asset); } return ( - content: content, - options: data.options, - config: data.config, + slide: slide.copyWith(data: replacedData), + assets: assets, + config: data.config ); } +} - Future generateAndSaveMermaidImage(String mermaidSyntax) async { - final fileName = '${mermaidSyntax.hashCode}.png'; - - final existingAsset = await Pipeline.getAsset(fileName); - - if (existingAsset != null) { - return existingAsset; - } - - const tempDirPath = '.tmp_superdeck'; - final tempFilePath = p.join(tempDirPath, '$fileName.mmd'); - final tempFile = File(tempFilePath); - final tempOutputPath = p.join(tempDirPath, fileName); - - if (!await Directory(tempDirPath).exists()) { - await Directory(tempDirPath).create(recursive: true); - } - - try { - mermaidSyntax = mermaidSyntax.trim().replaceAll(r'\n', '\n'); - - await tempFile.writeAsString(mermaidSyntax); - - final imageSizeParams = '--scale 2'.split(' '); - final params = - '-t dark -b transparent -i $tempFilePath -o $tempOutputPath ' - .split(' '); - - // Check if can execute mmdc before executing command - final mmdcResult = await Process.run('mmdc', ['--version']); - - if (mmdcResult.exitCode != 0) { - log( - '"mmdc" not found. You need mermaid cli installed to process mermaid syntax', - ); - - return null; - } - - final result = await Process.run('mmdc', [...params, ...imageSizeParams]); - - if (result.exitCode != 0) { - log('Error while processing mermaid syntax'); - log(result.stderr); - return null; - } - - final output = await File(tempOutputPath).readAsBytes(); - - return Pipeline.saveAsset( - fileName, - data: output, - ); - } catch (e) { - log('Error while processing mermaid syntax: $e'); - return null; - } finally { - final tempDir = Directory(tempDirPath); - if (await tempDir.exists()) { - await tempDir.delete(recursive: true); - } +Slide _buildSlide(Map slide) { + final layout = slide['layout'] as String?; + + const config = 'config'; + try { + switch (layout) { + case LayoutType.simple: + case null: + SimpleSlide.schema.validateOrThrow(config, slide); + return SimpleSlide.fromMap(slide); + case LayoutType.image: + ImageSlide.schema.validateOrThrow(config, slide); + return ImageSlide.fromMap(slide); + case LayoutType.widget: + WidgetSlide.schema.validateOrThrow(config, slide); + return WidgetSlide.fromMap(slide); + case LayoutType.twoColumn: + TwoColumnSlide.schema.validateOrThrow(config, slide); + return TwoColumnSlide.fromMap(slide); + case LayoutType.twoColumnHeader: + TwoColumnHeaderSlide.schema.validateOrThrow(config, slide); + return TwoColumnHeaderSlide.fromMap(slide); + default: + return InvalidSlide.invalidTemplate(layout); } + } on SchemaValidationException catch (e) { + return InvalidSlide.schemaError(e.result); + } on Exception catch (e) { + return InvalidSlide.exception(e); + } catch (e) { + return InvalidSlide.message('# Unknown Error \n $e'); } } diff --git a/lib/models/syntax_tag.dart b/lib/helpers/section_tag.dart similarity index 94% rename from lib/models/syntax_tag.dart rename to lib/helpers/section_tag.dart index e54c9dd..1ad778c 100644 --- a/lib/models/syntax_tag.dart +++ b/lib/helpers/section_tag.dart @@ -1,5 +1,5 @@ -class Section { - const Section._(); +class SectionTag { + const SectionTag._(); static const header = 'header'; static const left = 'left'; static const right = 'right'; @@ -20,7 +20,7 @@ String _getTagName(String line) { Map parseContentSections(String input) { final result = {}; final lines = input.split('\n'); - var currentTag = Section.first; + var currentTag = SectionTag.first; var currentContent = ''; // If ::tag:: is inside a ``` block, it should be ignored diff --git a/lib/helpers/slide_to_image.dart b/lib/helpers/slide_to_image.dart deleted file mode 100644 index 9aaa454..0000000 --- a/lib/helpers/slide_to_image.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; - -import '../components/atoms/slide_view.dart'; -import '../helpers/constants.dart'; -import '../models/slide_model.dart'; -import 'config.dart'; - -enum ExportQuality { - low('Low', pixelRatio: 0.4), - good('Good', pixelRatio: 1), - better('Better', pixelRatio: 2), - best('Best', pixelRatio: 3); - - const ExportQuality(this.label, {required this.pixelRatio}); - - final String label; - final double pixelRatio; -} - -// final Map _imageCache = {}; -// Create a simple cache class that also stores the image in application folder -// to avoid generating the image again - -class ImageCacheService { - const ImageCacheService({ - required this.slide, - required this.quality, - required this.cacheKey, - }); - - final Slide slide; - final ExportQuality quality; - final String cacheKey; - - File _getAssetFile() { - final directory = kConfig.assetsImageDir; - return File('${directory.path}/sd_slide_${cacheKey}_thumb.png'); - } - - Future loadAssetFile() async { - try { - if (kCanRunProcess) { - final file = _getAssetFile(); - if (await file.exists()) { - return await file.readAsBytes(); - } else { - return null; - } - } - final asset = await rootBundle.load(_getAssetFile().path); - return asset.buffer.asUint8List(); - } catch (e) { - return null; - } - } - - Future set(Uint8List image) async { - if (kCanRunProcess) { - final file = _getAssetFile(); - - await file.writeAsBytes(image); - return; - } - - throw Exception('Cannot cache image on the web'); - } - - Future get() async { - try { - return await loadAssetFile(); - } on Exception catch (e) { - log('Error loading image from cache: $e'); - return null; - } - } -} - -class ImageGenerationService { - ImageGenerationService._(); - - static ImageGenerationService get instance => _instance; - - static final _instance = ImageGenerationService._(); - - Future generate({ - required BuildContext context, - ExportQuality quality = ExportQuality.good, - required Slide slide, - }) async { - if (!context.mounted) { - throw Exception('Context is not mounted'); - } - - final image = await _fromWidgetToImage( - SlideView.snapshot(slide), - context: context, - pixelRatio: quality.pixelRatio, - targetSize: kResolution, - ); - final convertedImage = await _getImageInBytes(image); - image.dispose(); - - return convertedImage; - } - - Future _getImageInBytes(ui.Image image) async { - final byteData = await image.toByteData(format: ui.ImageByteFormat.png); - return byteData!.buffer.asUint8List(); - } - - Future _fromWidgetToImage( - Widget widget, { - required double pixelRatio, - required BuildContext context, - Size? targetSize, - }) async { - Widget child = widget; - - child = InheritedTheme.captureAll( - context, - MediaQuery( - data: MediaQuery.of(context), - child: MaterialApp( - debugShowCheckedModeBanner: false, - theme: Theme.of(context), - color: Colors.transparent, - home: Scaffold(body: child), - ), - ), - ); - - final repaintBoundary = RenderRepaintBoundary(); - final platformDispatcher = WidgetsBinding.instance.platformDispatcher; - - final view = View.maybeOf(context) ?? platformDispatcher.views.first; - final logicalSize = targetSize ?? view.physicalSize / view.devicePixelRatio; - - int retryCount = 5; - bool isDirty = false; - - final renderView = RenderView( - view: view, - child: RenderPositionedBox( - alignment: Alignment.center, - child: repaintBoundary, - ), - configuration: ViewConfiguration( - size: logicalSize, - devicePixelRatio: pixelRatio, - ), - ); - - final pipelineOwner = PipelineOwner( - onNeedVisualUpdate: () { - isDirty = true; - }, - ); - final buildOwner = BuildOwner( - focusManager: FocusManager(), - onBuildScheduled: () { - isDirty = true; - }, - ); - - pipelineOwner.rootNode = renderView; - renderView.prepareInitialFrame(); - - final rootElement = RenderObjectToWidgetAdapter( - container: repaintBoundary, - child: Directionality( - textDirection: TextDirection.ltr, - child: child, - ), - ).attachToRenderTree(buildOwner); - ui.Image? image; - while (retryCount > 0) { - isDirty = false; - - if (!context.mounted) { - break; - } - - image = await _captureImage( - buildOwner: buildOwner, - rootElement: rootElement, - pipelineOwner: pipelineOwner, - repaintBoundary: repaintBoundary, - pixelRatio: pixelRatio, - logicalSize: logicalSize, - ); - - if (!isDirty) { - break; - } - - // await Future.delayed(Durations.short2); - - retryCount--; - } - - try { - buildOwner.finalizeTree(); - } catch (e) { - log('Error finalizing tree: $e'); - } - - return image!; - } - - Future _captureImage({ - required BuildOwner buildOwner, - required RenderObjectToWidgetElement rootElement, - required PipelineOwner pipelineOwner, - required RenderRepaintBoundary repaintBoundary, - required double pixelRatio, - required Size logicalSize, - }) async { - buildOwner.buildScope(rootElement); - buildOwner.finalizeTree(); - - await _waitForImagesLoaded(rootElement); - pipelineOwner.flushLayout(); - pipelineOwner.flushCompositingBits(); - pipelineOwner.flushPaint(); - await Future.delayed(Durations.short2); - return repaintBoundary.toImage(pixelRatio: pixelRatio); - } - - Future _waitForImagesLoaded(Element rootElement) async { - final List> futures = []; - - void traverseElement(Element element) { - if (element.widget is Image) { - final imageProvider = (element.widget as Image).image; - - final stream = imageProvider.resolve(ImageConfiguration.empty); - - final completer = Completer(); - - late ImageStreamListener listener; - listener = ImageStreamListener( - (ImageInfo image, bool synchronousCall) { - completer.complete(); - stream.removeListener(listener); - }, - onError: (dynamic exception, StackTrace? stackTrace) { - completer.completeError(exception, stackTrace); - stream.removeListener(listener); - }, - ); - - stream.addListener(listener); - futures.add(completer.future); - } - - element.visitChildren(traverseElement); - } - - rootElement.visitChildren(traverseElement); - - await Future.wait(futures); - } -} diff --git a/lib/models/asset_model.dart b/lib/models/asset_model.dart index fe153d6..2d5f922 100644 --- a/lib/models/asset_model.dart +++ b/lib/models/asset_model.dart @@ -1,134 +1,73 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:ui' as ui; - import 'package:dart_mappable/dart_mappable.dart'; -import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import '../helpers/config.dart'; part 'asset_model.mapper.dart'; -@MappableClass(includeCustomMappers: [AssetFileBytesMapper()]) -class SlideAsset with SlideAssetMappable { - final AssetFileBytes _file; +@MappableClass( + discriminatorKey: 'type', +) +abstract class SlideAsset with SlideAssetMappable { final double width; final double height; - final String path; + final String hash; + final String localPath; + final String type; @MappableConstructor() const SlideAsset({ - required AssetFileBytes bytes, required this.width, required this.height, - required this.path, - }) : _file = bytes; + required this.localPath, + required this.hash, + required this.type, + }); - Uint8List get bytes => _file.bytes; + static const fromMap = SlideAssetMapper.fromMap; + static const fromJson = SlideAssetMapper.fromJson; - String get extension => path.split('.').last; + String get extension => localPath.split('.').last; String get relativePath => p.relative( - path, - from: kConfig.assetsDir.parent.path, + localPath, + from: sdConfig.assetsDir.parent.path, ); - static Future maybeLoad(File file) async { - if (!await file.exists()) { - return null; - } - final bytes = await file.readAsBytes(); - final codec = await ui.instantiateImageCodec(bytes); - final frame = await codec.getNextFrame(); - - return SlideAsset( - path: file.path, - bytes: AssetFileBytes.fromBytes(bytes), - width: frame.image.width.toDouble(), - height: frame.image.height.toDouble(), - ); - } - - static Future load(File file) async { - final asset = await maybeLoad(file); - return asset ?? (throw Exception('Invalid asset file: ${file.path}')); - } - - static File buildFile(String fileName) { - // Check file file contains an allowed extension - final ext = p.extension(fileName).substring(1); - if (!SlideAsset.allowedExtensions.contains(ext)) { - throw Exception('Invalid file extension: $ext'); - } - return File( - p.join( - kConfig.assetsImageDir.path, - '${SlideAsset.assetPrefix}$fileName', - ), - ); - } - - String get name { - // Get name without extnesion and remove prefix - final fileName = path.split('/').last.split('.').first; - // remove asset prefix - return fileName.startsWith(SlideAsset.assetPrefix) - ? fileName.substring(SlideAsset.assetPrefix.length) - : fileName; - } - - static const assetPrefix = 'sd_asset_'; + static const cachedPrefix = 'sd_cached_'; + static const generatedPrefix = 'sd_generated_'; + static const thumbnailPrefix = 'sd_thumb_'; static const allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp']; - - static const fromMap = SlideAssetMapper.fromMap; - static const fromJson = SlideAssetMapper.fromJson; } -class AssetFileBytesMapper extends SimpleMapper { - const AssetFileBytesMapper(); - - @override - AssetFileBytes decode(dynamic value) { - return AssetFileBytes(value); - } - - @override - dynamic encode(AssetFileBytes self) { - return self.base64; - } +@MappableClass(discriminatorValue: 'generated') +class GeneratedAsset extends SlideAsset with GeneratedAssetMappable { + @MappableConstructor() + GeneratedAsset({ + required super.width, + required super.height, + required super.localPath, + required super.hash, + }) : super(type: 'generated'); } -class AssetFileBytes { - final Uint8List bytes; - final String base64; - - AssetFileBytes._({ - required this.bytes, - required this.base64, - }); - - factory AssetFileBytes(String base64) { - return AssetFileBytes._( - bytes: base64Decode(base64), - base64: base64, - ); - } - - factory AssetFileBytes.fromBytes(Uint8List bytes) { - return AssetFileBytes._( - bytes: bytes, - base64: base64Encode(bytes), - ); - } - - @override - operator ==(Object other) { - if (identical(this, other)) return true; - return other is AssetFileBytes && other.base64 == base64; - } +@MappableClass(discriminatorValue: 'cached') +class CachedAsset extends SlideAsset with CachedAssetMappable { + CachedAsset({ + required super.hash, + required super.width, + required super.height, + required super.localPath, + }) : super(type: 'cached'); +} - @override - int get hashCode => base64.hashCode; +@MappableClass(discriminatorValue: 'thumbnail') +class ThumbnailAsset extends SlideAsset with ThumbnailAssetMappable { + ThumbnailAsset({ + required super.hash, + required super.width, + required super.height, + required super.localPath, + }) : super(type: 'thumbnail'); } diff --git a/lib/models/asset_model.mapper.dart b/lib/models/asset_model.mapper.dart index 75d2c2f..996b39e 100644 --- a/lib/models/asset_model.mapper.dart +++ b/lib/models/asset_model.mapper.dart @@ -13,7 +13,9 @@ class SlideAssetMapper extends ClassMapperBase { static SlideAssetMapper ensureInitialized() { if (_instance == null) { MapperContainer.globals.use(_instance = SlideAssetMapper._()); - MapperContainer.globals.useAll([AssetFileBytesMapper()]); + GeneratedAssetMapper.ensureInitialized(); + CachedAssetMapper.ensureInitialized(); + ThumbnailAssetMapper.ensureInitialized(); } return _instance!; } @@ -21,32 +23,32 @@ class SlideAssetMapper extends ClassMapperBase { @override final String id = 'SlideAsset'; - static AssetFileBytes _$_file(SlideAsset v) => v._file; - static const Field _f$_file = - Field('_file', _$_file, key: 'bytes'); static double _$width(SlideAsset v) => v.width; static const Field _f$width = Field('width', _$width); static double _$height(SlideAsset v) => v.height; static const Field _f$height = Field('height', _$height); - static String _$path(SlideAsset v) => v.path; - static const Field _f$path = Field('path', _$path); + static String _$localPath(SlideAsset v) => v.localPath; + static const Field _f$localPath = + Field('localPath', _$localPath, key: 'local_path'); + static String _$hash(SlideAsset v) => v.hash; + static const Field _f$hash = Field('hash', _$hash); + static String _$type(SlideAsset v) => v.type; + static const Field _f$type = Field('type', _$type); @override final MappableFields fields = const { - #_file: _f$_file, #width: _f$width, #height: _f$height, - #path: _f$path, + #localPath: _f$localPath, + #hash: _f$hash, + #type: _f$type, }; @override final bool ignoreNull = true; static SlideAsset _instantiate(DecodingData data) { - return SlideAsset( - bytes: data.dec(_f$_file), - width: data.dec(_f$width), - height: data.dec(_f$height), - path: data.dec(_f$path)); + throw MapperException.missingSubclass( + 'SlideAsset', 'type', '${data.value['type']}'); } @override @@ -62,77 +64,438 @@ class SlideAssetMapper extends ClassMapperBase { } mixin SlideAssetMappable { + String toJson(); + Map toMap(); + SlideAssetCopyWith get copyWith; +} + +abstract class SlideAssetCopyWith<$R, $In extends SlideAsset, $Out> + implements ClassCopyWith<$R, $In, $Out> { + $R call({double? width, double? height, String? localPath, String? hash}); + SlideAssetCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class GeneratedAssetMapper extends SubClassMapperBase { + GeneratedAssetMapper._(); + + static GeneratedAssetMapper? _instance; + static GeneratedAssetMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = GeneratedAssetMapper._()); + SlideAssetMapper.ensureInitialized().addSubMapper(_instance!); + } + return _instance!; + } + + @override + final String id = 'GeneratedAsset'; + + static double _$width(GeneratedAsset v) => v.width; + static const Field _f$width = Field('width', _$width); + static double _$height(GeneratedAsset v) => v.height; + static const Field _f$height = + Field('height', _$height); + static String _$localPath(GeneratedAsset v) => v.localPath; + static const Field _f$localPath = + Field('localPath', _$localPath, key: 'local_path'); + static String _$hash(GeneratedAsset v) => v.hash; + static const Field _f$hash = Field('hash', _$hash); + static String _$type(GeneratedAsset v) => v.type; + static const Field _f$type = + Field('type', _$type, mode: FieldMode.member); + + @override + final MappableFields fields = const { + #width: _f$width, + #height: _f$height, + #localPath: _f$localPath, + #hash: _f$hash, + #type: _f$type, + }; + @override + final bool ignoreNull = true; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'generated'; + @override + late final ClassMapperBase superMapper = SlideAssetMapper.ensureInitialized(); + + static GeneratedAsset _instantiate(DecodingData data) { + return GeneratedAsset( + width: data.dec(_f$width), + height: data.dec(_f$height), + localPath: data.dec(_f$localPath), + hash: data.dec(_f$hash)); + } + + @override + final Function instantiate = _instantiate; + + static GeneratedAsset fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static GeneratedAsset fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin GeneratedAssetMappable { String toJson() { - return SlideAssetMapper.ensureInitialized() - .encodeJson(this as SlideAsset); + return GeneratedAssetMapper.ensureInitialized() + .encodeJson(this as GeneratedAsset); } Map toMap() { - return SlideAssetMapper.ensureInitialized() - .encodeMap(this as SlideAsset); + return GeneratedAssetMapper.ensureInitialized() + .encodeMap(this as GeneratedAsset); } - SlideAssetCopyWith get copyWith => - _SlideAssetCopyWithImpl(this as SlideAsset, $identity, $identity); + GeneratedAssetCopyWith + get copyWith => _GeneratedAssetCopyWithImpl( + this as GeneratedAsset, $identity, $identity); @override String toString() { - return SlideAssetMapper.ensureInitialized() - .stringifyValue(this as SlideAsset); + return GeneratedAssetMapper.ensureInitialized() + .stringifyValue(this as GeneratedAsset); } @override bool operator ==(Object other) { - return SlideAssetMapper.ensureInitialized() - .equalsValue(this as SlideAsset, other); + return GeneratedAssetMapper.ensureInitialized() + .equalsValue(this as GeneratedAsset, other); } @override int get hashCode { - return SlideAssetMapper.ensureInitialized().hashValue(this as SlideAsset); + return GeneratedAssetMapper.ensureInitialized() + .hashValue(this as GeneratedAsset); } } -extension SlideAssetValueCopy<$R, $Out> - on ObjectCopyWith<$R, SlideAsset, $Out> { - SlideAssetCopyWith<$R, SlideAsset, $Out> get $asSlideAsset => - $base.as((v, t, t2) => _SlideAssetCopyWithImpl(v, t, t2)); +extension GeneratedAssetValueCopy<$R, $Out> + on ObjectCopyWith<$R, GeneratedAsset, $Out> { + GeneratedAssetCopyWith<$R, GeneratedAsset, $Out> get $asGeneratedAsset => + $base.as((v, t, t2) => _GeneratedAssetCopyWithImpl(v, t, t2)); } -abstract class SlideAssetCopyWith<$R, $In extends SlideAsset, $Out> - implements ClassCopyWith<$R, $In, $Out> { - $R call({AssetFileBytes? bytes, double? width, double? height, String? path}); - SlideAssetCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +abstract class GeneratedAssetCopyWith<$R, $In extends GeneratedAsset, $Out> + implements SlideAssetCopyWith<$R, $In, $Out> { + @override + $R call({double? width, double? height, String? localPath, String? hash}); + GeneratedAssetCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); +} + +class _GeneratedAssetCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, GeneratedAsset, $Out> + implements GeneratedAssetCopyWith<$R, GeneratedAsset, $Out> { + _GeneratedAssetCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + GeneratedAssetMapper.ensureInitialized(); + @override + $R call({double? width, double? height, String? localPath, String? hash}) => + $apply(FieldCopyWithData({ + if (width != null) #width: width, + if (height != null) #height: height, + if (localPath != null) #localPath: localPath, + if (hash != null) #hash: hash + })); + @override + GeneratedAsset $make(CopyWithData data) => GeneratedAsset( + width: data.get(#width, or: $value.width), + height: data.get(#height, or: $value.height), + localPath: data.get(#localPath, or: $value.localPath), + hash: data.get(#hash, or: $value.hash)); + + @override + GeneratedAssetCopyWith<$R2, GeneratedAsset, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _GeneratedAssetCopyWithImpl($value, $cast, t); +} + +class CachedAssetMapper extends SubClassMapperBase { + CachedAssetMapper._(); + + static CachedAssetMapper? _instance; + static CachedAssetMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = CachedAssetMapper._()); + SlideAssetMapper.ensureInitialized().addSubMapper(_instance!); + } + return _instance!; + } + + @override + final String id = 'CachedAsset'; + + static String _$hash(CachedAsset v) => v.hash; + static const Field _f$hash = Field('hash', _$hash); + static double _$width(CachedAsset v) => v.width; + static const Field _f$width = Field('width', _$width); + static double _$height(CachedAsset v) => v.height; + static const Field _f$height = Field('height', _$height); + static String _$localPath(CachedAsset v) => v.localPath; + static const Field _f$localPath = + Field('localPath', _$localPath, key: 'local_path'); + static String _$type(CachedAsset v) => v.type; + static const Field _f$type = + Field('type', _$type, mode: FieldMode.member); + + @override + final MappableFields fields = const { + #hash: _f$hash, + #width: _f$width, + #height: _f$height, + #localPath: _f$localPath, + #type: _f$type, + }; + @override + final bool ignoreNull = true; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'cached'; + @override + late final ClassMapperBase superMapper = SlideAssetMapper.ensureInitialized(); + + static CachedAsset _instantiate(DecodingData data) { + return CachedAsset( + hash: data.dec(_f$hash), + width: data.dec(_f$width), + height: data.dec(_f$height), + localPath: data.dec(_f$localPath)); + } + + @override + final Function instantiate = _instantiate; + + static CachedAsset fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static CachedAsset fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin CachedAssetMappable { + String toJson() { + return CachedAssetMapper.ensureInitialized() + .encodeJson(this as CachedAsset); + } + + Map toMap() { + return CachedAssetMapper.ensureInitialized() + .encodeMap(this as CachedAsset); + } + + CachedAssetCopyWith get copyWith => + _CachedAssetCopyWithImpl(this as CachedAsset, $identity, $identity); + @override + String toString() { + return CachedAssetMapper.ensureInitialized() + .stringifyValue(this as CachedAsset); + } + + @override + bool operator ==(Object other) { + return CachedAssetMapper.ensureInitialized() + .equalsValue(this as CachedAsset, other); + } + + @override + int get hashCode { + return CachedAssetMapper.ensureInitialized().hashValue(this as CachedAsset); + } +} + +extension CachedAssetValueCopy<$R, $Out> + on ObjectCopyWith<$R, CachedAsset, $Out> { + CachedAssetCopyWith<$R, CachedAsset, $Out> get $asCachedAsset => + $base.as((v, t, t2) => _CachedAssetCopyWithImpl(v, t, t2)); +} + +abstract class CachedAssetCopyWith<$R, $In extends CachedAsset, $Out> + implements SlideAssetCopyWith<$R, $In, $Out> { + @override + $R call({String? hash, double? width, double? height, String? localPath}); + CachedAssetCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); +} + +class _CachedAssetCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, CachedAsset, $Out> + implements CachedAssetCopyWith<$R, CachedAsset, $Out> { + _CachedAssetCopyWithImpl(super.value, super.then, super.then2); + + @override + late final ClassMapperBase $mapper = + CachedAssetMapper.ensureInitialized(); + @override + $R call({String? hash, double? width, double? height, String? localPath}) => + $apply(FieldCopyWithData({ + if (hash != null) #hash: hash, + if (width != null) #width: width, + if (height != null) #height: height, + if (localPath != null) #localPath: localPath + })); + @override + CachedAsset $make(CopyWithData data) => CachedAsset( + hash: data.get(#hash, or: $value.hash), + width: data.get(#width, or: $value.width), + height: data.get(#height, or: $value.height), + localPath: data.get(#localPath, or: $value.localPath)); + + @override + CachedAssetCopyWith<$R2, CachedAsset, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t) => + _CachedAssetCopyWithImpl($value, $cast, t); +} + +class ThumbnailAssetMapper extends SubClassMapperBase { + ThumbnailAssetMapper._(); + + static ThumbnailAssetMapper? _instance; + static ThumbnailAssetMapper ensureInitialized() { + if (_instance == null) { + MapperContainer.globals.use(_instance = ThumbnailAssetMapper._()); + SlideAssetMapper.ensureInitialized().addSubMapper(_instance!); + } + return _instance!; + } + + @override + final String id = 'ThumbnailAsset'; + + static String _$hash(ThumbnailAsset v) => v.hash; + static const Field _f$hash = Field('hash', _$hash); + static double _$width(ThumbnailAsset v) => v.width; + static const Field _f$width = Field('width', _$width); + static double _$height(ThumbnailAsset v) => v.height; + static const Field _f$height = + Field('height', _$height); + static String _$localPath(ThumbnailAsset v) => v.localPath; + static const Field _f$localPath = + Field('localPath', _$localPath, key: 'local_path'); + static String _$type(ThumbnailAsset v) => v.type; + static const Field _f$type = + Field('type', _$type, mode: FieldMode.member); + + @override + final MappableFields fields = const { + #hash: _f$hash, + #width: _f$width, + #height: _f$height, + #localPath: _f$localPath, + #type: _f$type, + }; + @override + final bool ignoreNull = true; + + @override + final String discriminatorKey = 'type'; + @override + final dynamic discriminatorValue = 'thumbnail'; + @override + late final ClassMapperBase superMapper = SlideAssetMapper.ensureInitialized(); + + static ThumbnailAsset _instantiate(DecodingData data) { + return ThumbnailAsset( + hash: data.dec(_f$hash), + width: data.dec(_f$width), + height: data.dec(_f$height), + localPath: data.dec(_f$localPath)); + } + + @override + final Function instantiate = _instantiate; + + static ThumbnailAsset fromMap(Map map) { + return ensureInitialized().decodeMap(map); + } + + static ThumbnailAsset fromJson(String json) { + return ensureInitialized().decodeJson(json); + } +} + +mixin ThumbnailAssetMappable { + String toJson() { + return ThumbnailAssetMapper.ensureInitialized() + .encodeJson(this as ThumbnailAsset); + } + + Map toMap() { + return ThumbnailAssetMapper.ensureInitialized() + .encodeMap(this as ThumbnailAsset); + } + + ThumbnailAssetCopyWith + get copyWith => _ThumbnailAssetCopyWithImpl( + this as ThumbnailAsset, $identity, $identity); + @override + String toString() { + return ThumbnailAssetMapper.ensureInitialized() + .stringifyValue(this as ThumbnailAsset); + } + + @override + bool operator ==(Object other) { + return ThumbnailAssetMapper.ensureInitialized() + .equalsValue(this as ThumbnailAsset, other); + } + + @override + int get hashCode { + return ThumbnailAssetMapper.ensureInitialized() + .hashValue(this as ThumbnailAsset); + } +} + +extension ThumbnailAssetValueCopy<$R, $Out> + on ObjectCopyWith<$R, ThumbnailAsset, $Out> { + ThumbnailAssetCopyWith<$R, ThumbnailAsset, $Out> get $asThumbnailAsset => + $base.as((v, t, t2) => _ThumbnailAssetCopyWithImpl(v, t, t2)); +} + +abstract class ThumbnailAssetCopyWith<$R, $In extends ThumbnailAsset, $Out> + implements SlideAssetCopyWith<$R, $In, $Out> { + @override + $R call({String? hash, double? width, double? height, String? localPath}); + ThumbnailAssetCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>( + Then<$Out2, $R2> t); } -class _SlideAssetCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, SlideAsset, $Out> - implements SlideAssetCopyWith<$R, SlideAsset, $Out> { - _SlideAssetCopyWithImpl(super.value, super.then, super.then2); +class _ThumbnailAssetCopyWithImpl<$R, $Out> + extends ClassCopyWithBase<$R, ThumbnailAsset, $Out> + implements ThumbnailAssetCopyWith<$R, ThumbnailAsset, $Out> { + _ThumbnailAssetCopyWithImpl(super.value, super.then, super.then2); @override - late final ClassMapperBase $mapper = - SlideAssetMapper.ensureInitialized(); + late final ClassMapperBase $mapper = + ThumbnailAssetMapper.ensureInitialized(); @override - $R call( - {AssetFileBytes? bytes, - double? width, - double? height, - String? path}) => + $R call({String? hash, double? width, double? height, String? localPath}) => $apply(FieldCopyWithData({ - if (bytes != null) #bytes: bytes, + if (hash != null) #hash: hash, if (width != null) #width: width, if (height != null) #height: height, - if (path != null) #path: path + if (localPath != null) #localPath: localPath })); @override - SlideAsset $make(CopyWithData data) => SlideAsset( - bytes: data.get(#bytes, or: $value._file), + ThumbnailAsset $make(CopyWithData data) => ThumbnailAsset( + hash: data.get(#hash, or: $value.hash), width: data.get(#width, or: $value.width), height: data.get(#height, or: $value.height), - path: data.get(#path, or: $value.path)); + localPath: data.get(#localPath, or: $value.localPath)); @override - SlideAssetCopyWith<$R2, SlideAsset, $Out2> $chain<$R2, $Out2>( + ThumbnailAssetCopyWith<$R2, ThumbnailAsset, $Out2> $chain<$R2, $Out2>( Then<$Out2, $R2> t) => - _SlideAssetCopyWithImpl($value, $cast, t); + _ThumbnailAssetCopyWithImpl($value, $cast, t); } diff --git a/lib/models/options_model.dart b/lib/models/options_model.dart index 68b8219..5b7a9a6 100644 --- a/lib/models/options_model.dart +++ b/lib/models/options_model.dart @@ -278,7 +278,7 @@ abstract class ExampleWidget { } } - ArgsSchema? get schema; + ArgsSchema? get schema => null; SchemaValidationResult _validate(Map args) { if (schema?.validator == null) { @@ -451,6 +451,19 @@ enum LayoutPosition { static final schema = EnumSchema( values: LayoutPosition.values.map((e) => e.name.snakeCase).toList(), ); + + bool isHorizontal() { + return switch (this) { + LayoutPosition.left => true, + LayoutPosition.right => true, + LayoutPosition.top => false, + LayoutPosition.bottom => false, + }; + } + + bool isVertical() { + return !isHorizontal(); + } } @MappableEnum() diff --git a/lib/models/slide_model.dart b/lib/models/slide_model.dart index b80643e..c6eebbb 100644 --- a/lib/models/slide_model.dart +++ b/lib/models/slide_model.dart @@ -1,9 +1,8 @@ import 'package:dart_mappable/dart_mappable.dart'; +import '../helpers/section_tag.dart'; import '../schema/schema.dart'; import '../superdeck.dart'; -import 'options_model.dart'; -import 'syntax_tag.dart'; part 'slide_model.mapper.dart'; @@ -13,6 +12,7 @@ abstract class Slide extends Config with SlideMappable { final String layout; final String data; final String? raw; + final String hash; @MappableField(key: 'content') final ContentOptions? contentOptions; @@ -26,7 +26,7 @@ abstract class Slide extends Config with SlideMappable { required super.background, required super.style, required super.transition, - }); + }) : hash = raw.hashCode.toString(); SlideVariant? get styleVariant { return style == null ? null : SlideVariant(style!); @@ -187,9 +187,9 @@ class TwoColumnSlide extends SectionsSlide with TwoColumnSlideMappable { super.raw, }) : super(layout: LayoutType.twoColumn); - SectionData get left => getSection(Section.left, Section.first); + SectionData get left => getSection(SectionTag.left, SectionTag.first); - SectionData get right => getSection(Section.right); + SectionData get right => getSection(SectionTag.right); static const fromMap = TwoColumnSlideMapper.fromMap; @@ -217,11 +217,11 @@ class TwoColumnHeaderSlide extends SectionsSlide super.raw, }) : super(layout: LayoutType.twoColumnHeader); - SectionData get header => getSection(Section.header, Section.first); + SectionData get header => getSection(SectionTag.header, SectionTag.first); - SectionData get left => getSection(Section.left); + SectionData get left => getSection(SectionTag.left); - SectionData get right => getSection(Section.right); + SectionData get right => getSection(SectionTag.right); static const fromMap = TwoColumnHeaderSlideMapper.fromMap; diff --git a/lib/models/slide_model.mapper.dart b/lib/models/slide_model.mapper.dart index 0d5c2f7..8415bca 100644 --- a/lib/models/slide_model.mapper.dart +++ b/lib/models/slide_model.mapper.dart @@ -46,6 +46,9 @@ class SlideMapper extends SubClassMapperBase { static TransitionOptions? _$transition(Slide v) => v.transition; static const Field _f$transition = Field('transition', _$transition); + static String _$hash(Slide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -57,6 +60,7 @@ class SlideMapper extends SubClassMapperBase { #background: _f$background, #style: _f$style, #transition: _f$transition, + #hash: _f$hash, }; @override final bool ignoreNull = true; @@ -150,6 +154,9 @@ class SimpleSlideMapper extends SubClassMapperBase { static String _$layout(SimpleSlide v) => v.layout; static const Field _f$layout = Field('layout', _$layout, mode: FieldMode.member); + static String _$hash(SimpleSlide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -161,6 +168,7 @@ class SimpleSlideMapper extends SubClassMapperBase { #raw: _f$raw, #data: _f$data, #layout: _f$layout, + #hash: _f$hash, }; @override final bool ignoreNull = true; @@ -349,6 +357,9 @@ class SplitSlideMapper extends SubClassMapperBase { static const Field _f$layout = Field('layout', _$layout); static String? _$raw(SplitSlide v) => v.raw; static const Field _f$raw = Field('raw', _$raw); + static String _$hash(SplitSlide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -361,6 +372,7 @@ class SplitSlideMapper extends SubClassMapperBase { #data: _f$data, #layout: _f$layout, #raw: _f$raw, + #hash: _f$hash, }; @override final bool ignoreNull = true; @@ -468,6 +480,9 @@ class ImageSlideMapper extends SubClassMapperBase { static String _$layout(ImageSlide v) => v.layout; static const Field _f$layout = Field('layout', _$layout, mode: FieldMode.member); + static String _$hash(ImageSlide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -480,6 +495,7 @@ class ImageSlideMapper extends SubClassMapperBase { #options: _f$options, #raw: _f$raw, #layout: _f$layout, + #hash: _f$hash, }; @override final bool ignoreNull = true; @@ -680,6 +696,9 @@ class WidgetSlideMapper extends SubClassMapperBase { static String _$layout(WidgetSlide v) => v.layout; static const Field _f$layout = Field('layout', _$layout, mode: FieldMode.member); + static String _$hash(WidgetSlide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -692,6 +711,7 @@ class WidgetSlideMapper extends SubClassMapperBase { #data: _f$data, #raw: _f$raw, #layout: _f$layout, + #hash: _f$hash, }; @override final bool ignoreNull = true; @@ -896,6 +916,9 @@ class SectionsSlideMapper extends SubClassMapperBase { Field('layout', _$layout); static String? _$raw(SectionsSlide v) => v.raw; static const Field _f$raw = Field('raw', _$raw); + static String _$hash(SectionsSlide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -908,6 +931,7 @@ class SectionsSlideMapper extends SubClassMapperBase { #sections: _f$sections, #layout: _f$layout, #raw: _f$raw, + #hash: _f$hash, }; @override final bool ignoreNull = true; @@ -1010,6 +1034,9 @@ class TwoColumnSlideMapper extends SubClassMapperBase { static String _$layout(TwoColumnSlide v) => v.layout; static const Field _f$layout = Field('layout', _$layout, mode: FieldMode.member); + static String _$hash(TwoColumnSlide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -1022,6 +1049,7 @@ class TwoColumnSlideMapper extends SubClassMapperBase { #sections: _f$sections, #raw: _f$raw, #layout: _f$layout, + #hash: _f$hash, }; @override final bool ignoreNull = true; @@ -1228,6 +1256,9 @@ class TwoColumnHeaderSlideMapper static String _$layout(TwoColumnHeaderSlide v) => v.layout; static const Field _f$layout = Field('layout', _$layout, mode: FieldMode.member); + static String _$hash(TwoColumnHeaderSlide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -1240,6 +1271,7 @@ class TwoColumnHeaderSlideMapper #sections: _f$sections, #raw: _f$raw, #layout: _f$layout, + #hash: _f$hash, }; @override final bool ignoreNull = true; @@ -1442,6 +1474,9 @@ class InvalidSlideMapper extends SubClassMapperBase { static String _$layout(InvalidSlide v) => v.layout; static const Field _f$layout = Field('layout', _$layout, mode: FieldMode.member); + static String _$hash(InvalidSlide v) => v.hash; + static const Field _f$hash = + Field('hash', _$hash, mode: FieldMode.member); @override final MappableFields fields = const { @@ -1453,6 +1488,7 @@ class InvalidSlideMapper extends SubClassMapperBase { #data: _f$data, #raw: _f$raw, #layout: _f$layout, + #hash: _f$hash, }; @override final bool ignoreNull = true; diff --git a/lib/providers/deck_provider.dart b/lib/providers/deck_provider.dart index 1a0a009..20e6dc1 100644 --- a/lib/providers/deck_provider.dart +++ b/lib/providers/deck_provider.dart @@ -1,69 +1,35 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:localstorage/localstorage.dart'; import 'package:signals/signals_flutter.dart'; -import 'package:watcher/watcher.dart'; import '../helpers/constants.dart'; import '../helpers/loader.dart'; -import '../helpers/utils.dart'; -import '../models/options_model.dart'; import '../superdeck.dart'; class SuperDeckProvider { SuperDeckProvider._(); - static SuperDeckProvider get instance => _instance; + static final instance = SuperDeckProvider._(); - static final _instance = SuperDeckProvider._(); + final _data = futureSignal(() => SlidesLoader.instance.loadFromStorage()); - final data = futureSignal(() async { - if (kCanRunProcess) { - await SlidesLoader.generate(); - } - return SlidesLoader.loadFromStorage(); - }); - - List> _subscriptions = []; - - late final listenToSlideChanges = effect(() { - slides.value; - final previousValue = slides.previousValue ?? []; - - if (listEquals(slides.value, previousValue)) { - return; - } - - final changes = compareListChanges(previousValue, slides.value); - - for (var added in changes.added) { - log('Added: $added'); - } - - for (var removed in changes.removed) { - log('Removed: $removed'); - } - }); + final style = signal(const Style.empty(), debugLabel: 'Style'); - final style = signal(const Style.empty()); - - late final loading = computed(() { - return data.value is AsyncLoading; - }); - - late final slides = computed(() => data.value.value?.slides ?? []); + late final loading = computed(() => _data.value is AsyncLoading); + late final slides = computed(() => _data.value.value?.slides ?? []); + late final assets = computed(() => _data.value.value?.assets ?? []); late final config = - computed(() => data.value.value?.config ?? const ProjectConfig.empty()); + computed(() => _data.value.value?.config ?? const ProjectConfig.empty()); final examples = mapSignal({}); late final error = computed( () { - final data = this.data.value; + final data = _data.value; return data is AsyncError ? data.error : null; }, ); @@ -87,35 +53,20 @@ class SuperDeckProvider { Style? style, }) async { // Unsubscribe to listeners in case its a retry - _unsubscribe(); - batch(() { - this.style.value = defaultStyle.merge(style); - this.examples.assign(_examplesToMap(examples)); - }); + await _data.future; + update(examples: examples, style: style); if (kCanRunProcess) { - _subscriptions = SlidesLoader.listen( - onChange: () async { - data.refresh(); - }, + SlidesLoader.instance.listen( + onChange: () => _data.refresh(), ); } } - void _unsubscribe() { - for (var sub in _subscriptions) { - sub.cancel(); - } - _subscriptions.clear(); - } - void dispose() { style.dispose(); - - slides.dispose(); - + _data.dispose(); examples.dispose(); - _unsubscribe(); } } diff --git a/lib/screens/export_screen.dart b/lib/screens/export_screen.dart index 740aac1..532c4a2 100644 --- a/lib/screens/export_screen.dart +++ b/lib/screens/export_screen.dart @@ -10,11 +10,10 @@ import 'package:signals/signals_flutter.dart'; import 'package:universal_html/html.dart' as html; import '../../helpers/constants.dart'; -import '../../models/slide_model.dart'; import '../components/atoms/linear_progresss_indicator_widget.dart'; import '../components/atoms/slide_view.dart'; import '../components/molecules/scaled_app.dart'; -import '../helpers/slide_to_image.dart'; +import '../services/image_generation_service.dart'; import '../superdeck.dart'; enum ExportProcessStatus { @@ -38,7 +37,7 @@ class ExportScreen extends StatefulWidget { class _ExportScreenState extends State { final navigation = NavigationProvider.instance; - late final _selectedQuality = createSignal(context, ExportQuality.good); + late final _selectedQuality = createSignal(context, SnapshotQuality.good); Future convertToPdf(BuildContext context) async { final lastState = navigation.sideIsOpen.value; @@ -66,7 +65,7 @@ class _ExportScreenState extends State { Overlay.of(context).insert(entry); } - void setQuality(ExportQuality? quality) { + void setQuality(SnapshotQuality? quality) { if (quality == null) throw Exception('Quality cannot be null'); _selectedQuality.value = quality; } @@ -75,9 +74,9 @@ class _ExportScreenState extends State { Widget build(BuildContext context) { final selectedQuality = _selectedQuality.watch(context); - List> buildRadioList() { - return ExportQuality.values.map((e) { - return RadioListTile.adaptive( + List> buildRadioList() { + return SnapshotQuality.values.map((e) { + return RadioListTile.adaptive( title: Text(e.label), value: e, groupValue: selectedQuality, @@ -125,7 +124,7 @@ class ExportingProcessScreen extends StatefulWidget { }); final void Function() onComplete; - final ExportQuality quality; + final SnapshotQuality quality; @override State createState() => _ExportingProcessScreenState(); @@ -155,14 +154,13 @@ class _ExportingProcessScreenState extends State { Future startConversion() async { try { - final generator = ImageGenerationService.instance; + final generator = ImageGenerationService(context); _status.value = ExportProcessStatus.converting; List> futures = []; Future convertSlide(Slide slide) async { final convertedImage = await generator.generate( - context: context, quality: widget.quality, slide: slide, ); diff --git a/lib/services/assets_service.dart b/lib/services/assets_service.dart new file mode 100644 index 0000000..8a16f41 --- /dev/null +++ b/lib/services/assets_service.dart @@ -0,0 +1,268 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as p; + +import '../helpers/config.dart'; +import '../helpers/constants.dart'; +import '../helpers/markdown_processor.dart'; +import '../models/asset_model.dart'; +import '../models/options_model.dart'; +import '../models/slide_model.dart'; + +const assetService = AssetService.instance; + +class AssetService { + const AssetService._(); + + static const instance = AssetService._(); + + static final _refs = sdConfig.references; + static final _imageDir = sdConfig.assetsImageDir; + + static Future initialize() async { + if (!await _imageDir.exists()) { + await _imageDir.create(recursive: true); + } + if (!await _refs.slides.exists()) { + await _refs.slides.writeAsString('[]'); + } + if (!await _refs.config.exists()) { + await _refs.config.create(recursive: true); + } + + if (!await _refs.assets.exists()) { + await _refs.assets.writeAsString('[]'); + } + } + + Future loadString(String path) async { + if (kCanRunProcess) { + return File(path).readAsString(); + } else { + return rootBundle.loadString(path); + } + } + + Future loadBytes(String path) async { + if (kCanRunProcess) { + return File(path).readAsBytes(); + } else { + return (await rootBundle.load(path)).buffer.asUint8List(); + } + } + + Future loadReferences() async { + final slides = await _loadSlides(); + final config = await _loadConfig(); + final assets = await loadAssets(); + + return (slides: slides, config: config, assets: assets); + } + + List _parseList( + String contents, T Function(Map) fromMap) { + final jsonList = jsonDecode(contents) as List; + final mapList = List.castFrom>(jsonList); + + return mapList.map(fromMap).toList(); + } + + Future> _loadSlides() async { + final slidesJson = await loadString(_refs.slides.path); + return _parseList(slidesJson, Slide.fromMap); + } + + Future _loadConfig() async { + final configJson = await loadString(_refs.config.path); + return ProjectConfig.fromJson(configJson); + } + + Future loadGeneratedAsset(String hash) async { + final assets = await loadAssets(); + + return assets + .whereType() + .firstWhereOrNull((e) => e.hash == hash); + } + + Future saveAsset(SlideAsset asset) async { + final assets = await loadAssets(); + + final list = [...assets, asset].map((e) => e.toMap()).toList(); + + _refs.assets.writeAsString(jsonEncode(list)); + } + + Future saveThumbnailAsset({ + required String hash, + required Uint8List data, + }) async { + final file = File( + p.join( + sdConfig.assetsImageDir.path, + '${SlideAsset.thumbnailPrefix}$hash.png', + ), + ); + + file.writeAsBytesSync(data); + + final size = await _getImageSize(data); + + final asset = ThumbnailAsset( + localPath: file.path, + hash: hash, + width: size.width, + height: size.height, + ); + + await saveAsset(asset); + return asset; + } + + Future loadThumbnailAsset(String hash) async { + final assets = await loadAssets(); + + return assets + .whereType() + .firstWhereOrNull((e) => e.hash == hash); + } + + Future loadCachedAsset(String url) async { + final assets = await loadAssets(); + + return assets + .whereType() + .firstWhereOrNull((e) => e.hash == url.hashCode.toString()); + } + + Future> loadAssets() async { + final assetsJson = await loadString(_refs.assets.path); + final assets = _parseList(assetsJson, SlideAsset.fromMap); + + // Just return if you cant run a process + if (!kCanRunProcess) { + return assets; + } + + final updatedAssets = []; + for (final asset in assets) { + final assetFile = File(asset.localPath); + if (await assetFile.exists()) { + updatedAssets.add(asset); + } + } + + return updatedAssets; + } + + Future saveReferences(DeckReferenceDto data) async { + final config = data.config; + final slides = data.slides; + + await _refs.config.ensureWrite(config.toJson()); + final map = slides.map((e) => e.toMap()).toList(); + await _refs.slides.ensureWrite(jsonEncode(map)); + } + + Future loadProjectConfig(File projectFile) async { + return await projectFile.exists() + ? ProjectConfig.fromYaml(await projectFile.readAsString()) + : const ProjectConfig.empty(); + } + + Future saveGeneratedAsset({ + required String hash, + required Uint8List data, + }) async { + final file = File( + p.join( + sdConfig.assetsImageDir.path, + '${SlideAsset.generatedPrefix}$hash.png', + ), + ); + + file.writeAsBytesSync(data); + + final size = await _getImageSize(data); + + final asset = GeneratedAsset( + localPath: file.path, + hash: hash, + width: size.width, + height: size.height, + ); + + await saveAsset(asset); + + return asset; + } + + Future saveCachedAsset(String url) async { + Uint8List assetData; + String extension; + if (!url.startsWith('http')) { + final file = File(url); + assetData = await file.readAsBytes(); + extension = file.path.split('.').last; + } else { + final client = HttpClient(); + final request = await client.getUrl(Uri.parse(url)); + final response = await request.close(); + + final contentType = response.headers.contentType; + // Default to .jpg if no extension is found + assetData = await consolidateHttpClientResponseBytes(response); + extension = contentType?.subType ?? 'jpg'; + + // Check if url has extension and is an image + } + if (!SlideAsset.allowedExtensions.contains(extension)) { + throw Exception('Invalid file extension: $extension'); + } + final file = File( + p.join( + sdConfig.assetsImageDir.path, + '${SlideAsset.cachedPrefix}${url.hashCode}.$extension', + ), + ); + + file.writeAsBytesSync(assetData); + + final size = await _getImageSize(assetData); + + final asset = CachedAsset( + hash: url.hashCode.toString(), + width: size.width, + height: size.height, + localPath: file.path, + ); + + await saveAsset(asset); + + return asset; + } +} + +extension on File { + Future ensureWrite(String content) async { + if (!await exists()) { + await create(recursive: true); + } + + await writeAsString(content); + } +} + +Future<({double width, double height})> _getImageSize(Uint8List data) async { + final codec = await ui.instantiateImageCodec(data); + final frame = await codec.getNextFrame(); + return ( + width: frame.image.width.toDouble(), + height: frame.image.height.toDouble(), + ); +} diff --git a/lib/services/image_generation_service.dart b/lib/services/image_generation_service.dart new file mode 100644 index 0000000..18abc40 --- /dev/null +++ b/lib/services/image_generation_service.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import '../components/atoms/slide_view.dart'; +import '../helpers/constants.dart'; +import '../models/slide_model.dart'; + +enum SnapshotQuality { + low('Low', pixelRatio: 0.4), + good('Good', pixelRatio: 1), + better('Better', pixelRatio: 2), + best('Best', pixelRatio: 3); + + const SnapshotQuality(this.label, {required this.pixelRatio}); + + final String label; + final double pixelRatio; +} + +// +class ImageGenerationService { + bool _isDisposed = false; + ImageGenerationService(this.context); + + final BuildContext context; + + void dispose() => _isDisposed = true; + + void checkDisposed() { + if (_isDisposed) { + throw Exception('ImageGenerationService is disposed'); + } + } + + Future generate({ + required SnapshotQuality quality, + required Slide slide, + }) async { + try { + final image = await _fromWidgetToImage( + SlideView.snapshot(slide), + context: context, + pixelRatio: quality.pixelRatio, + targetSize: kResolution, + ); + + return _imageToUint8List(image); + } catch (e, stackTrace) { + log('Error generating image: $e', stackTrace: stackTrace); + rethrow; + } + } + + Future _imageToUint8List(ui.Image image) async { + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + image.dispose(); + return byteData!.buffer.asUint8List(); + } + + Future _fromWidgetToImage( + Widget widget, { + required double pixelRatio, + required BuildContext context, + Size? targetSize, + }) async { + try { + Widget child = widget; + + child = InheritedTheme.captureAll( + context, + MediaQuery( + data: MediaQuery.of(context), + child: MaterialApp( + debugShowCheckedModeBanner: false, + theme: Theme.of(context), + color: Colors.transparent, + home: Scaffold(body: child), + ), + ), + ); + + final repaintBoundary = RenderRepaintBoundary(); + final platformDispatcher = WidgetsBinding.instance.platformDispatcher; + + final view = View.maybeOf(context) ?? platformDispatcher.views.first; + final logicalSize = + targetSize ?? view.physicalSize / view.devicePixelRatio; + + int retryCount = 10; + bool isDirty = false; + + final renderView = RenderView( + view: view, + child: RenderPositionedBox( + alignment: Alignment.center, + child: repaintBoundary, + ), + configuration: ViewConfiguration( + size: logicalSize, + devicePixelRatio: pixelRatio, + ), + ); + + final pipelineOwner = PipelineOwner( + onNeedVisualUpdate: () { + isDirty = true; + }, + ); + + final buildOwner = BuildOwner( + focusManager: FocusManager(), + onBuildScheduled: () { + isDirty = true; + log('Build scheduled'); + }, + ); + + pipelineOwner.rootNode = renderView; + renderView.prepareInitialFrame(); + + final rootElement = RenderObjectToWidgetAdapter( + container: repaintBoundary, + child: Directionality( + textDirection: TextDirection.ltr, + child: child, + ), + ).attachToRenderTree(buildOwner); + + while (retryCount > 0) { + isDirty = false; + buildOwner.buildScope(rootElement); + buildOwner.finalizeTree(); + pipelineOwner.flushLayout(); + pipelineOwner.flushCompositingBits(); + pipelineOwner.flushPaint(); + + await Future.delayed(const Duration(milliseconds: 250)); + + if (!isDirty) { + print('completed; '); + break; + } + + print('retrying... '); + + retryCount--; + } + + final image = await repaintBoundary.toImage(pixelRatio: pixelRatio); + + buildOwner.finalizeTree(); + + return image; + } catch (e) { + log('Error finalizing tree: $e'); + rethrow; + } + } +} diff --git a/lib/services/mermaid_service.dart b/lib/services/mermaid_service.dart new file mode 100644 index 0000000..a723038 --- /dev/null +++ b/lib/services/mermaid_service.dart @@ -0,0 +1,65 @@ +import 'dart:developer'; +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as p; + +const mermaidService = MermaidService.instance; + +class MermaidService { + const MermaidService._(); + + static const instance = MermaidService._(); + + Future generateImage(String mermaidSyntax) async { + final fileName = mermaidSyntax.hashCode; + + final tempDir = Directory('.tmp_superdeck'); + + final tempFile = File(p.join(tempDir.path, '$fileName.mmd')); + final outputFile = File(p.join(tempDir.path, '$fileName.png')); + + if (!await tempDir.exists()) { + await tempDir.create(recursive: true); + } + + try { + mermaidSyntax = mermaidSyntax.trim().replaceAll(r'\n', '\n'); + + await tempFile.writeAsString(mermaidSyntax); + + final imageSizeParams = '--scale 2'.split(' '); + final params = + '-t dark -b transparent -i ${tempFile.path} -o ${outputFile.path} ' + .split(' '); + + // Check if can execute mmdc before executing command + final mmdcResult = await Process.run('mmdc', ['--version']); + + if (mmdcResult.exitCode != 0) { + log( + '"mmdc" not found. You need mermaid cli installed to process mermaid syntax', + ); + + return null; + } + + final result = await Process.run('mmdc', [...params, ...imageSizeParams]); + + if (result.exitCode != 0) { + log('Error while processing mermaid syntax'); + log(result.stderr); + return null; + } + + return outputFile.readAsBytes(); + } catch (e) { + log('Error while processing mermaid syntax: $e'); + return null; + } finally { + if (await tempDir.exists()) { + await tempDir.delete(recursive: true); + } + } + } +} diff --git a/lib/styles/style_util.dart b/lib/styles/style_util.dart index 4dd14cb..9b09d9e 100644 --- a/lib/styles/style_util.dart +++ b/lib/styles/style_util.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:markdown_viewer/markdown_viewer.dart'; +import 'package:mix/mix.dart'; import '../superdeck.dart'; diff --git a/lib/superdeck.dart b/lib/superdeck.dart index a4b4524..60e970e 100644 --- a/lib/superdeck.dart +++ b/lib/superdeck.dart @@ -2,6 +2,10 @@ library superdeck; export 'package:mix/mix.dart'; export 'package:superdeck/components/superdeck_app.dart'; +export 'package:superdeck/models/asset_model.dart'; +export 'package:superdeck/models/options_model.dart'; +export 'package:superdeck/models/slide_model.dart'; + export 'package:superdeck/providers/deck_provider.dart'; export 'package:superdeck/styles/style_attribute.dart'; export 'package:superdeck/styles/style_dto.dart'; diff --git a/test/models/syntax_tag_test.dart b/test/models/syntax_tag_test.dart index 7dd1b9f..4f6bb2c 100644 --- a/test/models/syntax_tag_test.dart +++ b/test/models/syntax_tag_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:superdeck/models/syntax_tag.dart'; +import 'package:superdeck/helpers/section_tag.dart'; void main() { group('Tag Detection Tests', () { @@ -19,13 +19,13 @@ Description of content - Third bullet point '''; final expected = { - Section.left: ''' + SectionTag.left: ''' ## Content One Description of content ''', - Section.right: ''' + SectionTag.right: ''' #### Content Two @@ -46,7 +46,7 @@ Description of content Description of content '''; final expected = { - Section.first: ''' + SectionTag.first: ''' ## Content One Description of content ''' @@ -68,12 +68,12 @@ Description of content - Third bullet point '''; final expected = { - Section.first: ''' + SectionTag.first: ''' ## Content One Description of content ''', - Section.right: ''' + SectionTag.right: ''' #### Content Two @@ -99,12 +99,12 @@ Description of content - Third bullet point '''; final expected = { - Section.first: ''' + SectionTag.first: ''' ## Content One Description of content ''', - Section.left: ''' + SectionTag.left: ''' #### Content Two