diff --git a/.github/workflows/push_check_analyze.yml b/.github/workflows/push_check_analyze.yml new file mode 100644 index 0000000..abe74ab --- /dev/null +++ b/.github/workflows/push_check_analyze.yml @@ -0,0 +1,67 @@ +name: Push Analyze Check + +on: + push: + branches: + - '**' + +jobs: + push_check_analyze: + + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: '18.x' + + - name: Install Landa Messenger CLI + run: npm install @landamessenger/landa-messenger-api -g + + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + flutter-version: '3.24.3' + + - run: flutter pub get + + - run: flutter analyze + + - name: Handle job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Analysis Failed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check_analyze.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Analysis Canceled" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check_analyze.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Analysis Passed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check_analyze.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + diff --git a/.github/workflows/push_check_publish_dry_run.yml b/.github/workflows/push_check_publish_dry_run.yml new file mode 100644 index 0000000..ac126fd --- /dev/null +++ b/.github/workflows/push_check_publish_dry_run.yml @@ -0,0 +1,67 @@ +name: Push Publish Dry Run Check + +on: + push: + branches: + - '**' + +jobs: + push_check_publish_dry_run: + + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: '18.x' + + - name: Install Landa Messenger CLI + run: npm install @landamessenger/landa-messenger-api -g + + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + flutter-version: '3.24.3' + + - run: flutter pub get + + - run: dart pub publish --dry-run + + - name: Handle job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Dry Publish Failed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check_publish_dry_run.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Dry Publish Canceled" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check_publish_dry_run.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Dry Publish Passed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check_publish_dry_run.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + diff --git a/.github/workflows/push_check_test.yml b/.github/workflows/push_check_test.yml new file mode 100644 index 0000000..bb764c0 --- /dev/null +++ b/.github/workflows/push_check_test.yml @@ -0,0 +1,73 @@ +name: Push Test Check + +on: + push: + branches: + - '**' + +jobs: + push_check_test: + + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: '18.x' + + - name: Install Landa Messenger CLI + run: npm install @landamessenger/landa-messenger-api -g + + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + flutter-version: '3.24.3' + + - run: flutter pub get + + - run: flutter test --coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: coverage/lcov.info + + - name: Handle job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Test Failed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check_test.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Test Canceled" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Test Passed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/push_check.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + diff --git a/.github/workflows/tag_version_and_publish.yml b/.github/workflows/tag_version_and_publish.yml new file mode 100644 index 0000000..92192dd --- /dev/null +++ b/.github/workflows/tag_version_and_publish.yml @@ -0,0 +1,116 @@ +name: Tag Version and Publish on Push to Master + +on: + push: + branches: + - master + +jobs: + tag_version_and_publish: + + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Read version from pubspec.yml + id: read_version + run: | + VERSION=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2) + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Create tag + id: create_tag + run: | + # Checks if the tag already exists in the remote repository + if git rev-parse "v${{ env.VERSION }}" >/dev/null 2>&1; then + echo "Error: Tag v${{ env.VERSION }} already exists." + exit 1 + fi + + # Check if the version was found + if [ -z "${{ env.VERSION }}" ]; then + echo "Error: No version found in pubspec.yml" + exit 1 + fi + + git tag "v${{ env.VERSION }}" + git push origin "v${{ env.VERSION }}" + + - name: Handle job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Creation Tag Failed" \ + --body "${{ github.repository }}: Tag v${{ env.VERSION }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Creation Tag Canceled" \ + --body "${{ github.repository }}: Tag v${{ env.VERSION }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Creation Tag Passed" \ + --body "${{ github.repository }}: Tag v${{ env.VERSION }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + + - run: flutter pub get + + - run: dart pub publish --dry-run + + - run: dart pub publish -f + + - name: Handle publish job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Pub Publish Failed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Pub Publish Canceled" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Pub Publish Passed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/landamessenger/catalog/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + + diff --git a/.gitignore b/.gitignore index 970b7b4..ba89cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ *.lock .dart_tool .DS_Store -.idea/ \ No newline at end of file +.idea/ + +/build \ No newline at end of file diff --git a/.pubignore b/.pubignore index d75cb98..3fe9667 100644 --- a/.pubignore +++ b/.pubignore @@ -3,4 +3,9 @@ widget_preview.iml *.lock .dart_tool .DS_Store -.idea/ \ No newline at end of file +.idea/ + +/lib/.DS_Store +/lib/src/.DS_Store +/example/lib/.DS_Store +/example/lib/src/.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index caf2700..810ac8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -## 1.0.6 +## 2.0.0 -* Dependencies updated. +* Added `test` and `integration_test` generation. ## 1.0.5 diff --git a/README.md b/README.md index d847416..283bac5 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,16 @@ This package allows you to create a widget catalog and all kinds of screenshots. [- What are dummies?](https://github.com/landamessenger/catalog/wiki/Dummies#what-are-dummies) -[- Complex page catalog](https://github.com/landamessenger/catalog/wiki/Dummies#complex-page-catalog) - [- Prepare dummies](https://github.com/landamessenger/catalog/wiki/Dummies#prepare-dummies) +### [Test](https://github.com/landamessenger/catalog/wiki/Test) + +[- Tests](https://github.com/landamessenger/catalog/wiki/Test#tests) + +[- Integration Tests](https://github.com/landamessenger/catalog/wiki/Test#integration-tests) + +[- Sample](https://github.com/landamessenger/catalog/wiki/Test#sample) + ### [Build & Run](https://github.com/landamessenger/catalog/wiki/Build-&-Run) [- Building your catalog](https://github.com/landamessenger/catalog/wiki/Build-&-Run#building-your-catalog) @@ -52,4 +58,3 @@ This package allows you to create a widget catalog and all kinds of screenshots. [- Samples](https://github.com/landamessenger/catalog/wiki/Screenshots#samples) ### [Catalog Sample](https://landamessenger.com/catalog) - diff --git a/analysis_options.yaml b/analysis_options.yaml index 9d8c40b..a5d702a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,3 +1,7 @@ +analyzer: + errors: + dangling_library_doc_comments: ignore + include: package:flutter_lints/flutter.yaml linter: diff --git a/bin/build.dart b/bin/build.dart index 6e78b3b..a5a2113 100644 --- a/bin/build.dart +++ b/bin/build.dart @@ -1,10 +1,10 @@ -import 'tasks/main_task.dart'; -import 'utils/configuration.dart'; +import 'package:catalog/src/bin/tasks/main_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; const kDebugMode = true; void main(List arguments) async { - var dependencies = loadDependenciesFile(); + var dependencies = loadDependenciesFile(''); print(introMessage(dependencies['catalog'].toString())); - await MainTask().work(); + await MainTask().work([]); } diff --git a/bin/catalog_builder/catalog_builder.dart b/bin/catalog_builder/catalog_builder.dart deleted file mode 100644 index db7484c..0000000 --- a/bin/catalog_builder/catalog_builder.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'dart:io'; - -import 'package:catalog/src/annotations/preview.dart'; -import 'package:catalog/src/builders/catalog/built_component.dart'; -import 'package:catalog/src/builders/catalog/component_node.dart'; -import 'package:catalog/src/extensions/string_ext.dart'; - -Future findPreviewClassName(String path) async { - try { - File file = File(path); - final content = await file.readAsString(); - return '${content.split("class ")[1].split(" extends ParentPreviewWidget").first.trim()}()'; - } catch (e) { - print(e); - return null; - } -} - -Future createPage( - String appId, - String base, - String outputPath, - String outputFile, - String prefix, - Preview preview, - String import, - String name, -) async { - try { - var directory = Directory(outputPath); - await directory.create(recursive: true); - var id = preview.path.contains('/') - ? preview.path.split('/').last - : preview.path; - File file = File(outputPath + outputFile.replaceAll('.$prefix.', '.')); - - final clazzName = name.replaceAll('()', ''); - final pageClass = '${clazzName}PreviewPageDummy'; - - print( - '📃 Generating Catalog page for $clazzName - $pageClass (${file.path})'); - - var content = ''' -/// AUTOGENERATED FILE. DO NOT EDIT - -import 'package:flutter/material.dart'; -import '$import'; - -class $pageClass extends StatefulWidget { - - static String routeName = '$id'; - - const $pageClass({super.key}); - - @override - ${pageClass}State createState() => ${pageClass}State(); -} - -class ${pageClass}State extends State<$pageClass> { - @override - Widget build(BuildContext context) { - return const $clazzName(); - } -} - '''; - file.writeAsStringSync(content); - - var p = file.path.split(base)[1]; - var package = 'package:$appId$p'; - - return BuiltComponent( - path: file.path, - route: preview.path, - package: package, - clazzName: '${name.replaceAll('()', '')}PreviewPageDummy', - preview: preview, - ); - } catch (e) { - print(e); - return null; - } -} - -Future buildMiddlePages( - String appId, - String base, - String outputPath, - String outputFile, - String import, - String name, - String path, - String pageRoute, - List children, -) async { - try { - var directory = Directory(outputPath); - await directory.create(recursive: true); - File file = File('$outputPath/$outputFile'); - - final clazz = name.replaceAll('()', '').split('_').map((e) => e.capitalize()).join(); - final clazzName = '${clazz}PreviewPageDummy'; - - var content = ''' -/// AUTOGENERATED FILE. DO NOT EDIT - -import 'package:catalog/catalog.dart'; -import 'package:flutter/material.dart'; - -class $clazzName extends StatefulWidget { - - static String routeName = '${name.toLowerCase()}'; - - const $clazzName({super.key}); - - @override - ${clazzName}State createState() => ${clazzName}State(); -} - -class ${clazzName}State extends State<$clazzName> { - @override - Widget build(BuildContext context) { - return PreviewScaffold( - child: ListView( - children: [ - '''; - - for (ComponentNode node in children) { - content += ''' - ListTile( - title: const Text( - '${node.id}', - style: TextStyle( - color: Colors.black, - fontSize: 16, - letterSpacing: .3, - ), - ), - onTap: () { - context.go('/$pageRoute/$path/${node.id}'); - }, - ), - '''; - } - - content += ''' - ], - ), - ); - } -} - - '''; - file.writeAsStringSync(content); - - var p = file.path.split(base)[1]; - var package = 'package:$appId$p'; - - return BuiltComponent( - path: file.path, - route: path, - package: package, - clazzName: clazzName, - preview: null, - ); - } catch (e) { - return null; - } -} - -Future buildChildrenPages( - dynamic config, - String appId, - ComponentNode node, - String path, - String pageRoute, - int level, -) async { - try { - ComponentNode n = node; - String p = path; - if (level == 0) { - n.builtComponent = BuiltComponent(); - n.builtComponent!.path = - './${config['base']}/${config['output']}/${config['pageFile']}'; - n.builtComponent!.route = ''; - n.builtComponent!.clazzName = 'CatalogComponent'; - - final File file = File(n.builtComponent!.path); - var fp = file.path.split(config['base'])[1]; - n.builtComponent!.package = 'package:$appId$fp'; - - for (var entry in n.children.entries) { - var f = await buildChildrenPages( - config, - appId, - entry.value, - p, - pageRoute, - level + 1, - ); - if (f == null) continue; - n.children[entry.key] = f; - } - return n; - } - - if (n.builtComponent != null) { - return n; - } - - var current = n.id; - - if (p.isEmpty) { - p += current; - } else { - p += '/$current'; - } - - // print('Generating middle page: $current'); - var middleFolder = './${config['base']}/${config['output']}/$p'; - n.builtComponent = await buildMiddlePages( - appId, - config['base'], - middleFolder, - '${current.toLowerCase()}_preview_page_dummy.dart', - '', - current.capitalize(), - p, - pageRoute, - n.childrenList, - ); - - n.builtComponent!.route = p; - - for (var entry in n.children.entries) { - var f = await buildChildrenPages( - config, - appId, - entry.value, - p, - pageRoute, - level + 1, - ); - if (f == null) continue; - n.children[entry.key] = f; - } - return n; - } catch (e) { - return null; - } -} - -ComponentNode getNodesFrom( - String pageRoute, - Map components, -) { - var firstNode = ComponentNode(id: pageRoute, route: '/'); - - for (var entity in components.values.toList()) { - var parts = entity.route.split('/'); - if (parts.first == '.') { - parts.removeAt(0); - } - addNode(firstNode, parts, 0, entity); - } - - return firstNode; -} - -void addNode( - ComponentNode node, - List parts, - int index, - BuiltComponent component, -) { - if (index >= parts.length) { - return; - } - ComponentNode? no; - for (var n in node.children.values.toList()) { - if (n.id == parts[index]) { - no = n; - break; - } - } - - if (no == null) { - no = ComponentNode( - id: parts[index], - route: '/${parts[index]}', - builtComponent: parts.length - 1 == index ? component : null, - ); - // print('adding: ${no.id}'); - // print(' ${no.builtComponent?.preview?.path}'); - node.children[no.id] = no; - } - - if (index < parts.length) { - addNode(no, parts, index + 1, component); - } -} diff --git a/bin/integration_test.dart b/bin/integration_test.dart new file mode 100644 index 0000000..259287e --- /dev/null +++ b/bin/integration_test.dart @@ -0,0 +1,8 @@ +import 'package:catalog/src/bin/tasks/integration_test_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; + +void main(List arguments) async { + var dependencies = loadDependenciesFile(''); + print(introMessage(dependencies['catalog'].toString())); + await IntegrationTestTask().work([]); +} diff --git a/bin/preview.dart b/bin/preview.dart index 229ea40..f9981e0 100644 --- a/bin/preview.dart +++ b/bin/preview.dart @@ -1,8 +1,8 @@ -import 'tasks/preview_task.dart'; -import 'utils/configuration.dart'; +import 'package:catalog/src/bin/tasks/preview_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; void main(List arguments) async { - var dependencies = loadDependenciesFile(); + var dependencies = loadDependenciesFile(''); print(introMessage(dependencies['catalog'].toString())); - await PreviewTask().work(); + await PreviewTask().work([]); } diff --git a/bin/screenshots.dart b/bin/screenshots.dart index 2528bae..eb2a5df 100644 --- a/bin/screenshots.dart +++ b/bin/screenshots.dart @@ -1,8 +1,8 @@ -import 'tasks/tasks/server_task.dart'; -import 'utils/configuration.dart'; +import 'package:catalog/src/bin/tasks/tasks/server_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; void main(List arguments) async { - var dependencies = loadDependenciesFile(); + var dependencies = loadDependenciesFile(''); print(introMessage(dependencies['catalog'].toString())); - await ServerTask().work(); + await ServerTask().work([]); } diff --git a/bin/tasks/base/base_task.dart b/bin/tasks/base/base_task.dart deleted file mode 100644 index fefd2f0..0000000 --- a/bin/tasks/base/base_task.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract class BaseTask { - Future work(); -} diff --git a/bin/tasks/tasks/catalog_task.dart b/bin/tasks/tasks/catalog_task.dart deleted file mode 100644 index 5fd0f6e..0000000 --- a/bin/tasks/tasks/catalog_task.dart +++ /dev/null @@ -1,216 +0,0 @@ -import 'dart:io'; - -import 'package:catalog/src/base/serial.dart'; -import 'package:catalog/src/builders/catalog/built_component.dart'; -import 'package:catalog/src/builders/catalog/component_node.dart'; - -import '../../catalog_builder/catalog_builder.dart'; -import '../../utils/configuration.dart'; -import '../base/base_task.dart'; - -class CatalogTask extends BaseTask { - @override - Future work() async { - var appId = loadId(); - var config = loadConfigFile(); - - final page = config['pageFile'] ?? 'catalog_component.dart'; - final pageName = config['pageName'] ?? 'CatalogComponent'; - final pageRoute = config['pageRoute'] ?? 'catalog'; - final prefixValue = config['prefix'] ?? 'preview'; - - final dir = Directory('./${config['base']}'); - await dir.create(recursive: true); - - final dirOutPut = Directory('./${config['base']}/${config['output']}'); - await dirOutPut.create(recursive: true); - - final List entities = - await dir.list(recursive: true).toList(); - - final files = []; - - for (FileSystemEntity fileSystemEntity in entities) { - if (fileSystemEntity.path.contains('.$prefixValue.')) { - files.add(fileSystemEntity); - } - } - - var map = {}; - - for (FileSystemEntity fileSystemEntity in files) { - final File file = File(fileSystemEntity.path); - var p = file.path.split(config['base'])[1]; - var package = 'package:$appId$p'; - var className = await findPreviewClassName(file.path); - - if (className == null) { - print('No class name found ${file.path}'); - continue; - } - //print('- Building preview of $className in $package'); - - var preview = await previewOnFile( - config, - file.path, - ); - - if (preview == null) { - print('No preview data found ${file.path}'); - continue; - } - - // print('Preview: ${jsonEncode(preview.toJson())}'); - - var outputFolder = - './${config['base']}/${config['output']}/${preview.path}/'; - - var build = await createPage( - appId, - config['base'], - outputFolder, - p.split('/').last, - prefixValue, - preview, - package, - className, - ); - - if (build?.preview == null) { - print('No build for $className'); - continue; - } - // print('Built: ${jsonEncode(build!.toJson())}'); - - map[build!.preview!.path] = build; - } - - ComponentNode? node = getNodesFrom(pageRoute, map); - - node = await buildChildrenPages( - config, - appId, - node, - '', - pageRoute, - 0, - ); - - if (node == null) { - return; - } - - // print(node.toJson().toPrettyString()); - - final File assetsConfig = File('./${config['runtimeConfigHolder']}'); - assetsConfig.writeAsStringSync(node.toJson().toPrettyString()); - - final File catalogFile = - File('./${config['base']}/${config['output']}/$page'); - - // print(node.routerBuilder); - - var catalogContent = ''' -/// AUTOGENERATED FILE. DO NOT EDIT - -import 'package:flutter/material.dart'; -import 'package:catalog/catalog.dart'; -${node.imports} - -class $pageName extends StatefulWidget { - static String routeName = '/$pageRoute'; - static GoRoute route = ${node.routerBuilder}; - const $pageName({super.key}); - - @override - ${pageName}State createState() => ${pageName}State(); -} - -class ${pageName}State extends State<$pageName> { - - TreeController? treeController; - - - @override - Widget build(BuildContext context) { - return PreviewScaffold( - onBackPressed: Catalog().onBackPressed, - child: FutureBuilder( - initialData: null, - future: Catalog().get(context), - builder: (context, data) { - if (!data.hasData || data.data == null) { - return Container(); - } - final node = data.data as ComponentNode; - if (treeController == null) { - treeController = TreeController( - roots: [node], - childrenProvider: (ComponentNode node) => node.children.values, - ); - if (treeController!.isTreeCollapsed) { - treeController!.expandAll(); - } - } - return AnimatedTreeView( - treeController: treeController!, - nodeBuilder: - (BuildContext context, TreeEntry entry) { - return InkWell( - onTap: () { - // _nodePressed(node); - }, - child: TreeIndentation( - entry: entry, - child: Row( - children: [ - FolderButton( - color: Colors.black, - isOpen: entry.hasChildren ? entry.isExpanded : null, - onPressed: () => _nodePressed(entry), - ), - Text( - entry.node.id, - style: const TextStyle( - color: Colors.black, - fontSize: 16, - letterSpacing: .3, - ), - ), - ], - ), - ), - ); - }, - ); - }), - ); - } - - void _nodePressed(TreeEntry entry) { - if (entry.node.children.isEmpty) { - if (entry.node.builtComponent?.preview?.path != null) { - context.go( - '\${$pageName.routeName}/\${entry.node.builtComponent!.preview!.path}'); - } - } else { - if (!entry.isExpanded) { - treeController?.toggleExpansion(entry.node); - } else { - treeController?.collapse(entry.node); - } - } - } -} - - ''' - .replaceAll('"""', '\'\'\'') - .replaceAll(' []', ' const []'); - - catalogFile.writeAsStringSync(catalogContent); - - final File file = - File('./${config['base']}/${config['output']}/process.dart'); - if (file.existsSync()) await file.delete(); - } -} diff --git a/bin/tasks/tasks/format_task.dart b/bin/tasks/tasks/format_task.dart deleted file mode 100644 index 4ea7563..0000000 --- a/bin/tasks/tasks/format_task.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'dart:io'; - -import '../base/base_task.dart'; - -class FormatTask extends BaseTask { - @override - Future work() async { - var result = await Process.run( - 'dart', - ['format', 'lib/'], - workingDirectory: Directory.current.path, - ); - stdout.write(result.stdout); - stderr.write(result.stderr); - } -} diff --git a/bin/test.dart b/bin/test.dart new file mode 100644 index 0000000..a19721d --- /dev/null +++ b/bin/test.dart @@ -0,0 +1,8 @@ +import 'package:catalog/src/bin/tasks/test_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; + +void main(List arguments) async { + var dependencies = loadDependenciesFile(''); + print(introMessage(dependencies['catalog'].toString())); + await TestTask().work([]); +} diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 0d29021..92bb9a7 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -7,6 +7,9 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + errors: + dangling_library_doc_comments: ignore include: package:flutter_lints/flutter.yaml linter: diff --git a/example/assets/preview_config.json b/example/assets/preview_config.json index 06c2657..d710b33 100644 --- a/example/assets/preview_config.json +++ b/example/assets/preview_config.json @@ -1,113 +1,134 @@ { "id": "catalog", "route": "/", - "builtComponent": { - "path": "./lib/catalog/catalog_component.dart", - "route": "", - "package": "package:example/catalog/catalog_component.dart", - "clazzName": "CatalogComponent", - "preview": null - }, + "builtComponents": {}, "children": { "widgets": { "id": "widgets", - "route": "/widgets", - "builtComponent": { - "path": "./lib/catalog/widgets/widgets_preview_page_dummy.dart", - "route": "widgets", - "package": "package:example/catalog/widgets/widgets_preview_page_dummy.dart", - "clazzName": "WidgetsPreviewPageDummy", - "preview": null + "route": "widgets", + "builtComponents": { + "./lib/catalog/widgets/main_screen.dart": { + "path": "./lib/catalog/widgets/main_screen.dart", + "route": "widgets", + "package": "package:example/catalog/widgets/main_screen.dart", + "clazzName": "MainScreenPreviewPreviewPageDummy", + "preview": { + "id": "main_screen", + "path": "widgets", + "description": "", + "parameters": [ + "title", + "infoText", + "counter", + "incrementCounter" + ] + } + } }, "children": { - "body_widget": { - "id": "body_widget", - "route": "/body_widget", - "builtComponent": { - "path": "./lib/catalog/widgets/body_widget/body_widget.dart", - "route": "widgets/body_widget", - "package": "package:example/catalog/widgets/body_widget/body_widget.dart", - "clazzName": "BodyWidgetPreviewPreviewPageDummy", - "preview": { - "id": "BodyWidgetPreview", - "path": "widgets/body_widget", - "description": "", - "usesDummies": true, - "listParameters": [], - "dummyParameters": [ - "infoText", - "counter" - ], - "parameters": {} + "utils": { + "id": "utils", + "route": "widgets/utils", + "builtComponents": {}, + "children": { + "bottom": { + "id": "bottom", + "route": "widgets/utils/bottom", + "builtComponents": { + "./lib/catalog/widgets/utils/bottom/fab_widget.dart": { + "path": "./lib/catalog/widgets/utils/bottom/fab_widget.dart", + "route": "widgets/utils/bottom", + "package": "package:example/catalog/widgets/utils/bottom/fab_widget.dart", + "clazzName": "FabWidgetPreviewPreviewPageDummy", + "preview": { + "id": "fab_widget", + "path": "widgets/utils/bottom", + "description": "Basic fab widget", + "parameters": [ + "incrementCounter" + ] + } + } + }, + "children": {} } - }, - "children": {} + } }, - "fab_widget": { - "id": "fab_widget", - "route": "/fab_widget", - "builtComponent": { - "path": "./lib/catalog/widgets/fab_widget/fab_widget.dart", - "route": "widgets/fab_widget", - "package": "package:example/catalog/widgets/fab_widget/fab_widget.dart", - "clazzName": "FabWidgetPreviewPreviewPageDummy", - "preview": { - "id": "FabWidgetPreview", - "path": "widgets/fab_widget", - "description": "Basic fab widget", - "usesDummies": false, - "listParameters": [], - "dummyParameters": [], - "parameters": { - "incrementCounter": "void_function_snackbar" - } + "other_utils": { + "id": "other_utils", + "route": "widgets/other_utils", + "builtComponents": {}, + "children": { + "bottom": { + "id": "bottom", + "route": "widgets/other_utils/bottom", + "builtComponents": { + "./lib/catalog/widgets/other_utils/bottom/warning_info_widget.dart": { + "path": "./lib/catalog/widgets/other_utils/bottom/warning_info_widget.dart", + "route": "widgets/other_utils/bottom", + "package": "package:example/catalog/widgets/other_utils/bottom/warning_info_widget.dart", + "clazzName": "WarningInfoWidgetPreviewPreviewPageDummy", + "preview": { + "id": "warning_info_widget", + "path": "widgets/other_utils/bottom", + "description": "Basic warning info text for alert", + "parameters": [ + "infoText" + ] + } + } + }, + "children": {} } - }, - "children": {} + } }, - "main_screen_widget": { - "id": "main_screen_widget", - "route": "/main_screen_widget", - "builtComponent": { - "path": "./lib/catalog/widgets/main_screen_widget/main_screen.dart", - "route": "widgets/main_screen_widget", - "package": "package:example/catalog/widgets/main_screen_widget/main_screen.dart", - "clazzName": "MainScreenPreviewPreviewPageDummy", - "preview": { - "id": "MainScreenPreview", - "path": "widgets/main_screen_widget", - "description": "", - "usesDummies": true, - "listParameters": [], - "dummyParameters": [ - "title", - "infoText", - "counter", - "incrementCounter" - ], - "parameters": {} - } - }, - "children": {} - }, - "counter_widget": { - "id": "counter_widget", - "route": "/counter_widget", - "builtComponent": { - "path": "./lib/catalog/widgets/counter_widget/counter_widget.dart", - "route": "widgets/counter_widget", - "package": "package:example/catalog/widgets/counter_widget/counter_widget.dart", - "clazzName": "CounterWidgetPreviewPreviewPageDummy", - "preview": { - "id": "CounterWidgetPreview", - "path": "widgets/counter_widget", - "description": "Basic counter widget", - "usesDummies": true, - "listParameters": [], - "dummyParameters": [ - "counter" - ], - "parameters": {} + "screen": { + "id": "screen", + "route": "widgets/screen", + "builtComponents": { + "./lib/catalog/widgets/screen/body_widget.dart": { + "path": "./lib/catalog/widgets/screen/body_widget.dart", + "route": "widgets/screen", + "package": "package:example/catalog/widgets/screen/body_widget.dart", + "clazzName": "BodyWidgetPreviewPreviewPageDummy", + "preview": { + "id": "body_widget", + "path": "widgets/screen", + "description": "", + "parameters": [ + "infoText", + "counter" + ] + } + }, + "./lib/catalog/widgets/screen/sized_container.dart": { + "path": "./lib/catalog/widgets/screen/sized_container.dart", + "route": "widgets/screen", + "package": "package:example/catalog/widgets/screen/sized_container.dart", + "clazzName": "SizedContainerPreviewPreviewPageDummy", + "preview": { + "id": "sized_container", + "path": "widgets/screen", + "description": "Container with a max width", + "parameters": [ + "width", + "child" + ] + } + }, + "./lib/catalog/widgets/screen/counter_widget.dart": { + "path": "./lib/catalog/widgets/screen/counter_widget.dart", + "route": "widgets/screen", + "package": "package:example/catalog/widgets/screen/counter_widget.dart", + "clazzName": "CounterWidgetPreviewPreviewPageDummy", + "preview": { + "id": "counter_widget", + "path": "widgets/screen", + "description": "Basic counter widget", + "parameters": [ + "counter" + ] + } } }, "children": {} diff --git a/example/integration_test/catalog_widget_integration_test.dart b/example/integration_test/catalog_widget_integration_test.dart new file mode 100644 index 0000000..c34e55d --- /dev/null +++ b/example/integration_test/catalog_widget_integration_test.dart @@ -0,0 +1,33 @@ +/// AUTOGENERATED FILE. DO NOT EDIT + +/// Launch on Android or iOS as usual. +/// Launch on Web with: +/// +/// chromedriver --port=4444 +/// flutter drive --driver=test_driver/integration_test.dart --target=integration_test/catalog_widget_integration_test.dart -d chrome + +import 'package:integration_test/integration_test.dart'; + +import 'package:example/widgets/utils/bottom/catalog/integration_test/fab_widget_integration_test.dart' + as akcl; +import 'package:example/widgets/other_utils/bottom/catalog/integration_test/warning_info_widget_integration_test.dart' + as gjrq; +import 'package:example/widgets/screen/catalog/integration_test/sized_container_integration_test.dart' + as pvar; +import 'package:example/widgets/screen/catalog/integration_test/body_widget_integration_test.dart' + as utcx; +import 'package:example/widgets/screen/catalog/integration_test/counter_widget_integration_test.dart' + as ujyk; +import 'package:example/widgets/catalog/integration_test/main_screen_integration_test.dart' + as sqbo; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + akcl.FabWidgetIntegrationTest().main(); + gjrq.WarningInfoWidgetIntegrationTest().main(); + pvar.SizedContainerIntegrationTest().main(); + utcx.BodyWidgetIntegrationTest().main(); + ujyk.CounterWidgetIntegrationTest().main(); + sqbo.MainScreenIntegrationTest().main(); +} diff --git a/example/lib/catalog/README.md b/example/lib/catalog/README.md new file mode 100644 index 0000000..08a8dcc --- /dev/null +++ b/example/lib/catalog/README.md @@ -0,0 +1,18 @@ +# Catalog in example + +This is your catalog in example. It shows the widgets that contain `@Preview` in the header. + +You should not manipulate it yourself. If you observe any unexpected behavior please [open an issue on Github](https://github.com/landamessenger/catalog/issues). We will try to fix it as soon as possible. + +Generate dummies, previews (override every time) with: + +```bash +dart run catalog:preview +``` + +All the above plus catalog generation. + +```bash +dart run catalog:build +``` + \ No newline at end of file diff --git a/example/lib/catalog/catalog_component.dart b/example/lib/catalog/catalog_component.dart index a5c0cc3..cee5377 100644 --- a/example/lib/catalog/catalog_component.dart +++ b/example/lib/catalog/catalog_component.dart @@ -2,14 +2,20 @@ import 'package:flutter/material.dart'; import 'package:catalog/catalog.dart'; -import 'package:example/catalog/widgets/widgets_preview_page_dummy.dart'; -import 'package:example/catalog/widgets/body_widget/body_widget.dart'; -import 'package:example/catalog/widgets/fab_widget/fab_widget.dart'; -import 'package:example/catalog/widgets/main_screen_widget/main_screen.dart'; -import 'package:example/catalog/widgets/counter_widget/counter_widget.dart'; + +import 'package:example/catalog/widgets/utils/bottom/fab_widget.dart'; + +import 'package:example/catalog/widgets/other_utils/bottom/warning_info_widget.dart'; + +import 'package:example/catalog/widgets/screen/body_widget.dart'; +import 'package:example/catalog/widgets/screen/sized_container.dart'; +import 'package:example/catalog/widgets/screen/counter_widget.dart'; + +import 'package:example/catalog/widgets/main_screen.dart'; class CatalogComponent extends StatefulWidget { static String routeName = '/catalog'; + static GoRoute route = GoRoute( path: CatalogComponent.routeName, pageBuilder: (context, state) => NoTransitionPage( @@ -18,48 +24,104 @@ class CatalogComponent extends StatefulWidget { ), routes: [ GoRoute( - path: WidgetsPreviewPageDummy.routeName, - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const WidgetsPreviewPageDummy(), - ), + path: 'widgets', + redirect: (context, state) { + if (state.fullPath != state.matchedLocation) return null; + return CatalogComponent.routeName; + }, routes: [ GoRoute( - path: BodyWidgetPreviewPreviewPageDummy.routeName, + path: MainScreenPreviewPreviewPageDummy.routeName, pageBuilder: (context, state) => NoTransitionPage( key: state.pageKey, - child: const BodyWidgetPreviewPreviewPageDummy(), + child: const MainScreenPreviewPreviewPageDummy(), ), - routes: const [], ), GoRoute( - path: FabWidgetPreviewPreviewPageDummy.routeName, - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const FabWidgetPreviewPreviewPageDummy(), - ), - routes: const [], + path: 'utils', + redirect: (context, state) { + if (state.fullPath != state.matchedLocation) return null; + return CatalogComponent.routeName; + }, + routes: [ + GoRoute( + path: 'bottom', + redirect: (context, state) { + if (state.fullPath != state.matchedLocation) return null; + return CatalogComponent.routeName; + }, + routes: [ + GoRoute( + path: FabWidgetPreviewPreviewPageDummy.routeName, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const FabWidgetPreviewPreviewPageDummy(), + ), + ), + ], + ) + ], ), GoRoute( - path: MainScreenPreviewPreviewPageDummy.routeName, - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const MainScreenPreviewPreviewPageDummy(), - ), - routes: const [], + path: 'other_utils', + redirect: (context, state) { + if (state.fullPath != state.matchedLocation) return null; + return CatalogComponent.routeName; + }, + routes: [ + GoRoute( + path: 'bottom', + redirect: (context, state) { + if (state.fullPath != state.matchedLocation) return null; + return CatalogComponent.routeName; + }, + routes: [ + GoRoute( + path: WarningInfoWidgetPreviewPreviewPageDummy.routeName, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const WarningInfoWidgetPreviewPreviewPageDummy(), + ), + ), + ], + ) + ], ), GoRoute( - path: CounterWidgetPreviewPreviewPageDummy.routeName, - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const CounterWidgetPreviewPreviewPageDummy(), - ), - routes: const [], + path: 'screen', + redirect: (context, state) { + if (state.fullPath != state.matchedLocation) return null; + return CatalogComponent.routeName; + }, + routes: [ + GoRoute( + path: BodyWidgetPreviewPreviewPageDummy.routeName, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const BodyWidgetPreviewPreviewPageDummy(), + ), + ), + GoRoute( + path: SizedContainerPreviewPreviewPageDummy.routeName, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const SizedContainerPreviewPreviewPageDummy(), + ), + ), + GoRoute( + path: CounterWidgetPreviewPreviewPageDummy.routeName, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const CounterWidgetPreviewPreviewPageDummy(), + ), + ), + ], ) ], ) ], ); + const CatalogComponent({super.key}); @override @@ -67,76 +129,31 @@ class CatalogComponent extends StatefulWidget { } class CatalogComponentState extends State { - TreeController? treeController; - @override Widget build(BuildContext context) { - return PreviewScaffold( - onBackPressed: Catalog().onBackPressed, - child: FutureBuilder( - initialData: null, - future: Catalog().get(context), - builder: (context, data) { - if (!data.hasData || data.data == null) { - return Container(); - } - final node = data.data as ComponentNode; - if (treeController == null) { - treeController = TreeController( - roots: [node], - childrenProvider: (ComponentNode node) => node.children.values, - ); - if (treeController!.isTreeCollapsed) { - treeController!.expandAll(); - } - } - return AnimatedTreeView( - treeController: treeController!, - nodeBuilder: - (BuildContext context, TreeEntry entry) { - return InkWell( - onTap: () { - // _nodePressed(node); - }, - child: TreeIndentation( - entry: entry, - child: Row( - children: [ - FolderButton( - color: Colors.black, - isOpen: entry.hasChildren ? entry.isExpanded : null, - onPressed: () => _nodePressed(entry), - ), - Text( - entry.node.id, - style: const TextStyle( - color: Colors.black, - fontSize: 16, - letterSpacing: .3, - ), - ), - ], - ), - ), - ); - }, - ); - }), + return FutureBuilder( + initialData: null, + future: Catalog().get(context), + builder: (context, data) { + if (!data.hasData || data.data == null) { + return Container(); + } + final node = data.data as ComponentNode; + return PreviewScaffold( + basePath: CatalogComponent.routeName, + onBackPressed: Catalog().onBackPressed, + child: ListView( + children: [ + buildTreeWidget( + context, + CatalogComponent.routeName, + node, + 0, + ) + ], + ), + ); + }, ); } - - void _nodePressed(TreeEntry entry) { - if (entry.node.children.isEmpty) { - if (entry.node.builtComponent?.preview?.path != null) { - context.go( - '${CatalogComponent.routeName}/${entry.node.builtComponent!.preview!.path}'); - } - } else { - if (!entry.isExpanded) { - treeController?.toggleExpansion(entry.node); - } else { - treeController?.collapse(entry.node); - } - } - } } diff --git a/example/lib/catalog/widgets/main_screen_widget/main_screen.dart b/example/lib/catalog/widgets/main_screen.dart similarity index 81% rename from example/lib/catalog/widgets/main_screen_widget/main_screen.dart rename to example/lib/catalog/widgets/main_screen.dart index fe3df1d..2435fa2 100644 --- a/example/lib/catalog/widgets/main_screen_widget/main_screen.dart +++ b/example/lib/catalog/widgets/main_screen.dart @@ -1,10 +1,10 @@ /// AUTOGENERATED FILE. DO NOT EDIT import 'package:flutter/material.dart'; -import 'package:example/widgets/preview/main_screen.preview.dart'; +import 'package:example/widgets/catalog/preview/main_screen.preview.dart'; class MainScreenPreviewPreviewPageDummy extends StatefulWidget { - static String routeName = 'main_screen_widget'; + static String routeName = 'main_screen'; const MainScreenPreviewPreviewPageDummy({super.key}); diff --git a/example/lib/catalog/widgets/other_utils/bottom/warning_info_widget.dart b/example/lib/catalog/widgets/other_utils/bottom/warning_info_widget.dart new file mode 100644 index 0000000..bae6d1e --- /dev/null +++ b/example/lib/catalog/widgets/other_utils/bottom/warning_info_widget.dart @@ -0,0 +1,22 @@ +/// AUTOGENERATED FILE. DO NOT EDIT + +import 'package:flutter/material.dart'; +import 'package:example/widgets/other_utils/bottom/catalog/preview/warning_info_widget.preview.dart'; + +class WarningInfoWidgetPreviewPreviewPageDummy extends StatefulWidget { + static String routeName = 'warning_info_widget'; + + const WarningInfoWidgetPreviewPreviewPageDummy({super.key}); + + @override + WarningInfoWidgetPreviewPreviewPageDummyState createState() => + WarningInfoWidgetPreviewPreviewPageDummyState(); +} + +class WarningInfoWidgetPreviewPreviewPageDummyState + extends State { + @override + Widget build(BuildContext context) { + return const WarningInfoWidgetPreview(); + } +} diff --git a/example/lib/catalog/widgets/body_widget/body_widget.dart b/example/lib/catalog/widgets/screen/body_widget.dart similarity index 87% rename from example/lib/catalog/widgets/body_widget/body_widget.dart rename to example/lib/catalog/widgets/screen/body_widget.dart index 05e08ce..57f4c75 100644 --- a/example/lib/catalog/widgets/body_widget/body_widget.dart +++ b/example/lib/catalog/widgets/screen/body_widget.dart @@ -1,7 +1,7 @@ /// AUTOGENERATED FILE. DO NOT EDIT import 'package:flutter/material.dart'; -import 'package:example/widgets/preview/body_widget.preview.dart'; +import 'package:example/widgets/screen/catalog/preview/body_widget.preview.dart'; class BodyWidgetPreviewPreviewPageDummy extends StatefulWidget { static String routeName = 'body_widget'; diff --git a/example/lib/catalog/widgets/counter_widget/counter_widget.dart b/example/lib/catalog/widgets/screen/counter_widget.dart similarity index 87% rename from example/lib/catalog/widgets/counter_widget/counter_widget.dart rename to example/lib/catalog/widgets/screen/counter_widget.dart index 59e6b7a..dc4cda2 100644 --- a/example/lib/catalog/widgets/counter_widget/counter_widget.dart +++ b/example/lib/catalog/widgets/screen/counter_widget.dart @@ -1,7 +1,7 @@ /// AUTOGENERATED FILE. DO NOT EDIT import 'package:flutter/material.dart'; -import 'package:example/widgets/preview/counter_widget.preview.dart'; +import 'package:example/widgets/screen/catalog/preview/counter_widget.preview.dart'; class CounterWidgetPreviewPreviewPageDummy extends StatefulWidget { static String routeName = 'counter_widget'; diff --git a/example/lib/catalog/widgets/screen/sized_container.dart b/example/lib/catalog/widgets/screen/sized_container.dart new file mode 100644 index 0000000..6a8daf8 --- /dev/null +++ b/example/lib/catalog/widgets/screen/sized_container.dart @@ -0,0 +1,22 @@ +/// AUTOGENERATED FILE. DO NOT EDIT + +import 'package:flutter/material.dart'; +import 'package:example/widgets/screen/catalog/preview/sized_container.preview.dart'; + +class SizedContainerPreviewPreviewPageDummy extends StatefulWidget { + static String routeName = 'sized_container'; + + const SizedContainerPreviewPreviewPageDummy({super.key}); + + @override + SizedContainerPreviewPreviewPageDummyState createState() => + SizedContainerPreviewPreviewPageDummyState(); +} + +class SizedContainerPreviewPreviewPageDummyState + extends State { + @override + Widget build(BuildContext context) { + return const SizedContainerPreview(); + } +} diff --git a/example/lib/catalog/widgets/fab_widget/fab_widget.dart b/example/lib/catalog/widgets/utils/bottom/fab_widget.dart similarity index 86% rename from example/lib/catalog/widgets/fab_widget/fab_widget.dart rename to example/lib/catalog/widgets/utils/bottom/fab_widget.dart index 20974b6..5b10f36 100644 --- a/example/lib/catalog/widgets/fab_widget/fab_widget.dart +++ b/example/lib/catalog/widgets/utils/bottom/fab_widget.dart @@ -1,7 +1,7 @@ /// AUTOGENERATED FILE. DO NOT EDIT import 'package:flutter/material.dart'; -import 'package:example/widgets/preview/fab_widget.preview.dart'; +import 'package:example/widgets/utils/bottom/catalog/preview/fab_widget.preview.dart'; class FabWidgetPreviewPreviewPageDummy extends StatefulWidget { static String routeName = 'fab_widget'; diff --git a/example/lib/catalog/widgets/widgets_preview_page_dummy.dart b/example/lib/catalog/widgets/widgets_preview_page_dummy.dart deleted file mode 100644 index 0655749..0000000 --- a/example/lib/catalog/widgets/widgets_preview_page_dummy.dart +++ /dev/null @@ -1,77 +0,0 @@ -/// AUTOGENERATED FILE. DO NOT EDIT - -import 'package:catalog/catalog.dart'; -import 'package:flutter/material.dart'; - -class WidgetsPreviewPageDummy extends StatefulWidget { - static String routeName = 'widgets'; - - const WidgetsPreviewPageDummy({super.key}); - - @override - WidgetsPreviewPageDummyState createState() => WidgetsPreviewPageDummyState(); -} - -class WidgetsPreviewPageDummyState extends State { - @override - Widget build(BuildContext context) { - return PreviewScaffold( - child: ListView( - children: [ - ListTile( - title: const Text( - 'body_widget', - style: TextStyle( - color: Colors.black, - fontSize: 16, - letterSpacing: .3, - ), - ), - onTap: () { - context.go('/catalog/widgets/body_widget'); - }, - ), - ListTile( - title: const Text( - 'fab_widget', - style: TextStyle( - color: Colors.black, - fontSize: 16, - letterSpacing: .3, - ), - ), - onTap: () { - context.go('/catalog/widgets/fab_widget'); - }, - ), - ListTile( - title: const Text( - 'main_screen_widget', - style: TextStyle( - color: Colors.black, - fontSize: 16, - letterSpacing: .3, - ), - ), - onTap: () { - context.go('/catalog/widgets/main_screen_widget'); - }, - ), - ListTile( - title: const Text( - 'counter_widget', - style: TextStyle( - color: Colors.black, - fontSize: 16, - letterSpacing: .3, - ), - ), - onTap: () { - context.go('/catalog/widgets/counter_widget'); - }, - ), - ], - ), - ); - } -} diff --git a/example/lib/main.dart b/example/lib/main.dart index c467a08..1d073ee 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -36,7 +36,7 @@ void main() { */ runApp( CatalogRunner( - enabled: false, + enabled: true, application: const MyApp(), route: CatalogComponent.route, supportedLocales: Stringcare().locales, diff --git a/example/lib/widgets/preview/dummy/main_screen.dummy.dart b/example/lib/widgets/catalog/dummy/main_screen.dummy.dart similarity index 100% rename from example/lib/widgets/preview/dummy/main_screen.dummy.dart rename to example/lib/widgets/catalog/dummy/main_screen.dummy.dart diff --git a/example/lib/widgets/catalog/integration_test/main_screen_integration_test.dart b/example/lib/widgets/catalog/integration_test/main_screen_integration_test.dart new file mode 100644 index 0000000..aa42299 --- /dev/null +++ b/example/lib/widgets/catalog/integration_test/main_screen_integration_test.dart @@ -0,0 +1,46 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget MainScreen +/// + +import 'package:catalog/catalog.dart'; +import 'package:example/r.dart'; +import 'package:stringcare/stringcare.dart'; + +import '../dummy/main_screen.dummy.dart'; +import '../preview/main_screen.preview.dart'; + +class MainScreenIntegrationTest { + void main() { + group( + 'MainScreen - IntegrationTest Tests', + () { + testWidgets( + 'Finds title and info text', + (tester) async { + await tester.setupIntegrationTestContext(); + final dummy = MainScreenDummy().dummies.first; + final widget = buildMainScreen(dummy); + await tester.test(widget); + + expect(find.text(R.strings.title_app.string()), findsAny); + expect(find.text(R.strings.info_text.string()), findsAny); + }, + ); + + testWidgets( + 'Web title not displayed on widget', + (tester) async { + await tester.setupIntegrationTestContext(); + + final dummy = MainScreenDummy().dummies.first; + final widget = buildMainScreen(dummy); + await tester.test(widget); + + expect(find.text(R.strings.title.string()), findsNothing); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/preview/main_screen.preview.dart b/example/lib/widgets/catalog/preview/main_screen.preview.dart similarity index 88% rename from example/lib/widgets/preview/main_screen.preview.dart rename to example/lib/widgets/catalog/preview/main_screen.preview.dart index fd3de6d..7095ff7 100644 --- a/example/lib/widgets/preview/main_screen.preview.dart +++ b/example/lib/widgets/catalog/preview/main_screen.preview.dart @@ -3,13 +3,10 @@ import 'package:catalog/catalog.dart'; import 'package:flutter/material.dart'; import 'package:example/widgets/main_screen.dart'; -import 'dummy/main_screen.dummy.dart'; +import '../dummy/main_screen.dummy.dart'; @Preview( - id: 'MainScreenPreview', - path: 'widgets/main_screen_widget', - usesDummies: true, - dummyParameters: [ + parameters: [ 'title', 'infoText', 'counter', @@ -17,6 +14,12 @@ import 'dummy/main_screen.dummy.dart'; ], ) class MainScreenPreview extends ParentPreviewWidget { + @override + String get title => 'main_screen'; + + @override + String get basePath => '/catalog'; + const MainScreenPreview({super.key}); @override @@ -107,12 +110,7 @@ class MainScreenPreview extends ParentPreviewWidget { widgetKey: GlobalKey(), dummyBuilder: () => MainScreenDummy().dummies[i], builder: (BuildContext context, Dummy dummy) { - return MainScreen( - title: dummy.parameters['title'], - infoText: dummy.parameters['infoText'], - counter: dummy.parameters['counter'], - incrementCounter: dummy.parameters['incrementCounter'], - ); + return buildMainScreen(dummy); }, ), ], @@ -121,3 +119,12 @@ class MainScreenPreview extends ParentPreviewWidget { ); } } + +MainScreen buildMainScreen(Dummy dummy) { + return MainScreen( + title: dummy.parameters['title'], + infoText: dummy.parameters['infoText'], + counter: dummy.parameters['counter'], + incrementCounter: dummy.parameters['incrementCounter'], + ); +} diff --git a/example/lib/widgets/catalog/test/main_screen_test.dart b/example/lib/widgets/catalog/test/main_screen_test.dart new file mode 100644 index 0000000..2c8dd46 --- /dev/null +++ b/example/lib/widgets/catalog/test/main_screen_test.dart @@ -0,0 +1,46 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget MainScreen +/// + +import 'package:catalog/catalog.dart'; +import 'package:example/r.dart'; +import 'package:stringcare/stringcare.dart'; + +import '../dummy/main_screen.dummy.dart'; +import '../preview/main_screen.preview.dart'; + +class MainScreenTest { + void main() { + group( + 'MainScreen - Tests', + () { + testWidgets( + 'Finds title and info text', + (tester) async { + await tester.setupTestContext(); + final dummy = MainScreenDummy().dummies.first; + final widget = buildMainScreen(dummy); + await tester.test(widget); + + expect(find.text(R.strings.title_app.string()), findsOneWidget); + expect(find.text(R.strings.info_text.string()), findsOneWidget); + }, + ); + + testWidgets( + 'Web title not displayed on widget', + (tester) async { + await tester.setupTestContext(); + + final dummy = MainScreenDummy().dummies.first; + final widget = buildMainScreen(dummy); + await tester.test(widget); + + expect(find.text(R.strings.title.string()), findsNothing); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/main_screen.dart b/example/lib/widgets/main_screen.dart index 14f7f7e..9809d36 100644 --- a/example/lib/widgets/main_screen.dart +++ b/example/lib/widgets/main_screen.dart @@ -1,14 +1,11 @@ import 'package:catalog/catalog.dart'; import 'package:flutter/material.dart'; -import 'body_widget.dart'; -import 'fab_widget.dart'; +import 'screen/body_widget.dart'; +import 'utils/bottom/fab_widget.dart'; @Preview( - id: 'MainScreenPreview', - path: 'widgets/main_screen_widget', - usesDummies: true, - dummyParameters: [ + parameters: [ 'title', 'infoText', 'counter', diff --git a/example/lib/widgets/other_utils/bottom/catalog/dummy/warning_info_widget.dummy.dart b/example/lib/widgets/other_utils/bottom/catalog/dummy/warning_info_widget.dummy.dart new file mode 100644 index 0000000..d36a8d7 --- /dev/null +++ b/example/lib/widgets/other_utils/bottom/catalog/dummy/warning_info_widget.dummy.dart @@ -0,0 +1,17 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file for modify the preview of WarningInfoWidgetPreview +/// + +import 'package:catalog/catalog.dart'; + +class WarningInfoWidgetDummy extends PreviewDummy { + @override + List get dummies => [ + Dummy( + parameters: { + 'infoText': 'text sample', + }, + ), + ]; +} diff --git a/example/lib/widgets/other_utils/bottom/catalog/integration_test/warning_info_widget_integration_test.dart b/example/lib/widgets/other_utils/bottom/catalog/integration_test/warning_info_widget_integration_test.dart new file mode 100644 index 0000000..9522c70 --- /dev/null +++ b/example/lib/widgets/other_utils/bottom/catalog/integration_test/warning_info_widget_integration_test.dart @@ -0,0 +1,40 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget WarningInfoWidget +/// + +import 'package:catalog/catalog.dart'; + +import '../dummy/warning_info_widget.dummy.dart'; +import '../preview/warning_info_widget.preview.dart'; + +class WarningInfoWidgetIntegrationTest { + void main() { + group( + 'WarningInfoWidget - IntegrationTest Tests', + () { + testWidgets( + 'Finds text sample', + (tester) async { + final dummy = WarningInfoWidgetDummy().dummies.first; + final widget = buildWarningInfoWidget(dummy); + await tester.test(widget); + + expect(find.text('text sample'), findsAny); + }, + ); + + testWidgets( + 'Other lorem text not found', + (tester) async { + final dummy = WarningInfoWidgetDummy().dummies.first; + final widget = buildWarningInfoWidget(dummy); + await tester.test(widget); + + expect(find.text('ipsu lorem'), findsNothing); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/other_utils/bottom/catalog/preview/warning_info_widget.preview.dart b/example/lib/widgets/other_utils/bottom/catalog/preview/warning_info_widget.preview.dart new file mode 100644 index 0000000..71ec6ab --- /dev/null +++ b/example/lib/widgets/other_utils/bottom/catalog/preview/warning_info_widget.preview.dart @@ -0,0 +1,123 @@ +/// AUTOGENERATED FILE. DO NOT EDIT + +import 'package:catalog/catalog.dart'; +import 'package:flutter/material.dart'; +import 'package:example/widgets/other_utils/bottom/warning_info_widget.dart'; +import '../dummy/warning_info_widget.dummy.dart'; + +@Preview( + description: 'Basic warning info text for alert', + parameters: ['infoText'], +) +class WarningInfoWidgetPreview extends ParentPreviewWidget { + @override + String get title => 'warning_info_widget'; + + @override + String get basePath => '/catalog'; + + const WarningInfoWidgetPreview({super.key}); + + @override + Widget preview(BuildContext context) { + Catalog().widgetBasicPreviewMap.clear(); + Catalog().widgetDevicePreviewMap.clear(); + + if (WarningInfoWidgetDummy().dummies.isEmpty) { + return Container(); + } + + final deviceScreenshotsAvailable = + WarningInfoWidgetDummy().deviceScreenshotsAvailable; + final screenshotsAvailable = WarningInfoWidgetDummy().screenshotsAvailable; + + int basicScreenshots = screenshotsAvailable - deviceScreenshotsAvailable; + + return ListView( + children: [ + Column( + children: [ + if (basicScreenshots > 0) + Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Card( + clipBehavior: Clip.hardEdge, + child: Container( + padding: const EdgeInsets.all(15), + color: Colors.white, + child: Row( + children: [ + Expanded( + child: Center( + child: Text( + '$basicScreenshots basic screenshots available', + ), + ), + ), + const IconButton( + onPressed: processBasicScreenshots, + icon: Icon( + Icons.screenshot, + ), + ) + ], + ), + ), + ), + ), + ), + if (deviceScreenshotsAvailable > 0) + Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Card( + clipBehavior: Clip.hardEdge, + child: Container( + padding: const EdgeInsets.all(15), + color: Colors.white, + child: Row( + children: [ + Expanded( + child: Center( + child: Text( + '$deviceScreenshotsAvailable device screenshots available', + ), + ), + ), + const IconButton( + onPressed: processDeviceScreenshots, + icon: Icon( + Icons.screenshot, + ), + ) + ], + ), + ), + ), + ), + ), + for (int i = 0; i < WarningInfoWidgetDummy().dummies.length; i++) + PreviewBoundary( + widgetKey: GlobalKey(), + dummyBuilder: () => WarningInfoWidgetDummy().dummies[i], + builder: (BuildContext context, Dummy dummy) { + return buildWarningInfoWidget(dummy); + }, + ), + ], + ) + ], + ); + } +} + +WarningInfoWidget buildWarningInfoWidget(Dummy dummy) { + return WarningInfoWidget( + infoText: dummy.parameters['infoText'], + ); +} diff --git a/example/lib/widgets/other_utils/bottom/catalog/test/warning_info_widget_test.dart b/example/lib/widgets/other_utils/bottom/catalog/test/warning_info_widget_test.dart new file mode 100644 index 0000000..54a1cf0 --- /dev/null +++ b/example/lib/widgets/other_utils/bottom/catalog/test/warning_info_widget_test.dart @@ -0,0 +1,40 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget WarningInfoWidget +/// + +import 'package:catalog/catalog.dart'; + +import '../dummy/warning_info_widget.dummy.dart'; +import '../preview/warning_info_widget.preview.dart'; + +class WarningInfoWidgetTest { + void main() { + group( + 'WarningInfoWidget - Tests', + () { + testWidgets( + 'Finds text sample', + (tester) async { + final dummy = WarningInfoWidgetDummy().dummies.first; + final widget = buildWarningInfoWidget(dummy); + await tester.test(widget); + + expect(find.text('text sample'), findsAny); + }, + ); + + testWidgets( + 'Other lorem text not found', + (tester) async { + final dummy = WarningInfoWidgetDummy().dummies.first; + final widget = buildWarningInfoWidget(dummy); + await tester.test(widget); + + expect(find.text('ipsu lorem'), findsNothing); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/other_utils/bottom/warning_info_widget.dart b/example/lib/widgets/other_utils/bottom/warning_info_widget.dart new file mode 100644 index 0000000..acf52d6 --- /dev/null +++ b/example/lib/widgets/other_utils/bottom/warning_info_widget.dart @@ -0,0 +1,20 @@ +import 'package:catalog/catalog.dart'; +import 'package:flutter/material.dart'; + +@Preview( + description: 'Basic warning info text for alert', + parameters: ['infoText'], +) +class WarningInfoWidget extends StatelessWidget { + final String infoText; + + const WarningInfoWidget({ + super.key, + required this.infoText, + }); + + @override + Widget build(BuildContext context) { + return Text(infoText); + } +} diff --git a/example/lib/widgets/preview/fab_widget.preview.dart b/example/lib/widgets/preview/fab_widget.preview.dart deleted file mode 100644 index 8e342b7..0000000 --- a/example/lib/widgets/preview/fab_widget.preview.dart +++ /dev/null @@ -1,34 +0,0 @@ -/// AUTOGENERATED FILE. DO NOT EDIT - -import 'package:catalog/catalog.dart'; -import 'package:flutter/material.dart'; -import 'package:example/widgets/fab_widget.dart'; - -@Preview( - id: 'FabWidgetPreview', - path: 'widgets/fab_widget', - description: 'Basic fab widget', - parameters: { - 'incrementCounter': 'void_function_snackbar', - }, -) -class FabWidgetPreview extends ParentPreviewWidget { - const FabWidgetPreview({super.key}); - - @override - Widget preview(BuildContext context) => Container( - constraints: const BoxConstraints(maxWidth: 700), - child: FabWidget( - incrementCounter: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - duration: Duration(seconds: 2), - content: Text( - 'incrementCounter event', - ), - ), - ); - }, - ), - ); -} diff --git a/example/lib/widgets/body_widget.dart b/example/lib/widgets/screen/body_widget.dart similarity index 86% rename from example/lib/widgets/body_widget.dart rename to example/lib/widgets/screen/body_widget.dart index 83ee5f4..ce78232 100644 --- a/example/lib/widgets/body_widget.dart +++ b/example/lib/widgets/screen/body_widget.dart @@ -4,10 +4,7 @@ import 'package:flutter/material.dart'; import 'counter_widget.dart'; @Preview( - id: 'BodyWidgetPreview', - path: 'widgets/body_widget', - usesDummies: true, - dummyParameters: [ + parameters: [ 'infoText', 'counter', ], diff --git a/example/lib/widgets/preview/dummy/body_widget.dummy.dart b/example/lib/widgets/screen/catalog/dummy/body_widget.dummy.dart similarity index 100% rename from example/lib/widgets/preview/dummy/body_widget.dummy.dart rename to example/lib/widgets/screen/catalog/dummy/body_widget.dummy.dart diff --git a/example/lib/widgets/preview/dummy/counter_widget.dummy.dart b/example/lib/widgets/screen/catalog/dummy/counter_widget.dummy.dart similarity index 100% rename from example/lib/widgets/preview/dummy/counter_widget.dummy.dart rename to example/lib/widgets/screen/catalog/dummy/counter_widget.dummy.dart diff --git a/example/lib/widgets/screen/catalog/dummy/sized_container.dummy.dart b/example/lib/widgets/screen/catalog/dummy/sized_container.dummy.dart new file mode 100644 index 0000000..4325143 --- /dev/null +++ b/example/lib/widgets/screen/catalog/dummy/sized_container.dummy.dart @@ -0,0 +1,24 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to prepare the preview and the tests: +/// +/// - SizedContainerPreview +/// - SizedContainerTest +/// - SizedContainerIntegrationTest +/// + +import 'package:catalog/catalog.dart'; +import 'package:flutter/widgets.dart'; + +class SizedContainerDummy extends PreviewDummy { + @override + List get dummies => List.generate( + 8, + (index) => Dummy( + parameters: { + 'width': (index + 1) * 100.0, + 'child': Text(bigText * 4), + }, + ), + ); +} diff --git a/example/lib/widgets/screen/catalog/integration_test/body_widget_integration_test.dart b/example/lib/widgets/screen/catalog/integration_test/body_widget_integration_test.dart new file mode 100644 index 0000000..90ca22d --- /dev/null +++ b/example/lib/widgets/screen/catalog/integration_test/body_widget_integration_test.dart @@ -0,0 +1,47 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget BodyWidget +/// + +import 'package:catalog/catalog.dart'; +import 'package:example/r.dart'; +import 'package:stringcare/stringcare.dart'; + +import '../dummy/body_widget.dummy.dart'; +import '../preview/body_widget.preview.dart'; + +class BodyWidgetIntegrationTest { + void main() { + group( + 'BodyWidget - IntegrationTest Tests', + () { + testWidgets( + 'No title is found', + (tester) async { + await tester.setupIntegrationTestContext(); + + final dummy = BodyWidgetDummy().dummies.first; + final widget = buildBodyWidget(dummy); + await tester.test(widget); + + expect(find.text(R.strings.title_app.string()), findsNothing); + }, + ); + + testWidgets( + 'Info text is displayed', + (tester) async { + await tester.setupIntegrationTestContext(); + + final dummy = BodyWidgetDummy().dummies.first; + final widget = buildBodyWidget(dummy); + await tester.test(widget); + + expect(find.text('You have pushed the button this many times:'), + findsAny); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/screen/catalog/integration_test/counter_widget_integration_test.dart b/example/lib/widgets/screen/catalog/integration_test/counter_widget_integration_test.dart new file mode 100644 index 0000000..ceee9c9 --- /dev/null +++ b/example/lib/widgets/screen/catalog/integration_test/counter_widget_integration_test.dart @@ -0,0 +1,40 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget CounterWidget +/// + +import 'package:catalog/catalog.dart'; + +import '../dummy/counter_widget.dummy.dart'; +import '../preview/counter_widget.preview.dart'; + +class CounterWidgetIntegrationTest { + void main() { + group( + 'CounterWidget - IntegrationTest Tests', + () { + testWidgets( + 'Lorem text not found', + (tester) async { + final dummy = CounterWidgetDummy().dummies.first; + final widget = buildCounterWidget(dummy); + await tester.test(widget); + + expect(find.text('lorem ipsu'), findsNothing); + }, + ); + + testWidgets( + 'Other lorem text not found', + (tester) async { + final dummy = CounterWidgetDummy().dummies.first; + final widget = buildCounterWidget(dummy); + await tester.test(widget); + + expect(find.text('ipsu lorem'), findsNothing); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/screen/catalog/integration_test/sized_container_integration_test.dart b/example/lib/widgets/screen/catalog/integration_test/sized_container_integration_test.dart new file mode 100644 index 0000000..d3d5734 --- /dev/null +++ b/example/lib/widgets/screen/catalog/integration_test/sized_container_integration_test.dart @@ -0,0 +1,69 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget SizedContainer +/// + +import 'package:catalog/catalog.dart'; +import 'package:flutter/widgets.dart'; + +import '../dummy/sized_container.dummy.dart'; +import '../preview/sized_container.preview.dart'; + +class SizedContainerIntegrationTest { + void main() { + group( + 'SizedContainer - IntegrationTest Tests', + () { + testWidgets( + 'Child does not exceed the width', + (tester) async { + // prepare the context + await tester.setupTestContext(); + + for (var dummy in SizedContainerDummy().dummies) { + // prepare the widget + final widget = buildSizedContainer(dummy); + await tester.test(widget); + + // get the size of the widget + final widgetSize = tester.getSize(find.byWidget(widget)); + + // check the maximum width + expect( + widgetSize.width, + dummy.parameters['width'], + ); + } + }, + ); + + testWidgets( + 'Child does exceed the width', + (tester) async { + // prepare the context + await tester.setupTestContext(); + + for (var dummy in SizedContainerDummy().dummies) { + // prepare the widget + final widget = Container( + child: dummy.parameters['child'], + ); + await tester.test(widget); + + // get the size of the widget + final widgetSize = tester.getSize(find.byWidget(widget)); + + // check the maximum width + expect( + widgetSize.width, + greaterThanOrEqualTo( + dummy.parameters['width'], + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/preview/body_widget.preview.dart b/example/lib/widgets/screen/catalog/preview/body_widget.preview.dart similarity index 89% rename from example/lib/widgets/preview/body_widget.preview.dart rename to example/lib/widgets/screen/catalog/preview/body_widget.preview.dart index 288f102..24af584 100644 --- a/example/lib/widgets/preview/body_widget.preview.dart +++ b/example/lib/widgets/screen/catalog/preview/body_widget.preview.dart @@ -2,19 +2,22 @@ import 'package:catalog/catalog.dart'; import 'package:flutter/material.dart'; -import 'package:example/widgets/body_widget.dart'; -import 'dummy/body_widget.dummy.dart'; +import 'package:example/widgets/screen/body_widget.dart'; +import '../dummy/body_widget.dummy.dart'; @Preview( - id: 'BodyWidgetPreview', - path: 'widgets/body_widget', - usesDummies: true, - dummyParameters: [ + parameters: [ 'infoText', 'counter', ], ) class BodyWidgetPreview extends ParentPreviewWidget { + @override + String get title => 'body_widget'; + + @override + String get basePath => '/catalog'; + const BodyWidgetPreview({super.key}); @override @@ -105,10 +108,7 @@ class BodyWidgetPreview extends ParentPreviewWidget { widgetKey: GlobalKey(), dummyBuilder: () => BodyWidgetDummy().dummies[i], builder: (BuildContext context, Dummy dummy) { - return BodyWidget( - infoText: dummy.parameters['infoText'], - counter: dummy.parameters['counter'], - ); + return buildBodyWidget(dummy); }, ), ], @@ -117,3 +117,10 @@ class BodyWidgetPreview extends ParentPreviewWidget { ); } } + +BodyWidget buildBodyWidget(Dummy dummy) { + return BodyWidget( + infoText: dummy.parameters['infoText'], + counter: dummy.parameters['counter'], + ); +} diff --git a/example/lib/widgets/preview/counter_widget.preview.dart b/example/lib/widgets/screen/catalog/preview/counter_widget.preview.dart similarity index 89% rename from example/lib/widgets/preview/counter_widget.preview.dart rename to example/lib/widgets/screen/catalog/preview/counter_widget.preview.dart index 6848bf1..04f76ab 100644 --- a/example/lib/widgets/preview/counter_widget.preview.dart +++ b/example/lib/widgets/screen/catalog/preview/counter_widget.preview.dart @@ -2,19 +2,22 @@ import 'package:catalog/catalog.dart'; import 'package:flutter/material.dart'; -import 'package:example/widgets/counter_widget.dart'; -import 'dummy/counter_widget.dummy.dart'; +import 'package:example/widgets/screen/counter_widget.dart'; +import '../dummy/counter_widget.dummy.dart'; @Preview( - id: 'CounterWidgetPreview', - path: 'widgets/counter_widget', description: 'Basic counter widget', - usesDummies: true, - dummyParameters: [ + parameters: [ 'counter', ], ) class CounterWidgetPreview extends ParentPreviewWidget { + @override + String get title => 'counter_widget'; + + @override + String get basePath => '/catalog'; + const CounterWidgetPreview({super.key}); @override @@ -105,9 +108,7 @@ class CounterWidgetPreview extends ParentPreviewWidget { widgetKey: GlobalKey(), dummyBuilder: () => CounterWidgetDummy().dummies[i], builder: (BuildContext context, Dummy dummy) { - return CounterWidget( - counter: dummy.parameters['counter'], - ); + return buildCounterWidget(dummy); }, ), ], @@ -116,3 +117,9 @@ class CounterWidgetPreview extends ParentPreviewWidget { ); } } + +CounterWidget buildCounterWidget(Dummy dummy) { + return CounterWidget( + counter: dummy.parameters['counter'], + ); +} diff --git a/example/lib/widgets/screen/catalog/preview/sized_container.preview.dart b/example/lib/widgets/screen/catalog/preview/sized_container.preview.dart new file mode 100644 index 0000000..b32761d --- /dev/null +++ b/example/lib/widgets/screen/catalog/preview/sized_container.preview.dart @@ -0,0 +1,124 @@ +/// AUTOGENERATED FILE. DO NOT EDIT + +import 'package:catalog/catalog.dart'; +import 'package:flutter/material.dart'; +import 'package:example/widgets/screen/sized_container.dart'; +import '../dummy/sized_container.dummy.dart'; + +@Preview( + description: 'Container with a max width', + parameters: ['width', 'child'], +) +class SizedContainerPreview extends ParentPreviewWidget { + @override + String get title => 'sized_container'; + + @override + String get basePath => '/catalog'; + + const SizedContainerPreview({super.key}); + + @override + Widget preview(BuildContext context) { + Catalog().widgetBasicPreviewMap.clear(); + Catalog().widgetDevicePreviewMap.clear(); + + if (SizedContainerDummy().dummies.isEmpty) { + return Container(); + } + + final deviceScreenshotsAvailable = + SizedContainerDummy().deviceScreenshotsAvailable; + final screenshotsAvailable = SizedContainerDummy().screenshotsAvailable; + + int basicScreenshots = screenshotsAvailable - deviceScreenshotsAvailable; + + return ListView( + children: [ + Column( + children: [ + if (basicScreenshots > 0) + Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Card( + clipBehavior: Clip.hardEdge, + child: Container( + padding: const EdgeInsets.all(15), + color: Colors.white, + child: Row( + children: [ + Expanded( + child: Center( + child: Text( + '$basicScreenshots basic screenshots available', + ), + ), + ), + const IconButton( + onPressed: processBasicScreenshots, + icon: Icon( + Icons.screenshot, + ), + ) + ], + ), + ), + ), + ), + ), + if (deviceScreenshotsAvailable > 0) + Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Card( + clipBehavior: Clip.hardEdge, + child: Container( + padding: const EdgeInsets.all(15), + color: Colors.white, + child: Row( + children: [ + Expanded( + child: Center( + child: Text( + '$deviceScreenshotsAvailable device screenshots available', + ), + ), + ), + const IconButton( + onPressed: processDeviceScreenshots, + icon: Icon( + Icons.screenshot, + ), + ) + ], + ), + ), + ), + ), + ), + for (int i = 0; i < SizedContainerDummy().dummies.length; i++) + PreviewBoundary( + widgetKey: GlobalKey(), + dummyBuilder: () => SizedContainerDummy().dummies[i], + builder: (BuildContext context, Dummy dummy) { + return buildSizedContainer(dummy); + }, + ), + ], + ) + ], + ); + } +} + +SizedContainer buildSizedContainer(Dummy dummy) { + return SizedContainer( + width: dummy.parameters['width'], + child: dummy.parameters['child'], + ); +} diff --git a/example/lib/widgets/screen/catalog/test/body_widget_test.dart b/example/lib/widgets/screen/catalog/test/body_widget_test.dart new file mode 100644 index 0000000..966e690 --- /dev/null +++ b/example/lib/widgets/screen/catalog/test/body_widget_test.dart @@ -0,0 +1,47 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget BodyWidget +/// + +import 'package:catalog/catalog.dart'; +import 'package:example/r.dart'; +import 'package:stringcare/stringcare.dart'; + +import '../dummy/body_widget.dummy.dart'; +import '../preview/body_widget.preview.dart'; + +class BodyWidgetTest { + void main() { + group( + 'BodyWidget - Tests', + () { + testWidgets( + 'No title is found', + (tester) async { + await tester.setupTestContext(); + + final dummy = BodyWidgetDummy().dummies.first; + final widget = buildBodyWidget(dummy); + await tester.test(widget); + + expect(find.text(R.strings.title_app.string()), findsNothing); + }, + ); + + testWidgets( + 'Info text is displayed', + (tester) async { + await tester.setupTestContext(); + + final dummy = BodyWidgetDummy().dummies.first; + final widget = buildBodyWidget(dummy); + await tester.test(widget); + + expect(find.text('You have pushed the button this many times:'), + findsAny); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/screen/catalog/test/counter_widget_test.dart b/example/lib/widgets/screen/catalog/test/counter_widget_test.dart new file mode 100644 index 0000000..1682507 --- /dev/null +++ b/example/lib/widgets/screen/catalog/test/counter_widget_test.dart @@ -0,0 +1,40 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget CounterWidget +/// + +import 'package:catalog/catalog.dart'; + +import '../dummy/counter_widget.dummy.dart'; +import '../preview/counter_widget.preview.dart'; + +class CounterWidgetTest { + void main() { + group( + 'CounterWidget - Tests', + () { + testWidgets( + 'Lorem text not found', + (tester) async { + final dummy = CounterWidgetDummy().dummies.first; + final widget = buildCounterWidget(dummy); + await tester.test(widget); + + expect(find.text('lorem ipsu'), findsNothing); + }, + ); + + testWidgets( + 'Other lorem text not found', + (tester) async { + final dummy = CounterWidgetDummy().dummies.first; + final widget = buildCounterWidget(dummy); + await tester.test(widget); + + expect(find.text('ipsu lorem'), findsNothing); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/screen/catalog/test/sized_container_test.dart b/example/lib/widgets/screen/catalog/test/sized_container_test.dart new file mode 100644 index 0000000..640e06d --- /dev/null +++ b/example/lib/widgets/screen/catalog/test/sized_container_test.dart @@ -0,0 +1,69 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget SizedContainer +/// + +import 'package:catalog/catalog.dart'; +import 'package:flutter/widgets.dart'; + +import '../dummy/sized_container.dummy.dart'; +import '../preview/sized_container.preview.dart'; + +class SizedContainerTest { + void main() { + group( + 'SizedContainer - Tests', + () { + testWidgets( + 'Child does not exceed the width', + (tester) async { + // prepare the context + await tester.setupTestContext(); + + for (var dummy in SizedContainerDummy().dummies) { + // prepare the widget + final widget = buildSizedContainer(dummy); + await tester.test(widget); + + // get the size of the widget + final widgetSize = tester.getSize(find.byWidget(widget)); + + // check the maximum width + expect( + widgetSize.width, + dummy.parameters['width'], + ); + } + }, + ); + + testWidgets( + 'Child does exceed the width', + (tester) async { + // prepare the context + await tester.setupTestContext(); + + for (var dummy in SizedContainerDummy().dummies) { + // prepare the widget + final widget = Container( + child: dummy.parameters['child'], + ); + await tester.test(widget); + + // get the size of the widget + final widgetSize = tester.getSize(find.byWidget(widget)); + + // check the maximum width + expect( + widgetSize.width, + greaterThanOrEqualTo( + dummy.parameters['width'], + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/counter_widget.dart b/example/lib/widgets/screen/counter_widget.dart similarity index 80% rename from example/lib/widgets/counter_widget.dart rename to example/lib/widgets/screen/counter_widget.dart index 2f02523..16ca3b3 100644 --- a/example/lib/widgets/counter_widget.dart +++ b/example/lib/widgets/screen/counter_widget.dart @@ -2,11 +2,8 @@ import 'package:catalog/catalog.dart'; import 'package:flutter/material.dart'; @Preview( - id: 'CounterWidgetPreview', - path: 'widgets/counter_widget', description: 'Basic counter widget', - usesDummies: true, - dummyParameters: [ + parameters: [ 'counter', ], ) diff --git a/example/lib/widgets/screen/sized_container.dart b/example/lib/widgets/screen/sized_container.dart new file mode 100644 index 0000000..ed51fcd --- /dev/null +++ b/example/lib/widgets/screen/sized_container.dart @@ -0,0 +1,25 @@ +import 'package:catalog/catalog.dart'; +import 'package:flutter/material.dart'; + +@Preview( + description: 'Container with a max width', + parameters: ['width', 'child'], +) +class SizedContainer extends StatelessWidget { + final double width; + final Widget child; + + const SizedContainer({ + super.key, + this.width = 500, + required this.child, + }); + + @override + Widget build(BuildContext context) => Container( + constraints: BoxConstraints( + maxWidth: width, + ), + child: child, + ); +} diff --git a/example/lib/widgets/utils/bottom/catalog/dummy/fab_widget.dummy.dart b/example/lib/widgets/utils/bottom/catalog/dummy/fab_widget.dummy.dart new file mode 100644 index 0000000..d845f27 --- /dev/null +++ b/example/lib/widgets/utils/bottom/catalog/dummy/fab_widget.dummy.dart @@ -0,0 +1,19 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file for modify the preview of FabWidgetPreview +/// + +import 'package:catalog/catalog.dart'; + +class FabWidgetDummy extends PreviewDummy { + @override + List get dummies => [ + Dummy( + parameters: { + 'incrementCounter': () { + // TODO show snackbar + }, + }, + ), + ]; +} diff --git a/example/lib/widgets/utils/bottom/catalog/integration_test/fab_widget_integration_test.dart b/example/lib/widgets/utils/bottom/catalog/integration_test/fab_widget_integration_test.dart new file mode 100644 index 0000000..9e76b0f --- /dev/null +++ b/example/lib/widgets/utils/bottom/catalog/integration_test/fab_widget_integration_test.dart @@ -0,0 +1,40 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget FabWidget +/// + +import 'package:catalog/catalog.dart'; + +import '../dummy/fab_widget.dummy.dart'; +import '../preview/fab_widget.preview.dart'; + +class FabWidgetIntegrationTest { + void main() { + group( + 'FabWidget - IntegrationTest Tests', + () { + testWidgets( + 'Lorem text not found', + (tester) async { + final dummy = FabWidgetDummy().dummies.first; + final widget = buildFabWidget(dummy); + await tester.test(widget); + + expect(find.text('lorem ipsu'), findsNothing); + }, + ); + + testWidgets( + 'Other lorem text not found', + (tester) async { + final dummy = FabWidgetDummy().dummies.first; + final widget = buildFabWidget(dummy); + await tester.test(widget); + + expect(find.text('ipsu lorem'), findsNothing); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/utils/bottom/catalog/preview/fab_widget.preview.dart b/example/lib/widgets/utils/bottom/catalog/preview/fab_widget.preview.dart new file mode 100644 index 0000000..8b87e15 --- /dev/null +++ b/example/lib/widgets/utils/bottom/catalog/preview/fab_widget.preview.dart @@ -0,0 +1,123 @@ +/// AUTOGENERATED FILE. DO NOT EDIT + +import 'package:catalog/catalog.dart'; +import 'package:flutter/material.dart'; +import 'package:example/widgets/utils/bottom/fab_widget.dart'; +import '../dummy/fab_widget.dummy.dart'; + +@Preview( + description: 'Basic fab widget', + parameters: ['incrementCounter'], +) +class FabWidgetPreview extends ParentPreviewWidget { + @override + String get title => 'fab_widget'; + + @override + String get basePath => '/catalog'; + + const FabWidgetPreview({super.key}); + + @override + Widget preview(BuildContext context) { + Catalog().widgetBasicPreviewMap.clear(); + Catalog().widgetDevicePreviewMap.clear(); + + if (FabWidgetDummy().dummies.isEmpty) { + return Container(); + } + + final deviceScreenshotsAvailable = + FabWidgetDummy().deviceScreenshotsAvailable; + final screenshotsAvailable = FabWidgetDummy().screenshotsAvailable; + + int basicScreenshots = screenshotsAvailable - deviceScreenshotsAvailable; + + return ListView( + children: [ + Column( + children: [ + if (basicScreenshots > 0) + Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Card( + clipBehavior: Clip.hardEdge, + child: Container( + padding: const EdgeInsets.all(15), + color: Colors.white, + child: Row( + children: [ + Expanded( + child: Center( + child: Text( + '$basicScreenshots basic screenshots available', + ), + ), + ), + const IconButton( + onPressed: processBasicScreenshots, + icon: Icon( + Icons.screenshot, + ), + ) + ], + ), + ), + ), + ), + ), + if (deviceScreenshotsAvailable > 0) + Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Card( + clipBehavior: Clip.hardEdge, + child: Container( + padding: const EdgeInsets.all(15), + color: Colors.white, + child: Row( + children: [ + Expanded( + child: Center( + child: Text( + '$deviceScreenshotsAvailable device screenshots available', + ), + ), + ), + const IconButton( + onPressed: processDeviceScreenshots, + icon: Icon( + Icons.screenshot, + ), + ) + ], + ), + ), + ), + ), + ), + for (int i = 0; i < FabWidgetDummy().dummies.length; i++) + PreviewBoundary( + widgetKey: GlobalKey(), + dummyBuilder: () => FabWidgetDummy().dummies[i], + builder: (BuildContext context, Dummy dummy) { + return buildFabWidget(dummy); + }, + ), + ], + ) + ], + ); + } +} + +FabWidget buildFabWidget(Dummy dummy) { + return FabWidget( + incrementCounter: dummy.parameters['incrementCounter'], + ); +} diff --git a/example/lib/widgets/utils/bottom/catalog/test/fab_widget_test.dart b/example/lib/widgets/utils/bottom/catalog/test/fab_widget_test.dart new file mode 100644 index 0000000..be1de98 --- /dev/null +++ b/example/lib/widgets/utils/bottom/catalog/test/fab_widget_test.dart @@ -0,0 +1,40 @@ +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget FabWidget +/// + +import 'package:catalog/catalog.dart'; + +import '../dummy/fab_widget.dummy.dart'; +import '../preview/fab_widget.preview.dart'; + +class FabWidgetTest { + void main() { + group( + 'FabWidget - Tests', + () { + testWidgets( + 'Lorem text not found', + (tester) async { + final dummy = FabWidgetDummy().dummies.first; + final widget = buildFabWidget(dummy); + await tester.test(widget); + + expect(find.text('lorem ipsu'), findsNothing); + }, + ); + + testWidgets( + 'Other lorem text not found', + (tester) async { + final dummy = FabWidgetDummy().dummies.first; + final widget = buildFabWidget(dummy); + await tester.test(widget); + + expect(find.text('ipsu lorem'), findsNothing); + }, + ); + }, + ); + } +} diff --git a/example/lib/widgets/fab_widget.dart b/example/lib/widgets/utils/bottom/fab_widget.dart similarity index 79% rename from example/lib/widgets/fab_widget.dart rename to example/lib/widgets/utils/bottom/fab_widget.dart index 67b6f0a..0d05523 100644 --- a/example/lib/widgets/fab_widget.dart +++ b/example/lib/widgets/utils/bottom/fab_widget.dart @@ -2,12 +2,8 @@ import 'package:catalog/catalog.dart'; import 'package:flutter/material.dart'; @Preview( - id: 'FabWidgetPreview', - path: 'widgets/fab_widget', description: 'Basic fab widget', - parameters: { - 'incrementCounter': 'void_function_snackbar', - }, + parameters: ['incrementCounter'], ) class FabWidget extends StatelessWidget { final Function() incrementCounter; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 436588c..7b9e54f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,14 +12,17 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 - stringcare: ^0.1.6 # android ios linux macos web windows + stringcare: ^1.0.0 # android ios linux macos web windows catalog: path: ../ dev_dependencies: - flutter_test: + integration_test: sdk: flutter - flutter_lints: ^4.0.0 + flutter_test: + sdk: + flutter + flutter_lints: ^5.0.0 stringcare: lang: diff --git a/example/test/catalog_widget_test.dart b/example/test/catalog_widget_test.dart new file mode 100644 index 0000000..8c11d3d --- /dev/null +++ b/example/test/catalog_widget_test.dart @@ -0,0 +1,22 @@ +/// AUTOGENERATED FILE. DO NOT EDIT + +import 'package:example/widgets/utils/bottom/catalog/test/fab_widget_test.dart' + as aleo; +import 'package:example/widgets/other_utils/bottom/catalog/test/warning_info_widget_test.dart' + as dgjg; +import 'package:example/widgets/screen/catalog/test/sized_container_test.dart' + as ntbg; +import 'package:example/widgets/screen/catalog/test/body_widget_test.dart' + as wwjj; +import 'package:example/widgets/screen/catalog/test/counter_widget_test.dart' + as dqlj; +import 'package:example/widgets/catalog/test/main_screen_test.dart' as gpwz; + +void main() { + aleo.FabWidgetTest().main(); + dgjg.WarningInfoWidgetTest().main(); + ntbg.SizedContainerTest().main(); + wwjj.BodyWidgetTest().main(); + dqlj.CounterWidgetTest().main(); + gpwz.MainScreenTest().main(); +} diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 092d222..30fe7e9 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -5,11 +5,10 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. +import 'package:example/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:example/main.dart'; - void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. diff --git a/example/test_driver/integration_test.dart b/example/test_driver/integration_test.dart new file mode 100644 index 0000000..b38629c --- /dev/null +++ b/example/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/lib/.DS_Store b/lib/.DS_Store deleted file mode 100644 index 013242b..0000000 Binary files a/lib/.DS_Store and /dev/null differ diff --git a/lib/catalog.dart b/lib/catalog.dart index 67cdc1f..6816675 100644 --- a/lib/catalog.dart +++ b/lib/catalog.dart @@ -1,4 +1,4 @@ -library catalog; +library; import 'dart:convert'; import 'dart:typed_data'; @@ -11,13 +11,15 @@ import 'src/builders/preview/preview_boundary.dart'; import 'src/builders/preview/preview_render_widget.dart'; export 'package:catalog/src/annotations/preview.dart'; +export 'package:catalog/src/builders/catalog/component_node.dart'; +export 'package:catalog/src/builders/catalog/preview_scaffold.dart'; +export 'package:catalog/src/builders/catalog/tree_elements_builder.dart'; export 'package:catalog/src/builders/device/device.dart'; export 'package:catalog/src/builders/dummy/dummy.dart'; export 'package:catalog/src/builders/dummy/dummy_text.dart'; +export 'package:catalog/src/builders/dummy/preview_dummy.dart'; export 'package:catalog/src/builders/preview/parent_preview_widget.dart'; export 'package:catalog/src/builders/preview/preview_boundary.dart'; -export 'package:catalog/src/builders/dummy/preview_dummy.dart'; -export 'package:catalog/src/builders/catalog/preview_scaffold.dart'; export 'package:catalog/src/builders/screenshots/background.dart'; export 'package:catalog/src/builders/screenshots/op/screenshot_process.dart'; export 'package:catalog/src/builders/screenshots/screenshot.dart'; @@ -28,11 +30,12 @@ export 'package:catalog/src/builders/screenshots/types/apple/i_phone_55.dart'; export 'package:catalog/src/builders/screenshots/types/apple/i_phone_65.dart'; export 'package:catalog/src/builders/screenshots/types/apple/macos.dart'; export 'package:catalog/src/catalog_runner.dart'; -export 'package:catalog/src/builders/catalog/component_node.dart'; -export 'package:catalog/src/utils/constants.dart'; -export 'package:catalog/src/embed/flutter_fanacy_tree_view/flutter_fancy_tree_view.dart'; export 'package:catalog/src/extensions/locale_ext.dart'; +export 'package:catalog/src/extensions/widget_test_ext.dart'; +export 'package:catalog/src/utils/constants.dart'; export 'package:device_frame/device_frame.dart'; +export 'package:flutter_test/flutter_test.dart'; +export 'package:integration_test/integration_test.dart'; export 'package:go_router/go_router.dart'; class Catalog { diff --git a/lib/src/.DS_Store b/lib/src/.DS_Store deleted file mode 100644 index d94b6b0..0000000 Binary files a/lib/src/.DS_Store and /dev/null differ diff --git a/lib/src/annotations/internal_preview.dart b/lib/src/annotations/internal_preview.dart new file mode 100644 index 0000000..260dacc --- /dev/null +++ b/lib/src/annotations/internal_preview.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import 'package:catalog/src/base/serial.dart'; + +class InternalPreview implements Serial { + final String id; + final String path; + final String description; + final List parameters; + + const InternalPreview({ + this.id = '', + this.path = '', + this.description = '', + this.parameters = const [], + }); + + @override + InternalPreview fromJson(Map json) => InternalPreview( + id: json['id'] ?? '', + path: json['path'] ?? '', + description: json['description'] ?? '', + parameters: Serial.listObjectFromBasicType( + json['parameters'] ?? [], + ), + ); + + @override + String getId() => id; + + @override + InternalPreview instance() => const InternalPreview(id: '', path: ''); + + @override + Map toJson() => { + 'id': id, + 'path': path, + 'description': description, + 'parameters': parameters, + }; + + @override + InternalPreview fromString(String value) { + Map map = jsonDecode(value); + return fromJson(map); + } + + @override + String stringValue() { + var map = toJson(); + return jsonEncode(map); + } + + InternalPreview copyWith({ + String? id, + String? path, + String? description, + List? parameters, + }) => + InternalPreview( + id: id ?? this.id, + path: path ?? this.path, + description: description ?? this.description, + parameters: parameters ?? this.parameters, + ); +} diff --git a/lib/src/annotations/preview.dart b/lib/src/annotations/preview.dart index 89bd98e..b85c5ef 100644 --- a/lib/src/annotations/preview.dart +++ b/lib/src/annotations/preview.dart @@ -3,51 +3,31 @@ import 'dart:convert'; import 'package:catalog/src/base/serial.dart'; class Preview implements Serial { - final String id; - final String path; final String description; - final bool usesDummies; - final List dummyParameters; - final List listParameters; - final Map parameters; + final List parameters; const Preview({ - required this.id, - required this.path, this.description = '', - this.usesDummies = false, - this.parameters = const {}, - this.listParameters = const [], - this.dummyParameters = const [], + this.parameters = const [], }); @override Preview fromJson(Map json) => Preview( - id: json['id'] ?? '', - path: json['path'] ?? '', description: json['description'] ?? '', - usesDummies: json['usesDummies'] ?? false, - dummyParameters: Serial.listObjectFromBasicType( - json['dummyParameters'] ?? [], + parameters: Serial.listObjectFromBasicType( + json['parameters'] ?? [], ), - listParameters: json['listParameters'] ?? [], - parameters: json['parameters'] ?? {}, ); @override - String getId() => id; + String getId() => ''; @override - Preview instance() => const Preview(id: '', path: ''); + Preview instance() => const Preview(); @override Map toJson() => { - 'id': id, - 'path': path, 'description': description, - 'usesDummies': usesDummies, - 'listParameters': listParameters, - 'dummyParameters': dummyParameters, 'parameters': parameters, }; @@ -62,4 +42,13 @@ class Preview implements Serial { var map = toJson(); return jsonEncode(map); } + + Preview copyWith({ + String? description, + List? parameters, + }) => + Preview( + description: description ?? this.description, + parameters: parameters ?? this.parameters, + ); } diff --git a/lib/src/base/serial.dart b/lib/src/base/serial.dart index 8e0739f..d6ed282 100644 --- a/lib/src/base/serial.dart +++ b/lib/src/base/serial.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:catalog/src/annotations/internal_preview.dart'; import 'package:catalog/src/annotations/preview.dart'; import 'package:catalog/src/builders/catalog/built_component.dart'; import 'package:catalog/src/builders/catalog/component_node.dart'; @@ -78,7 +79,8 @@ abstract class Serial { static List instances = [ ComponentNode(), BuiltComponent(), - const Preview(id: '', path: ''), + const Preview(), + const InternalPreview(id: '', path: ''), ]; static Map internalLinkerToMap(map) { diff --git a/lib/src/bin/builders/catalog_builder.dart b/lib/src/bin/builders/catalog_builder.dart new file mode 100644 index 0000000..0d48fc4 --- /dev/null +++ b/lib/src/bin/builders/catalog_builder.dart @@ -0,0 +1,154 @@ +import 'dart:io'; + +import 'package:catalog/src/annotations/internal_preview.dart'; +import 'package:catalog/src/builders/catalog/built_component.dart'; +import 'package:catalog/src/builders/catalog/component_node.dart'; + +Future findPreviewClassName(String path) async { + try { + File file = File(path); + final content = await file.readAsString(); + return '${content.split("class ")[1].split(" extends ParentPreviewWidget").first.trim()}()'; + } catch (e) { + print(e); + return null; + } +} + +Future createPage( + String appId, + String base, + String outputPath, + String outputFile, + String prefix, + InternalPreview preview, + String import, + String name, +) async { + try { + var directory = Directory(outputPath); + await directory.create(recursive: true); + var id = preview.id; + File file = File(outputPath + outputFile.replaceAll('.$prefix.', '.')); + + final clazzName = name.replaceAll('()', ''); + final pageClass = '${clazzName}PreviewPageDummy'; + + print( + '📃 Generating Catalog page for $clazzName - $pageClass (${file.path})'); + + var content = ''' +/// AUTOGENERATED FILE. DO NOT EDIT + +import 'package:flutter/material.dart'; +import '$import'; + +class $pageClass extends StatefulWidget { + + static String routeName = '$id'; + + const $pageClass({super.key}); + + @override + ${pageClass}State createState() => ${pageClass}State(); +} + +class ${pageClass}State extends State<$pageClass> { + @override + Widget build(BuildContext context) { + return const $clazzName(); + } +} + '''; + file.writeAsStringSync(content); + + var p = file.path.split(base)[1]; + var package = 'package:$appId$p'; + + return BuiltComponent( + path: file.path, + route: preview.path, + package: package, + clazzName: '${name.replaceAll('()', '')}PreviewPageDummy', + preview: preview, + ); + } catch (e) { + print(e); + return null; + } +} + +ComponentNode buildTreeFromMap( + String pageRoute, + Map> componentsMap, +) { + ComponentNode root = ComponentNode(id: pageRoute, route: '/'); + + ComponentNode findOrCreateNode( + ComponentNode current, + List pathParts, + String fullPath, + ) { + if (pathParts.isEmpty) { + return current; + } + + String part = pathParts.removeAt(0); + if (!current.children.containsKey(part)) { + current.children[part] = ComponentNode( + id: part, + route: fullPath.substring(0, fullPath.indexOf(part) + part.length), + ); + } + + return findOrCreateNode(current.children[part]!, pathParts, fullPath); + } + + componentsMap.forEach((path, builtComponents) { + List pathParts = + path.split('/').where((part) => part.isNotEmpty).toList(); + + ComponentNode currentNode = findOrCreateNode(root, pathParts, path); + + for (var component in builtComponents) { + currentNode.builtComponents[component.path] = component; + } + }); + + return root; +} + +Future generateCatalogReadme( + String basePath, + dynamic config, +) async { + String readmeFolderPath = './$basePath${config['base']}/${config['output']}'; + + await Directory(readmeFolderPath).create(recursive: true); + + File file = File('$readmeFolderPath/README.md'); + + print('📃 Updating catalog README.md (${file.path})'); + + var content = ''' +# Catalog in example + +This is your catalog in example. It shows the widgets that contain `@Preview` in the header. + +You should not manipulate it yourself. If you observe any unexpected behavior please [open an issue on Github](https://github.com/landamessenger/catalog/issues). We will try to fix it as soon as possible. + +Generate dummies, previews (override every time) with: + +```bash +dart run catalog:preview +``` + +All the above plus catalog generation. + +```bash +dart run catalog:build +``` + '''; + + await file.writeAsString(content); +} diff --git a/lib/src/bin/builders/common_builder.dart b/lib/src/bin/builders/common_builder.dart new file mode 100644 index 0000000..5487ac0 --- /dev/null +++ b/lib/src/bin/builders/common_builder.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +Future findClassName(String path) async { + try { + File file = File(path); + final content = await file.readAsString(); + return '${content.split("class ")[1].split(" ").first.trim()}()'; + } catch (e) { + print(e); + return null; + } +} + +Future findPreviewAnnotation(String path) async { + try { + File file = File(path); + final content = await file.readAsString(); + return '@Preview${content.split("@Preview")[1].split(''') +class''').first.trim()})'; + } catch (e) { + print(e); + return null; + } +} diff --git a/bin/preview_builder/dummy_builder.dart b/lib/src/bin/builders/dummy_builder.dart similarity index 72% rename from bin/preview_builder/dummy_builder.dart rename to lib/src/bin/builders/dummy_builder.dart index b34e5f7..0ecd87d 100644 --- a/bin/preview_builder/dummy_builder.dart +++ b/lib/src/bin/builders/dummy_builder.dart @@ -9,11 +9,11 @@ Future generateDummy( String fileName = srcPath.split('/').last; String name = fileName.split('.').first; String dirPath = srcPath.replaceAll(fileName, ''); - String previewPath = '${dirPath}preview/dummy/'; + String dummyPath = '${dirPath}catalog/dummy/'; - await Directory(previewPath).create(recursive: true); + await Directory(dummyPath).create(recursive: true); - String dummyFile = '$previewPath$name.dummy.dart'; + String dummyFile = '$dummyPath$name.dummy.dart'; File file = File(dummyFile); if (await file.exists()) { @@ -27,7 +27,11 @@ Future generateDummy( var content = ''' /// AUTOGENERATED FILE. /// -/// Use this file for modify the preview of ${clazz}Preview +/// Use this file to prepare the preview and the tests: +/// +/// - ${clazz}Preview +/// - ${clazz}Test +/// - ${clazz}IntegrationTest /// import 'package:catalog/catalog.dart'; diff --git a/bin/preview_builder/preview_builder.dart b/lib/src/bin/builders/preview_builder.dart similarity index 57% rename from bin/preview_builder/preview_builder.dart rename to lib/src/bin/builders/preview_builder.dart index 70b47c7..4ea79b3 100644 --- a/bin/preview_builder/preview_builder.dart +++ b/lib/src/bin/builders/preview_builder.dart @@ -1,35 +1,19 @@ import 'dart:io'; -import 'package:catalog/src/annotations/preview.dart'; - -Future findClassName(String path) async { - try { - File file = File(path); - final content = await file.readAsString(); - return '${content.split("class ")[1].split(" ").first.trim()}()'; - } catch (e) { - print(e); - return null; - } -} +import 'package:catalog/src/annotations/internal_preview.dart'; Future generatePreview( + dynamic config, String srcPath, String classImport, String previewAnnotation, String className, String prefix, - Preview preview, + InternalPreview preview, ) async { var clazz = className.replaceAll('()', ''); - var widgetCompose = ''; - - if (preview.usesDummies) { - widgetCompose = dummyWidgetContent(className, preview); - } else { - widgetCompose = basicWidgetContent(className, preview); - } + var widgetCompose = dummyWidgetContent(className, preview); String fileName = srcPath.split('/').last; String name = fileName.split('.').first; @@ -40,22 +24,19 @@ Future generatePreview( import 'package:catalog/catalog.dart'; import 'package:flutter/material.dart'; import '$classImport'; -${preview.usesDummies ? '''import 'dummy/$name.dummy.dart';''' : ''} +import '../dummy/$name.dummy.dart'; $previewAnnotation class ${clazz}Preview extends ParentPreviewWidget { - const ${clazz}Preview({super.key}); - ${!preview.usesDummies ? ''' @override - Widget preview(BuildContext context) => Container( - constraints: const BoxConstraints(maxWidth: 700), - child: ${(preview.parameters.containsKey('child') || preview.parameters.isEmpty) ? 'const' : ''} $widgetCompose, - ); - ''' : ''} + String get title => '$name'; - ${preview.usesDummies ? ''' + @override + String get basePath => '/${config['pageRoute']}'; + const ${clazz}Preview({super.key}); + @override Widget preview(BuildContext context) { Catalog().widgetBasicPreviewMap.clear(); @@ -140,19 +121,23 @@ class ${clazz}Preview extends ParentPreviewWidget { ), ), for (int i = 0; i < ${clazz}Dummy().dummies.length; i++) - ${dummyWidgetBuilder(clazz, widgetCompose)} + ${dummyWidgetBuilder(clazz)} ], ) ], ); } - ''' : ''} + +} + +$clazz build$clazz(Dummy dummy) { + return $widgetCompose; } '''; String dirPath = srcPath.replaceAll(fileName, ''); - String previewPath = '${dirPath}preview/'; + String previewPath = '${dirPath}catalog/preview/'; await Directory(previewPath).create(recursive: true); @@ -164,90 +149,28 @@ class ${clazz}Preview extends ParentPreviewWidget { await file.writeAsString(content); } -String dummyWidgetBuilder(String clazz, String widgetCompose) { +String dummyWidgetBuilder(String clazz) { return ''' PreviewBoundary( widgetKey: GlobalKey(), dummyBuilder: () => ${clazz}Dummy().dummies[i], builder: (BuildContext context, Dummy dummy) { - return $widgetCompose; + return build$clazz(dummy); }, ), '''; } -String basicWidgetContent(String className, Preview preview) { - var clazz = className.replaceAll('()', ''); - - var widgetCompose = '$clazz('; - - for (dynamic element in preview.listParameters) { - if (element is String) { - widgetCompose += '\'$element\','; - } - } - var params = preview.parameters.entries.toList(); - if (params.isNotEmpty) { - widgetCompose += ''; - for (MapEntry entry in params) { - if (entry.value is String) { - if (entry.value == 'void_function_snackbar') { - widgetCompose += '''${entry.key}: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - duration: Duration(seconds: 2), - content: Text( - '${entry.key} event', - ), - ), - ); - },'''; - } else if (entry.value == 'dummy_text_small') { - widgetCompose += '''child: DummyText(),'''; - } else { - widgetCompose += '${entry.key}: \'${entry.value}\','; - } - } - } - widgetCompose += ''; - } - - widgetCompose += ')'; - - return widgetCompose; -} - -String dummyWidgetContent(String className, Preview preview) { +String dummyWidgetContent(String className, InternalPreview preview) { var clazz = className.replaceAll('()', ''); var widgetCompose = '$clazz('; - /* - for (dynamic element in preview.listParameters) { - if (element is String) { - widgetCompose += '\'$element\','; - } - }*/ - var params = preview.dummyParameters; + var params = preview.parameters; if (params.isNotEmpty) { widgetCompose += ''; for (String key in params) { - if (key == 'void_function_snackbar') { - widgetCompose += '''$key: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - duration: Duration(seconds: 2), - content: Text( - 'callback $key', - ), - ), - ); - },'''; - } else if (key == 'dummy_text_small') { - widgetCompose += '''child: const DummyText(),'''; - } else { - widgetCompose += '''$key: dummy.parameters['$key'],'''; - } + widgetCompose += '''$key: dummy.parameters['$key'],'''; } widgetCompose += ''; } @@ -256,15 +179,3 @@ String dummyWidgetContent(String className, Preview preview) { return widgetCompose; } - -Future findPreviewAnnotation(String path) async { - try { - File file = File(path); - final content = await file.readAsString(); - return '@Preview${content.split("@Preview")[1].split(''') -class''').first.trim()})'; - } catch (e) { - print(e); - return null; - } -} diff --git a/lib/src/bin/builders/test_builder.dart b/lib/src/bin/builders/test_builder.dart new file mode 100644 index 0000000..fb41dd5 --- /dev/null +++ b/lib/src/bin/builders/test_builder.dart @@ -0,0 +1,278 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:catalog/src/bin/utils/test_builder_info.dart'; + +Future generateTest( + dynamic config, + String srcPath, + String className, + String classImport, + String prefix, +) async { + var clazz = className.replaceAll('()', ''); + + String fileName = srcPath.split('/').last; + String name = fileName.split('.').first; + String dirPath = srcPath.replaceAll(fileName, ''); + String testPath = '${dirPath}catalog/test/'; + + await Directory(testPath).create(recursive: true); + + String testFile = '$testPath${name}_test.dart'; + File file = File(testFile); + + var importParts = classImport.split('/'); + importParts.removeAt(importParts.length - 1); + importParts.add(config['output']); + importParts.add('test'); + importParts.add('${name}_test.dart'); + + if (await file.exists()) { + print( + '🧪 👌 Test file already exist for $clazz - ${clazz}Test ($testFile)'); + return TestBuilderInfo( + alias: buildTestAlias(4), + clazzName: '${clazz}Test', + import: importParts.join('/'), + ); + } + + print('🧪 Generating test for $clazz - ${clazz}Test ($testFile)'); + + var content = ''' +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget $clazz +/// + +import 'package:catalog/catalog.dart'; + +import '../dummy/$name.dummy.dart'; +import '../preview/$name.$prefix.dart'; + +class ${clazz}Test { + void main() { + group( + '$clazz - Tests', + () { + testWidgets( + 'Lorem text not found', + (tester) async { + // prepare the context + await tester.setupTestContext(); + + // prepare the widget + final dummy = ${clazz}Dummy().dummies.first; + final widget = build$clazz(dummy); + await tester.test(widget); + + // check + expect(find.text('cómo están los máquinas'), findsNothing); + }, + ); + + testWidgets( + 'Other lorem text not found', + (tester) async { + // prepare the context + await tester.setupTestContext(); + + // prepare the widget + final dummy = ${clazz}Dummy().dummies.first; + final widget = build$clazz(dummy); + await tester.test(widget); + + // check + expect(find.text('lo primero de todo'), findsNothing); + }, + ); + }, + ); + } +} + + '''; + + await file.writeAsString(content); + + return TestBuilderInfo( + alias: buildTestAlias(4), + clazzName: '${clazz}Test', + import: importParts.join('/'), + ); +} + +Future generateIntegrationTest( + dynamic config, + String srcPath, + String className, + String classImport, + String prefix, +) async { + var clazz = className.replaceAll('()', ''); + + String fileName = srcPath.split('/').last; + String name = fileName.split('.').first; + String dirPath = srcPath.replaceAll(fileName, ''); + String testPath = '${dirPath}catalog/integration_test/'; + + await Directory(testPath).create(recursive: true); + + String testFile = '$testPath${name}_integration_test.dart'; + File file = File(testFile); + + var importParts = classImport.split('/'); + importParts.removeAt(importParts.length - 1); + importParts.add(config['output']); + importParts.add('integration_test'); + importParts.add('${name}_integration_test.dart'); + + if (await file.exists()) { + print( + '🧪 👌 Test file already exist for $clazz - ${clazz}IntegrationTest ($testFile)'); + return TestBuilderInfo( + alias: buildTestAlias(4), + clazzName: '${clazz}IntegrationTest', + import: importParts.join('/'), + ); + } + + print('🧪 Generating test for $clazz - ${clazz}Test ($testFile)'); + + var content = ''' +/// AUTOGENERATED FILE. +/// +/// Use this file to test the widget $clazz +/// + +import 'package:catalog/catalog.dart'; + +import '../dummy/$name.dummy.dart'; +import '../preview/$name.$prefix.dart'; + +class ${clazz}IntegrationTest { + void main() { + group( + '$clazz - IntegrationTest Tests', + () { + testWidgets( + 'Lorem text not found', + (tester) async { + // prepare the context + await tester.setupIntegrationTestContext(); + + // prepare the widget + final dummy = ${clazz}Dummy().dummies.first; + final widget = build$clazz(dummy); + await tester.test(widget); + + // check + expect(find.text('cómo están los máquinas'), findsNothing); + }, + ); + + testWidgets( + 'Other lorem text not found', + (tester) async { + // prepare the context + await tester.setupIntegrationTestContext(); + + // prepare the widget + final dummy = ${clazz}Dummy().dummies.first; + final widget = build$clazz(dummy); + await tester.test(widget); + + // check + expect(find.text('lo primero de todo'), findsNothing); + }, + ); + }, + ); + } +} + + '''; + + await file.writeAsString(content); + + return TestBuilderInfo( + alias: buildTestAlias(4), + clazzName: '${clazz}IntegrationTest', + import: importParts.join('/'), + ); +} + +Future generateMainTest( + String basePath, + List tests, +) async { + File file = File('./${basePath}test/catalog_widget_test.dart'); + + print('🧪 Updating catalog test collector (${file.path})'); + + var content = ''' +/// AUTOGENERATED FILE. DO NOT EDIT + +${tests.map((t) { + return 'import \'${t.import}\' as ${t.alias};'; + }).join('\n')} + +void main() { + ${tests.map((t) { + return '${t.alias}.${t.clazzName}().main();'; + }).join('\n')} +} + + '''; + + await file.writeAsString(content); +} + +Future generateMainIntegrationTest( + String basePath, + List tests, +) async { + String testPath = './${basePath}integration_test/'; + + await Directory(testPath).create(recursive: true); + + File file = File( + './${basePath}integration_test/catalog_widget_integration_test.dart'); + + print('🧪 Updating catalog integration test collector (${file.path})'); + + var content = ''' +/// AUTOGENERATED FILE. DO NOT EDIT + +/// Launch on Android or iOS as usual. +/// Launch on Web with: +/// +/// chromedriver --port=4444 +/// flutter drive --driver=test_driver/integration_test.dart --target=integration_test/catalog_widget_integration_test.dart -d chrome + +import 'package:integration_test/integration_test.dart'; + +${tests.map((t) { + return 'import \'${t.import}\' as ${t.alias};'; + }).join('\n')} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + ${tests.map((t) { + return '${t.alias}.${t.clazzName}().main();'; + }).join('\n')} +} + + '''; + + await file.writeAsString(content); +} + +String buildTestAlias(int size) { + const chars = 'abcdefghijklmnopqrstuvwxyz'; + Random random = Random(); + return String.fromCharCodes(Iterable.generate( + size, (_) => chars.codeUnitAt(random.nextInt(chars.length)))); +} diff --git a/lib/src/bin/tasks/base/base_task.dart b/lib/src/bin/tasks/base/base_task.dart new file mode 100644 index 0000000..4c5e2d4 --- /dev/null +++ b/lib/src/bin/tasks/base/base_task.dart @@ -0,0 +1,3 @@ +abstract class BaseTask { + Future work(List args); +} diff --git a/lib/src/bin/tasks/integration_test_task.dart b/lib/src/bin/tasks/integration_test_task.dart new file mode 100644 index 0000000..080df25 --- /dev/null +++ b/lib/src/bin/tasks/integration_test_task.dart @@ -0,0 +1,25 @@ +import 'package:catalog/src/bin/utils/messages.dart'; + +import 'base/base_task.dart'; +import 'tasks/format_task.dart'; +import 'tasks/integration_test_task.dart' as test; + +class IntegrationTestTask extends BaseTask { + final tasks = [ + test.IntegrationTestTask(), + FormatTask(), + ]; + + @override + Future work(List args) async { + for (BaseTask task in tasks) { + try { + print('\n - Running ${task.runtimeType.toString()} \n'); + await task.work(args); + } catch (e) { + print(e); + } + } + print(commonMessage('🧪📱 Integration tests generated')); + } +} diff --git a/bin/tasks/main_task.dart b/lib/src/bin/tasks/main_task.dart similarity index 53% rename from bin/tasks/main_task.dart rename to lib/src/bin/tasks/main_task.dart index 8fcf816..e27c8ba 100644 --- a/bin/tasks/main_task.dart +++ b/lib/src/bin/tasks/main_task.dart @@ -1,25 +1,35 @@ +import 'package:catalog/src/bin/utils/messages.dart'; + import 'base/base_task.dart'; import 'tasks/catalog_task.dart'; import 'tasks/format_task.dart'; +import 'tasks/integration_test_task.dart'; import 'tasks/preview_task.dart'; +import 'tasks/test_task.dart'; class MainTask extends BaseTask { final tasks = [ PreviewTask(), + TestTask(), + IntegrationTestTask(), CatalogTask(), FormatTask(), ]; @override - Future work() async { + Future work(List args) async { for (BaseTask task in tasks) { try { print('\n - Running ${task.runtimeType.toString()} \n'); - await task.work(); + await task.work(args); } catch (e) { print(e); } } - print('\n Previews and catalog generated \n'); + print( + commonMessage( + 'Previews, tests, integration tests and catalog generated', + ), + ); } } diff --git a/bin/tasks/preview_task.dart b/lib/src/bin/tasks/preview_task.dart similarity index 75% rename from bin/tasks/preview_task.dart rename to lib/src/bin/tasks/preview_task.dart index c4db964..519e8f7 100644 --- a/bin/tasks/preview_task.dart +++ b/lib/src/bin/tasks/preview_task.dart @@ -9,15 +9,15 @@ class PreviewTask extends BaseTask { ]; @override - Future work() async { + Future work(List args) async { for (BaseTask task in tasks) { try { print('\n - Running ${task.runtimeType.toString()} \n'); - await task.work(); + await task.work(args); } catch (e) { print(e); } } - print('\n Previews generated \n'); + print('\n Previews and tests generated \n'); } } diff --git a/lib/src/bin/tasks/tasks/catalog_task.dart b/lib/src/bin/tasks/tasks/catalog_task.dart new file mode 100644 index 0000000..1a0bebd --- /dev/null +++ b/lib/src/bin/tasks/tasks/catalog_task.dart @@ -0,0 +1,160 @@ +import 'dart:io'; + +import 'package:catalog/src/base/serial.dart'; +import 'package:catalog/src/bin/builders/catalog_builder.dart'; +import 'package:catalog/src/bin/tasks/base/base_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; +import 'package:catalog/src/builders/catalog/built_component.dart'; +import 'package:catalog/src/builders/catalog/component_node.dart'; + +class CatalogTask extends BaseTask { + @override + Future work(List args) async { + final base = args.isEmpty ? '' : '${args.first}/'; + + var appId = loadId(base); + var config = loadConfigFile(base); + + final page = config['pageFile'] ?? 'catalog_component.dart'; + final pageName = config['pageName'] ?? 'CatalogComponent'; + final pageRoute = config['pageRoute'] ?? 'catalog'; + final prefixValue = config['prefix'] ?? 'preview'; + + final dir = Directory('./$base${config['base']}'); + await dir.create(recursive: true); + + final dirOutPut = Directory('./$base${config['base']}/${config['output']}'); + await dirOutPut.create(recursive: true); + + final List entities = await dir + .list( + recursive: true, + ) + .toList(); + + final files = []; + + for (FileSystemEntity fileSystemEntity in entities) { + if (fileSystemEntity.path.contains('.$prefixValue.')) { + files.add(fileSystemEntity); + } + } + + var map = >{}; + + for (FileSystemEntity fileSystemEntity in files) { + final File file = File(fileSystemEntity.path); + var p = file.path.split(config['base'])[1]; + var package = 'package:$appId$p'; + var className = await findPreviewClassName(file.path); + + if (className == null) { + print('No class name found ${file.path}'); + continue; + } + + var preview = await previewOnFile( + base, + config, + file.path, + ); + + if (preview == null) { + print('No preview data found ${file.path}'); + continue; + } + + var outputFolder = + './$base${config['base']}/${config['output']}/${preview.path}/'; + + var build = await createPage( + appId, + config['base'], + outputFolder, + p.split('/').last, + prefixValue, + preview, + package, + className, + ); + + if (build == null) { + print('No build for $className'); + continue; + } + + if (map[build.route] == null) { + map[build.route] = []; + } + (map[build.route] as List).add(build); + } + + ComponentNode node = buildTreeFromMap(pageRoute, map); + + final File assetsConfig = File('./$base${config['runtimeConfigHolder']}'); + assetsConfig.writeAsStringSync(node.toJson().toPrettyString()); + + final File catalogFile = File( + './$base${config['base']}/${config['output']}/$page', + ); + + // print(node.routerBuilder); + + var catalogContent = ''' +/// AUTOGENERATED FILE. DO NOT EDIT + +import 'package:flutter/material.dart'; +import 'package:catalog/catalog.dart'; +${node.imports} + +class $pageName extends StatefulWidget { + static String routeName = '/$pageRoute'; + + static GoRoute route = ${node.routerBuilder}; + + const $pageName({super.key}); + + @override + ${pageName}State createState() => ${pageName}State(); +} + +class ${pageName}State extends State<$pageName> { + + @override + Widget build(BuildContext context) { + return FutureBuilder( + initialData: null, + future: Catalog().get(context), + builder: (context, data) { + if (!data.hasData || data.data == null) { + return Container(); + } + final node = data.data as ComponentNode; + return PreviewScaffold( + basePath: CatalogComponent.routeName, + onBackPressed: Catalog().onBackPressed, + child: ListView( + children: [ + buildTreeWidget( + context, + CatalogComponent.routeName, + node, + 0, + ) + ], + ), + ); + }, + ); + } +} + + ''' + .replaceAll('"""', '\'\'\'') + .replaceAll(' []', ' const []'); + + catalogFile.writeAsStringSync(catalogContent); + + await generateCatalogReadme(base, config); + } +} diff --git a/lib/src/bin/tasks/tasks/format_task.dart b/lib/src/bin/tasks/tasks/format_task.dart new file mode 100644 index 0000000..59579a9 --- /dev/null +++ b/lib/src/bin/tasks/tasks/format_task.dart @@ -0,0 +1,50 @@ +import 'dart:io'; + +import 'package:catalog/src/bin/utils/configuration.dart'; + +import '../base/base_task.dart'; + +class FormatTask extends BaseTask { + @override + Future work(List args) async { + final base = args.isEmpty ? '' : '${args.first}/'; + + var config = loadConfigFile(base); + + final File file = + File('./$base${config['base']}/${config['output']}/process.dart'); + if (file.existsSync()) await file.delete(); + + var resultFix = await Process.run( + 'dart', + ['fix', '--apply'], + workingDirectory: Directory.current.path, + ); + stdout.write(resultFix.stdout); + stderr.write(resultFix.stderr); + + var resultFormatLib = await Process.run( + 'dart', + ['format', 'lib/'], + workingDirectory: Directory.current.path, + ); + stdout.write(resultFormatLib.stdout); + stderr.write(resultFormatLib.stderr); + + var resultFormatTest = await Process.run( + 'dart', + ['format', 'test/'], + workingDirectory: Directory.current.path, + ); + stdout.write(resultFormatTest.stdout); + stderr.write(resultFormatTest.stderr); + + var resultFormatInstrumentedTest = await Process.run( + 'dart', + ['format', 'integration_test/'], + workingDirectory: Directory.current.path, + ); + stdout.write(resultFormatInstrumentedTest.stdout); + stderr.write(resultFormatInstrumentedTest.stderr); + } +} diff --git a/lib/src/bin/tasks/tasks/integration_test_task.dart b/lib/src/bin/tasks/tasks/integration_test_task.dart new file mode 100644 index 0000000..78138ef --- /dev/null +++ b/lib/src/bin/tasks/tasks/integration_test_task.dart @@ -0,0 +1,71 @@ +import 'dart:io'; + +import 'package:catalog/src/bin/builders/common_builder.dart'; +import 'package:catalog/src/bin/builders/test_builder.dart'; +import 'package:catalog/src/bin/tasks/base/base_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; +import 'package:catalog/src/bin/utils/test_builder_info.dart'; + +class IntegrationTestTask extends BaseTask { + @override + Future work(List args) async { + final base = args.isEmpty ? '' : '${args.first}/'; + + var appId = loadId(base); + var config = loadConfigFile(base); + + final prefixValue = config['prefix'] ?? 'preview'; + + final dir = Directory('./$base${config['base']}'); + await dir.create(recursive: true); + + final dirOutPut = Directory('./$base${config['base']}/${config['output']}'); + await dirOutPut.create(recursive: true); + + final List entities = + await dir.list(recursive: true).toList(); + + final files = []; + + for (FileSystemEntity fileSystemEntity in entities) { + try { + if (fileSystemEntity is Directory) continue; + if (fileSystemEntity.path.endsWith('.DS_Store')) continue; + final File file = File(fileSystemEntity.path); + if (file.path.contains('.$prefixValue.')) continue; + final content = await file.readAsString(); + if (content.contains('@Preview(')) { + files.add(fileSystemEntity); + } + } catch (e) { + print(e); + } + } + + final test = []; + + for (FileSystemEntity fileSystemEntity in files) { + final File file = File(fileSystemEntity.path); + var p = file.path.split(config['base'])[1]; + var classImport = 'package:$appId$p'; + var preview = await previewOnFile(base, config, file.path); + var previewAnnotation = await findPreviewAnnotation(file.path); + if (previewAnnotation == null) continue; + var className = await findClassName(file.path); + if (className == null) continue; + if (preview == null) continue; + + final testFile = await generateIntegrationTest( + config, + file.path, + className, + classImport, + prefixValue, + ); + + test.add(testFile); + } + + await generateMainIntegrationTest(base, test); + } +} diff --git a/bin/tasks/tasks/preview_task.dart b/lib/src/bin/tasks/tasks/preview_task.dart similarity index 71% rename from bin/tasks/tasks/preview_task.dart rename to lib/src/bin/tasks/tasks/preview_task.dart index fdbf655..79737dd 100644 --- a/bin/tasks/tasks/preview_task.dart +++ b/lib/src/bin/tasks/tasks/preview_task.dart @@ -1,22 +1,26 @@ import 'dart:io'; -import '../../preview_builder/dummy_builder.dart'; -import '../../preview_builder/preview_builder.dart'; +import 'package:catalog/src/bin/builders/common_builder.dart'; + +import '../../builders/dummy_builder.dart'; +import '../../builders/preview_builder.dart'; import '../../utils/configuration.dart'; import '../base/base_task.dart'; class PreviewTask extends BaseTask { @override - Future work() async { - var appId = loadId(); - var config = loadConfigFile(); + Future work(List args) async { + final base = args.isEmpty ? '' : '${args.first}/'; + + var appId = loadId(base); + var config = loadConfigFile(base); final prefixValue = config['prefix'] ?? 'preview'; - final dir = Directory('./${config['base']}'); + final dir = Directory('./$base${config['base']}'); await dir.create(recursive: true); - final dirOutPut = Directory('./${config['base']}/${config['output']}'); + final dirOutPut = Directory('./$base${config['base']}/${config['output']}'); await dirOutPut.create(recursive: true); final List entities = @@ -43,7 +47,7 @@ class PreviewTask extends BaseTask { final File file = File(fileSystemEntity.path); var p = file.path.split(config['base'])[1]; var classImport = 'package:$appId$p'; - var preview = await previewOnFile(config, file.path); + var preview = await previewOnFile(base, config, file.path); var previewAnnotation = await findPreviewAnnotation(file.path); if (previewAnnotation == null) continue; var className = await findClassName(file.path); @@ -51,6 +55,7 @@ class PreviewTask extends BaseTask { if (preview == null) continue; await generatePreview( + config, file.path, classImport, previewAnnotation, @@ -59,12 +64,10 @@ class PreviewTask extends BaseTask { preview, ); - if (preview.usesDummies) { - await generateDummy( - file.path, - className, - ); - } + await generateDummy( + file.path, + className, + ); } } } diff --git a/bin/tasks/tasks/server_task.dart b/lib/src/bin/tasks/tasks/server_task.dart similarity index 79% rename from bin/tasks/tasks/server_task.dart rename to lib/src/bin/tasks/tasks/server_task.dart index efd9ebc..ab82f2b 100644 --- a/bin/tasks/tasks/server_task.dart +++ b/lib/src/bin/tasks/tasks/server_task.dart @@ -3,7 +3,7 @@ import '../base/base_task.dart'; class ServerTask extends BaseTask { @override - Future work() async { + Future work(List args) async { await ServiceWorkerImpl().start(); } } diff --git a/lib/src/bin/tasks/tasks/test_task.dart b/lib/src/bin/tasks/tasks/test_task.dart new file mode 100644 index 0000000..b11389d --- /dev/null +++ b/lib/src/bin/tasks/tasks/test_task.dart @@ -0,0 +1,71 @@ +import 'dart:io'; + +import 'package:catalog/src/bin/builders/common_builder.dart'; +import 'package:catalog/src/bin/builders/test_builder.dart'; +import 'package:catalog/src/bin/tasks/base/base_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; +import 'package:catalog/src/bin/utils/test_builder_info.dart'; + +class TestTask extends BaseTask { + @override + Future work(List args) async { + final base = args.isEmpty ? '' : '${args.first}/'; + + var appId = loadId(base); + var config = loadConfigFile(base); + + final prefixValue = config['prefix'] ?? 'preview'; + + final dir = Directory('./$base${config['base']}'); + await dir.create(recursive: true); + + final dirOutPut = Directory('./$base${config['base']}/${config['output']}'); + await dirOutPut.create(recursive: true); + + final List entities = + await dir.list(recursive: true).toList(); + + final files = []; + + for (FileSystemEntity fileSystemEntity in entities) { + try { + if (fileSystemEntity is Directory) continue; + if (fileSystemEntity.path.endsWith('.DS_Store')) continue; + final File file = File(fileSystemEntity.path); + if (file.path.contains('.$prefixValue.')) continue; + final content = await file.readAsString(); + if (content.contains('@Preview(')) { + files.add(fileSystemEntity); + } + } catch (e) { + print(e); + } + } + + final test = []; + + for (FileSystemEntity fileSystemEntity in files) { + final File file = File(fileSystemEntity.path); + var p = file.path.split(config['base'])[1]; + var classImport = 'package:$appId$p'; + var preview = await previewOnFile(base, config, file.path); + var previewAnnotation = await findPreviewAnnotation(file.path); + if (previewAnnotation == null) continue; + var className = await findClassName(file.path); + if (className == null) continue; + if (preview == null) continue; + + final testFile = await generateTest( + config, + file.path, + className, + classImport, + prefixValue, + ); + + test.add(testFile); + } + + await generateMainTest(base, test); + } +} diff --git a/lib/src/bin/tasks/test_task.dart b/lib/src/bin/tasks/test_task.dart new file mode 100644 index 0000000..7be1cf6 --- /dev/null +++ b/lib/src/bin/tasks/test_task.dart @@ -0,0 +1,25 @@ +import 'package:catalog/src/bin/utils/messages.dart'; + +import 'base/base_task.dart'; +import 'tasks/format_task.dart'; +import 'tasks/test_task.dart' as test; + +class TestTask extends BaseTask { + final tasks = [ + test.TestTask(), + FormatTask(), + ]; + + @override + Future work(List args) async { + for (BaseTask task in tasks) { + try { + print('\n - Running ${task.runtimeType.toString()} \n'); + await task.work(args); + } catch (e) { + print(e); + } + } + print(commonMessage('🧪 Tests generated')); + } +} diff --git a/bin/utils/communicator/service_worker.dart b/lib/src/bin/utils/communicator/service_worker.dart similarity index 93% rename from bin/utils/communicator/service_worker.dart rename to lib/src/bin/utils/communicator/service_worker.dart index 72fc2ae..0e2dae0 100644 --- a/bin/utils/communicator/service_worker.dart +++ b/lib/src/bin/utils/communicator/service_worker.dart @@ -21,8 +21,7 @@ abstract class ServiceWorker { serviceStarted = true; - final handler = const Pipeline() - .addHandler(_echoRequest); + final handler = const Pipeline().addHandler(_echoRequest); final server = await shelf_io.serve(handler, ip, port); closeServer = () async { diff --git a/bin/utils/communicator/service_worker_impl.dart b/lib/src/bin/utils/communicator/service_worker_impl.dart similarity index 100% rename from bin/utils/communicator/service_worker_impl.dart rename to lib/src/bin/utils/communicator/service_worker_impl.dart diff --git a/bin/utils/configuration.dart b/lib/src/bin/utils/configuration.dart similarity index 61% rename from bin/utils/configuration.dart rename to lib/src/bin/utils/configuration.dart index 9d1a3dd..6e7cb04 100644 --- a/bin/utils/configuration.dart +++ b/lib/src/bin/utils/configuration.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:catalog/src/annotations/preview.dart'; +import 'package:catalog/src/annotations/internal_preview.dart'; import 'package:yaml/yaml.dart'; import 'exceptions.dart'; @@ -19,8 +19,8 @@ String introMessage(String version) => ''' ════════════════════════════════════════════ '''; -Map loadConfigFile() { - final File file = File("pubspec.yaml"); +Map loadConfigFile(String basePath) { + final File file = File("${basePath}pubspec.yaml"); final String yamlString = file.readAsStringSync(); // ignore: always_specify_types final Map yamlMap = loadYaml(yamlString); @@ -42,8 +42,8 @@ Map loadConfigFile() { return config; } -Map loadDependenciesFile() { - final File file = File("pubspec.yaml"); +Map loadDependenciesFile(String basePath) { + final File file = File("${basePath}pubspec.yaml"); final String yamlString = file.readAsStringSync(); // ignore: always_specify_types final Map yamlMap = loadYaml(yamlString); @@ -65,8 +65,8 @@ Map loadDependenciesFile() { return config; } -String loadId() { - final File file = File("pubspec.yaml"); +String loadId(String basePath) { + final File file = File("${basePath}pubspec.yaml"); final String yamlString = file.readAsStringSync(); // ignore: always_specify_types final Map yamlMap = loadYaml(yamlString); @@ -74,78 +74,68 @@ String loadId() { return yamlMap[nammeId]; } -Future previewOnFile( +Future previewOnFile( + String basePath, dynamic config, String originalFilePath, ) async { try { - var output = './${config['base']}/${config['output']}/'; + var output = './$basePath${config['base']}/${config['output']}/'; var fileName = 'process.dart'; File originalFile = File(originalFilePath); var originalContent = originalFile.readAsStringSync(); var original = - 'Preview${originalContent.split('@Preview')[1].split(')').first});'; + 'InternalPreview${originalContent.split('@Preview')[1].split(')').first});'; var dir = Directory(output); await dir.create(recursive: true); + File file = File('$output/$fileName'); var content = ''' import 'dart:convert'; import 'package:catalog/src/base/serial.dart'; -class Preview implements Serial { +class InternalPreview implements Serial { final String id; final String path; final String description; - final bool usesDummies; - final List dummyParameters; - final List listParameters; - final Map parameters; - - const Preview({ - required this.id, - required this.path, + final List parameters; + + const InternalPreview({ + this.id = '', + this.path = '', this.description = '', - this.usesDummies = false, - this.parameters = const {}, - this.listParameters = const [], - this.dummyParameters = const [], + this.parameters = const [], }); @override - Preview fromJson(Map json) => Preview( + InternalPreview fromJson(Map json) => InternalPreview( id: json['id'] ?? '', path: json['path'] ?? '', description: json['description'] ?? '', - usesDummies: json['usesDummies'] ?? false, - dummyParameters: Serial.listObjectFromBasicType( - json['dummyParameters'] ?? [], + parameters: Serial.listObjectFromBasicType( + json['parameters'] ?? [], ), - listParameters: json['listParameters'] ?? [], - parameters: json['parameters'] ?? {}, ); @override String getId() => id; @override - Preview instance() => const Preview(id: '', path: ''); + InternalPreview instance() => const InternalPreview(id: '', path: ''); @override Map toJson() => { 'id': id, 'path': path, 'description': description, - 'usesDummies': usesDummies, - 'listParameters': listParameters, - 'dummyParameters': dummyParameters, 'parameters': parameters, }; @override - Preview fromString(String value) { + InternalPreview fromString(String value) { Map map = jsonDecode(value); return fromJson(map); } @@ -155,6 +145,19 @@ class Preview implements Serial { var map = toJson(); return jsonEncode(map); } + + InternalPreview copyWith({ + String? id, + String? path, + String? description, + List? parameters, + }) => + InternalPreview( + id: id ?? this.id, + path: path ?? this.path, + description: description ?? this.description, + parameters: parameters ?? this.parameters, + ); } void main(List arguments) async { @@ -166,8 +169,32 @@ void main(List arguments) async { var path = file.absolute.path; var result = await Process.run('dart', ['run', path]); - var preview = - const Preview(id: '', path: '').fromJson(jsonDecode(result.stdout)); + var preview = InternalPreview( + id: '', + path: '', + ).fromJson( + jsonDecode(result.stdout), + ); + + if (preview.id.isEmpty) { + preview = preview.copyWith( + id: originalFilePath.split('/').last.split('.').first, + ); + } + + if (preview.path.isEmpty) { + if (originalFilePath.contains('/catalog')) { + preview = preview.copyWith( + path: originalFilePath.split('/lib/').last.split('/catalog').first, + ); + } else { + var parts = originalFilePath.split('/lib/').last.split('/'); + preview = preview.copyWith( + path: parts.sublist(0, parts.length - 1).join('/'), + ); + } + } + return preview; } catch (e) { print(e); diff --git a/bin/utils/exceptions.dart b/lib/src/bin/utils/exceptions.dart similarity index 100% rename from bin/utils/exceptions.dart rename to lib/src/bin/utils/exceptions.dart diff --git a/lib/src/bin/utils/messages.dart b/lib/src/bin/utils/messages.dart new file mode 100644 index 0000000..894ad1d --- /dev/null +++ b/lib/src/bin/utils/messages.dart @@ -0,0 +1,8 @@ +String commonMessage(String message) => ''' + +✅ $message. + +🐛 Report any issue on https://github.com/landamessenger/catalog/issues + +ℹ️ Check the documentation on https://github.com/landamessenger/catalog/wiki +'''; diff --git a/lib/src/bin/utils/test_builder_info.dart b/lib/src/bin/utils/test_builder_info.dart new file mode 100644 index 0000000..3aa77d4 --- /dev/null +++ b/lib/src/bin/utils/test_builder_info.dart @@ -0,0 +1,11 @@ +class TestBuilderInfo { + String alias; + String clazzName; + String import; + + TestBuilderInfo({ + required this.alias, + required this.clazzName, + required this.import, + }); +} diff --git a/lib/src/builders/catalog/built_component.dart b/lib/src/builders/catalog/built_component.dart index 64c2a77..3024bb1 100644 --- a/lib/src/builders/catalog/built_component.dart +++ b/lib/src/builders/catalog/built_component.dart @@ -1,4 +1,4 @@ -import 'package:catalog/src/annotations/preview.dart'; +import 'package:catalog/src/annotations/internal_preview.dart'; import 'package:catalog/src/base/serial.dart'; class BuiltComponent extends Serial { @@ -6,7 +6,13 @@ class BuiltComponent extends Serial { String route = ''; String package = ''; String clazzName = ''; - Preview? preview; + InternalPreview? preview; + + String get name { + if (!path.contains('/')) return path; + var parts = path.split('/'); + return parts.last; + } BuiltComponent({ this.path = '', @@ -29,7 +35,8 @@ class BuiltComponent extends Serial { package = json['package'] ?? ''; clazzName = json['clazzName'] ?? ''; if (json['preview'] != null) { - preview = const Preview(id: '', path: '').fromJson(json['preview'] ?? {}); + preview = const InternalPreview(id: '', path: '') + .fromJson(json['preview'] ?? {}); } return this; } diff --git a/lib/src/builders/catalog/component_node.dart b/lib/src/builders/catalog/component_node.dart index ec2a1d0..fc82785 100644 --- a/lib/src/builders/catalog/component_node.dart +++ b/lib/src/builders/catalog/component_node.dart @@ -4,24 +4,33 @@ import 'package:catalog/src/builders/catalog/built_component.dart'; class ComponentNode extends Serial { String id = ''; String route = ''; - BuiltComponent? builtComponent; + + String get routePath { + if (!route.contains('/')) return route; + var parts = route.split('/'); + return parts.last; + } + + Map builtComponents = {}; + Map children = {}; + List get builtComponentList => + builtComponents.values.toList(); + List get childrenList => children.values.toList(); ComponentNode({ this.id = '', this.route = '', - this.builtComponent, }); @override ComponentNode fromJson(Map json) { id = json['id'] ?? ''; route = json['route'] ?? ''; - if (json['builtComponent'] != null) { - builtComponent = BuiltComponent().fromJson(json['builtComponent'] ?? {}); - } + builtComponents = + Serial.fromComplexMap(json['builtComponents'] ?? {}); children = Serial.fromComplexMap(json['children'] ?? {}); return this; } @@ -36,39 +45,78 @@ class ComponentNode extends Serial { Map toJson() => { 'id': id, 'route': route, - 'builtComponent': builtComponent?.toJson(), + 'builtComponents': + builtComponents.isEmpty ? {} : Serial.toMap(builtComponents), 'children': children.isEmpty ? {} : Serial.toMap(children), }; - String get routerBuilder => ''' - GoRoute( - path: ${builtComponent?.clazzName}.routeName, - pageBuilder: (context, state) => NoTransitionPage( - key: state.pageKey, - child: const ${builtComponent?.clazzName}(), - ), - routes: [${childrenList.map((e) => e.routerBuilder).toList().join(',')}], - ) + String get routerBuilder { + if (route == '/') { + return ''' + GoRoute( + path: CatalogComponent.routeName, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const CatalogComponent(), + ), + routes: [ + ${childrenList.map((e) => e.routerBuilder).toList().join(',')} + ], + ) +'''; + } + return ''' + GoRoute( + path: '$routePath', + redirect: (context, state) { + if (state.fullPath != state.matchedLocation) return null; + return CatalogComponent.routeName; + }, + routes: [ + ${builtComponentList.map((e) { + return ''' + + GoRoute( + path: ${e.clazzName}.routeName, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const ${e.clazzName}(), + ), + ) + + '''; + }).toList().join(',')} + ${builtComponentList.isNotEmpty ? ',' : ''} + ${childrenList.map((e) => e.routerBuilder).toList().join(',')} + ], + ) '''; + } + + String get routerBuilderB { + return ''' + ${builtComponentList.map((e) { + return ''' + + GoRoute( + path: ${e.clazzName}.routeName, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: const ${e.clazzName}(), + ), + ) + + '''; + }).toList().join(',')} + ${childrenList.map((e) => e.routerBuilder).toList().join(',')} + '''; + } String get imports { - String value = ''; - if (builtComponent == null) { - return value; - } - if (route == "/") { - value += '''${childrenList.map((e) => e.imports).toList().join('')} -'''; - } else { - if (childrenList.isEmpty) { - value += '''import '${builtComponent!.package}'; -'''; - } else { - value += '''import '${builtComponent!.package}'; -${childrenList.map((e) => e.imports).toList().join('')} -'''; - } - } + String value = ''' +${childrenList.map((e) => e.imports).toList().join('')}\n +${builtComponentList.map((e) => 'import \'${e.package}\';').toList().join('')}\n + '''; return value; } } diff --git a/lib/src/builders/catalog/drawer_preview.dart b/lib/src/builders/catalog/drawer_preview.dart index 81f4048..d2c4798 100644 --- a/lib/src/builders/catalog/drawer_preview.dart +++ b/lib/src/builders/catalog/drawer_preview.dart @@ -3,10 +3,12 @@ import 'package:flutter/material.dart'; class DrawerPreview extends StatefulWidget { final void Function()? onBackPressed; + final String basePath; const DrawerPreview({ super.key, this.onBackPressed, + required this.basePath, }); @override @@ -15,7 +17,6 @@ class DrawerPreview extends StatefulWidget { class DrawerPreviewState extends State { ComponentNode? node; - TreeController? treeController; @override void initState() { @@ -23,22 +24,14 @@ class DrawerPreviewState extends State { Catalog().get(context).then((value) { if (value == null) return; node = value; - if (treeController == null) { - treeController = TreeController( - roots: [value], - childrenProvider: (ComponentNode node) => node.children.values, - ); - if (treeController!.isTreeCollapsed) { - treeController!.expandAll(); - } - } setState(() {}); }); } @override Widget build(BuildContext context) { - if (node == null || treeController == null) { + final n = node; + if (n == null) { return Container(); } var height = MediaQuery.of(context).size.height; @@ -75,53 +68,19 @@ class DrawerPreviewState extends State { ), SizedBox( height: height - 200, - child: AnimatedTreeView( - treeController: treeController!, - nodeBuilder: - (BuildContext context, TreeEntry entry) { - return InkWell( - onTap: () { - // _nodePressed(node); - }, - child: TreeIndentation( - entry: entry, - child: Row( - children: [ - FolderButton( - isOpen: entry.hasChildren ? entry.isExpanded : null, - onPressed: () => _nodePressed(node!, entry), - ), - Text( - entry.node.id, - style: const TextStyle( - fontSize: 16, - letterSpacing: .3, - ), - ), - ], - ), - ), - ); - }, + child: ListView( + children: [ + buildTreeWidget( + context, + widget.basePath, + n, + 0, + ) + ], ), ), ], ), ); } - - void _nodePressed(ComponentNode baseNode, TreeEntry entry) { - if (entry.node.children.isEmpty) { - if (entry.node.builtComponent?.preview?.path != null) { - context - .go('/${baseNode.id}/${entry.node.builtComponent!.preview!.path}'); - } - } else { - if (!entry.isExpanded) { - treeController?.toggleExpansion(entry.node); - } else { - treeController?.collapse(entry.node); - } - } - } } diff --git a/lib/src/builders/catalog/preview_scaffold.dart b/lib/src/builders/catalog/preview_scaffold.dart index bb45bff..3950baa 100644 --- a/lib/src/builders/catalog/preview_scaffold.dart +++ b/lib/src/builders/catalog/preview_scaffold.dart @@ -9,6 +9,7 @@ class PreviewScaffold extends StatelessWidget { final Widget child; final String? title; final void Function()? onBackPressed; + final String basePath; const PreviewScaffold({ super.key, @@ -16,14 +17,19 @@ class PreviewScaffold extends StatelessWidget { this.title, this.drawer, this.onBackPressed, + required this.basePath, }); @override Widget build(BuildContext context) { return Scaffold( - drawer: Drawer( - child: DrawerPreview( - onBackPressed: onBackPressed ?? Catalog().onBackPressed, + drawer: SizedBox( + width: 600.0, + child: Drawer( + child: DrawerPreview( + basePath: basePath, + onBackPressed: onBackPressed ?? Catalog().onBackPressed, + ), ), ), appBar: AppBar( diff --git a/lib/src/builders/catalog/tree_elements_builder.dart b/lib/src/builders/catalog/tree_elements_builder.dart new file mode 100644 index 0000000..1b90227 --- /dev/null +++ b/lib/src/builders/catalog/tree_elements_builder.dart @@ -0,0 +1,59 @@ +import 'package:catalog/src/builders/catalog/component_node.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +Widget buildTreeWidget( + BuildContext context, + String basePath, + ComponentNode node, + int level, +) { + var padding = 32.0; + return Stack( + children: [ + Padding( + padding: EdgeInsets.only(left: level == 0 ? 0 : padding), + child: Theme( + data: Theme.of(context).copyWith( + dividerColor: Colors.transparent, + ), + child: ExpansionTile( + key: PageStorageKey(node.route), + initiallyExpanded: true, + leading: const Icon(Icons.folder), + iconColor: Colors.teal, + collapsedIconColor: Colors.grey, + title: Text(node.id.isNotEmpty ? node.id : 'Root'), + children: [ + ...node.builtComponentList.map( + (builtComponent) => Padding( + padding: EdgeInsets.only(left: padding), + child: ListTile( + title: Text(builtComponent.name), + leading: const Icon( + Icons.insert_drive_file, + ), + onTap: () { + final id = builtComponent.preview?.id; + if (id != null) { + context.go('$basePath/${builtComponent.route}/$id'); + } + }, + ), + ), + ), + ...node.childrenList.map( + (childNode) => buildTreeWidget( + context, + basePath, + childNode, + level + 1, + ), + ), + ], + ), + ), + ), + ], + ); +} diff --git a/lib/src/builders/dummy/dummy.dart b/lib/src/builders/dummy/dummy.dart index 38acb39..3fc1bd8 100644 --- a/lib/src/builders/dummy/dummy.dart +++ b/lib/src/builders/dummy/dummy.dart @@ -14,8 +14,6 @@ class Dummy { final Screenshot screenshot; - final List listParameters; - final Map parameters; const Dummy({ @@ -23,7 +21,6 @@ class Dummy { this.parameters = const {}, this.device = const Device(), this.screenshot = const Screenshot(), - this.listParameters = const [], }); Dummy copyWith({ @@ -31,14 +28,12 @@ class Dummy { Map? parameters, Device? device, Screenshot? screenshot, - List? listParameters, }) => Dummy( description: description ?? this.description, parameters: parameters ?? this.parameters, device: device ?? this.device, screenshot: screenshot ?? this.screenshot, - listParameters: listParameters ?? this.listParameters, ); bool isDeviceDummy() => device.deviceInfo != null; diff --git a/lib/src/builders/dummy/preview_dummy.dart b/lib/src/builders/dummy/preview_dummy.dart index fc1efaa..af44cc0 100644 --- a/lib/src/builders/dummy/preview_dummy.dart +++ b/lib/src/builders/dummy/preview_dummy.dart @@ -14,4 +14,10 @@ abstract class PreviewDummy { } return index; } + + Dummy get(int index) { + if (dummies.isEmpty) throw Exception('Empty dummies list'); + if (index >= dummies.length) return dummies.first; + return dummies[index]; + } } diff --git a/lib/src/builders/preview/parent_preview_widget.dart b/lib/src/builders/preview/parent_preview_widget.dart index 55aca93..de9b6f2 100644 --- a/lib/src/builders/preview/parent_preview_widget.dart +++ b/lib/src/builders/preview/parent_preview_widget.dart @@ -6,6 +6,10 @@ abstract class ParentPreviewWidget extends StatelessWidget { bool get center => true; + String get title => 'preview_page'; + + String get basePath => '/catalog'; + const ParentPreviewWidget({super.key}); Widget preview(BuildContext context); @@ -13,7 +17,8 @@ abstract class ParentPreviewWidget extends StatelessWidget { @override Widget build(BuildContext context) { return PreviewScaffold( - title: runtimeType.toString(), + basePath: basePath, + title: title, child: Center( child: preview(context), ), diff --git a/lib/src/embed/flutter_fanacy_tree_view/flutter_fancy_tree_view.dart b/lib/src/embed/flutter_fanacy_tree_view/flutter_fancy_tree_view.dart deleted file mode 100644 index 0be7089..0000000 --- a/lib/src/embed/flutter_fanacy_tree_view/flutter_fancy_tree_view.dart +++ /dev/null @@ -1,10 +0,0 @@ -/// A collection of widgets and slivers that helps bringing hierarchical data -/// to life. -library flutter_fancy_tree_view; - -export 'src/folder_button.dart'; -export 'src/sliver_animated_tree.dart'; -export 'src/sliver_tree.dart'; -export 'src/tree_controller.dart'; -export 'src/tree_indentation.dart'; -export 'src/tree_view.dart'; diff --git a/lib/src/embed/flutter_fanacy_tree_view/src/folder_button.dart b/lib/src/embed/flutter_fanacy_tree_view/src/folder_button.dart deleted file mode 100644 index a698e5e..0000000 --- a/lib/src/embed/flutter_fanacy_tree_view/src/folder_button.dart +++ /dev/null @@ -1,371 +0,0 @@ -import 'package:flutter/material.dart'; - -/// The default transition builder used by [FolderButton]. -/// -/// Wraps [child] in a [ScaleTransition]. -Widget defaultFolderButtonTransitionBuilder( - Widget child, - Animation animation, -) { - return ScaleTransition(scale: animation, child: child); -} - -/// A wrapper around [IconButton] and [AnimatedSwitcher] that animates between -/// [icon], [openedIcon] and [closedIcon] depending on the value of [isOpen]. -/// -/// The value of [isOpen] is mapped as follows: -/// `null` -> [icon] -> [Icons.article] -/// `true` -> [openedIcon] -> [Icons.folder_open] -/// `false` -> [closedIcon] -> [Icons.folder] -/// -/// Example: -/// ```dart -/// final TreeEntry entry; -/// final TreeController controller; -/// -/// @override -/// Widget build(BuildContext context) { -/// bool? isOpen; -/// VoidCallback? onPressed; -/// -/// if (entry.hasChildren) { -/// isOpen = entry.isExpanded; -/// onPressed = () => controller.toggleExpansion(entry.node); -/// } -/// -/// return FolderButton( -/// isOpen: isOpen, -/// onPressed: onPressed, -/// ); -/// } -/// ``` -/// -/// In the above example, the [isOpen] property is composed depending on the -/// context of a [TreeEntry]. This widget will show [icon] when the entry is a -/// leaf (i.e., has no children), [openedIcon] when the expansion state is set -/// to `true` and [closedIcon] if the expansion state is set to `false`. The -/// [onPressed] callback is set to `null` when the entry is a leaf disabling -/// the button. -class FolderButton extends StatelessWidget { - /// Creates a [FolderButton]. - const FolderButton({ - super.key, - this.isOpen = true, - this.icon = const Icon(Icons.article), - this.openedIcon = const Icon(Icons.folder_open), - this.closedIcon = const Icon(Icons.folder), - this.iconSize, - this.visualDensity, - this.padding = const EdgeInsets.all(8.0), - this.alignment = Alignment.center, - this.splashRadius, - this.focusColor, - this.hoverColor, - this.color, - this.splashColor, - this.highlightColor, - this.disabledColor, - this.onPressed, - this.mouseCursor, - this.focusNode, - this.autofocus = false, - this.tooltip, - this.enableFeedback = true, - this.constraints, - this.style, - this.duration = kThemeAnimationDuration, - this.curve = Curves.linear, - this.transitionBuilder = defaultFolderButtonTransitionBuilder, - }); - - /// Defines which of [icon], [openedIcon] and [closedIcon] is currently shown. - final bool? isOpen; - - /// The icon to show when [isOpen] is set to `null`. - /// - /// Defaults to `Icon(Icons.article)`. - final Widget icon; - - /// The icon to show when [isOpen] is set to `true`. - /// - /// Defaults to `Icon(Icons.folder_open)`. - final Widget openedIcon; - - /// The icon to show when [isOpen] is set to `false`. - /// - /// Defaults to `Icon(Icons.folder)`. - final Widget closedIcon; - - /// The size of the icon inside the button. - /// - /// If null, uses [IconThemeData.size]. If it is also null, the default size - /// is 24.0. - /// - /// The size given here is passed down to the widget in the [icon] property - /// via an [IconTheme]. Setting the size here instead of in, for example, the - /// [Icon.size] property allows the [IconButton] to size the splash area to - /// fit the [Icon]. If you were to set the size of the [Icon] using - /// [Icon.size] instead, then the [IconButton] would default to 24.0 and then - /// the [Icon] itself would likely get clipped. - final double? iconSize; - - /// Defines how compact the icon button's layout will be. - /// - /// {@macro flutter.material.themedata.visualDensity} - /// - /// See also: - /// - /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all - /// widgets within a [Theme]. - final VisualDensity? visualDensity; - - /// The padding around the button's icon. The entire padded icon will react - /// to input gestures. - /// - /// This property must not be null. It defaults to 8.0 padding on all sides. - final EdgeInsetsGeometry padding; - - /// Defines how the icon is positioned within the IconButton. - /// - /// This property must not be null. It defaults to [Alignment.center]. - /// - /// See also: - /// - /// * [Alignment], a class with convenient constants typically used to - /// specify an [AlignmentGeometry]. - /// * [AlignmentDirectional], like [Alignment] for specifying alignments - /// relative to text direction. - final AlignmentGeometry alignment; - - /// The splash radius. - /// - /// If [ThemeData.useMaterial3] is set to true, this will not be used. - /// - /// If null, default splash radius of [Material.defaultSplashRadius] is used. - final double? splashRadius; - - /// The color for the button when it has the input focus. - /// - /// If [ThemeData.useMaterial3] is set to true, this [focusColor] will be mapped - /// to be the [ButtonStyle.overlayColor] in focused state, which paints on top of - /// the button, as an overlay. Therefore, using a color with some transparency - /// is recommended. For example, one could customize the [focusColor] below: - /// - /// ```dart - /// IconButton( - /// focusColor: Colors.orange.withOpacity(0.3), - /// ) - /// ``` - /// - /// Defaults to [ThemeData.focusColor] of the ambient theme. - final Color? focusColor; - - /// The color for the button when a pointer is hovering over it. - /// - /// If [ThemeData.useMaterial3] is set to true, this [hoverColor] will be mapped - /// to be the [ButtonStyle.overlayColor] in hovered state, which paints on top of - /// the button, as an overlay. Therefore, using a color with some transparency - /// is recommended. For example, one could customize the [hoverColor] below: - /// - /// ```dart - /// IconButton( - /// hoverColor: Colors.orange.withOpacity(0.3), - /// ) - /// ``` - /// - /// Defaults to [ThemeData.hoverColor] of the ambient theme. - final Color? hoverColor; - - /// The color to use for the icon inside the button, if the icon is enabled. - /// Defaults to leaving this up to the [icon] widget. - /// - /// The icon is enabled if [onPressed] is not null. - /// - /// ```dart - /// IconButton( - /// color: Colors.blue, - /// onPressed: _handleTap, - /// icon: Icons.widgets, - /// ) - /// ``` - final Color? color; - - /// The primary color of the button when the button is in the down (pressed) state. - /// The splash is represented as a circular overlay that appears above the - /// [highlightColor] overlay. The splash overlay has a center point that matches - /// the hit point of the user touch event. The splash overlay will expand to - /// fill the button area if the touch is held for long enough time. If the splash - /// color has transparency then the highlight and button color will show through. - /// - /// If [ThemeData.useMaterial3] is set to true, this will not be used. Use - /// [highlightColor] instead to show the overlay color of the button when the button - /// is in the pressed state. - /// - /// Defaults to the Theme's splash color, [ThemeData.splashColor]. - final Color? splashColor; - - /// The secondary color of the button when the button is in the down (pressed) - /// state. The highlight color is represented as a solid color that is overlaid over the - /// button color (if any). If the highlight color has transparency, the button color - /// will show through. The highlight fades in quickly as the button is held down. - /// - /// If [ThemeData.useMaterial3] is set to true, this [highlightColor] will be mapped - /// to be the [ButtonStyle.overlayColor] in pressed state, which paints on top - /// of the button, as an overlay. Therefore, using a color with some transparency - /// is recommended. For example, one could customize the [highlightColor] below: - /// - /// ```dart - /// IconButton( - /// highlightColor: Colors.orange.withOpacity(0.3), - /// ) - /// ``` - /// - /// Defaults to the Theme's highlight color, [ThemeData.highlightColor]. - final Color? highlightColor; - - /// The color to use for the icon inside the button, if the icon is disabled. - /// Defaults to the [ThemeData.disabledColor] of the current [Theme]. - /// - /// The icon is disabled if [onPressed] is null. - final Color? disabledColor; - - /// The callback that is called when the button is tapped or otherwise activated. - /// - /// If this is set to null, the button will be disabled. - final VoidCallback? onPressed; - - /// {@macro flutter.material.RawMaterialButton.mouseCursor} - /// - /// If set to null, will default to - /// - [SystemMouseCursors.basic], if [onPressed] is null - /// - [SystemMouseCursors.click], otherwise - final MouseCursor? mouseCursor; - - /// {@macro flutter.widgets.Focus.focusNode} - final FocusNode? focusNode; - - /// {@macro flutter.widgets.Focus.autofocus} - final bool autofocus; - - /// Text that describes the action that will occur when the button is pressed. - /// - /// This text is displayed when the user long-presses on the button and is - /// used for accessibility. - final String? tooltip; - - /// Whether detected gestures should provide acoustic and/or haptic feedback. - /// - /// For example, on Android a tap will produce a clicking sound and a - /// long-press will produce a short vibration, when feedback is enabled. - /// - /// See also: - /// - /// * [Feedback] for providing platform-specific feedback to certain actions. - final bool enableFeedback; - - /// Optional size constraints for the button. - /// - /// When unspecified, defaults to: - /// ```dart - /// const BoxConstraints( - /// minWidth: kMinInteractiveDimension, - /// minHeight: kMinInteractiveDimension, - /// ) - /// ``` - /// where [kMinInteractiveDimension] is 48.0, and then with visual density - /// applied. - /// - /// The default constraints ensure that the button is accessible. - /// Specifying this parameter enables creation of buttons smaller than - /// the minimum size, but it is not recommended. - /// - /// The visual density uses the [visualDensity] parameter if specified, - /// and `Theme.of(context).visualDensity` otherwise. - final BoxConstraints? constraints; - - /// Customizes this button's appearance. - /// - /// Non-null properties of this style override the corresponding - /// properties in [_IconButtonM3.themeStyleOf] and [_IconButtonM3.defaultStyleOf]. - /// [MaterialStateProperty]s that resolve to non-null values will similarly - /// override the corresponding [MaterialStateProperty]s in [_IconButtonM3.themeStyleOf] - /// and [_IconButtonM3.defaultStyleOf]. - /// - /// The [style] is only used for Material 3 [IconButton]. If [ThemeData.useMaterial3] - /// is set to true, [style] is preferred for icon button customization, and any - /// parameters defined in [style] will override the same parameters in [IconButton]. - /// - /// For example, if [IconButton]'s [visualDensity] is set to [VisualDensity.standard] - /// and [style]'s [visualDensity] is set to [VisualDensity.compact], - /// the icon button will have [VisualDensity.compact] to define the button's layout. - /// - /// Null by default. - final ButtonStyle? style; - - /// The duration of the transition of the icons. - /// - /// Defaults to [kThemeAnimationDuration]. - final Duration duration; - - /// The animation curve to use when transitioning the icons. - /// - /// Defaults to [Curves.linear] - final Curve curve; - - /// The transition funciton used to animate the icons swap. - /// - /// Default to wrapping the icon in a [ScaleTransition]. - /// - /// See also: - /// - /// * [AnimatedSwitcherTransitionBuilder] for more information about - /// how a transition builder should function. - final AnimatedSwitcherTransitionBuilder transitionBuilder; - - Widget get _effectiveIcon { - switch (isOpen) { - case true: - return openedIcon; - case false: - return closedIcon; - case null: - default: - return icon; - } - } - - @override - Widget build(BuildContext context) { - return IconButton( - iconSize: iconSize, - visualDensity: visualDensity, - padding: padding, - alignment: alignment, - splashRadius: splashRadius, - focusColor: focusColor, - hoverColor: hoverColor, - color: color, - splashColor: splashColor, - highlightColor: highlightColor, - disabledColor: disabledColor, - onPressed: onPressed, - mouseCursor: mouseCursor, - focusNode: focusNode, - autofocus: autofocus, - tooltip: tooltip, - enableFeedback: enableFeedback, - constraints: constraints, - style: style, - icon: AnimatedSwitcher( - duration: duration, - switchInCurve: curve, - switchOutCurve: curve, - transitionBuilder: transitionBuilder, - child: KeyedSubtree( - key: Key('FolderButton#$isOpen'), - child: _effectiveIcon, - ), - ), - ); - } -} diff --git a/lib/src/embed/flutter_fanacy_tree_view/src/sliver_animated_tree.dart b/lib/src/embed/flutter_fanacy_tree_view/src/sliver_animated_tree.dart deleted file mode 100644 index 5af82c5..0000000 --- a/lib/src/embed/flutter_fanacy_tree_view/src/sliver_animated_tree.dart +++ /dev/null @@ -1,434 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'sliver_tree.dart'; -import 'tree_controller.dart'; - -// Examples can assume: -// -// class Node { -// Node(this.children); -// List children; -// } -// -// final TreeController treeController = TreeController( -// root: [ -// Node([]), -// ], -// childrenProvider: (Node node) => node.children, -// ); - -/// Signature for a function that takes a widget and an animation to apply -/// transitions if needed. -typedef TreeTransitionBuilder = Widget Function( - BuildContext context, - Widget child, - Animation animation, -); - -/// The default transition builder used by [SliverTree] to animate the expansion -/// state changes of a tree node. -/// -/// Wraps [child] in a [SizeTransition]. -Widget defaultTreeTransitionBuilder( - BuildContext context, - Widget child, - Animation animation, -) { - return SizeTransition(sizeFactor: animation, child: child); -} - -/// A wrapper around [SliverList] that adds tree viewing capabilities with -/// support for automatic expand and collapse animations. -/// -/// Usage: -/// ```dart -/// @override -/// Widget build(BuildContext context) { -/// return CustomScrollView( -/// slivers: [ -/// SliverAnimatedTree( -/// controller: treeController, -/// duration: const Duration(milliseconds, 300), -/// curve: Curves.linear, -/// maxNodesToShowWhenAnimating: 50, -/// nodeBuilder: (BuildContext context, TreeEntry entry) { -/// ... -/// }, -/// ), -/// ], -/// ); -/// } -/// ``` -/// -/// This widget will listen to [controller] and rebuild the inner flat -/// representation of the tree keeping a map of the expansion state of tree -/// nodes to then check if the cached value is different from the current -/// node expansion state when visiting that node during flattening and will -/// mark it to be animated. -/// When a node is marked to animate, its subtree won't be traversed during -/// flattening to later on be rendered in the same list item of the subtree -/// root's node widget. Once the animation completes, the node is removed -/// from the set of animating nodes and the tree is flattened again so the -/// animating subtree can go back to being one list item per node. -/// -/// See also: -/// * [AnimatedTreeView], which covers the [CustomScrollView] boilerplate. -/// * [SliverTree], a tree sliver with no custom behaviors. -class SliverAnimatedTree extends SliverTree { - /// Creates a [SliverAnimatedTree]. - const SliverAnimatedTree({ - super.key, - required super.controller, - required super.nodeBuilder, - this.transitionBuilder = defaultTreeTransitionBuilder, - this.duration = const Duration(milliseconds: 300), - this.curve = Curves.linear, - this.maxNodesToShowWhenAnimating = 50, - }) : assert(maxNodesToShowWhenAnimating > 0); - - /// {@template flutter_fancy_tree_view.SliverAnimatedTree.transitionBuilder} - /// A widget builder used to apply a transition to the expansion state changes - /// of a node subtree when animations are enabled. - /// - /// See also: - /// - /// * [defaultTreeTransitionBuilder] which uses a [SizeTransition]. - /// {@endtemplate} - final TreeTransitionBuilder transitionBuilder; - - /// {@template flutter_fancy_tree_view.SliverAnimatedTree.duration} - /// The default duration to use when animating the expand/collapse operations. - /// - /// Provide a [duration] of `Duration.zero` to disable animations. - /// - /// Defaults to `Duration(milliseconds: 300)`. - /// {@endtemplate} - final Duration duration; - - /// {@template flutter_fancy_tree_view.SliverAnimatedTree.curve} - /// The default curve to use when animating the expand/collapse operations. - /// - /// Defaults to `Curves.linear`. - /// {@endtemplate} - final Curve curve; - - /// {@template flutter_fancy_tree_view.SliverAnimatedTree.maxNodesToShowWhenAnimating} - /// The amount of nodes that are going to be shown on an animating subtree. - /// - /// Must be greater than `0`. - /// - /// When animating the expand/collapse state changes, all descendant nodes - /// whose visibility will change are rendered along with the toggled node, - /// i.e. a [Column] is used, therefore rendering the entire subtree regardless - /// of being a "lazy" rendered view. - /// - /// This value can be used to limit how many nodes are actually rendered - /// during the animation, since there could be cases where not all widgets - /// are visible due to scroll offsets. - /// - /// Defaults to `50`. - /// {@endtemplate} - final int maxNodesToShowWhenAnimating; - - @override - State> createState() => _SliverAnimatedTreeState(); -} - -class _SliverAnimatedTreeState - extends State> { - Map get _expansionStates => _expansionStatesCache ??= {}; - Map? _expansionStatesCache; - - List> _flatTree = const []; - - void _updateFlatTree() { - final Map oldExpansionStates = Map.of(_expansionStates); - - final Map currentExpansionStates = {}; - final List> flatTree = >[]; - - final Visitor> onTraverse; - - if (widget.duration == Duration.zero) { - onTraverse = (TreeEntry entry) { - flatTree.add(entry); - currentExpansionStates[entry.node] = entry.isExpanded; - }; - } else { - onTraverse = (TreeEntry entry) { - flatTree.add(entry); - currentExpansionStates[entry.node] = entry.isExpanded; - - final bool? previousState = oldExpansionStates[entry.node]; - if (previousState != null && previousState != entry.isExpanded) { - _animatingNodes.add(entry.node); - } - }; - } - - widget.controller.depthFirstTraversal( - onTraverse: onTraverse, - descendCondition: (TreeEntry entry) { - if (_animatingNodes.contains(entry.node)) { - // The descendants of a node that is animating are not included in - // the flattened tree since those nodes are going to be rendered in - // a single list item. - return false; - } - return entry.isExpanded; - }, - ); - - _flatTree = flatTree; - _expansionStatesCache = currentExpansionStates; - } - - void _rebuild() => setState(_updateFlatTree); - - final Set _animatingNodes = {}; - - void _onAnimationComplete(T node) { - _animatingNodes.remove(node); - _rebuild(); - } - - List> _buildSubtree(TreeEntry entry) { - final List> subtree = >[]; - widget.controller.depthFirstTraversal( - rootEntry: entry, - onTraverse: subtree.add, - ); - if (subtree.length > widget.maxNodesToShowWhenAnimating) { - return subtree.sublist(0, widget.maxNodesToShowWhenAnimating); - } - return subtree; - } - - @override - void initState() { - super.initState(); - widget.controller.addListener(_rebuild); - _updateFlatTree(); - } - - @override - void didUpdateWidget(covariant SliverAnimatedTree oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_rebuild); - widget.controller.addListener(_rebuild); - _updateFlatTree(); - } - } - - @override - void dispose() { - widget.controller.removeListener(_rebuild); - _animatingNodes.clear(); - _flatTree = const []; - _expansionStatesCache = null; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SliverList( - delegate: SliverChildBuilderDelegate( - childCount: _flatTree.length, - (BuildContext context, int index) { - final TreeEntry entry = _flatTree[index]; - return _TreeEntry( - key: _SaltedTreeNodeKey(entry.node), - entry: entry, - nodeBuilder: widget.nodeBuilder, - buildFlatSubtree: _buildSubtree, - transitionBuilder: widget.transitionBuilder, - onAnimationComplete: _onAnimationComplete, - curve: widget.curve, - duration: widget.duration, - showSubtree: _animatingNodes.contains(entry.node), - ); - }, - ), - ); - } -} - -class _SaltedTreeNodeKey extends GlobalObjectKey { - const _SaltedTreeNodeKey(super.value); -} - -typedef _FlatSubtreeBuilder = List> Function( - TreeEntry virtualRoot, -); - -class _TreeEntry extends StatefulWidget { - const _TreeEntry({ - super.key, - required this.entry, - required this.nodeBuilder, - required this.buildFlatSubtree, - required this.transitionBuilder, - required this.onAnimationComplete, - required this.curve, - required this.duration, - required this.showSubtree, - }); - - final TreeEntry entry; - final TreeNodeBuilder nodeBuilder; - final _FlatSubtreeBuilder buildFlatSubtree; - - final TreeTransitionBuilder transitionBuilder; - final ValueSetter onAnimationComplete; - final Curve curve; - final Duration duration; - final bool showSubtree; - - @override - State<_TreeEntry> createState() => _TreeEntryState(); -} - -class _TreeEntryState extends State<_TreeEntry> - with SingleTickerProviderStateMixin { - TreeEntry get entry => widget.entry; - T get node => entry.node; - - late final AnimationController animationController; - late final CurveTween curveTween; - - bool isExpanded = false; - - void onAnimationComplete() { - widget.onAnimationComplete(node); - if (!mounted) return; - setState(() {}); - } - - void expand() { - // Sometimes when [isExpanded] changes and [widget.shouldAnimate] is set to - // `false`, the animation value is not reset, then later when a subsequent - // animation starts, the controller is already completed and no animation - // is played at all. - final double? from = animationController.value == 1.0 ? 0.0 : null; - animationController.forward(from: from).whenComplete(onAnimationComplete); - } - - void collapse() { - // Sometimes when [isExpanded] changes and [widget.shouldAnimate] is set to - // `false`, the animation value is not reset, then later when a subsequent - // animation starts, the controller is already completed and no animation - // is played at all. - final double? from = animationController.value == 0.0 ? 1.0 : null; - animationController.reverse(from: from).whenComplete(onAnimationComplete); - } - - @override - void initState() { - super.initState(); - isExpanded = entry.isExpanded; - - curveTween = CurveTween(curve: widget.curve); - animationController = AnimationController( - vsync: this, - value: isExpanded ? 1.0 : 0.0, - duration: widget.duration, - ); - } - - @override - void didUpdateWidget(covariant _TreeEntry oldWidget) { - super.didUpdateWidget(oldWidget); - - curveTween.curve = widget.curve; - animationController.duration = widget.duration; - - final bool expansionState = entry.isExpanded; - - if (isExpanded != expansionState) { - isExpanded = expansionState; - - if (widget.showSubtree) { - isExpanded ? expand() : collapse(); - } - } - } - - @override - void dispose() { - animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final Widget tile = widget.nodeBuilder(context, entry); - - late final Widget subtree = _Subtree( - virtualRoot: entry, - nodeBuilder: widget.nodeBuilder, - buildFlatSubtree: widget.buildFlatSubtree, - ); - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - tile, - if (widget.showSubtree && animationController.isAnimating) - widget.transitionBuilder( - context, - subtree, - animationController.drive(curveTween), - ), - ], - ); - } -} - -class _Subtree extends StatefulWidget { - const _Subtree({ - super.key, - required this.virtualRoot, - required this.nodeBuilder, - required this.buildFlatSubtree, - }); - - final TreeEntry virtualRoot; - final TreeNodeBuilder nodeBuilder; - final _FlatSubtreeBuilder buildFlatSubtree; - - @override - State<_Subtree> createState() => _SubtreeState(); -} - -class _SubtreeState extends State<_Subtree> { - late List> virtualEntries; - - @override - void initState() { - super.initState(); - virtualEntries = widget.buildFlatSubtree(widget.virtualRoot); - } - - @override - void dispose() { - virtualEntries = const []; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return IgnorePointer( - ignoring: true, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - for (final TreeEntry virtualEntry in virtualEntries) - widget.nodeBuilder(context, virtualEntry), - ], - ), - ); - } -} diff --git a/lib/src/embed/flutter_fanacy_tree_view/src/sliver_tree.dart b/lib/src/embed/flutter_fanacy_tree_view/src/sliver_tree.dart deleted file mode 100644 index 18fc75a..0000000 --- a/lib/src/embed/flutter_fanacy_tree_view/src/sliver_tree.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'tree_controller.dart'; - -// Examples can assume: -// -// class Node { -// Node(this.children); -// List children; -// } -// -// final TreeController treeController = TreeController( -// root: [ -// Node([]), -// ], -// childrenProvider: (Node node) => node.children, -// ); - -/// Signature of a widget builder function for tree views. -typedef TreeNodeBuilder = Widget Function( - BuildContext context, - TreeEntry entry, -); - -/// A wrapper around [SliverList] that adds basic tree viewing capabilities. -/// -/// Usage: -/// ```dart -/// @override -/// Widget build(BuildContext context) { -/// return CustomScrollView( -/// slivers: [ -/// SliverTree( -/// controller: treeController, -/// nodeBuilder: (BuildContext context, TreeEntry entry) { -/// ... -/// }, -/// ), -/// ], -/// ); -/// } -/// ``` -/// -/// See also: -/// * [TreeView], which covers the [CustomScrollView] boilerplate. -/// * [AnimatedTreeView], a [TreeView] that animates the expansion state changes -/// of tree nodes. -class SliverTree extends StatefulWidget { - /// Creates a [SliverTree]. - const SliverTree({ - super.key, - required this.controller, - required this.nodeBuilder, - }); - - /// {@template flutter_fancy_tree_view.SliverTree.controller} - /// The object responsible for providing access to tree nodes and its states. - /// - /// This widget will listen to the notifications of this controller and - /// rebuild the internal flat represetantion of the tree to make sure the - /// presented tree view is always up to date. - /// {@endtemplate} - final TreeController controller; - - /// {@template flutter_fancy_tree_view.SliverTree.nodeBuilder} - /// Callback used to map tree nodes into widgets. - /// - /// The `TreeEntry entry` parameter contains important information about - /// the current tree context of the particular [TreeEntry.node] that it holds, - /// like the index, level, expansion state, parent, etc. - /// {@endtemplate} - final TreeNodeBuilder nodeBuilder; - - @override - State> createState() => _SliverTreeState(); -} - -class _SliverTreeState extends State> { - List> _flatTree = const []; - - void _updateFlatTree() { - final List> flatTree = []; - widget.controller.depthFirstTraversal(onTraverse: flatTree.add); - _flatTree = flatTree; - } - - void _rebuild() => setState(_updateFlatTree); - - @override - void initState() { - super.initState(); - widget.controller.addListener(_rebuild); - _updateFlatTree(); - } - - @override - void didUpdateWidget(covariant SliverTree oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - oldWidget.controller.removeListener(_rebuild); - widget.controller.addListener(_rebuild); - _updateFlatTree(); - } - } - - @override - void dispose() { - widget.controller.removeListener(_rebuild); - _flatTree = const []; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SliverList( - delegate: SliverChildBuilderDelegate( - childCount: _flatTree.length, - (BuildContext context, int index) { - return widget.nodeBuilder(context, _flatTree[index]); - }, - ), - ); - } -} diff --git a/lib/src/embed/flutter_fanacy_tree_view/src/tree_controller.dart b/lib/src/embed/flutter_fanacy_tree_view/src/tree_controller.dart deleted file mode 100644 index 3bcbf30..0000000 --- a/lib/src/embed/flutter_fanacy_tree_view/src/tree_controller.dart +++ /dev/null @@ -1,536 +0,0 @@ -import 'package:flutter/foundation.dart'; - -/// The default level used for root nodes when flattening the tree. -const int treeRootLevel = 0; - -/// Signature of a function that takes a `T` node and returns an `Iterable`. -/// -/// Used to get the children of a node in a tree. -typedef ChildrenProvider = Iterable Function(T node); - -/// Signature of a function that takes a `T` node and returns a `T?` parent. -/// -/// Used to get the parent of a node in a tree. -typedef ParentProvider = T? Function(T node); - -/// Signature of a function used to visit nodes during tree traversal. -typedef Visitor = void Function(T node); - -/// Signature of a function that takes a `T` node and returns a `bool`. -/// -/// Used when traversing a tree to decide if the children of [node] should be -/// traversed or skipped. -typedef DescendCondition = bool Function(T node); - -/// Signature of a function that takes a `T` node and returns a `bool`. -/// -/// Used when traversing the tree in breadth first order to decide whether the -/// traversal should stop. -typedef ReturnCondition = bool Function(T node); - -/// A controller used to dynamically manage the state of a tree. -/// -/// Whenever this controller notifies its listeners any attached tree views -/// will assume that the tree structure changed in some way and will rebuild -/// their internal flat representaton of the tree, showing/hiding the updated -/// nodes (if any). -/// -/// Usage: -/// ```dart -/// class Node { -/// Node(this.children); -/// List children; -/// } -/// -/// final TreeController treeController = TreeController( -/// roots: [ -/// Node([]), -/// ], -/// childrenProvider: (Node node) => node.children, -/// ); -/// ``` -/// -/// The default implementations of [getExpansionState] and [setExpansionState] -/// use a [Set] to manage the expansion state of tree nodes as follows: -/// - getExpansionState(node) = [Set.contains] -/// - setExpansionState(node, true) = [Set.add] -/// - setExpansionState(node, false) = [Set.remove] -/// -/// Those methods can be overridden to use other data structures if desired. -/// Example: -/// ```dart -/// class Node { -/// bool isExpanded = false; -/// } -/// -/// class MyTreeController extends TreeController { -/// @override -/// bool getExpansionState(Node node) => node.isExpanded; -/// -/// // Do not call `notifyListeners` from this method as it is called many -/// // times recursively in cascading operations. -/// @override -/// void setExpansionState(Node node, bool expanded) { -/// node.isExpanded = expanded; -/// } -/// } -/// ``` -class TreeController with ChangeNotifier { - /// Creates a [TreeController]. - /// - /// The [roots] parameter should contain all nodes that occupy the level `0` - /// of the tree, these nodes are going to be used as a starting point when - /// traversing the tree and building tree views. - TreeController({ - required Iterable roots, - required this.childrenProvider, - }) : _roots = roots; - - /// The roots of the tree. - /// - /// These nodes are used as a starting point when traversing the tree. - Iterable get roots => _roots; - Iterable _roots; - set roots(Iterable nodes) { - if (nodes == _roots) return; - _roots = nodes; - rebuild(); - } - - /// A callback used when building the flat representation of the tree to get - /// the direct children of the tree node passed to it. - /// - /// Avoid doing heavy computations in this callback since it is going to be - /// called a lot when traversing the tree. - /// - /// Example using nested objects: - /// ```dart - /// class Node { - /// List children; - /// } - /// - /// Iterable childrenProvider(Node node) => node.children; - /// ``` - /// - /// Example using a Map cache: - /// ```dart - /// class Data { - /// final int id; - /// } - /// - /// final Map> childrenCache = >{}; - /// - /// Iterable childrenProvider(Data parent) { - /// return childrenCache[parent.id] ?? const Iterable.empty(); - /// }, - /// ``` - /// - /// Do not attempt to load the children of a node in this callback as it - /// would significantly slow down tree traversal which might couse the ui to - /// hang. Prefer doing such operations on a user interaction (e.g., a button - /// press, keyboard shortcut, etc.). When lazy loading, temporarily return - /// an empty iterable so tree traversal can continue. Once the loading is - /// done, set the expansion state of the parent node to `true` and call - /// [rebuild] to reveal the loaded nodes. - final ChildrenProvider childrenProvider; - - Set get _expandedNodes => _expandedNodesCache ??= {}; - Set? _expandedNodesCache; - - /// The current expansion state of [node]. - /// - /// If this method returns `true`, the children of [node] should be visible - /// in tree views. - bool getExpansionState(T node) { - return _expandedNodesCache?.contains(node) ?? false; - } - - /// Updates the expansion state of [node] to the value of [expanded]. - /// - /// When overriding this method, do not call `notifyListeners` as this may be - /// called many times recursively in cascading operations. - void setExpansionState(T node, bool expanded) { - expanded ? _expandedNodes.add(node) : _expandedNodes.remove(node); - } - - /// Notify listeners that the tree structure changed in some way. - /// - /// Call this method whenever the tree nodes are updated (i.e., expansion - /// state changed, node added/removed/reordered, etc...), so that listeners - /// may handle the updated values. Most methods of this controller (like - /// expand, collapse, etc.) already call [rebuild] implicitly. - /// - /// Example: - /// ```dart - /// class Node { - /// List children; - /// } - /// - /// TreeController controller = ...; - /// - /// void addChildren(Node parent, Iterable children) { - /// parent.children.addAll(children); - /// controller.rebuild(); - /// } - ///``` - void rebuild() => notifyListeners(); - - void _collapse(T node) => setExpansionState(node, false); - void _expand(T node) => setExpansionState(node, true); - - /// Updates the expansion state of [node] to the opposite state, then calls - /// [rebuild]. - void toggleExpansion(T node) { - setExpansionState(node, !getExpansionState(node)); - rebuild(); - } - - /// Sets the expansion state of [node] to `true`, then calls [rebuild]. - /// - /// If [node] is already expanded, nothing happens. - void expand(T node) { - if (getExpansionState(node)) return; - _expand(node); - rebuild(); - } - - /// Sets the expansion state of [node] to `false`, then calls [rebuild]. - /// - /// If [node] is already collapsed, nothing happens. - void collapse(T node) { - if (!getExpansionState(node)) return; - _collapse(node); - rebuild(); - } - - void _applyCascadingAction(Iterable nodes, Visitor action) { - for (final T node in nodes) { - action(node); - _applyCascadingAction(childrenProvider(node), action); - } - } - - /// Traverses the subtrees of [nodes] in depth first order expanding every - /// visited node, then calls [rebuild]. - void expandCascading(Iterable nodes) { - if (nodes.isEmpty) return; - _applyCascadingAction(nodes, _expand); - rebuild(); - } - - /// Traverses the subtrees of [nodes] in depth first order collapsing every - /// visited node, then calls [rebuild]. - void collapseCascading(Iterable nodes) { - if (nodes.isEmpty) return; - _applyCascadingAction(nodes, _collapse); - rebuild(); - } - - /// Expands all nodes of this tree recursively. - /// - /// This method delegates its call to [expandCascading] passing in [roots] - /// as the nodes to be expanded. - void expandAll() => expandCascading(roots); - - /// Collapses all nodes of this tree recursively. - /// - /// This method delegates its call to [collapseCascading] passing in [roots] - /// as the nodes to be collapsed. - void collapseAll() => collapseCascading(roots); - - /// Walks up the ancestors of [node] setting their expansion state to `true`. - /// Note: [node] is not expanded by this method. - /// - /// This can be used to reveal a hidden node (e.g. when searching for a node - /// in a search view). - /// - /// [parentProvider] should return the direct parent of the given node or - /// `null` if the root node is reached, this callback is used to traverse the - /// ancestors of [node]. - void expandAncestors(T node, ParentProvider parentProvider) { - T? current = parentProvider(node); - - if (current == null) return; - - while (current != null) { - _expand(current); - current = parentProvider(current); - } - - rebuild(); - } - - /// Whether all root nodes of this tree are expanded. - bool get areAllRootsExpanded => roots.every(getExpansionState); - - /// Whether all root nodes of this tree are collapsed. - bool get areAllRootsCollapsed => !roots.any(getExpansionState); - - /// Whether **all** nodes of this tree are expanded. - /// - /// Traverses the tree in breadth first order checking the expansion state of - /// each visited node. The traversal will return early if it finds a collapsed - /// node. - bool get isTreeExpanded { - bool allNodesExpanded = false; - - breadthFirstSearch( - returnCondition: (T node) { - final bool isExpanded = getExpansionState(node); - allNodesExpanded = isExpanded; - // Stop the traversal if [node] is not expanded - return !isExpanded; - }, - ); - - return allNodesExpanded; - } - - /// Whether **all** nodes of this tree are collapsed. - /// - /// Traverses the tree in breadth first order checking the expansion state of - /// each visited node. The traversal will return early if it finds an expanded - /// node. - bool get isTreeCollapsed { - bool allNodesCollapsed = true; - - breadthFirstSearch( - returnCondition: (T node) { - final bool isExpanded = getExpansionState(node); - allNodesCollapsed = !isExpanded; - // Stop the traversal if [node] is expanded - return isExpanded; - }, - ); - - return allNodesCollapsed; - } - - /// Traverses the subtrees of [startingNodes] in breadth first order. If - /// [startingNodes] is not provided, [roots] will be used instead. - /// - /// [descendCondition] is used to determine if the descendants of the node - /// passed to it should be traversed. When not provided, defaults to - /// [alwaysReturnsTrue], a function that always returns `true` which leads - /// to every node on the tree being visited by this traversal. - /// - /// [returnCondition] is used as a predicate to decide if the iteration should - /// be stopped. If this callback returns `true` the node that was passed to - /// it is returned from this method. When not provided, defaults to - /// [alwaysReturnsFalse], a function that always returns `false` which leads - /// to every node on the tree being visited by this traversal. - /// - /// An optional [onTraverse] callback can be provided to apply an action to - /// each visited node. This callback is called prior to [returnCondition] and - /// [descendCondition] making it possible to update a node before checking - /// its properties. - T? breadthFirstSearch({ - Iterable? startingNodes, - DescendCondition descendCondition = alwaysReturnsTrue, - ReturnCondition returnCondition = alwaysReturnsFalse, - Visitor? onTraverse, - }) { - final List nodes = List.of(startingNodes ?? roots); - - while (nodes.isNotEmpty) { - final T node = nodes.removeAt(0); - - onTraverse?.call(node); - - if (returnCondition(node)) { - return node; - } - - if (descendCondition(node)) { - nodes.addAll(childrenProvider(node)); - } - } - - return null; - } - - /// Traverses the subtrees of [roots] creating [TreeEntry] instances for - /// each visited node. - /// - /// Every new [TreeEntry] instance is provided to [onTraverse] right after it - /// is created, before descending into its subtrees. - /// - /// [descendCondition] is used to determine if the descendants of the entry - /// passed to it should be traversed. When not provided, defaults to - /// `(TreeEntry entry) => entry.isExpanded`. - /// - /// If [rootEntry] is provided, its children will be used instead of [roots] - /// as the roots during traversal. This entry can be used to build a subtree - /// keeping the context of the ancestors in the main tree. This parameter - /// is used by [SliverAnimatedTree] when animating the expand and collapse - /// operations to animate subtrees in and out of the view without losing - /// indentation context of the main tree. - void depthFirstTraversal({ - required Visitor> onTraverse, - DescendCondition>? descendCondition, - TreeEntry? rootEntry, - }) { - final DescendCondition> shouldDescend = - descendCondition ?? defaultDescendCondition; - - int treeIndex = 0; - - void createTreeEntriesRecursively({ - required TreeEntry? parent, - required Iterable nodes, - required int level, - }) { - TreeEntry? entry; - - for (final T node in nodes) { - final Iterable children = childrenProvider(node); - entry = TreeEntry( - parent: parent, - node: node, - index: treeIndex++, - isExpanded: getExpansionState(node), - level: level, - hasChildren: children.isNotEmpty, - ); - - onTraverse(entry); - - if (shouldDescend(entry) && entry.hasChildren) { - createTreeEntriesRecursively( - parent: entry, - nodes: children, - level: level + 1, - ); - } - } - - entry?._hasNextSibling = false; - } - - if (rootEntry != null) { - createTreeEntriesRecursively( - parent: rootEntry, - nodes: childrenProvider(rootEntry.node), - level: rootEntry.level + 1, - ); - } else { - createTreeEntriesRecursively( - parent: null, - nodes: roots, - level: treeRootLevel, - ); - } - } - - /// The default [DescendCondition] used by [depthFirstTraversal]. - @visibleForTesting - bool defaultDescendCondition(TreeEntry entry) => entry.isExpanded; - - @override - void dispose() { - _roots = const Iterable.empty(); - _expandedNodesCache = null; - super.dispose(); - } -} - -/// A function that can take a nullable [Object] and will always return `true`. -/// -/// Used in other function declarations as a constant default parameter. -bool alwaysReturnsTrue([Object? _]) => true; - -/// A function that can take a nullable [Object] and will always return `false`. -/// -/// Used in other function declarations as a constant default parameter. -bool alwaysReturnsFalse([Object? _]) => false; - -/// Used to store useful information about [node] in a tree. -/// -/// Instances of this class are short lived, created by [TreeController] -/// when traversing the tree; each traversal creates a new [TreeEntry] -/// for each visited node with up to date values. -/// -/// To make sure that tree views are always up to date make sure to call -/// [TreeController.rebuild] to notify its listeners that the tree structure -/// changed in some way and they should update their cached values. -class TreeEntry with Diagnosticable { - /// Creates a [TreeEntry]. - TreeEntry({ - required this.parent, - required this.node, - required this.index, - required this.level, - required this.isExpanded, - required this.hasChildren, - bool hasNextSibling = true, - }) : _hasNextSibling = hasNextSibling; - - /// The direct parent of [node] on the tree, which was collected during - /// traversal. - final TreeEntry? parent; - - /// The tree node that originated this entry. - final T node; - - /// The index of [node] in the flat tree list that originated this entry. - final int index; - - /// The level of the node that owns this entry on the tree. Example: - /// - /// 0 1 2 3 - /// A ⋅ ⋅ ⋅ - /// └─ B ⋅ ⋅ - /// ⋅ ├─ C ⋅ - /// ⋅ │ └─ D - /// ⋅ └─ E - /// F ⋅ - /// └─ G - final int level; - - /// The expansion state of [node]. - /// - /// This value may have changed since this entry was created. - final bool isExpanded; - - /// Whether [node] has any child nodes. - /// - /// This value is gotten from calling [TreeController.childrenProvider] with - /// [node] an checking if the returned iterable is not empty. - /// - /// This value may have changed since this entry was created. - final bool hasChildren; - - /// Whether the node that owns this entry has another node after it at the - /// same level. - /// - /// Used when painting lines to decide if a node should have a vertical line - /// that connects it to its next sibling. If a node is the last child of its - /// parent, a half vertical line "└─" is painted instead of a full one "├─". - /// - /// Example: - /// - /// Root - /// ├─ Node <- `hasNextSibling = true` - /// ├─ Node <- `hasNextSibling = true` - /// └─ Node <- `hasNextSibling = false` - bool get hasNextSibling => _hasNextSibling; - bool _hasNextSibling; - - /// Whether this entry should skip being indented. - /// - /// Nodes with a level smaller or equal to [treeRootLevel] are not indented. - bool get skipIndentAndPaint => level <= treeRootLevel; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('parent node', parent?.node)) - ..add(DiagnosticsProperty('node', node)) - ..add(DiagnosticsProperty('index', index)) - ..add(DiagnosticsProperty('expanded', isExpanded)) - ..add(DiagnosticsProperty('level', level)) - ..add(DiagnosticsProperty('has children', hasChildren)) - ..add(DiagnosticsProperty('has next sibling', hasNextSibling)); - } -} diff --git a/lib/src/embed/flutter_fanacy_tree_view/src/tree_indentation.dart b/lib/src/embed/flutter_fanacy_tree_view/src/tree_indentation.dart deleted file mode 100644 index 7a746bf..0000000 --- a/lib/src/embed/flutter_fanacy_tree_view/src/tree_indentation.dart +++ /dev/null @@ -1,508 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'tree_controller.dart' show TreeEntry; - -/// Widget responsible for indenting tree nodes and painting lines (if enabled). -/// -/// Check out the factory constructors of [IndentGuide] to discover the -/// available indent guide decorations. -/// -/// Example: -/// ```dart -/// final TreeEntry entry; -/// -/// @override -/// Widget build(BuildContext context) { -/// return TreeIndentation( -/// entry: entry, -/// guide: IndentGuide.connectingLines( -/// indent: 40, -/// color: Colors.grey, -/// thickness: 1.0, -/// origin: 0.5, -/// roundCorners: true, -/// ), -/// child: ... -/// ); -/// } -/// ``` -/// -/// If [guide] is not provided, [DefaultIndentGuide.of] will be used instead. -class TreeIndentation extends StatelessWidget { - /// Creates a [TreeIndentation]. - /// - /// If [guide] is not provided, [DefaultIndentGuide.of] will be used instead. - const TreeIndentation({ - super.key, - required this.child, - required this.entry, - this.guide, - }); - - /// The widget that is going to be displayed to the side of the indentation. - final Widget child; - - /// The [TreeEntry] that will provide the relevant details (i.e., level, - /// line offsets, etc.) when indenting and/or painting indent guides. - final TreeEntry entry; - - /// The configuration used to indent and paint lines (if enabled). - /// - /// If not provided, [DefaultIndentGuide.of] will be used. - /// - /// Check out the factory constructors of [IndentGuide] to discover the - /// available indent guide decorations. - final IndentGuide? guide; - - @override - Widget build(BuildContext context) { - if (entry.skipIndentAndPaint) { - return child; - } - - final IndentGuide effectiveGuide = guide ?? DefaultIndentGuide.of(context); - return effectiveGuide.wrap(context, child, entry); - } -} - -/// An [InheritedTheme] that provides a default [IndentGuide] to its widget tree. -/// -/// The [TreeIndentation] widget will use the value returned from -/// [DefaultIndentGuide.of] if its internal [TreeIndentation.guide] is `null`. -/// -/// If [TreeIndentation.guide] is `null` and there's no [DefaultIndentGuide] in -/// its context, a default [ConnectingLinesGuide] will be returned. -/// -/// Check out the factory constructors of [IndentGuide] to discover the -/// available indent guide decorations. -class DefaultIndentGuide extends InheritedTheme { - /// Creates a [DefaultIndentGuide]. - const DefaultIndentGuide({ - super.key, - required super.child, - required this.guide, - }); - - /// The default [IndentGuide] provided to the widget tree of [child]. - /// - /// Check out the factory constructors of [IndentGuide] to discover the - /// available indent guide decorations. - final IndentGuide guide; - - /// The [IndentGuide] from the closest instance of this class that encloses - /// the given context. - /// - /// If there is no [DefaultIndentGuide] ancestor in the widget tree at the - /// given context, then this will return a [ConnectingLinesGuide] with its - /// default constructor values. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// IndentGuide guide = DefaultIndentGuide.of(context); - /// ``` - static IndentGuide of(BuildContext context) { - final DefaultIndentGuide? instance = - context.dependOnInheritedWidgetOfExactType(); - - return instance?.guide ?? const ConnectingLinesGuide(); - } - - @override - bool updateShouldNotify(DefaultIndentGuide oldWidget) { - return oldWidget.guide != guide; - } - - @override - Widget wrap(BuildContext context, Widget child) { - return DefaultIndentGuide(guide: guide, child: child); - } -} - -/// The configuration used to indent and paint optional guides for tree nodes. -/// -/// This indent guide only indents tree nodes without decorations. Check out the -/// factory constructors of this class to discover the available indent guide -/// decorations. -class IndentGuide { - /// Creates an [IndentGuide]. - const IndentGuide({ - this.indent = 40.0, - }) : assert(indent >= 0.0); - - /// Convenient constructor to create a [ConnectingLinesGuide]. - const factory IndentGuide.connectingLines({ - double indent, - Color color, - double thickness, - double origin, - bool roundCorners, - }) = ConnectingLinesGuide; - - /// Convenient constructor to create a [ScopingLinesGuide]. - const factory IndentGuide.scopingLines({ - double indent, - Color color, - double thickness, - double origin, - }) = ScopingLinesGuide; - - /// The amount of indent to apply for each level of the tree. - /// - /// The indentation of tree nodes is calculated as follows: - /// ```dart - /// final TreeEntry entry; - /// final IndentGuide guide; - /// final double indentation = entry.level * guide.indent; - /// ``` - final double indent; - - /// Method used to wrap [child] in the desired decoration/painting. - /// - /// Subclasses must override this method to customize whats shown inside of - /// [TreeIndentation]. - /// - /// See also: - /// * [AbstractLineGuide], an interface for working with line painting; - Widget wrap(BuildContext context, Widget child, TreeEntry entry) { - return Padding( - padding: EdgeInsetsDirectional.only(start: entry.level * indent), - child: child, - ); - } - - @override - int get hashCode => indent.hashCode; - - @override - operator ==(Object other) { - if (identical(other, this)) return true; - - return other.runtimeType == runtimeType && - other is IndentGuide && - other.indent == indent; - } -} - -/// An interface for configuring how to paint line guides in the indentation of -/// a tree node. -/// -/// Check out the factory constructors of [IndentGuide] to discover the -/// available indent guide decorations. -abstract class AbstractLineGuide extends IndentGuide { - /// Constructor with requried parameters for building the indent line guides. - const AbstractLineGuide({ - super.indent, - this.color = Colors.grey, - this.thickness = 2.0, - this.origin = 0.5, - }) : assert(thickness >= 0.0), - assert( - 0.0 <= origin && origin <= 1.0, - '`origin` must be a value between `0.0` and `1.0`.', - ), - originOffset = indent - (indent * origin); - - /// The color to use when painting the lines on the canvas. - /// - /// Defaults to [Colors.grey]. - final Color color; - - /// The width each line should have. - /// - /// Defaults to `2.0`. - final double thickness; - - /// Defines where horizontally inside [indent] to start painting the vertical - /// lines. - /// - /// The [originOffset] is calculated from [indent] and [origin]: - /// ```dart - /// final double originOffset = indent - (indent * origin); - /// ``` - /// - /// Must be a value between `0.0` and `1.0`, Being: - /// - `0.0`: start; - /// - `0.5`: center; - /// - `1.0`: end; - final double origin; - - /// The value that results from `indent - (indent * origin)`. - /// - /// Used when painting to horizontally position a line on each [indent] level. - final double originOffset; - - /// Subclasses must override this method to provide the [CustomPainter] that - /// will handle line painting. - CustomPainter createPainter(BuildContext context, TreeEntry entry); - - /// Creates the [Paint] object that will be used to paint lines. - Paint createPaint() => Paint() - ..color = color - ..strokeWidth = thickness - ..style = PaintingStyle.stroke; - - /// Calculates the origin offset of the line drawn for the given [level]. - double offsetOfLevel(int level) => (level * indent) - originOffset; - - @override - Widget wrap(BuildContext context, Widget child, TreeEntry entry) { - return RepaintBoundary( - child: CustomPaint( - painter: createPainter(context, entry), - child: super.wrap(context, child, entry), - ), - ); - } -} - -/// The [IndentGuide] configuration for painting vertical lines at every level -/// of the tree. -/// -/// Check out the factory constructors of [IndentGuide] to discover the -/// available indent guide decorations. -class ScopingLinesGuide extends AbstractLineGuide { - /// Creates a [ScopingLinesGuide]. - const ScopingLinesGuide({ - super.indent, - super.color, - super.thickness, - super.origin, - }); - - @override - CustomPainter createPainter(BuildContext context, TreeEntry entry) { - return _ScopingLinesPainter( - guide: this, - nodeLevel: entry.level, - textDirection: Directionality.maybeOf(context), - ); - } - - /// Creates a copy of this indent guide but with the given fields replaced - /// with the new values. - ScopingLinesGuide copyWith({ - double? indent, - Color? color, - double? thickness, - double? origin, - }) { - return ScopingLinesGuide( - indent: indent ?? this.indent, - color: color ?? this.color, - thickness: thickness ?? this.thickness, - origin: origin ?? this.origin, - ); - } - - @override - int get hashCode => Object.hash( - indent, - color, - thickness, - origin, - ); - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other.runtimeType == runtimeType && - other is ScopingLinesGuide && - other.indent == indent && - other.color == color && - other.thickness == thickness && - other.origin == origin; - } -} - -class _ScopingLinesPainter extends CustomPainter { - _ScopingLinesPainter({ - required this.guide, - required this.nodeLevel, - required this.textDirection, - }); - - final ScopingLinesGuide guide; - final int nodeLevel; - final TextDirection? textDirection; - - @override - void paint(Canvas canvas, Size size) { - late double Function(int level) calculateOffset; - - if (textDirection == TextDirection.rtl) { - calculateOffset = (int level) => size.width - guide.offsetOfLevel(level); - } else { - calculateOffset = guide.offsetOfLevel; - } - - final Path path = Path(); - - for (int level = 1; level <= nodeLevel; level++) { - final double x = calculateOffset(level); - path - ..moveTo(x, size.height) - ..lineTo(x, 0); - } - - canvas.drawPath(path, guide.createPaint()); - } - - @override - bool shouldRepaint(covariant _ScopingLinesPainter oldDelegate) => - oldDelegate.guide != guide || - oldDelegate.nodeLevel != nodeLevel || - oldDelegate.textDirection != textDirection; -} - -/// The [IndentGuide] configuration for painting vertical lines that have a -/// horizontal connection to its tree node. -/// -/// Check out the factory constructors of [IndentGuide] to discover the -/// available indent guide decorations. -class ConnectingLinesGuide extends AbstractLineGuide { - /// Creates a [ConnectingLinesGuide]. - const ConnectingLinesGuide({ - super.indent, - super.color, - super.thickness, - super.origin, - this.roundCorners = false, - }); - - /// Determines if the connection between a horizontal and a vertical line - /// should be rounded. - final bool roundCorners; - - @override - CustomPainter createPainter(BuildContext context, TreeEntry entry) { - return _ConnectingLinesPainter( - guide: this, - entry: entry, - textDirection: Directionality.maybeOf(context), - ); - } - - /// Creates a copy of this indent guide but with the given fields replaced - /// with the new values. - ConnectingLinesGuide copyWith({ - double? indent, - Color? color, - double? thickness, - double? origin, - bool? roundCorners, - }) { - return ConnectingLinesGuide( - indent: indent ?? this.indent, - color: color ?? this.color, - thickness: thickness ?? this.thickness, - origin: origin ?? this.origin, - roundCorners: roundCorners ?? this.roundCorners, - ); - } - - @override - int get hashCode => Object.hash( - indent, - color, - thickness, - origin, - roundCorners, - ); - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other.runtimeType == runtimeType && - other is ConnectingLinesGuide && - other.indent == indent && - other.color == color && - other.thickness == thickness && - other.origin == origin && - other.roundCorners == roundCorners; - } -} - -class _ConnectingLinesPainter extends CustomPainter { - _ConnectingLinesPainter({ - required this.guide, - required this.entry, - this.textDirection, - }) : indentation = entry.level * guide.indent; - - final ConnectingLinesGuide guide; - final TreeEntry entry; - final TextDirection? textDirection; - final double indentation; - - void runForEachAncestorLevelThatHasNextSibling( - void Function(int level) action, - ) { - TreeEntry? current = entry; - while (current != null && current.level > 0) { - if (current.hasNextSibling) { - action(current.level); - } - current = current.parent; - } - } - - @override - void paint(Canvas canvas, Size size) { - late double connectionEnd; - late double connectionStart; - late double Function(int level) calculateOffset; - - if (textDirection == TextDirection.rtl) { - connectionEnd = size.width - indentation; - connectionStart = connectionEnd + guide.originOffset; - calculateOffset = (int level) => size.width - guide.offsetOfLevel(level); - } else { - connectionEnd = indentation; - connectionStart = indentation - guide.originOffset; - calculateOffset = guide.offsetOfLevel; - } - - final Path path = Path(); - - // Add vertical lines - runForEachAncestorLevelThatHasNextSibling((int level) { - final double x = calculateOffset(level); - path - ..moveTo(x, size.height) - ..lineTo(x, 0); - }); - - // Add connection - - final double y = size.height * 0.5; - - path.moveTo(connectionStart, 0.0); - - if (guide.roundCorners) { - path.quadraticBezierTo(connectionStart, y, connectionEnd, y); - } else { - // if [entry] has a sibling after it, a full vertical line was - // painted at [entry.level] and we only need to move to the start - // of the horizontal line, otherwise add half of a vertical line - // to connect to the horizontal one. - if (entry.hasNextSibling) { - path.moveTo(connectionStart, y); - } else { - path.lineTo(connectionStart, y); - } - - path.lineTo(connectionEnd, y); - } - - canvas.drawPath(path, guide.createPaint()); - } - - @override - bool shouldRepaint(covariant _ConnectingLinesPainter oldDelegate) => - oldDelegate.entry != entry || - oldDelegate.textDirection != textDirection || - oldDelegate.guide != guide; -} diff --git a/lib/src/embed/flutter_fanacy_tree_view/src/tree_view.dart b/lib/src/embed/flutter_fanacy_tree_view/src/tree_view.dart deleted file mode 100644 index 0a4ee6b..0000000 --- a/lib/src/embed/flutter_fanacy_tree_view/src/tree_view.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'sliver_animated_tree.dart'; -import 'sliver_tree.dart'; -import 'tree_controller.dart'; - -// Examples can assume: -// -// class Node { -// Node(this.children); -// List children; -// } -// -// final TreeController treeController = TreeController( -// root: [ -// Node([]), -// ], -// childrenProvider: (Node node) => node.children, -// ); - -/// A widget used to visualize tree hierarchies. -/// -/// Usage: -/// ```dart -/// @override -/// Widget build(BuildContext context) { -/// return TreeView( -/// treeController: treeController, -/// nodeBuilder: (BuildContext context, TreeEntry entry) { -/// ... -/// }, -/// ); -/// } -/// ``` -/// -/// See also: -/// * [SliverTree], which is created internally by [TreeView]. It can be used -/// to create more sophisticated scrolling experiences. -/// * [AnimatedTreeView], a version of this widget that animates the expansion -/// state changes of tree nodes. -class TreeView extends BoxScrollView { - /// Creates a [TreeView]. - const TreeView({ - super.key, - required this.treeController, - required this.nodeBuilder, - super.padding, - super.controller, - super.primary, - super.physics, - super.shrinkWrap, - super.cacheExtent, - super.semanticChildCount, - super.dragStartBehavior, - super.keyboardDismissBehavior, - super.restorationId, - super.clipBehavior, - }); - - /// {@macro flutter_fancy_tree_view.SliverTree.controller} - final TreeController treeController; - - /// {@macro flutter_fancy_tree_view.SliverTree.nodeBuilder} - final TreeNodeBuilder nodeBuilder; - - @override - Widget buildChildLayout(BuildContext context) { - return SliverTree( - controller: treeController, - nodeBuilder: nodeBuilder, - ); - } -} - -/// A [TreeView] that animates the expansion state changes of tree nodes. -/// -/// Usage: -/// ```dart -/// @override -/// Widget build(BuildContext context) { -/// return AnimatedTreeView( -/// treeController: treeController, -/// duration: const Duration(milliseconds: 300), -/// curve: Curves.linear, -/// maxNodesToShowWhenAnimating: 50, -/// transitionBuilder: (BuildContext context, Widget child, Animation animation) { -/// return FadeTransition( -/// opacity: animation, -/// child: SizeTransition( -/// sizeFactor: animation, -/// child: child, -/// ), -/// ); -/// }, -/// nodeBuilder: (BuildContext context, TreeEntry entry) { -/// ... -/// }, -/// ); -/// } -/// ``` -/// -/// See also: -/// * [SliverAnimatedTree], which is created internally by [AnimatedTreeView]. -/// It can be used to create more sophisticated scrolling experiences. -/// * [TreeView], a version of this widget that has no custom behaviors. -class AnimatedTreeView extends TreeView { - /// Creates a [TreeView]. - const AnimatedTreeView({ - super.key, - required super.treeController, - required super.nodeBuilder, - this.transitionBuilder = defaultTreeTransitionBuilder, - this.duration = const Duration(milliseconds: 300), - this.curve = Curves.linear, - this.maxNodesToShowWhenAnimating = 50, - super.padding, - super.controller, - super.primary, - super.physics, - super.shrinkWrap, - super.cacheExtent, - super.semanticChildCount, - super.dragStartBehavior, - super.keyboardDismissBehavior, - super.restorationId, - super.clipBehavior, - }); - - /// {@macro flutter_fancy_tree_view.SliverAnimatedTree.transitionBuilder} - final TreeTransitionBuilder transitionBuilder; - - /// {@macro flutter_fancy_tree_view.SliverAnimatedTree.duration} - final Duration duration; - - /// {@macro flutter_fancy_tree_view.SliverAnimatedTree.curve} - final Curve curve; - - /// {@macro flutter_fancy_tree_view.SliverAnimatedTree.maxNodesToShowWhenAnimating} - final int maxNodesToShowWhenAnimating; - - @override - Widget buildChildLayout(BuildContext context) { - return SliverAnimatedTree( - controller: treeController, - nodeBuilder: nodeBuilder, - transitionBuilder: transitionBuilder, - duration: duration, - curve: curve, - maxNodesToShowWhenAnimating: maxNodesToShowWhenAnimating, - ); - } -} diff --git a/lib/src/extensions/widget_test_ext.dart b/lib/src/extensions/widget_test_ext.dart new file mode 100644 index 0000000..2168aca --- /dev/null +++ b/lib/src/extensions/widget_test_ext.dart @@ -0,0 +1,48 @@ +import 'package:catalog/catalog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stringcare/stringcare.dart'; + +extension WidgetTestExt on WidgetTester { + Future test(Widget widget) async { + await pumpWidget( + MaterialApp( + navigatorKey: Stringcare().navigatorKey, + supportedLocales: Stringcare().locales, + localizationsDelegates: Stringcare().delegates, + home: Center(child: widget), + ), + ); + await pumpAndSettle(); + } + + /// The original unencrypted resources are used on test. + /// Uses `dart:io` under the hood. + Future setupTestContext() => _setupContext( + useEncrypted: false, + ); + + /// The encrypted resources are consumed. + Future setupIntegrationTestContext() => _setupContext( + useEncrypted: true, + ); + + /// Disables the Stringcare native libs (.so and .dylib) and uses the Dart + /// implementation. Also configures the encryption status. + Future _setupContext({ + required bool useEncrypted, + }) async { + Stringcare().disableNative = true; + Stringcare().useEncrypted = useEncrypted; + await pumpWidget( + MaterialApp( + navigatorKey: Stringcare().navigatorKey, + supportedLocales: Stringcare().locales, + localizationsDelegates: Stringcare().delegates, + home: Container(), + ), + ); + await pumpAndSettle(); + await Stringcare().load(); + } +} diff --git a/lib/src/utils/svg.dart b/lib/src/utils/svg.dart index 9aa52fb..0482f24 100644 --- a/lib/src/utils/svg.dart +++ b/lib/src/utils/svg.dart @@ -1,4 +1,4 @@ -library flutter_svg_provider; +library; import 'dart:async'; import 'dart:io'; diff --git a/pubspec.yaml b/pubspec.yaml index 068a10b..b85049c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: catalog description: A Flutter package to render widgets in real time and generate screenshots for fastlane. -version: 1.0.6 +version: 2.0.0 homepage: https://landamessenger.com/ repository: https://github.com/landamessenger/catalog @@ -15,17 +15,18 @@ dependencies: device_frame: ^1.2.0 # android ios linux macos web windows flutter_svg: ^2.0.10+1 # android ios linux macos windows global_refresh: ^1.0.0 # android ios linux macos web windows - go_router: ^14.2.0 # android ios linux macos web windows - http: ^1.2.1 # android ios linux macos web windows + go_router: ^14.3.0 # android ios linux macos web windows + http: ^1.2.2 # android ios linux macos web windows image: ^4.2.0 # android ios linux macos web windows shelf: ^1.4.1 # android ios linux macos web windows + stringcare: ^1.0.0 # android ios linux macos web windows vector_graphics: ^1.1.11+1 # android ios linux macos web windows yaml: ^3.1.2 # android ios linux macos web windows yaml_writer: ^2.0.0 # android ios linux macos web windows - -dev_dependencies: + integration_test: + sdk: flutter flutter_test: sdk: flutter - flutter_lints: ^4.0.0 -flutter: +dev_dependencies: + flutter_lints: ^5.0.0 diff --git a/test/catalog_test.dart b/test/catalog_test.dart index d6f2093..174acf3 100644 --- a/test/catalog_test.dart +++ b/test/catalog_test.dart @@ -1,16 +1,63 @@ +import 'package:catalog/src/bin/tasks/integration_test_task.dart'; +import 'package:catalog/src/bin/tasks/main_task.dart'; +import 'package:catalog/src/bin/tasks/preview_task.dart'; +import 'package:catalog/src/bin/tasks/test_task.dart'; +import 'package:catalog/src/bin/utils/configuration.dart'; import 'package:catalog/src/catalog_runner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; void main() { - test('Basic dummy test', () { - final runner = CatalogRunner( - application: Container(), - route: GoRoute( - path: '/catalog', - ), - ); - expect(runner.enabled, false); - }); + final dependency = 'catalog'; + final exampleFolder = 'example'; + + test( + 'Basic dummy test', + () { + final runner = CatalogRunner( + application: Container(), + route: + GoRoute(path: '/catalog', builder: (context, state) => Container()), + ); + expect(runner.enabled, false); + }, + ); + + test( + 'Test Preview task (preview + format)', + () async { + var dependencies = loadDependenciesFile('$exampleFolder/'); + print(introMessage(dependencies[dependency].toString())); + await PreviewTask().work([exampleFolder]); + }, + ); + + test( + 'Test Test task (test + format)', + () async { + var dependencies = loadDependenciesFile('$exampleFolder/'); + print(introMessage(dependencies[dependency].toString())); + await TestTask().work([exampleFolder]); + }, + ); + + test( + 'Test Integration Test task (integration_test + format)', + () async { + var dependencies = loadDependenciesFile('$exampleFolder/'); + print(introMessage(dependencies[dependency].toString())); + await IntegrationTestTask().work([exampleFolder]); + }, + ); + + test( + 'Test Main task (preview + test + integration_test + catalog + format)', + () async { + var dependencies = loadDependenciesFile('$exampleFolder/'); + print(introMessage(dependencies[dependency].toString())); + await MainTask().work([exampleFolder]); + assert(true); + }, + ); }