diff --git a/.github/merge.yml b/.github/merge.yml new file mode 100644 index 0000000..f939b67 --- /dev/null +++ b/.github/merge.yml @@ -0,0 +1,109 @@ +name: "Merge" + +on: + push: + # pull_request: + # types: [closed] + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: "Build Changelog" + id: build_changelog + uses: mikepenz/release-changelog-builder-action@{latest-release} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event.pull_request.merged + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get all changed *.dart, files in docs or pubspec.yaml + id: changed-files + uses: tj-actions/changed-files@v37 + with: + base_sha: ${{ github.event.pull_request.base.sha }} + sha: ${{ github.event.pull_request.head.sha }} + files: | + **/*.dart + pubspec.yaml + merge: + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: changes + if: needs.changes.outputs.files == 'true' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + - name: Setup Dart SDK + uses: dart-lang/setup-dart@v1.5.0 + - name: install cider + run: dart pub global activate cider + - name: Change flutter version tag + run: | + echo "FLUTTER_BUILD_VERSION=$(cider bump patch --bump-build)" >> "$GITHUB_ENV" + - name: Update + id: update + run: | + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + git add -A + git commit --amend --no-edit + git push -f + echo "newSha=$(git rev-parse ${{github.event.pull_request.head.sha}})" >> $GITHUB_ENV + - name: Create tag + uses: actions/github-script@v6 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/${{env.FLUTTER_BUILD_VERSION}}', + sha: "${{env.newSha}}" + }) + - name: release + uses: actions/github-script@v5 + if: ${{ (steps.bump.outputs.release == 'true') && (env.modified == 'true') }} + with: + script: | + github.rest.repos.createRelease({ + draft: false, + generate_release_notes: true, + name: "${{steps.bump.outputs.version}}", + owner: context.repo.owner, + prerelease: false, + repo: context.repo.repo, + tag_name: "${{steps.bump.outputs.version}}", + }); + - name: "Build Changelog" + id: build_changelog + uses: mikepenz/release-changelog-builder-action@{latest-release} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # build_and_deploy: + # if: github.event.pull_request.merged + # runs-on: ubuntu-latest + # needs: merge + # steps: + # - uses: actions/checkout@v3 + # - uses: subosito/flutter-action@v2 + # with: + # flutter-version: "3.13.x" + # channel: "stable" + # - name: Setup flutter + # run: flutter pub get + # - name: build + # run: | + # cd example + # flutter build web -o ../build --no-tree-shake-icons + # - name: Deploy + # uses: JamesIves/github-pages-deploy-action@v4 + # with: + # folder: build diff --git a/.github/pr.yml b/.github/pr.yml new file mode 100644 index 0000000..4dc4a15 --- /dev/null +++ b/.github/pr.yml @@ -0,0 +1,68 @@ +name: "PR" + +on: + pull_request: + +jobs: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + files: ${{steps.changed-files.outputs.any_changed}} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + - name: Check branch is up to date + run: | + if git merge-base --is-ancestor ${{ github.event.pull_request.base.sha}} ${{ github.event.pull_request.head.sha}} + then + echo "Your branch is up to date." + exit 0 + else + echo "You need to merge / rebase." + exit 1 + fi + - name: Get all changed *.dart, files in docs or pubspec.yaml + id: changed-files + uses: tj-actions/changed-files@v37 + with: + base_sha: ${{ github.event.pull_request.base.sha }} + sha: ${{ github.event.pull_request.head.sha }} + files: | + **/*.dart + pubspec.yaml + analyze: + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: changes + if: needs.changes.outputs.files == 'true' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.13.x" + channel: "stable" + - name: Setup flutter + run: flutter pub get + - name: Lint and format + run: | + dart format . -l 120 + dart fix --apply + flutter analyze + - name: Check for modified files + id: git-check + run: echo "modified=$(if [ -n "$(git status --porcelain)" ]; then echo "true"; else echo "false"; fi)" >> $GITHUB_ENV + - name: Update changes in GitHub repository + if: env.modified == 'true' + run: | + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + git add -A + git commit -m '[automated commit] lint format and import sort' + git push diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml new file mode 100644 index 0000000..f110cb0 --- /dev/null +++ b/.github/workflows/merge.yml @@ -0,0 +1,95 @@ +name: "Merge" + +on: + pull_request: + types: [closed] + +jobs: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.event.pull_request.merged + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get all changed *.dart, files in docs or pubspec.yaml + id: changed-files + uses: tj-actions/changed-files@v37 + with: + base_sha: ${{ github.event.pull_request.base.sha }} + sha: ${{ github.event.pull_request.head.sha }} + files: | + **/*.dart + pubspec.yaml + merge: + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: changes + if: needs.changes.outputs.files == 'true' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + - name: Setup Dart SDK + uses: dart-lang/setup-dart@v1.5.0 + - name: install cider + run: dart pub global activate cider + - name: Change flutter version tag + run: | + echo "FLUTTER_BUILD_VERSION=$(cider bump patch --bump-build)" >> "$GITHUB_ENV" + - name: Update + id: update + run: | + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + git add -A + git commit --amend --no-edit + git push -f + echo "newSha=$(git rev-parse ${{github.event.pull_request.head.sha}})" >> $GITHUB_ENV + - name: Create tag + uses: actions/github-script@v6 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/${{env.FLUTTER_BUILD_VERSION}}', + sha: "${{env.newSha}}" + }) + - name: release + uses: actions/github-script@v5 + if: ${{ (steps.bump.outputs.release == 'true') && (env.modified == 'true') }} + with: + script: | + github.rest.repos.createRelease({ + draft: false, + generate_release_notes: true, + name: "${{steps.bump.outputs.version}}", + owner: context.repo.owner, + prerelease: false, + repo: context.repo.repo, + tag_name: "${{steps.bump.outputs.version}}", + }); + build_and_deploy: + if: github.event.pull_request.merged + runs-on: ubuntu-latest + needs: merge + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.13.x" + channel: "stable" + - name: Setup flutter + run: flutter pub get + - name: build + # TODO(thelukewalton): check this works + run: | + cd example + flutter build web -o ../build --no-tree-shake-icons + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: build diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..8018333 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,68 @@ +name: "PR" + +on: + pull_request: + +jobs: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + files: ${{steps.changed-files.outputs.any_changed}} + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + - name: Check branch is up to date + run: | + if git merge-base --is-ancestor ${{ github.event.pull_request.base.sha}} ${{ github.event.pull_request.head.sha}} + then + echo "Your branch is up to date." + exit 0 + else + echo "You need to merge / rebase." + exit 1 + fi + - name: Get all changed *.dart, files in docs or pubspec.yaml + id: changed-files + uses: tj-actions/changed-files@v37 + with: + base_sha: ${{ github.event.pull_request.base.sha }} + sha: ${{ github.event.pull_request.head.sha }} + files: | + **/*.dart + pubspec.yaml + analyze: + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: changes + if: needs.changes.outputs.files == 'true' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.13.x" + channel: "stable" + - name: Setup flutter + run: flutter pub get + - name: Lint and format + run: | + dart format . -l 120 + dart fix --apply + dart analyze + - name: Check for modified files + id: git-check + run: echo "modified=$(if [ -n "$(git status --porcelain)" ]; then echo "true"; else echo "false"; fi)" >> $GITHUB_ENV + - name: Update changes in GitHub repository + if: env.modified == 'true' + run: | + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + git add -A + git commit -m '[automated commit] lint format and import sort' + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4abf86f --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +**/.vscode/* + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +pubspec.lock + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 + + +**/node_modules +.master/ +**/.fvm/ \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..89d95f1 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: adc687823a831bbebe28bdccfac1a628ca621513 + channel: stable + +project_type: package diff --git a/.pubignore b/.pubignore new file mode 100644 index 0000000..bfe002f --- /dev/null +++ b/.pubignore @@ -0,0 +1,3 @@ +example/ +assets/ +test/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4f3cf44 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Zebra Technologies Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE-3RD-PARTY b/LICENSE-3RD-PARTY new file mode 100644 index 0000000..75ade83 --- /dev/null +++ b/LICENSE-3RD-PARTY @@ -0,0 +1,5 @@ +----------------------------------------------------------------------------- + BSD-3-Clause + applies to: + - DatePickerDialog +----------------------------------------------------------------------------- \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f42c43d --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# ZDS Flutter + +A library of Flutter components made by Zebra Technologies based on the Zebra Design System, or ZDS. + +![GitHub-Mark-Light](assets/zds-light.png#gh-light-mode-only)![GitHub-Mark-Dark](assets/zds-dark.png#gh-dark-mode-only) + +## Requirements + +Make sure your app meets the following requirements before using ZDS Flutter + +- sdk: >=3.0.1 <4.0.0 +- flutter: >=2.10.0 + +## Usage + +Add the following as a dependency in your pubspec.yaml file. + +```yml +zds-flutter: ^1.0.0 +``` + +## Getting started + +In order to use components from ZDS Flutter, you must change your app widget to be a `ZdsApp` + +```dart +class DemoApp extends StatelessWidget { + const DemoApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ZdsApp( + title: 'ZDS Flutter demo', + routes: kAllRoutes, + ); + } +} + +``` + +## Viewing the components + +To view examples of all the components in the library, you can run the example app in this repo or go to [TODO: INSERT LINK HERE] + +## Theming + +The ZDS uses the color system from another Zebra design library - [zeta_flutter](https://pub.dev/packages/zeta_flutter). + +--- + +## Platform support + +Full support on Android and iOS. + +On Web, Windows, Mac and Linux some features may be missing, but most functionality remains. + +## Licensing + +This software is licensed with the MIT license (see [LICENSE](./LICENSE)) and uses some components that are distributed under their own terms (see [LICENSE-3RD-PARTY](./LICENSE-3RD-PARTY)). + +--- + +### Future project Todos: + +[ ] Add [Widgetbook](https://pub.dev/packages/widgetbook). + +[ ] Update version in readme for each release. + +[ ] Fix untranslated strings - see `untranslated.json`. + +[ ] Set up themes to inherit from parent classes - think of toolbar within bottom of appbar. + +[ ] Integrate ZetaColors better. + +[ ] Analyze dependencies for weaknesses, possibly phase out some. + +[ ] Improve Github actions to be safer and to add a change log. + +[ ] Remove code from this repo that is not strictly needed for flutter - github actions could be in another repo? diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml new file mode 100644 index 0000000..bf6b1ae --- /dev/null +++ b/all_lint_rules.yaml @@ -0,0 +1,223 @@ +linter: + rules: + - always_use_package_imports + - avoid_dynamic_calls + - avoid_empty_else + - avoid_print + - avoid_relative_lib_imports + - avoid_returning_null_for_future + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + - avoid_web_libraries_in_flutter + - cancel_subscriptions + - close_sinks + - collection_methods_unrelated_type + - comment_references + - control_flow_in_finally + - deprecated_member_use_from_same_package + - diagnostic_describe_all_properties + - discarded_futures + - empty_statements + - hash_and_equals + - implicit_reopen + - invalid_case_patterns + - iterable_contains_unrelated_type + - list_remove_unrelated_type + - literal_only_boolean_expressions + - no_adjacent_strings_in_list + - no_duplicate_case_values + - no_logic_in_create_state + - no_self_assignments + - no_wildcard_variable_uses + - prefer_relative_imports + - prefer_void_to_null + - test_types_in_equals + - throw_in_finally + - unnecessary_statements + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + - use_key_in_widget_constructors + - valid_regexps + - depend_on_referenced_packages + - package_names + - secure_pubspec_urls + - sort_pub_dependencies + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + - always_specify_types + - annotate_overrides + - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - await_only_futures + - camel_case_extensions + - camel_case_types + - cascade_invocations + - cast_nullable_to_non_nullable + - combinators_ordering + - conditional_uri_does_not_exist + - constant_identifier_names + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - deprecated_consistency + - directives_ordering + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - implementation_imports + - implicit_call_tearoffs + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + - lines_longer_than_80_chars + - matching_super_parameters + - missing_whitespace_between_adjacent_strings + - no_default_cases + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_runtimeType_toString + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_null_aware_operators + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - require_trailing_commas + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_unnamed_constructors_first + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - unreachable_from_main + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_late_for_private_fields_and_variables + - use_named_constants + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - void_checks diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a730826 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,45 @@ +include: all_lint_rules.yaml +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "test/.test_coverage.dart" + - "bin/cache/**" + - "lib/generated_plugin_registrant.dart" + + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + + errors: + included_file_warning: ignore + missing_required_param: error + missing_return: error + deprecated_member_use_from_same_package: ignore + parameter_assignments: warning + switch_case_completes_normally: false + +linter: + rules: + always_put_control_body_on_new_line: false + always_put_required_named_parameters_first: false + always_specify_types: false + always_use_package_imports: false + avoid_classes_with_only_static_members: false + avoid_positional_boolean_parameters: false + avoid_types_on_closure_parameters: false + cascade_invocations: false + close_sinks: false + lines_longer_than_80_chars: false + omit_local_variable_types: false + prefer_constructors_over_static_methods: false + prefer_double_quotes: false + prefer_expression_function_bodies: false + prefer_final_parameters: false + prefer_int_literals: false + sort_constructors_first: false + unnecessary_final: false + avoid_catches_without_on_clauses: false + avoid_annotating_with_dynamic: false + avoid_catching_errors: false diff --git a/assets/zds-dark.png b/assets/zds-dark.png new file mode 100644 index 0000000..d45fbc1 Binary files /dev/null and b/assets/zds-dark.png differ diff --git a/assets/zds-light.png b/assets/zds-light.png new file mode 100644 index 0000000..077c0a9 Binary files /dev/null and b/assets/zds-light.png differ diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..f116db2 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "e1e47221e86272429674bec4f1bd36acc4fc7b77" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + - platform: macos + create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..e69de29 diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..0a741cb --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..13866c8 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,59 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 33 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + minSdkVersion 23 + targetSdkVersion 33 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..c208884 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f085e7e --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 0000000..e793a00 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d74aa35 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/xml/provider_paths.xml b/example/android/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..ffa74ab --- /dev/null +++ b/example/android/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..c208884 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..1dd7260 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,32 @@ +buildscript { + ext.kotlin_version = '1.7.21' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} + +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cc5527d --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..8b808a0 --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + end + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..66320a6 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,222 @@ +PODS: + - connectivity_plus (0.0.1): + - Flutter + - ReachabilitySwift + - DKImagePickerController/Core (4.3.4): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.4) + - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.4) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - flutter_image_compress (1.0.0): + - Flutter + - Mantle + - SDWebImage + - SDWebImageWebPCoder + - flutter_inappwebview (0.0.1): + - Flutter + - flutter_inappwebview/Core (= 0.0.1) + - OrderedSet (~> 5.0) + - flutter_inappwebview/Core (0.0.1): + - Flutter + - OrderedSet (~> 5.0) + - flutter_keyboard_visibility (0.0.1): + - Flutter + - fluttertoast (0.0.2): + - Flutter + - Toast + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - image_compression_flutter (1.0.0): + - Flutter + - image_editor_common (1.0.0): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - libwebp (1.3.1): + - libwebp/demux (= 1.3.1) + - libwebp/mux (= 1.3.1) + - libwebp/sharpyuv (= 1.3.1) + - libwebp/webp (= 1.3.1) + - libwebp/demux (1.3.1): + - libwebp/webp + - libwebp/mux (1.3.1): + - libwebp/demux + - libwebp/sharpyuv (1.3.1) + - libwebp/webp (1.3.1): + - libwebp/sharpyuv + - Mantle (2.2.0): + - Mantle/extobjc (= 2.2.0) + - Mantle/extobjc (2.2.0) + - nb_utils (0.0.1): + - Flutter + - open_filex (0.0.2): + - Flutter + - OrderedSet (5.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.1.1): + - Flutter + - ReachabilitySwift (5.0.0) + - SDWebImage (5.17.0): + - SDWebImage/Core (= 5.17.0) + - SDWebImage/Core (5.17.0) + - SDWebImageWebPCoder (0.13.0): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.17) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FMDB (>= 2.7.5) + - SwiftyGif (5.4.4) + - Toast (4.0.0) + - url_launcher_ios (0.0.1): + - Flutter + - video_compress (0.3.0): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + +DEPENDENCIES: + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - flutter_image_compress (from `.symlinks/plugins/flutter_image_compress/ios`) + - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - image_compression_flutter (from `.symlinks/plugins/image_compression_flutter/ios`) + - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - nb_utils (from `.symlinks/plugins/nb_utils/ios`) + - open_filex (from `.symlinks/plugins/open_filex/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_compress (from `.symlinks/plugins/video_compress/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - FMDB + - libwebp + - Mantle + - OrderedSet + - ReachabilitySwift + - SDWebImage + - SDWebImageWebPCoder + - SwiftyGif + - Toast + +EXTERNAL SOURCES: + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + flutter_image_compress: + :path: ".symlinks/plugins/flutter_image_compress/ios" + flutter_inappwebview: + :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + image_compression_flutter: + :path: ".symlinks/plugins/image_compression_flutter/ios" + image_editor_common: + :path: ".symlinks/plugins/image_editor_common/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + nb_utils: + :path: ".symlinks/plugins/nb_utils/ios" + open_filex: + :path: ".symlinks/plugins/open_filex/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_compress: + :path: ".symlinks/plugins/video_compress/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + +SPEC CHECKSUMS: + connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e + DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433 + flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + image_compression_flutter: 5327fc5ee43f5ee6107d44ff0d4480a363788358 + image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 + image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + libwebp: 33dc822fbbf4503668d09f7885bbfedc76c45e96 + Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d + nb_utils: ada4338858d8827ec92fdab2a545206b4ba4cfb1 + open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 + SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102 + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe + webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f + +PODFILE CHECKSUM: ef2759a31e8883d6b534c00ef22a7f4104f8de0d + +COCOAPODS: 1.12.1 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9f93cb7 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,566 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3D3AAF756CA80E49B749AA81 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 525030F3249BE5805514C9E2 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2108C8DFE1A942E20F62EEBB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 52361143A9C897E0EDF29C93 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 525030F3249BE5805514C9E2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5746C102481DD0CE2FDBF9BF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3D3AAF756CA80E49B749AA81 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4CCA392AFDE3CF386FAC31F6 /* Pods */ = { + isa = PBXGroup; + children = ( + 5746C102481DD0CE2FDBF9BF /* Pods-Runner.debug.xcconfig */, + 2108C8DFE1A942E20F62EEBB /* Pods-Runner.release.xcconfig */, + 52361143A9C897E0EDF29C93 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 826C3C5439B7851241E4A5D3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 525030F3249BE5805514C9E2 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 4CCA392AFDE3CF386FAC31F6 /* Pods */, + 826C3C5439B7851241E4A5D3 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + CCFE906A2CEE0C6A25910D29 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + EB347F71DB431DBD76862E64 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + CCFE906A2CEE0C6A25910D29 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EB347F71DB431DBD76862E64 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + EXCLUDED_ARCHS = arm64; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = arm64; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + EXCLUDED_ARCHS = arm64; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + EXCLUDED_ARCHS = arm64; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + EXCLUDED_ARCHS = arm64; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = arm64; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = arm64; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..6239cbb --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,77 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSApplicationQueriesSchemes + + https + http + + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + NSAppleMusicUsageDescription + Allow app to play music trough Apple music. + NSBluetoothAlwaysUsageDescription + Allow application to access device Bluetooth. + NSCameraUsageDescription + Use camera to scan QR code and take pictures or videos. Pictures or videos can be uploaded as documents. + NSFaceIDUsageDescription + Allow application to use Face ID/Touch ID for authentication purposes. + NSLocationWhenInUseUsageDescription + You might restricted to use the application based on your location + NSMicrophoneUsageDescription + To capture video, please allow access to Microphone + NSPhotoLibraryAddUsageDescription + Allow application to add photos to your Photo Roll + NSPhotoLibraryUsageDescription + Use pictures on phone and upload them along with content + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/lib/home.dart b/example/lib/home.dart new file mode 100644 index 0000000..af355be --- /dev/null +++ b/example/lib/home.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; + +import 'package:zds_flutter/zds_flutter.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import 'routes.dart'; + +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + + @override + State createState() => _HomePageState(); +} + +final Map colorMap = { + 'Red': ZetaColorBase.red, + 'Orange': ZetaColorBase.orange, + 'Yellow': ZetaColorBase.yellow, + 'Green': ZetaColorBase.green, + 'Blue': ZetaColorBase.blue, + 'Teal': ZetaColorBase.teal, + 'Pink': ZetaColorBase.pink, +}; + +const String _darkModeKey = 'dark'; + +class _HomePageState extends State { + GlobalKey key = GlobalKey(); + + @override + Widget build(BuildContext _) { + return Builder(builder: (context) { + return Scaffold( + appBar: AppBar( + title: const Text('ZDS_Flutter Demo'), + centerTitle: true, + actions: [ + ZdsPopupMenu( + onSelected: (dynamic val) { + if (val is ZetaColorSwatch) { + ZetaColors.setColors(context, ZetaColors(primary: val, secondary: val)); + } else if (val is String && val == _darkModeKey) { + ZetaColors.setDarkMode(context, !ZetaColors.of(context).isDarkMode); + } + setState(() { + key = GlobalKey(); + }); + }, + builder: (_, open) => IconButton( + splashRadius: 20, + visualDensity: VisualDensity.compact, + onPressed: open, + icon: const Icon(ZdsIcons.more_vert), + ), + items: [ + ...colorMap.entries + .map( + (e) => ZdsPopupMenuItem( + value: e.value, + child: ListTile( + visualDensity: VisualDensity.compact, + title: Text(e.key), + ), + ), + ) + .toList(), + ZdsPopupMenuItem( + value: _darkModeKey, + child: ListTile( + visualDensity: VisualDensity.compact, + title: Text(ZetaColors.of(context).isDarkMode ? 'Light mode' : 'Dark mode'), + ), + ), + ], + ), + ], + ), + body: Builder( + key: key, + builder: (context) { + print('BUILDERING'); + return ZdsList.builder( + padding: const EdgeInsets.symmetric(vertical: 14), + itemCount: kRoutes.length, + itemBuilder: (context, index) { + final rec = kRoutes.entries.toList()[index]; + final items = rec.value..sort((a, b) => a.title.compareTo(b.title)); + return ZdsExpansionTile( + title: Text( + rec.key, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: ZetaColors.of(context).textDefault, + ), + ), + child: ZdsListGroup( + items: items.map((route) { + return ZdsListTile( + title: Text(route.title), + trailing: Icon( + Icons.keyboard_arrow_right, + color: ZetaColors.of(context).textDefault, + ), + onTap: () => Navigator.of(context).pushNamed(route.routeName)); + }).toList(), + ).space(14), + ); + }, + ); + }), + ); + }); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..3c8b3cf --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +import 'routes.dart'; + +void main() => runApp(const DemoApp()); + +class DemoApp extends StatelessWidget { + const DemoApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ZdsApp( + title: 'zds_flutter Demo', + routes: kAllRoutes, + ); + } +} diff --git a/example/lib/pages/assets/animations.dart b/example/lib/pages/assets/animations.dart new file mode 100644 index 0000000..9ac6f33 --- /dev/null +++ b/example/lib/pages/assets/animations.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:lottie/lottie.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class AnimationsDemo extends StatelessWidget { + const AnimationsDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final List animations = [ + { + 'name': 'ZdsAnimations.checkCircle', + 'animation': ZdsAnimations.checkCircle, + }, + { + 'name': 'ZdsAnimations.twoChecks', + 'animation': ZdsAnimations.twoChecks, + }, + { + 'name': 'ZdsAnimations.thumbsUpApproved', + 'animation': ZdsAnimations.thumbsUpApproved, + }, + { + 'name': 'ZdsAnimations.thumbsUp', + 'animation': ZdsAnimations.thumbsUp, + }, + { + 'name': 'ZdsAnimations.checkRipple', + 'animation': ZdsAnimations.checkRipple, + }, + { + 'name': 'ZdsAnimations.timeApprovedBox', + 'animation': ZdsAnimations.timeApprovedBox, + }, + { + 'name': 'ZdsAnimations.approvalStamped', + 'animation': ZdsAnimations.approvalStamped, + }, + { + 'name': 'ZdsAnimations.check', + 'animation': ZdsAnimations.check, + }, + { + 'name': 'ZdsAnimations.checkGlimmer', + 'animation': ZdsAnimations.checkGlimmer, + }, + { + 'name': 'ZdsAnimations.timeApproved', + 'animation': ZdsAnimations.timeApproved, + }, + { + 'name': 'ZdsAnimations.timecardTapping', + 'animation': ZdsAnimations.timecardTapping, + }, + { + 'name': 'ZdsAnimations.timeApprovedGlimmer', + 'animation': ZdsAnimations.timeApprovedGlimmer, + }, + ]..sort((a, b) => a['name'].compareTo(b['name'])); + return Wrap( + spacing: 80, + runSpacing: 80, + children: animations.map((e) => AnimationBox(name: e['name'], animation: e['animation'])).toList(), + ); + } +} + +class AnimationBox extends StatelessWidget { + final String name; + final String animation; + + const AnimationBox({Key? key, required this.name, required this.animation}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200 * MediaQuery.devicePixelRatioOf(context), + height: 200 * MediaQuery.devicePixelRatioOf(context), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Lottie.asset(animation, height: 84), + const SizedBox(height: 8), + Text(name), + ], + ), + ); + } +} diff --git a/example/lib/pages/assets/icons.dart b/example/lib/pages/assets/icons.dart new file mode 100644 index 0000000..acb6666 --- /dev/null +++ b/example/lib/pages/assets/icons.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'dart:core'; + +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class IconsDemo extends StatefulWidget { + const IconsDemo({Key? key}) : super(key: key); + + @override + State createState() => _IconsDemoState(); +} + +class _IconsDemoState extends State { + List keys = []; + + Future>> getData(BuildContext context) async { + final List> icons = []; + final data = jsonDecode( + await DefaultAssetBundle.of(context).loadString('packages/zds_flutter/lib/assets/fonts/selection.json'), + )['icons']; + for (final e in data) { + icons.add({ + 'ZdsIcons.${e['properties']['name']}': + IconData(e['properties']['code'], fontFamily: 'zds-icons', fontPackage: 'zds_flutter') + }); + keys.add(GlobalKey()); + } + + return icons; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: getData(context), + builder: (BuildContext context, AsyncSnapshot future) { + if (future.connectionState == ConnectionState.done && future.data != null) { + final List> icons = future.data as List>; + return GridView.count( + crossAxisCount: (MediaQuery.of(context).size.width / 100).floor(), + padding: const EdgeInsets.symmetric(vertical: 32), + children: [ + ...icons.map((e) { + return Column( + children: [ + Icon( + e.values.first, + color: ZdsColors.darkGrey, + size: 48, + ).withPopOver( + (context) => Container( + constraints: const BoxConstraints(maxHeight: 300, maxWidth: 300), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), + child: Text(e.keys.first), + ), + ) + ], + ); + }).toList() + ], + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); + } +} diff --git a/example/lib/pages/assets/images.dart b/example/lib/pages/assets/images.dart new file mode 100644 index 0000000..bf9a3c9 --- /dev/null +++ b/example/lib/pages/assets/images.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ImagesDemo extends StatelessWidget { + const ImagesDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final List images = [ + { + 'name': 'ZdsImages.notifications', + 'image': ZdsImages.notifications, + }, + { + 'name': 'ZdsImages.chat', + 'image': ZdsImages.chat, + }, + { + 'name': 'ZdsImages.notes', + 'image': ZdsImages.notes, + }, + { + 'name': 'ZdsImages.calendar', + 'image': ZdsImages.calendar, + }, + { + 'name': 'ZdsImages.completedTasks', + 'image': ZdsImages.completedTasks, + }, + { + 'name': 'ZdsImages.emptyBox', + 'image': ZdsImages.emptyBox, + }, + { + 'name': 'ZdsImages.sadZebra', + 'image': ZdsImages.sadZebra, + }, + { + 'name': 'ZdsImages.sleepingZebra', + 'image': ZdsImages.sleepingZebra, + }, + { + 'name': 'ZdsImages.search', + 'image': ZdsImages.search, + }, + { + 'name': 'ZdsImages.loadFail', + 'image': ZdsImages.loadFail, + }, + { + 'name': 'ZdsImages.cloudFail', + 'image': ZdsImages.cloudFail, + }, + { + 'name': 'ZdsImages.serverFail', + 'image': ZdsImages.serverFail, + }, + { + 'name': 'ZdsImages.punch', + 'image': ZdsImages.punch, + }, + { + 'name': 'ZdsImages.map', + 'image': ZdsImages.map, + }, + ]; + + return SingleChildScrollView( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + child: Column( + children: images + .map((e) => ImgBox(img: e['image'], name: e['name'])) + .toList() + .divide(const SizedBox(height: 32)) + .toList(), + ), + ), + ); + } +} + +class ImgBox extends StatelessWidget { + final Widget img; + final String name; + + const ImgBox({Key? key, required this.img, required this.name}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + img, + const SizedBox(height: 12), + Text(name), + const SizedBox(height: 32), + ], + ); + } +} diff --git a/example/lib/pages/components/app_bar.dart b/example/lib/pages/components/app_bar.dart new file mode 100644 index 0000000..528d228 --- /dev/null +++ b/example/lib/pages/components/app_bar.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class AppBarDemo extends StatefulWidget { + const AppBarDemo({Key? key}) : super(key: key); + + @override + _AppBarDemoState createState() => _AppBarDemoState(); +} + +class _AppBarDemoState extends State with SingleTickerProviderStateMixin { + TabController? controller; + + @override + void initState() { + super.initState(); + controller = TabController(length: 5, vsync: this); + } + + @override + void dispose() { + controller!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final tabs = ZdsTabBar( + controller: controller, + tabs: const [ + ZdsTab( + icon: Icon(ZdsIcons.details), + label: 'Details', + ), + ZdsTab( + icon: Icon(ZdsIcons.edit), + label: 'Edit', + ), + ZdsTab( + icon: Icon(ZdsIcons.delete), + label: 'Discard', + ), + ZdsTab( + icon: Icon(ZdsIcons.unclaim), + label: 'Unclaim', + ), + ZdsTab( + icon: Icon(ZdsIcons.history), + label: 'History', + ), + ], + ); + final actions = [ + IconButton( + onPressed: () {}, + icon: const Icon(ZdsIcons.edit), + ), + ]; + return Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + ZdsAppBar( + title: const Text('Primary color scheme'), + subtitle: const Text('Subtitle!'), + icon: CircleAvatar( + radius: 15, + backgroundColor: ZdsColors.white, + child: Text( + 'DM', + style: TextStyle( + color: Theme.of(context).colorScheme.primaryContainer, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + actions: actions, + bottom: tabs, + ), + ZdsAppBar( + color: ZdsTabBarColor.basic, + title: const Text('Basic color scheme'), + icon: CircleAvatar( + radius: 15, + backgroundColor: ZdsColors.white, + child: Text( + 'DM', + style: TextStyle( + color: Theme.of(context).colorScheme.primaryContainer, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + subtitle: const Text('Subtitle!'), + actions: actions, + bottom: tabs, + ), + ZdsAppBar( + title: const Text('Surface color scheme'), + subtitle: const Text('Subtitle!'), + color: ZdsTabBarColor.surface, + icon: CircleAvatar( + radius: 15, + backgroundColor: ZdsColors.white, + child: Text( + 'DM', + style: TextStyle( + color: Theme.of(context).colorScheme.primaryContainer, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + actions: actions, + bottom: tabs, + ), + ].divide(const SizedBox(height: 40)).toList(), + ), + ), + ); + } +} diff --git a/example/lib/pages/components/big_toggle_button.dart b/example/lib/pages/components/big_toggle_button.dart new file mode 100644 index 0000000..6311d7d --- /dev/null +++ b/example/lib/pages/components/big_toggle_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class BigToggleButtonDemo extends StatefulWidget { + const BigToggleButtonDemo({Key? key}) : super(key: key); + + @override + State createState() => _BigToggleButtonDemoState(); +} + +class _BigToggleButtonDemoState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + ZdsToggleButton( + values: const ['A', 'B', 'C', 'D'], + onToggleCallback: print, + backgroundColor: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(height: 10), + const ZdsToggleButton( + values: ['Approved', 'Rejected'], + onToggleCallback: print, + margin: EdgeInsets.zero, + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/components/block_table.dart b/example/lib/pages/components/block_table.dart new file mode 100644 index 0000000..3ffcbb7 --- /dev/null +++ b/example/lib/pages/components/block_table.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class BlockTableDemo extends StatelessWidget { + const BlockTableDemo({super.key}); + + @override + Widget build(BuildContext context) { + final headers = [ + ZdsBlockTableHeader(text: 'Sun'), + ZdsBlockTableHeader(text: 'Mon'), + ZdsBlockTableHeader(text: 'Tue'), + ZdsBlockTableHeader(text: 'Wed'), + ZdsBlockTableHeader(text: 'Thur'), + ZdsBlockTableHeader(text: 'Fri'), + ZdsBlockTableHeader(text: 'Sat'), + ]; + + final data = [ + ZdsBlockTableRow( + header: 'Grouping 1', + titleCell: ZdsBlockTableCellData(text: 'Row 1'), + data: [ + ZdsBlockTableCellData(text: '10am', isSelected: true), + ZdsBlockTableCellData( + child: const Padding( + padding: EdgeInsets.all(8), + child: Wrap( + children: [ + Icon( + ZdsIcons.add, + color: Colors.black, + ), + ], + ), + ), + ), + ZdsBlockTableCellData(child: ZdsImages.sadZebra), + ZdsBlockTableCellData(text: '1pm'), + ZdsBlockTableCellData(text: '2pm'), + ZdsBlockTableCellData(text: '3pm'), + ZdsBlockTableCellData(text: '10am'), + ], + ), + ZdsBlockTableRow( + header: 'Grouping 1', + titleCell: ZdsBlockTableCellData(text: 'Row 1'), + data: [ + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ], + ), + ZdsBlockTableRow( + titleCell: ZdsBlockTableCellData(text: 'Row 1'), + data: [ + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ZdsBlockTableCellData(text: '10am'), + ], + ), + ]; + + return Scaffold( + body: ZdsBlockTable( + cellHeight: 90, + headers: headers, + rows: data, + ), + ); + } +} diff --git a/example/lib/pages/components/bottom_bar.dart b/example/lib/pages/components/bottom_bar.dart new file mode 100644 index 0000000..a32a3a4 --- /dev/null +++ b/example/lib/pages/components/bottom_bar.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class BottomBarDemo extends StatelessWidget { + const BottomBarDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + bottomNavigationBar: ZdsBottomBar( + child: Row( + children: [ + ZdsButton.outlined( + child: const Text('Save'), + onTap: () {}, + ), + const Spacer(), + ZdsButton.outlined( + child: const Text('Clear'), + onTap: () {}, + ), + const SizedBox(width: 8), + ZdsButton( + child: const Text('Cancel'), + onTap: () {}, + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/bottom_sheet.dart b/example/lib/pages/components/bottom_sheet.dart new file mode 100644 index 0000000..28b13f9 --- /dev/null +++ b/example/lib/pages/components/bottom_sheet.dart @@ -0,0 +1,482 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class BottomSheetDemo extends StatefulWidget { + const BottomSheetDemo({Key? key}) : super(key: key); + + @override + _BottomSheetDemoState createState() => _BottomSheetDemoState(); +} + +class _BottomSheetDemoState extends State with SingleTickerProviderStateMixin { + TabController? controller; + + @override + void initState() { + controller = TabController(length: 2, vsync: this); + + super.initState(); + } + + @override + void dispose() { + controller!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ZdsButton( + onTap: bottomSheetUsingBuildSheetBars, + child: const Text('bottom sheet for Tablet and Mobile'), + ), + ZdsButton( + onTap: bottomSheetUsingBuildSheetBars2, + child: const Text('bottom sheet for Tablet and Mobile within builder'), + ), + ZdsButton( + onTap: showBottomSheet1, + child: const Text('Show with tab bar'), + ), + ZdsButton( + child: const Text('Enforce sheet on Tablets/iPads'), + onTap: () => showBottomSheet3(enforceSheet: true), + ), + ZdsButton.muted( + onTap: showBottomSheet3, + child: const Text('Show bottomsheet 5 items'), + ), + ].divide(const SizedBox(height: 20)).toList(), + ), + ); + } + + void showBottomSheet1() { + Widget buildList() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ZdsListGroup( + headerLabel: Text('Search for title, description and unique id'), + items: [ + ZdsListTile( + title: Text('Title'), + trailing: Text('Title'), + ), + ZdsListTile( + title: Text('Priority'), + trailing: Text('All'), + ), + ], + ), + ZdsListTile( + title: const Text('Favorite'), + trailing: Switch( + onChanged: (v) {}, + value: true, + ), + ), + ZdsListGroup( + headerLabel: Text('Event Tags'), + items: [ + ZdsListTile( + title: Text('State'), + subtitle: Text('Active'), + trailing: Icon(ZdsIcons.chevron_right), + ), + ZdsListTile( + title: Text('Event Type'), + subtitle: Text('All'), + trailing: Icon(ZdsIcons.chevron_right), + ), + ZdsListTile( + title: Text('Message Type'), + subtitle: Text('All stores'), + trailing: Icon(ZdsIcons.chevron_right), + ), + ], + ), + ], + ); + } + + Widget tabView1() { + return ZdsList( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: buildList(), + ), + ], + ); + } + + Widget tabView2() { + return const SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ZdsListGroup( + headerLabel: Text('System Filters'), + items: [ + ZdsListTile( + title: Text('System (New this week)'), + ), + ZdsListTile( + title: Text('System (New today)'), + ), + ZdsListTile( + title: Text('System (Completed today)'), + ), + ZdsListTile( + title: Text('System (Due today)'), + ), + ZdsListTile( + title: Text('System (Due in the last 7 days)'), + ), + ZdsListTile( + title: Text('System (Due this week)'), + ), + ZdsListTile( + title: Text('System (Favorite)'), + ), + ZdsListTile( + title: Text('System (Informational project)'), + ), + ZdsListTile( + title: Text('System (Overdue)'), + ), + ZdsListTile( + title: Text('System (Project edited)'), + ), + ZdsListTile( + title: Text('System (Upcoming)'), + ), + ], + ), + ZdsListGroup( + headerLabel: Text('Custom Filters'), + items: [ + ZdsListTile( + title: Text('Feeds Action'), + ), + ZdsListTile( + title: Text('Filter 3'), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + showZdsBottomSheet( + context: context, + headerBuilder: (context) => ZdsTabBar( + controller: controller, + tabs: const [ + ZdsTab( + label: 'Filter', + ), + ZdsTab( + label: 'Saved Filter', + ), + ], + ), + bottomBuilder: (context) => ZdsBottomBar( + child: Row( + children: [ + ZdsButton.outlined( + child: const Text('Save'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + const Spacer(), + ZdsButton.outlined( + child: const Text('Clear'), + onTap: () {}, + ), + const SizedBox(width: 8), + ZdsButton( + child: const Text('Cancel'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ), + builder: (context) => ExampleTabView( + controller: controller, + children: [ + tabView1(), + tabView2(), + ], + ), + ); + } + + void showBottomSheet3({bool enforceSheet = false}) { + showZdsBottomSheet( + enforceSheet: enforceSheet, + context: context, + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Select priority').textStyle(Theme.of(context).textTheme.headlineSmall).paddingOnly(bottom: 20), + ZdsListTile( + leading: const Text('Urgent'), + trailing: ZdsIndex( + color: ZdsColors.red, + child: const Text('U'), + ), + ), + ZdsListTile( + leading: const Text('Hight'), + trailing: ZdsIndex( + color: ZdsColors.orange, + child: const Text('1'), + ), + ), + ZdsListTile( + leading: const Text('Medium'), + trailing: ZdsIndex( + color: Theme.of(context).colorScheme.primaryContainer, + child: const Text('2'), + ), + ), + ZdsListTile( + leading: const Text('Low'), + trailing: ZdsIndex( + color: ZdsColors.green, + child: const Text('3'), + ), + ), + ], + ).paddingOnly(left: 16, right: 16); + }, + ); + } + + void bottomSheetUsingBuildSheetBars() { + final List sheetBarWidgets = buildSheetBars( + context: context, + title: 'Filter', + primaryActionText: 'Apply', + primaryActionOnTap: () {}, + secondaryActionText: 'Cancel', + ternaryActionText: 'Reset', + ternaryActionOnTap: () {}, + showClose: true, + ); + final ZdsValueController fromDateController = ZdsValueController(); + final ZdsValueController toDateController = ZdsValueController(); + const bool isDayOff = false; + + showZdsBottomSheet( + context: context, + bottomInset: 0, + builder: (context) => ListView( + shrinkWrap: true, + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ExcludeSemantics( + child: Text( + 'Date range', + style: Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.greySwatch(context)[1000]), + ).paddingOnly(left: 8), + ), + const SizedBox(height: 8), + ZdsDateRangePickerTile( + initialDateController: fromDateController, + finalDateController: toDateController, + ), + const SizedBox(height: 8), + Row( + children: [ + ExcludeSemantics( + child: Text( + 'Type', + style: Theme.of(context).textTheme.titleSmall!.copyWith( + color: ZdsColors.greySwatch(context)[1000], + ), + ).paddingOnly(left: 8), + ), + const Spacer(), + ], + ), + const SizedBox(height: 8), + ZdsListGroup( + padding: EdgeInsets.zero, + items: [ + Semantics( + checked: isDayOff, + child: ZdsListTile( + title: const Text('Day off'), + onTap: () {}, + trailing: Icon(ZdsIcons.check, color: Theme.of(context).colorScheme.secondary), + ), + ), + Semantics( + checked: isDayOff, + child: ZdsListTile( + title: const Text('Time off'), + onTap: () {}, + ), + ), + ], + ), + ], + ).paddingOnly(left: 16, right: 16, top: 12), + ), + const SizedBox(height: 16), + ], + ), + headerBuilder: (context) => sheetBarWidgets[0] as PreferredSizeWidget, + bottomBuilder: (context) => + sheetBarWidgets.asMap().containsKey(1) ? sheetBarWidgets[1] as PreferredSizeWidget : null, + ); + } + + void bottomSheetUsingBuildSheetBars2() { + final List sheetBarWidgets = buildSheetBars( + context: context, + title: 'Filter', + primaryActionText: 'Apply', + primaryActionOnTap: () {}, + secondaryActionText: 'Cancel', + ternaryActionText: 'Reset', + ternaryActionOnTap: () {}, + showClose: true, + ); + final ZdsValueController fromDateController = ZdsValueController(); + final ZdsValueController toDateController = ZdsValueController(); + const bool isDayOff = false; + + showZdsBottomSheet( + context: context, + bottomInset: 0, + maxHeight: 440, + builder: (context) => Column( + children: [ + sheetBarWidgets[0], + Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ExcludeSemantics( + child: Text( + 'Date range', + style: + Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.greySwatch(context)[1000]), + ).paddingOnly(left: 10), + ), + const SizedBox(height: 8), + ZdsDateRangePickerTile( + initialDateController: fromDateController, + finalDateController: toDateController, + ), + const SizedBox(height: 8), + Row( + children: [ + ExcludeSemantics( + child: Text( + 'Type', + style: Theme.of(context).textTheme.titleSmall!.copyWith( + color: ZdsColors.greySwatch(context)[1000], + ), + ).paddingOnly(left: 10), + ), + const Spacer(), + ], + ), + const SizedBox(height: 8), + ZdsListGroup( + padding: EdgeInsets.zero, + items: [ + Semantics( + checked: isDayOff, + child: ZdsListTile( + title: const Text('Day off'), + onTap: () {}, + trailing: Icon(ZdsIcons.check, color: Theme.of(context).colorScheme.secondary), + ), + ), + Semantics( + checked: isDayOff, + child: ZdsListTile( + title: const Text('Time off'), + onTap: () {}, + ), + ), + ], + ), + ], + ), + ).paddingInsets(const EdgeInsets.symmetric(horizontal: 40, vertical: 12)), + ), + sheetBarWidgets[1] + ], + ), + ); + } +} + +class ExampleTabView extends StatefulWidget { + final TabController? controller; + final List children; + + const ExampleTabView({Key? key, required this.controller, required this.children}) : super(key: key); + + @override + _ExampleTabViewState createState() => _ExampleTabViewState(); +} + +class _ExampleTabViewState extends State with SingleTickerProviderStateMixin { + int index = 0; + + @override + void initState() { + super.initState(); + index = widget.controller!.index; + widget.controller!.addListener(listener); + } + + void listener() { + setState(() { + index = widget.controller!.index; + }); + } + + @override + void dispose() { + widget.controller!.removeListener(listener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.children[index]; + } +} diff --git a/example/lib/pages/components/bottom_tab_bar.dart b/example/lib/pages/components/bottom_tab_bar.dart new file mode 100644 index 0000000..f664735 --- /dev/null +++ b/example/lib/pages/components/bottom_tab_bar.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class BottomTabBarDemo extends StatefulWidget { + const BottomTabBarDemo({Key? key}) : super(key: key); + + @override + _BottomTabBarDemoState createState() => _BottomTabBarDemoState(); +} + +class _BottomTabBarDemoState extends State { + int currentIndex = 0; + static const _boxSize = 120.0; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Container( + height: _boxSize, + width: _boxSize, + color: Theme.of(context).colorScheme.primary, + child: IconWithBadge( + Icons.article_outlined, + unread: 4, + size: 60, + iconContainerColor: Theme.of(context).colorScheme.primary, + semanticsLabel: '4 unread forms', + ), + ), + Container( + height: _boxSize, + width: _boxSize, + color: Theme.of(context).colorScheme.surface, + child: IconWithBadge( + Icons.article_outlined, + unread: 40, + size: 60, + color: Theme.of(context).colorScheme.primary, + semanticsLabel: '4 unread forms', + ), + ), + Container( + height: _boxSize, + width: _boxSize, + color: Theme.of(context).colorScheme.secondary, + child: IconWithBadge( + Icons.article_outlined, + unread: 100, + size: 60, + iconContainerColor: Theme.of(context).colorScheme.secondary, + semanticsLabel: '4 unread forms', + ), + ) + ].map((e) { + return ZdsCard( + padding: EdgeInsets.zero, + child: e, + ); + }).toList(), + ), + ), + ), + bottomNavigationBar: ZdsBottomTabBar( + currentIndex: currentIndex, + onTap: (index) => setState(() => currentIndex = index), + items: const [ + ZdsNavItem( + icon: IconWithBadge( + Icons.article_outlined, + unread: 4, + semanticsLabel: '4 unread forms', + ), + label: 'Forms', + ), + ZdsNavItem( + icon: IconWithBadge( + Icons.search, + ), + label: 'Search', + ), + ZdsNavItem( + icon: IconWithBadge( + Icons.analytics_outlined, + unread: 50000, + maximumDigits: 4, + ), + label: 'Reports', + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/components/bottom_tab_scaffold.dart b/example/lib/pages/components/bottom_tab_scaffold.dart new file mode 100644 index 0000000..4f2ec6c --- /dev/null +++ b/example/lib/pages/components/bottom_tab_scaffold.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class BottomTabScaffoldDemo extends StatelessWidget { + const BottomTabScaffoldDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ZdsBottomTabScaffold( + tabs: const [ + ZdsNavItem( + icon: IconWithBadge( + Icons.article_outlined, + unread: 4, + semanticsLabel: '4 unread forms', + ), + label: 'Forms', + ), + ZdsNavItem( + icon: IconWithBadge( + Icons.search, + ), + label: 'Search', + ), + ZdsNavItem( + icon: IconWithBadge( + Icons.analytics_outlined, + unread: 50000, + maximumDigits: 4, + ), + label: 'Reports', + ), + ], + bodyBuilder: (context, index) => Center(child: Text(index.toString())), + ); + } +} diff --git a/example/lib/pages/components/button.dart b/example/lib/pages/components/button.dart new file mode 100644 index 0000000..823e875 --- /dev/null +++ b/example/lib/pages/components/button.dart @@ -0,0 +1,345 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ButtonDemo extends StatefulWidget { + const ButtonDemo({Key? key}) : super(key: key); + + @override + State createState() => _ButtonDemoState(); +} + +class _ButtonDemoState extends State { + bool _isLabelVisible = true; + ScrollController scrollController = ScrollController(); + bool _isFabExtended = true; + + late bool isChecked; + late bool isButtonSelected; + + @override + void initState() { + scrollController.addListener(() { + if (scrollController.position.userScrollDirection == ScrollDirection.reverse && scrollController.offset > 32) { + setState(() { + _isLabelVisible = false; + }); + } else if (scrollController.position.userScrollDirection == ScrollDirection.forward) { + setState(() { + _isLabelVisible = true; + }); + } + }); + isChecked = false; + isButtonSelected = false; + super.initState(); + } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final floatingActionButton = ZdsFloatingActionButton.extended( + icon: const Icon(ZdsIcons.edit), + extendedIconLabelSpacing: _isLabelVisible ? null : 0, + extendedPadding: _isLabelVisible ? null : const EdgeInsetsDirectional.only(start: 10, end: 8), + label: AnimatedSwitcher( + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition(sizeFactor: animation, axis: Axis.horizontal, axisAlignment: -1, child: child); + }, + duration: const Duration(milliseconds: 300), + child: _isLabelVisible ? const Text('Show non-expanding FAB') : const SizedBox.shrink(), + ), + onPressed: () => setState(() => _isFabExtended = !_isFabExtended), + ); + + final slideableButtonKey = GlobalKey(); + return Scaffold( + floatingActionButton: _isFabExtended + ? floatingActionButton + : ZdsFloatingActionButton( + icon: const Icon(ZdsIcons.edit), + onPressed: () => setState(() => _isFabExtended = !_isFabExtended), + ), + body: SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + for (final hasOnTap in [true, false]) ...[ + Text('${hasOnTap ? 'with' : 'without'} onTap', style: Theme.of(context).textTheme.displayLarge), + const SizedBox(height: 10), + ZdsButton.filled( + onTap: hasOnTap ? () {} : null, + child: const Text('Filled'), + ), + const SizedBox(height: 10), + ZdsButton.filled( + isDangerButton: true, + onTap: hasOnTap ? () {} : null, + child: const Text('Filled danger'), + ), + const SizedBox(height: 10), + ZdsButton.outlined( + onTap: hasOnTap ? () {} : null, + child: const Text('Outlined'), + ), + const SizedBox(height: 10), + ZdsButton.outlined( + isDangerButton: true, + onTap: hasOnTap ? () {} : null, + child: const Text('Outlined danger'), + ), + const SizedBox(height: 10), + ZdsButton.text( + onTap: hasOnTap ? () {} : null, + child: const Text('Text'), + ), + const SizedBox( + height: 10, + ), + Container( + color: Theme.of(context).primaryColor, + child: ZdsButton.text( + onTap: hasOnTap ? () {} : null, + isOnDarkBackground: true, + child: const Text('Text onDarkBackground'), + ), + ), + const SizedBox(height: 10), + ZdsButton.muted( + onTap: hasOnTap ? () {} : null, + child: const Text('Muted'), + ), + ], + // Row( + // children: [ + // Expanded( + // child: SingleChildScrollView( + // scrollDirection: Axis.horizontal, + // child: Row( + // children: [ + // const SizedBox(width: 24), + // ...ZetaColors.of(context) + // .rainbow + // .map( + // (e) => ZdsButton.filled( + // onTap: () {}, + // color: e, + // child: const Text('Button'), + // ), + // ) + // .divide(const SizedBox(width: 16)) + // .toList(), + // const SizedBox(width: 24), + // ], + // ), + // ), + // ) + // ], + // ).paddingInsets(const EdgeInsets.symmetric(vertical: 24)), + Align( + alignment: Alignment.centerRight, + child: ZdsPopupMenu( + builder: (_, open) => IconButton( + splashRadius: 20, + visualDensity: VisualDensity.compact, + onPressed: open, + color: ZdsColors.greySwatch(context)[800], + icon: const Icon(ZdsIcons.more_vert), + ), + items: const [ + ZdsPopupMenuItem( + child: ListTile( + visualDensity: VisualDensity.compact, + title: Text('More'), + ), + ), + ], + ), + ), + Text('Checkable', style: Theme.of(context).textTheme.displayLarge), + const SizedBox(height: 10), + ZdsCheckableButton( + isChecked: isChecked, + icon: ZdsIcons.add, + onChanged: () => setState(() => isChecked = !isChecked), + ), + const SizedBox(height: 10), + ZdsCheckableButton( + isChecked: !isChecked, + label: 'Mon', + onChanged: () => setState(() => isChecked = !isChecked), + ), + const SizedBox(height: 10), + const ZdsCheckableButton( + icon: ZdsIcons.add, + ), + const SizedBox(height: 10), + const ZdsCheckableButton( + isChecked: true, + label: 'Mon', + ), + const SizedBox(height: 50), + Text('Selection pills', style: Theme.of(context).textTheme.displayLarge), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ZdsSelectionPill( + selected: isButtonSelected, + label: 'All', + onTap: () => setState(() => isButtonSelected = !isButtonSelected), + leadingIcon: const Icon(ZdsIcons.person_info), + onClose: () {}, + // color: ZetaColorBase.green, + ), + ZdsSelectionPill( + selected: !isButtonSelected, + label: 'Approved', + onTap: () => setState(() => isButtonSelected = !isButtonSelected), + onClose: () {}, + ), + const ZdsSelectionPill( + selected: true, + label: 'Pending', + ), + const ZdsSelectionPill( + label: 'Declined', + ), + ], + ), + ), + ), + const SizedBox(height: 100), + Text('Slidable', style: Theme.of(context).textTheme.displayLarge), + const SizedBox(height: 12), + Text('Disabled', style: Theme.of(context).textTheme.headlineMedium), + const SizedBox(height: 12), + SizedBox( + width: 336, + child: Column( + children: [ + ZdsSlidableButton( + buttonColor: Theme.of(context).colorScheme.secondary, + buttonText: 'Clock In', + buttonSliderColor: Theme.of(context).primaryColorLight, + buttonIcon: ZdsIcons.clock_start, + ), + const SizedBox(height: 12), + ZdsSlidableButton( + buttonColor: ZdsColors.greyWarmSwatch.shade900, + buttonText: 'Clock Out', + buttonSliderColor: ZdsColors.greyWarmSwatch.shade200, + buttonIcon: ZdsIcons.clock_stop, + ), + const SizedBox(height: 36), + Text('Active, with animation, stays completed', style: Theme.of(context).textTheme.headlineMedium), + const SizedBox(height: 12), + ZdsButton.filled( + child: const Text('Reset toggle'), + onTap: () { + slideableButtonKey.currentState?.reset(); + }, + ), + const SizedBox(height: 12), + ZdsSlidableButton( + key: slideableButtonKey, + buttonColor: Theme.of(context).colorScheme.secondary, + stayCompleted: true, + buttonSliderColor: Theme.of(context).primaryColorLight, + buttonText: 'Meal Start', + buttonIcon: ZdsIcons.meal, + onSlideComplete: () async { + debugPrint('Done!'); + return true; + }, + ), + const SizedBox(height: 36), + Text('Active, basic without animation', style: Theme.of(context).textTheme.headlineMedium), + const SizedBox(height: 12), + ZdsSlidableButton( + buttonColor: ZdsColors.greyWarmSwatch.shade900, + buttonText: 'Clock Out', + buttonIcon: ZdsIcons.clock_stop, + buttonSliderColor: ZdsColors.greyWarmSwatch.shade200, + onSlideComplete: () async { + debugPrint('Done!'); + return true; + }, + ), + const SizedBox(height: 36), + Text( + 'Active, changes colors, loading indicator', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 12), + ZdsSlidableButton( + buttonColor: ZdsColors.greyWarmSwatch.shade900, + buttonColorEnd: Theme.of(context).colorScheme.secondary, + buttonText: 'Clock Out', + buttonTextEnd: DateTime.now().format('KK:mm a'), + buttonIcon: ZdsIcons.clock_stop, + buttonIconEnd: ZdsIcons.check_circle, + buttonSliderColor: ZdsColors.greyWarmSwatch.shade200, + buttonSliderColorEnd: Theme.of(context).primaryColorLight, + onSlideComplete: () async { + debugPrint('Done!'); + await Future.delayed(const Duration(seconds: 1)); + return true; + }, + ), + const SizedBox(height: 36), + Text('Disabled, no message', style: Theme.of(context).textTheme.headlineMedium), + const SizedBox(height: 12), + ZdsSlidableButton( + buttonColor: ZdsColors.greyWarmSwatch.shade900, + buttonText: 'Clock Out', + buttonIcon: ZdsIcons.clock_stop, + buttonSliderColor: ZdsColors.greyWarmSwatch.shade200, + ), + const SizedBox(height: 36), + Text('Disabled with message', style: Theme.of(context).textTheme.headlineMedium), + const SizedBox(height: 12), + ZdsSlidableButton( + buttonColor: ZdsColors.greyWarmSwatch.shade900, + buttonText: 'Clock Out', + buttonIcon: ZdsIcons.clock_stop, + buttonSliderColor: ZdsColors.greyWarmSwatch.shade200, + disabledMessage: 'Disabled message that is quite long and goes over two lines', + ), + ], + ), + ), + const SizedBox(height: 300), + ], + ), + ), + ); + } + + void showToast(BuildContext context, Color color, String title, {Color? backgroundColor}) { + ScaffoldMessenger.of(context).showZdsToast( + ZdsToast( + multiLine: true, + title: Text(title), + leading: const Icon(ZdsIcons.check_circle), + actions: [ + IconButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + icon: const Icon(ZdsIcons.close), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/components/calendar.dart b/example/lib/pages/components/calendar.dart new file mode 100644 index 0000000..8485930 --- /dev/null +++ b/example/lib/pages/components/calendar.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class CalendarDemo extends StatefulWidget { + const CalendarDemo({Key? key}) : super(key: key); + + @override + _CalendarDemoState createState() => _CalendarDemoState(); +} + +class _CalendarDemoState extends State { + DateTime currentDate = DateTime.now(); + DateTime focusedDate = DateTime.now(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + TextButton( + onPressed: () { + setState(() { + focusedDate = focusedDate.add(const Duration(days: 1)); + }); + }, + child: const Text('increase day'), + ), + ZdsCalendar( + selectedDay: focusedDate, + events: [ + CalendarEvent(id: 'a', date: DateTime.now()), + CalendarEvent(id: 'b', date: DateTime.now().subtract(const Duration(days: 1))), + CalendarEvent(id: 'c', date: DateTime.now().add(const Duration(days: 1))), + ], + holidayEvents: [DateUtils.dateOnly(DateTime(2023, 05, 23))], + onDaySelected: (selectedDay, focusedDay) { + debugPrint('Focused day is $focusedDay'); + debugPrint('Selected day is $selectedDay'); + }, + onFormatChanged: (format) => debugPrint('The format has changed and it is now $format'), + onPageChanged: (focusedDay) => debugPrint('The page has changed and the focused day is now $focusedDay'), + ), + const SizedBox(height: 50), + ZdsCalendar.monthly( + isRangeSelectable: true, + events: [ + CalendarEvent(id: 'a', date: DateTime.now()), + CalendarEvent(id: 'b', date: DateTime.now().subtract(const Duration(days: 1))), + CalendarEvent(id: 'c', date: DateTime.now().add(const Duration(days: 1))), + ], + onRangeSelected: (start, end, focusedDay) { + debugPrint('A range has been selected, from $start to $end. The focused day is $focusedDay'); + }, + ), + const SizedBox( + height: 50, + ), + ZdsCalendar.weekly( + startingDayOfWeek: StartingDayOfWeek.thursday, + initialSelectedDay: DateTime(currentDate.year, currentDate.month, currentDate.day + 1), + events: [ + CalendarEvent(id: 'a', date: DateTime.now()), + CalendarEvent(id: 'b', date: DateTime.now().subtract(const Duration(days: 1))), + CalendarEvent(id: 'c', date: DateTime.now().add(const Duration(days: 1))), + ], + onDaySelected: (selectedDay, focusedDay) { + debugPrint('Focused day is $focusedDay'); + debugPrint('Selected day is $selectedDay'); + }, + onAllSelected: (start, end, focusedDay) { + debugPrint('A Week has been selected, from $start to $end. The focused day is $focusedDay'); + }, + ), + const SizedBox(height: 50), + ZdsCalendar.weekly( + initialSelectedWeek: DateTime(2022, 09, 10), + startingDayOfWeek: StartingDayOfWeek.wednesday, + showAllButton: true, + events: const [], + onDaySelected: (selectedDay, focusedDay) { + debugPrint('Focused day is $focusedDay'); + debugPrint('Selected day is $selectedDay'); + }, + onAllSelected: (start, end, focusedDay) { + debugPrint('A Week has been selected, from $start to $end. The focused day is $focusedDay'); + }, + ), + const SizedBox(height: 50), + ZdsCalendar.monthly( + headerPadding: const EdgeInsets.fromLTRB(4, 14, 8, 14), + events: [ + CalendarEvent(id: 'a', date: DateTime.now()), + CalendarEvent(id: 'b', date: DateTime.now().subtract(const Duration(days: 1))), + CalendarEvent(id: 'c', date: DateTime.now().add(const Duration(days: 1))), + ], + isRangeSelectable: true, + holidayEvents: List.generate(12, (index) { + return DateTime(DateTime.now().year, index + 1, 10); + }), + isGridShown: true, + singleMarkerBuilder: (context, date, _) { + return Container( + decoration: BoxDecoration( + color: date.isAfter(DateTime.now()) ? ZdsColors.red : ZdsColors.green, shape: BoxShape.circle), + width: 5, + height: 5, + ).paddingOnly(top: 7); + }, + onRangeSelected: (start, end, focusedDay) { + debugPrint('A range has been selected, from $start to $end. The focused day is $focusedDay'); + }, + weekIcons: [ + WeekIcon( + child: const Icon(Icons.abc_sharp), + firstDayOfWeek: DateTime.now().getFirstDayOfWeek().toMidnight, + ), + WeekIcon( + child: const Icon(Icons.accessibility_new_outlined), + firstDayOfWeek: DateTime.now().add(const Duration(days: 7)).getFirstDayOfWeek().toMidnight, + ), + ], + ), + const SizedBox(height: 50), + ZdsButton.filled( + child: const Text('DateRangePickerDialog'), + onTap: () { + showDialog( + context: context, + builder: (context) { + return Padding( + padding: const EdgeInsets.all(16), + child: ZdsDateRangePickerDialog( + lastDate: DateTime.now().add(const Duration(days: 50)), + firstDate: DateTime.now().subtract(const Duration(days: 50)), + ), + ); + }, + ); + }, + ), + const SizedBox(height: 50), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/card.dart b/example/lib/pages/components/card.dart new file mode 100644 index 0000000..456ab74 --- /dev/null +++ b/example/lib/pages/components/card.dart @@ -0,0 +1,101 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class CardDemo extends StatelessWidget { + const CardDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: SingleChildScrollView( + child: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + ZdsCard( + onTap: () {}, + onTapHint: 'perform action', + semanticLabel: 'semantic label', + backgroundColor: Theme.of(context).primaryColor, + child: const SizedBox( + height: 200, + width: 300, + child: Text('color and ontap'), + ), + ), + const SizedBox(height: 10), + ZdsCard( + variant: ZdsCardVariant.outlined, + onTap: () {}, + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Color(0xff0CACF0), + Color(0xff007ABA), + ], + ), + child: const SizedBox( + height: 200, + width: 300, + child: Text('Outline, gradient and ontap'), + ), + ), + const SizedBox(height: 16), + ZdsCard( + onTap: () {}, + child: const SizedBox( + height: 200, + width: 300, + child: Text('default color and ontap'), + ), + ), + const SizedBox(height: 16), + ZdsCard( + backgroundColor: Theme.of(context).primaryColor, + child: const SizedBox( + height: 200, + width: 300, + child: Text('color and no ontap'), + ), + ), + const SizedBox(height: 16), + ZdsCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + ZdsCardHeader( + leading: IconTheme.merge( + data: IconThemeData(size: 20, color: Theme.of(context).colorScheme.primary), + child: const Icon(ZdsIcons.walk) + .frame(width: 30, height: 30, alignment: Alignment.center) + .backgroundColor(Theme.of(context).colorScheme.primary.withLight(0.1)) + .circle(30), + ), + trailing: IconButton( + onPressed: () {}, + icon: Transform.rotate( + angle: math.pi / 2, + child: Icon(ZdsIcons.more_vert, color: ZdsColors.blueGrey), + ), + ), + child: const Text('With Header'), + ), + const SizedBox(height: 150), + ], + ), + ), + const SizedBox(height: 150), + const ZdsDashedLine(child: SizedBox(height: 150, width: 150)), + const SizedBox(height: 150), + ], + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/pages/components/card_actions.dart b/example/lib/pages/components/card_actions.dart new file mode 100644 index 0000000..134aa11 --- /dev/null +++ b/example/lib/pages/components/card_actions.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class CardActionsDemo extends StatelessWidget { + const CardActionsDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + ZdsCard( + padding: EdgeInsets.zero, + onTap: () {}, + child: const Column( + children: [ + SizedBox(height: 50), + ZdsCardActions( + children: [ + ZdsTag(child: Text('Incomplete')), + ], + ), + ], + ), + ), + const SizedBox(height: 10), + ZdsCard( + padding: EdgeInsets.zero, + onTap: () {}, + child: const Column( + children: [ + SizedBox(height: 50), + ZdsCardActions( + children: [ + ZdsTag( + color: ZdsTagColor.primary, + child: Text('Pending Review'), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 10), + ZdsCard( + padding: EdgeInsets.zero, + onTap: () {}, + child: const Column( + children: [ + SizedBox(height: 50), + ZdsCardActions( + children: [ + ZdsLabel( + icon: Icons.calendar_today, + child: Text('Valid 5 days'), + ), + ZdsTag( + rounded: true, + prefix: Text('1'), + child: Text('High Priority'), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 10), + ZdsCard( + padding: EdgeInsets.zero, + onTap: () {}, + child: const Column( + children: [ + SizedBox(height: 50), + ZdsCardActions( + children: [ + Row( + children: [ + ZdsLabel( + icon: ZdsIcons.schedule, + child: Text('30 min'), + ), + ZdsLabel( + icon: Icons.calendar_today, + child: Text('4 days'), + ), + ], + ), + ZdsTag( + rounded: true, + color: ZdsTagColor.error, + prefix: Text('U'), + child: Text('Urgent'), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 30), + ZdsCardWithActions( + actions: const [ + ZdsTag(child: Text('Incomplete')), + ], + children: [ + const CircleAvatar(), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 2), + Text( + 'District Manager Monthly Walk', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 8), + Text( + '04/19/2021 03:35 PM', + style: Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.blueGrey), + ), + const SizedBox(height: 16), + Text( + 'SR-ROC-Rockford IL.00102', + style: Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.blueGrey), + ), + ], + ) + ], + ), + const SizedBox(height: 10), + ZdsCardWithActions( + direction: ZdsCardDirection.vertical, + actions: const [ + ZdsTag(child: Text('Incomplete')), + ], + children: [ + Text( + 'Lorem ipsum dolor sit amet consectetur adipiscing elit proin sagittis ipsum at velit bibendum non.', + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/date_picker.dart b/example/lib/pages/components/date_picker.dart new file mode 100644 index 0000000..8e24d55 --- /dev/null +++ b/example/lib/pages/components/date_picker.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class DatePickerDemo extends StatefulWidget { + const DatePickerDemo({Key? key}) : super(key: key); + + @override + _DatePickerDemoState createState() => _DatePickerDemoState(); +} + +class _DatePickerDemoState extends State { + late ZdsValueController _controller; + late ZdsValueController _startController; + late ZdsValueController _endController; + DateTime? tileInitialDate; + DateTime? tileFinalDate; + final GlobalKey formKey = GlobalKey(); + + @override + void initState() { + _controller = ZdsValueController(); + _startController = ZdsValueController(); + _endController = ZdsValueController(); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ZdsList( + padding: const EdgeInsets.symmetric(vertical: 27, horizontal: 16), + children: [ + ZdsDateTimePicker( + emptyLabel: 'select date', + format: 'MMM dd, yyyy', + helpText: 'Help text example', + minDate: DateTime.now(), + inputDecoration: ZdsInputDecoration( + labelText: 'Date', + ), + onChange: (dateTime) {}, + ), + ZdsDateTimePicker( + emptyLabel: 'select date', + format: 'MMM dd, yyyy', + minDate: DateTime.now(), + helpText: 'Help text example', + controller: _controller, + inputDecoration: ZdsInputDecoration( + labelText: 'Optional Date', + suffixIcon: IconButton( + icon: const Icon( + ZdsIcons.close_circle, + size: 24, + ), + onPressed: () => _controller.value = null, + ), + ), + onChange: (dateTime) {}, + ), + ZdsDateTimePicker( + emptyLabel: 'select time', + format: 'hh:mm a', + textAlign: TextAlign.center, + minDate: DateTime.now(), + mode: DateTimePickerMode.time, + ), + ZdsDateTimePicker( + emptyLabel: 'time picker with interval', + format: 'hh:mm a', + textAlign: TextAlign.center, + minDate: DateTime.now(), + mode: DateTimePickerMode.time, + interval: 15, + ), + ZdsListTile( + title: const Text('Build Time'), + trailing: ZdsDateTimePicker( + emptyLabel: 'select date & time', + format: 'MMM dd, yyyy hh:mm a', + textAlign: TextAlign.end, + minDate: DateTime.now(), + mode: DateTimePickerMode.dateAndTime, + ), + ), + const Text('Example with controllers and custom validation:').paddingOnly(top: 8), + ZdsDateRangePickerTile( + initialDate: tileInitialDate, + finalDate: tileFinalDate, + onInitialDateChanged: (selectedDate) => tileInitialDate = selectedDate, + onFinalDateChanged: (selectedDate) => tileFinalDate = selectedDate, + initialDateController: _startController, + finalDateController: _endController, + errorMessage: 'Invalid range entered', + ), + const Text('Example using built-in form validation:').paddingOnly(top: 8), + Form( + key: formKey, + child: ZdsDateRangePickerTileForm( + validator: (value) => value.isValid == true || value.isIncomplete ? null : 'Please enter a valid value', + ), + ), + ZdsButton( + child: const Text('Clear range values'), + onTap: () { + formKey.currentState!.reset(); + _startController.value = null; + _endController.value = null; + }, + ) + ].divide(const SizedBox(height: 16)).toList(), + ); + } +} diff --git a/example/lib/pages/components/day_picker_demo.dart b/example/lib/pages/components/day_picker_demo.dart new file mode 100644 index 0000000..b19290e --- /dev/null +++ b/example/lib/pages/components/day_picker_demo.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class DayPickerDemo extends StatefulWidget { + const DayPickerDemo({Key? key}) : super(key: key); + + @override + State createState() => _DayPickerDemoState(); +} + +class _DayPickerDemoState extends State { + int inc = 0; + List initialSelectedDates = [DateTime.now().add(const Duration(days: 2))]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + const Text('Select individual'), + ZdsDayPicker( + startingWeekDate: DateTime.now().add(const Duration(days: -2)), + header: 'Days', + disableDaysList: const [0, 1], + onDaySelected: (List selectedDates) { + debugPrint('selected day details $selectedDates'); + }, + ).padding(10), + const Text('Select multiple').paddingOnly(top: 20), + ZdsDayPicker( + allowMultiSelect: true, + startingWeekDate: DateTime.now().add(const Duration(days: 1)), + header: 'Days', + disableDaysList: const [2, 3], + onDaySelected: (List selectedDates) { + debugPrint('selected day details $selectedDates'); + }, + ).padding(10), + const Text('Select multiple').paddingOnly(top: 20), + ZdsButton( + child: const Text('change initial selected dates'), + onTap: () { + setState(() { + initialSelectedDates = [DateTime.now().add(Duration(days: inc))]; + inc++; + if (inc > 6) { + inc = 0; + } + }); + }, + ), + ZdsDayPicker( + startingWeekDate: DateTime.now(), + initialSelectedDates: initialSelectedDates, + header: 'Days', + onDaySelected: (List selectedDates) { + debugPrint('selected day details $selectedDates'); + }, + allowMultiSelect: true, + ).padding(10), + const Text('Show outside card').paddingOnly(top: 20), + ZdsCard( + child: ZdsDayPicker( + startingWeekDate: DateTime.now(), + showInCard: false, + initialSelectedDates: [DateTime.now().add(const Duration(days: 2))], + header: 'Days', + onDaySelected: (List selectedDates) { + debugPrint('selected day details $selectedDates'); + }, + allowMultiSelect: true, + ).padding(10), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/components/empty_list_view.dart b/example/lib/pages/components/empty_list_view.dart new file mode 100644 index 0000000..d829bb4 --- /dev/null +++ b/example/lib/pages/components/empty_list_view.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class EmptyListView extends StatelessWidget { + const EmptyListView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + children: [ + ZdsButton( + child: const Text('show empty list view'), + onTap: () => showBottomSheet1(context), + ), + ZdsButton( + child: const Text('show empty list view 2'), + onTap: () => showBottomSheet2(context), + ), + ], + ), + ); + } + + void showBottomSheet1(BuildContext context) { + showZdsBottomSheet( + context: context, + headerBuilder: (context) => PreferredSize( + preferredSize: const Size(double.infinity, 82), + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 10), + color: Theme.of(context).colorScheme.surface, + child: IconTheme( + data: IconThemeData(color: Theme.of(context).colorScheme.primary), + child: Row( + children: [ + const Expanded( + flex: 6, + child: ZdsSearchField( + variant: ZdsSearchFieldVariant.outlined, + ), + ), + Expanded( + child: IconButton( + onPressed: () {}, + icon: const Icon(ZdsIcons.filter), + ), + ), + Expanded( + child: IconButton( + onPressed: () {}, + icon: const Icon(ZdsIcons.info), + ), + ), + ], + ), + ), + ), + ), + builder: (context) => ZdsList( + showEmpty: true, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + ), + ); + } + + void showBottomSheet2(BuildContext context) { + showZdsBottomSheet( + context: context, + headerBuilder: (context) => PreferredSize( + preferredSize: const Size(double.infinity, 82), + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 10), + color: Theme.of(context).colorScheme.surface, + child: IconTheme( + data: IconThemeData(color: Theme.of(context).colorScheme.primary), + child: Row( + children: [ + const Expanded( + flex: 6, + child: ZdsSearchField( + variant: ZdsSearchFieldVariant.outlined, + ), + ), + Expanded( + child: IconButton( + onPressed: () {}, + icon: const Icon(ZdsIcons.filter), + ), + ), + Expanded( + child: IconButton( + onPressed: () {}, + icon: const Icon(ZdsIcons.info), + ), + ), + ], + ), + ), + ), + ), + builder: (context) => ZdsList.builder( + physics: const ClampingScrollPhysics(), + showEmpty: true, + shrinkWrap: true, + itemBuilder: (context, index) { + return const ZdsListTile( + title: Text('List tile'), + ); + }, + ), + ); + } +} diff --git a/example/lib/pages/components/empty_view.dart b/example/lib/pages/components/empty_view.dart new file mode 100644 index 0000000..fd8b9e7 --- /dev/null +++ b/example/lib/pages/components/empty_view.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class EmptyViewDemo extends StatelessWidget { + const EmptyViewDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Custom').padding(16), + const ZdsEmpty( + icon: Icon(ZdsIcons.funnel), + message: Text('No saved filters available'), + ), + const Divider().paddingOnly(top: 32, bottom: 16), + const Text('Default'), + const ZdsEmpty(), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/expandable.dart b/example/lib/pages/components/expandable.dart new file mode 100644 index 0000000..0247abd --- /dev/null +++ b/example/lib/pages/components/expandable.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ExpandableDemo extends StatelessWidget { + const ExpandableDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + ZdsCard( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20).copyWith(bottom: 0), + child: const Text( + 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet END.', + textAlign: TextAlign.justify, + ).readMore( + collapsedButtonText: 'Read More', + expandedButtonText: 'Collapse', + ), + ), + const SizedBox( + height: 16, + ), + const ZdsExpandable( + collapsedButtonText: 'Read More', + expandedButtonText: 'Collapse', + child: Text( + 'START Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet END.', + textAlign: TextAlign.justify, + ), + ), + ], + ).content(), + ); + } +} diff --git a/example/lib/pages/components/expansion_tile.dart b/example/lib/pages/components/expansion_tile.dart new file mode 100644 index 0000000..4f26ad5 --- /dev/null +++ b/example/lib/pages/components/expansion_tile.dart @@ -0,0 +1,250 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ExpansionTileDemo extends StatefulWidget { + const ExpansionTileDemo({Key? key}) : super(key: key); + + @override + State createState() => _ExpansionTileDemoState(); +} + +class _ExpansionTileDemoState extends State { + final List _items = [ + Location('Store 101', 'North Main Street'), + Location('Store 102', 'North Main Street'), + ]; + + List _selectedItems = []; + + void _itemChange(Location item, bool isSelected) { + setState(() { + if (isSelected) { + _selectedItems.remove(item); + } else { + _selectedItems.add(item); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Column( + children: [ + ZdsCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + ZdsExpansionTile( + initiallyExpanded: true, + title: const Text('Covid Survey'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Emergency Form 2', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 3', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 19', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + ZdsExpansionTile( + title: const Text('Customer Service'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Customer Service Report', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('New Hire Training Confirmation', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + ZdsExpansionTile( + title: const Text('Management Audit Checklist'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Copy-1712 products and services...', style: Theme.of(context).textTheme.bodyMedium) + .space(), + ], + ), + ), + ZdsExpansionTile( + title: const Text('Store survey Checklist to evaluate the other'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Survey displays to evaluate if they are...', + style: Theme.of(context).textTheme.bodyMedium, + ).space(8), + Text( + 'Window displays to evaluate if they are...', + style: Theme.of(context).textTheme.bodyMedium, + ).space(), + ], + ), + ), + ZdsExpansionTile( + title: const Text('Collect feedback from customers who was'), + initiallyExpanded: true, + titleColor: const Color(0xff007ABA).withOpacity(0.1), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Products and services offered, pricing', style: Theme.of(context).textTheme.bodyMedium) + .space(), + ], + ), + ), + ], + ), + ).space(20), + ZdsCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + ZdsExpansionTile( + initiallyExpanded: true, + title: const Text('Covid Survey'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Emergency Form 2', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 3', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 19', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ZdsButton.outlined( + child: const Text('Clear'), + onTap: () {}, + ), + ZdsButton( + child: const Text('Apply'), + onTap: () {}, + ), + ], + ), + ), + ], + ), + ).space(20), + ZdsExpansionTile( + title: const Text('Tile outside of a card'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Sub-tile №1', style: Theme.of(context).textTheme.bodyMedium).space(6), + Text( + 'Another tile that will be hidden after pressing on the expansion tile', + style: Theme.of(context).textTheme.bodyMedium, + ).space(), + ], + ), + ).space(20), + ZdsExpansionTile( + title: const Text('Placeholder Name One'), + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14).copyWith(bottom: 6), + bottom: const Padding( + padding: EdgeInsets.only(left: 22, right: 22, bottom: 8), + child: Row( + children: [ + ZdsTag( + rectangular: true, + child: Text('Read'), + ), + ZdsTag( + rectangular: true, + child: Text('Write'), + ), + ZdsTag( + rectangular: true, + child: Text('Delete'), + ), + ], + ), + ), + child: ZdsList( + shrinkWrap: true, + children: [ + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 15), + dense: true, + title: const Text('Read'), + value: true, + onChanged: (val) {}, + ), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 15), + dense: true, + title: const Text('Write'), + value: true, + onChanged: (val) {}, + ), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 15), + dense: true, + title: const Text('Delete'), + value: true, + onChanged: (val) {}, + ), + ], + ), + ).space(60), + ZdsCard( + padding: EdgeInsets.zero, + child: ZdsExpansionTile.selectable( + titlePadding: const EdgeInsets.all(14), + contentPadding: const EdgeInsets.all(2), + title: const Text('Region 3'), + selected: _selectedItems.length > 1, + onSelected: (isSelected) { + setState(() { + if (isSelected) { + _selectedItems.addAll(_items); + } else { + _selectedItems = []; + } + }); + }, + child: ZdsList.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: _items.length, + itemBuilder: (context, index) { + return ZdsListTile( + shrinkWrap: true, + backgroundColor: Colors.transparent, + title: Text(_items[index].title!).paddingOnly(left: 52), + trailing: _selectedItems.contains(_items[index]) + ? Icon(ZdsIcons.check, color: Theme.of(context).colorScheme.secondary) + : null, + onTap: () { + _itemChange(_items[index], _selectedItems.contains(_items[index])); + }, + ); + }, + ), + ), + ).space(20), + ], + ), + ), + ), + ); + } +} + +class Location { + final String? title; + final String? subtitle; + + Location(this.title, this.subtitle); +} diff --git a/example/lib/pages/components/file_picker.dart b/example/lib/pages/components/file_picker.dart new file mode 100644 index 0000000..1194575 --- /dev/null +++ b/example/lib/pages/components/file_picker.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class FilePickerDemo extends StatefulWidget { + const FilePickerDemo({Key? key}) : super(key: key); + + @override + _FilePickerDemoState createState() => _FilePickerDemoState(); +} + +class _FilePickerDemoState extends State { + final ZdsFilePickerController controller = ZdsFilePickerController(); + + static const pickerConfig = FilePickerConfig( + maxFilesAllowed: 5, + maxFileSize: 2500000, + options: [ + FilePickerOptions.FILE, + FilePickerOptions.GIF, + FilePickerOptions.LINK, + FilePickerOptions.CAMERA, + FilePickerOptions.GALLERY, + ], + ); + + @override + void initState() { + super.initState(); + controller.addListener(() { + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return ZdsList( + padding: const EdgeInsets.all(14), + children: [ + ZdsFilePicker( + useCard: false, + config: pickerConfig, + controller: controller, + visualDensity: VisualDensity.compact, + optionDisplay: ZdsOptionDisplay.plain, + displayStyle: ZdsFilePickerDisplayStyle.horizontal, + onChange: (files) { + debugPrint('files: $files'); + }, + ), + ZdsFilePicker( + config: pickerConfig, + controller: controller, + optionDisplay: ZdsOptionDisplay.plain, + displayStyle: ZdsFilePickerDisplayStyle.horizontal, + onChange: (files) { + debugPrint('files: $files'); + }, + ), + ZdsFilePicker( + useCard: false, + config: pickerConfig, + controller: controller, + visualDensity: VisualDensity.compact, + onChange: (files) { + debugPrint('files: $files'); + }, + ), + ZdsFilePicker( + config: pickerConfig, + controller: controller, + onChange: (files) { + debugPrint('files: $files'); + }, + ), + ].divide(const SizedBox(height: 20)).toList(), + ); + } +} diff --git a/example/lib/pages/components/icon_text_button.dart b/example/lib/pages/components/icon_text_button.dart new file mode 100644 index 0000000..6097e3c --- /dev/null +++ b/example/lib/pages/components/icon_text_button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class IconTextButtonDemo extends StatelessWidget { + const IconTextButtonDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: SingleChildScrollView( + child: SizedBox( + width: double.infinity, + child: Row( + // Replace with a Column for vertical + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const ZdsIconTextButton( + icon: ZdsIcons.timecard, + label: 'Timecard', + ), + ZdsIconTextButton( + onTap: () => {onButtonTapped(context)}, + icon: ZdsIcons.clock_switch, + label: 'Shift Trade', + backgroundColor: ZetaColors.of(context).orange, + ), + ZdsIconTextButton( + onTap: () => {onButtonTapped(context)}, + icon: ZdsIcons.clock_available, + label: 'Availability', + backgroundColor: ZetaColors.of(context).pink, + ) + ], + ), + ), + ), + ); + } +} + +void onButtonTapped(BuildContext context) { + // Button click event handled here +} diff --git a/example/lib/pages/components/image_picker.dart b/example/lib/pages/components/image_picker.dart new file mode 100644 index 0000000..7107fbd --- /dev/null +++ b/example/lib/pages/components/image_picker.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ImagePickerDemo extends StatelessWidget { + const ImagePickerDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.surface, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ImagePicker( + icon: Icon( + ZdsIcons.camera, + size: 48, + color: Theme.of(context).colorScheme.primary, + ), + backgroundColor: Theme.of(context).colorScheme.surface, + onChange: (path) { + debugPrint(path); + }, + ), + ImagePicker( + size: 48, + showBorder: false, + backgroundColor: const Color.fromRGBO(0, 122, 186, 0.1), + icon: Icon( + ZdsIcons.camera, + size: 32, + color: Theme.of(context).colorScheme.primary, + ), + onChange: (path) { + debugPrint(path); + }, + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/index.dart b/example/lib/pages/components/index.dart new file mode 100644 index 0000000..91997e9 --- /dev/null +++ b/example/lib/pages/components/index.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class IndexDemo extends StatelessWidget { + const IndexDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView( + children: [ + ZdsListTile( + leading: ZdsIndex( + child: Text('2'), + color: ZdsColors.blue, + ), + title: Text('Showcase Extravaganza'), + ), + ZdsListTile( + leading: ZdsIndex( + child: Text('2'), + color: ZdsColors.purple, + ), + title: Text('Showcase Extravaganza'), + ), + const ZdsListTile( + leading: ZdsIndex( + child: Text('2'), + ), + title: Text('Showcase Extravaganza'), + ), + ZdsListTile( + title: Row( + children: [ + Text( + 'Priority', + style: TextStyle(color: Theme.of(context).primaryColor), + ), + const SizedBox(width: 6), + ZdsIndex( + color: Theme.of(context).colorScheme.error, + child: const Text('U'), + ), + ], + ), + trailing: Icon( + ZdsIcons.chevron_right, + color: Theme.of(context).primaryColor, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/infinite_list.dart b/example/lib/pages/components/infinite_list.dart new file mode 100644 index 0000000..8c0b682 --- /dev/null +++ b/example/lib/pages/components/infinite_list.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class InfiniteListDemo extends StatefulWidget { + const InfiniteListDemo({Key? key}) : super(key: key); + + @override + State createState() => _InfiniteListDemoState(); +} + +class _InfiniteListDemoState extends State { + List items = List.generate(20, (i) => i); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ZdsInfiniteListView( + compact: true, + itemBuilder: (context, index) { + return ZdsListTile( + title: Text(index.toString()), + onTap: () {}, + ); + }, + itemCount: items.length, + hasMore: true, + onLoadMore: () async { + await Future.delayed(const Duration(milliseconds: 300)); + setState(() => items = [...items, ...List.generate(10, (i) => i + items.length)]); + }, + ), + ), + ); + } +} diff --git a/example/lib/pages/components/information_bar.dart b/example/lib/pages/components/information_bar.dart new file mode 100644 index 0000000..0490263 --- /dev/null +++ b/example/lib/pages/components/information_bar.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class InformationBarDemo extends StatelessWidget { + const InformationBarDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column(children: [ + ZdsInformationBar( + zetaColorSwatch: ZetaColors.of(context).green, + icon: ZdsIcons.check, + label: 'Approved', + ), + SizedBox(height: 10), + ZdsInformationBar( + zetaColorSwatch: ZetaColors.of(context).blue, + icon: ZdsIcons.hourglass, + label: 'Pending', + ), + SizedBox(height: 10), + ZdsInformationBar( + zetaColorSwatch: ZetaColors.of(context).red, + icon: ZdsIcons.close, + label: 'Declined', + ), + SizedBox(height: 10), + ZdsInformationBar( + icon: Icons.category, + label: 'Neutral text', + zetaColorSwatch: ZetaColors.of(context).warm, + ) + ]), + ); + } +} diff --git a/example/lib/pages/components/list.dart b/example/lib/pages/components/list.dart new file mode 100644 index 0000000..ec3c114 --- /dev/null +++ b/example/lib/pages/components/list.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +import 'package:zds_flutter/zds_flutter.dart'; + +class ListDemo extends StatelessWidget { + const ListDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + ZdsListGroup( + headerLabel: const Text('Search for title'), + headerActions: [ + InkWell( + onTap: () {}, + child: const Row( + children: [Icon(ZdsIcons.add), Text('Add Tile')], + ), + ), + ], + items: [ + ListView.separated( + separatorBuilder: (_, i) => const Divider(), + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: 5, + itemBuilder: (_, index) { + return ZdsListTile( + onTap: () {}, + title: const Text('View summary'), + trailing: const Icon(ZdsIcons.chevron_right), + ); + }, + ) + ], + ), + ZdsHorizontalList( + caption: const Text('Your Shift'), + children: [ + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + ], + ), + ZdsHorizontalList( + caption: const Text( + 'Your Shift', + ).font(size: 15), + children: [ + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + ], + ), + ZdsHorizontalList( + children: [ + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + ], + ), + ZdsHorizontalList( + caption: const Text('Reduced Height List'), + isReducedHeight: true, + children: [ + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + const SizedBox(width: 160, height: 220).backgroundColor(Colors.red).padding(10), + ], + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/components/list_tile.dart b/example/lib/pages/components/list_tile.dart new file mode 100644 index 0000000..2f4b03c --- /dev/null +++ b/example/lib/pages/components/list_tile.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:zds_flutter/zds_flutter.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class ListTileDemo extends StatelessWidget { + const ListTileDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final ZetaColors colors = ZetaColors.of(context); + + return Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView( + children: [ + const SizedBox(height: 20), + ZdsSelectableListTile( + title: const Text('Urgent'), + subTitle: const Text('32 hours available'), + selected: true, + trailing: ZdsIndex(color: colors.red, child: const Text('U')), + onTap: () {}, + ), + ZdsSelectableListTile( + title: const Text('High'), + trailing: ZdsIndex(color: colors.orange, child: const Text('1')), + onTap: () {}, + ), + ZdsSelectableListTile( + title: const Text('Medium'), + trailing: ZdsIndex(color: colors.teal, child: const Text('2')), + onTap: () {}, + ), + ZdsSelectableListTile( + title: const Text('Low'), + trailing: ZdsIndex(color: colors.green, child: const Text('3')), + onTap: () {}, + ), + ZdsSelectableListTile.checkable( + title: const Text('Checkable unselected'), + onTap: () {}, + ), + ZdsSelectableListTile.checkable( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Checkable selected'), + Text( + 'Checkable unselected', + style: Theme.of(context).textTheme.bodySmall, + ) + ], + ), + selected: true, + onTap: () {}, + ), + const SizedBox(height: 20), + ZdsListTile( + title: const Text('View summary'), + trailing: TextFormField( + textAlign: TextAlign.end, + decoration: const InputDecoration( + hintText: 'First Name', + ), + ), + ), + ZdsListTile( + leading: const Icon(ZdsIcons.camera), + title: const Text('View summary'), + subtitle: const Text('subtitle'), + trailing: Switch( + onChanged: (_) {}, + value: true, + ), + ), + ZdsListGroup( + headerLabel: const Text('Search for title, description and unique ID'), + items: [ + const ZdsListTile( + title: Text('View summary'), + subtitle: Text('subtitle'), + trailing: Icon(ZdsIcons.chevron_right), + ), + ZdsListTile( + leading: const Icon(ZdsIcons.search), + title: const Text('With onTap'), + trailing: const Icon(ZdsIcons.chevron_right), + onTap: () {}, + ), + ], + ), + const ZdsListTile( + leading: Icon(ZdsIcons.pdf), + title: Text('View summary'), + trailing: Icon( + ZdsIcons.chevron_right, + ), + ), + ZdsListTile( + leading: IconButton( + icon: const Icon(ZdsIcons.pdf), + onPressed: () {}, + ), + title: const Text('This is a very very very long loooong list tile'), + trailing: const Icon( + ZdsIcons.chevron_right, + ), + ), + ZdsListTile( + leading: IconButton( + icon: const Icon(ZdsIcons.pdf), + onPressed: () {}, + ), + title: const Text('With icon button normal'), + trailing: IconButton( + onPressed: () {}, + iconSize: 24, + icon: const Icon( + ZdsIcons.chevron_right, + ), + ), + ), + ZdsListGroup( + items: [ + ZdsListTile( + title: Text( + 'Team Leader - Store', + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + trailing: Icon( + ZdsIcons.check, + color: Theme.of(context).colorScheme.primary, + ), + ), + const ZdsListTile( + title: Text('View summary'), + trailing: Icon(ZdsIcons.check), + ), + const ZdsListTile( + title: Text('View summary'), + trailing: Icon(ZdsIcons.check), + ), + ], + ), + ZdsListTile( + title: Text( + 'Team Leader - Store', + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + trailing: Icon( + Icons.check, + color: Theme.of(context).colorScheme.primary, + ), + ), + ZdsCard( + child: ZdsPropertiesList( + direction: ZdsPropertiesListDirection.vertical, + properties: { + 'Das URL': '', + }, + ), + ), + ZdsCard( + child: ZdsPropertiesList( + direction: ZdsPropertiesListDirection.vertical, + properties: { + 'Application URL': 'None', + }, + ), + ), + ZdsListTile( + title: const ZdsPropertiesList( + direction: ZdsPropertiesListDirection.vertical, + properties: { + 'Domain Key': 'Please add', + }, + ), + trailing: IconButton( + icon: Icon( + Icons.add, + color: Theme.of(context).primaryColor, + ), + onPressed: () {}, + ), + ), + const ZdsListTile( + title: Text('View summary'), + trailing: Icon(ZdsIcons.check), + ), + const ZdsListTile( + title: Text('View summary'), + trailing: Icon(ZdsIcons.chevron_right), + ), + const ZdsListTile( + leading: Icon(ZdsIcons.pdf), + title: Text('Unique ID'), + trailing: Text('Unique ID'), + ), + const ZdsListTile( + leading: Icon(ZdsIcons.pdf), + title: Text('View type'), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('My walks'), + SizedBox(width: 10), + Icon(ZdsIcons.chevron_right), + ], + ), + ), + ZdsListTile( + onTap: () {}, + title: const Text('Notes'), + bottom: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('• This is a bullet point list'), + Text('• Next point'), + Text('• More information'), + ], + ).textStyle(Theme.of(context).textTheme.bodyLarge), + ), + ], + ), + ), + ZdsListTile( + onTap: () {}, + contentPadding: Theme.of(context).zdsListTileThemeData.contentPadding.copyWith(left: 0), + title: const Text('Not scheduled today'), + leading: Container( + width: 6, + height: 65, + color: ZetaColors.of(context).red, + ), + ), + ZdsNotificationTile( + dateLabel: 'MMM dd, yyyy hh:mm a', + content: 'PTO Request approved for Mon at Jan 11 at 11:00 am', + ), + ZdsNotificationTile( + onTap: () {}, + dateLabel: getNotificationDate(), + content: 'Meeting with Jordan Smith at Jan 11 at 11:00 am', + leadingData: Icon( + ZdsIcons.lightbulb, + color: ZetaColors.of(context).orange, + size: 16, + ), + ), + ZdsFieldsListTile( + shrink: false, + title: const Text( + 'Title of the Project - 0001 Client > Zone 6_thiscan go upwards of two or three lines_ Coporateconfiguration', + ), + fields: const [ + TileField( + start: Text('Start/End'), + end: Text('07/05/2021 - 07/05/2021'), + ), + TileField( + start: Text('Approval Date'), + end: Text('07/06/2021 16;45 PST'), + ), + TileField( + start: Text('Status?'), + end: Text('N/A'), + ), + ], + footnote: const Text( + 'You have exceeded your balance. You will be eligible to take more time off after March 1.', + ), + data: 'Any object', + onTap: (String? data) { + debugPrint(data ?? ''); + }, + ), + ZdsFieldsListTile( + title: Row( + children: [ + Icon( + ZdsIcons.task, + color: Theme.of(context).primaryColor, + size: 25, + ).paddingOnly(right: 10), + Text( + 'Manual Task Survey 2', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold), + ), + ], + ), + fieldsEndTextStyle: Theme.of(context).textTheme.bodyLarge, + fields: const [ + TileField( + start: Text('Execution level'), + end: Text('Store'), + ), + TileField( + start: Text('Assigned'), + end: Text('Store Manager'), + ), + TileField( + start: Text('Department'), + end: Text('Store'), + ), + TileField( + start: Text('Type'), + end: Text('System'), + ), + ], + data: 'Any object', + onTap: (String? data) {}, + ), + ZdsFieldsListTile( + startFieldFlexFactor: 2, + fieldsStartTextStyle: Theme.of(context).textTheme.bodyMedium, + fieldsEndTextStyle: Theme.of(context).textTheme.bodyMedium, + fields: const [ + TileField( + start: Text('Maximum number of days'), + end: Text('4 days'), + ), + TileField( + start: Text('Effective by'), + end: Text('Friday, Jan 20 2023'), + ), + ], + ) + ], + ), + ), + ); + } + + String getNotificationDate() { + final DateTime now = DateTime.now(); + return DateFormat('MMM dd, yyyy hh:mm a').format(now); + } +} diff --git a/example/lib/pages/components/modal.dart b/example/lib/pages/components/modal.dart new file mode 100644 index 0000000..f3e62b7 --- /dev/null +++ b/example/lib/pages/components/modal.dart @@ -0,0 +1,320 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ModalDemo extends StatelessWidget { + const ModalDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ZdsButton.muted( + child: const Text('Name modal'), + onTap: () { + showDialog( + builder: (BuildContext context) { + return ZdsModal( + actions: [ + ZdsButton.muted( + child: const Text('Cancel'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ZdsButton( + child: const Text('Save'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: TextField( + decoration: ZdsInputDecoration( + labelText: 'Edit Walk Name', + counter: const Text('Character left: 255'), + ), + ), + ), + ); + }, + context: context, + ); + }, + ), + ZdsButton.muted( + child: const Text('Expansion modal'), + onTap: () { + showDialog( + builder: (BuildContext context) { + return ZdsModal( + padding: EdgeInsets.zero, + actions: [ + ZdsButton.muted( + child: const Text('Cancel'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ZdsButton( + child: const Text('Save'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + child: Column( + children: [ + ZdsExpansionTile( + initiallyExpanded: true, + title: const Text('Covid Survey'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Emergency Form 2', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 3', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 19', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(left: 24, right: 24, top: 16), + child: TextField( + decoration: ZdsInputDecoration( + labelText: 'Question Tag and Response', + ), + ), + ), + ], + ), + ); + }, + context: context, + ); + }, + ), + ZdsButton.muted( + child: const Text('Input dialog'), + onTap: () { + showDialog( + builder: (BuildContext context) { + return ZdsInputDialog( + title: 'Save Filter', + hint: 'Enter filter name', + primaryAction: 'Save', + secondaryAction: 'Cancel', + characterCount: 30, + onValidate: (value) async { + if (value.isEmpty) { + return 'This field is mandatory'; + } else { + return null; + } + }, + ); + }, + context: context, + ); + }, + ), + ZdsButton.muted( + child: const Text('Multiple Expansion modal'), + onTap: () { + showDialog( + builder: (BuildContext context) { + return ZdsModal( + padding: EdgeInsets.zero, + actions: [ + ZdsButton.muted( + child: const Text('Cancel'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ZdsButton( + child: const Text('Save'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + child: Column( + children: [ + ZdsExpansionTile( + initiallyExpanded: true, + title: const Text('Form Instance Creation Date'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Emergency Form 2', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 3', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 19', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + ZdsExpansionTile( + title: const Text('Form Category'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Emergency Form 2', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 3', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 19', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + ZdsExpansionTile( + title: const Text('Question Tag and Response'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Emergency Form 2', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 3', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 19', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + ZdsExpansionTile( + title: const Text('Form Instance Creator Name'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Emergency Form 2', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 3', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 19', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + ZdsExpansionTile( + title: const Text('Form Unique Identifier'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Emergency Form 2', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 3', style: Theme.of(context).textTheme.bodyMedium).space(8), + Text('Emergency Form 19', style: Theme.of(context).textTheme.bodyMedium).space(), + ], + ), + ), + ], + ), + ); + }, + context: context, + ); + }, + ), + ZdsButton.muted( + child: const Text('Textboxes modal'), + onTap: () { + showDialog( + builder: (BuildContext context) { + return ZdsModal( + usesKeyboard: true, + actions: [ + ZdsButton.muted( + child: const Text('Cancel'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ZdsButton( + child: const Text('Save'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 16), + child: TextField( + decoration: ZdsInputDecoration( + labelText: 'Text box 1', + counter: const Text('Character left: 255'), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: TextField( + decoration: ZdsInputDecoration( + labelText: 'Text box 2', + counter: const Text('Character left: 255'), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: TextField( + decoration: ZdsInputDecoration( + labelText: 'Text box 3', + counter: const Text('Character left: 255'), + ), + ), + ), + ], + ), + ); + }, + context: context, + ); + }, + ), + ZdsButton.muted( + child: const Text('Icon modal'), + onTap: () { + showDialog( + builder: (BuildContext context) { + return ZdsModal( + icon: ZdsIcons.indicator_alert, + actions: [ + ZdsButton( + child: const Text('Ok'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + child: const Padding(padding: EdgeInsets.only(top: 16), child: Text('The icon can be changed.')), + ); + }, + context: context, + ); + }, + ), + ZdsButton.muted( + child: const Text('Aligned dialog'), + onTap: () { + showDialog( + builder: (BuildContext context) { + return ZdsModal( + icon: ZdsIcons.chevron_left, + crossAxisAlignment: CrossAxisAlignment.start, + actions: [ + ZdsButton( + child: const Text('Ok'), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ], + child: const Padding(padding: EdgeInsets.only(top: 16), child: Text('Slide to the left!')), + ); + }, + context: context, + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/navigation_menu.dart b/example/lib/pages/components/navigation_menu.dart new file mode 100644 index 0000000..9a5c7b6 --- /dev/null +++ b/example/lib/pages/components/navigation_menu.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class NavigationMenuDemo extends StatelessWidget { + const NavigationMenuDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ZdsList( + padding: EdgeInsets.zero, + children: [ + Container( + color: Theme.of(context).colorScheme.surface, + child: const ZdsProfile( + avatar: ZdsNetworkAvatar( + url: 'https://www.zebra.com/content/dam/zebra_dam/global/graphics/logos/zebra-logo-black-stacked.png', + initials: 'JD', + ), + nameText: Text('Jason Davis'), + jobTitleText: Text('Store Manager'), + ).padding(kMenuHorizontalPadding.left), + ), + const Divider(), + ZdsNavigationMenu( + children: [ + ZdsMenuItem( + label: const Text('Unit Name'), + title: const Text('SR-CHI-Downers Grove IL.00101'), + onTap: () {}, + trailing: const Icon(ZdsIcons.chevron_right), + ), + ZdsMenuItem( + label: const Text('Department'), + title: const Text('Store'), + onTap: () {}, + trailing: const Icon(ZdsIcons.chevron_right), + ), + ZdsMenuItem( + label: const Text('View type'), + title: const Text('My View'), + leading: const Icon(ZdsIcons.preview), + onTap: () {}, + trailing: const Icon(ZdsIcons.chevron_right), + ), + ], + ), + ZdsNavigationMenu( + withDividers: true, + label: const Text('Other apps'), + children: [ + ZdsMenuItem( + title: const Text('Pinboard'), + leading: const Icon(ZdsIcons.pin), + onTap: () {}, + trailing: const Icon(ZdsIcons.launch), + ), + ZdsMenuItem( + title: const Text('Q Chat'), + leading: const Icon(ZdsIcons.chat_unread_active), + onTap: () {}, + trailing: const Icon(ZdsIcons.launch), + ), + ZdsMenuItem( + title: const Text('Q Walk'), + leading: const Icon(ZdsIcons.walk), + onTap: () {}, + trailing: const Icon(ZdsIcons.launch), + ), + ZdsMenuItem( + title: const Text('Q Notes'), + leading: const Icon(ZdsIcons.new_message), + onTap: () {}, + trailing: const Icon(ZdsIcons.launch), + ), + ZdsMenuItem( + title: const Text('Q Check'), + leading: const Icon(ZdsIcons.report), + onTap: () {}, + trailing: const Icon(ZdsIcons.launch), + ), + ZdsMenuItem( + title: const Text('Q Forms'), + leading: const Icon(ZdsIcons.form), + onTap: () {}, + trailing: const Icon(ZdsIcons.launch), + ), + ZdsMenuItem( + title: const Text('Q Docs'), + leading: const Icon(ZdsIcons.project), + onTap: () {}, + trailing: const Icon(ZdsIcons.launch), + ), + ZdsMenuItem( + title: const Text('My Work'), + leading: const Icon(ZdsIcons.task), + onTap: () {}, + trailing: const Icon(ZdsIcons.launch), + ), + ], + ), + ZdsNavigationMenu( + withDividers: true, + children: [ + ZdsMenuItem( + title: const Text('How do I'), + leading: const Icon(ZdsIcons.how_do_I), + onTap: () {}, + trailing: const Icon(ZdsIcons.chevron_right), + ), + ZdsMenuItem( + title: const Text('About'), + leading: const Icon(ZdsIcons.info), + onTap: () {}, + trailing: const Icon(ZdsIcons.chevron_right), + ), + ZdsMenuItem( + title: const Text('Private Device'), + trailing: Switch( + onChanged: (_) {}, + value: true, + ), + ), + ], + ), + ZdsMenuItem( + title: Text( + 'Sign Out', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + leading: Icon(ZdsIcons.sign_out, color: Theme.of(context).colorScheme.error), + onTap: () {}, + ).paddingOnly(top: 10, bottom: 30), + ], + ); + } +} diff --git a/example/lib/pages/components/popover.dart b/example/lib/pages/components/popover.dart new file mode 100644 index 0000000..1ebc910 --- /dev/null +++ b/example/lib/pages/components/popover.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class PopOverDemo extends StatefulWidget { + const PopOverDemo({Key? key}) : super(key: key); + + @override + _PopOverDemoState createState() => _PopOverDemoState(); +} + +class _PopOverDemoState extends State { + final GlobalKey button1Key = GlobalKey(); + final GlobalKey button2Key = GlobalKey(); + final GlobalKey button3Key = GlobalKey(); + final GlobalKey button4Key = GlobalKey(); + final GlobalKey button5Key = GlobalKey(); + final GlobalKey button6Key = GlobalKey(); + + final EdgeInsets _contentPadding = const EdgeInsets.all(20); + + void _showPopover({required GlobalKey key, Widget? child, BoxConstraints? constraints}) { + showZdsPopOver( + context: key.currentContext ?? context, + builder: (BuildContext context) => Container( + constraints: constraints ?? const BoxConstraints(maxHeight: 300, maxWidth: 300), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), + child: child ?? + const Text( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ZdsAppBar( + title: const Text('Popover Demo'), + icon: const Icon(ZdsIcons.launch).withPopOver((context) => const FlutterLogo().paddingInsets(_contentPadding)), + actions: [ + ZdsPopOverIconButton( + icon: const Icon(ZdsIcons.info), + popOverBuilder: (context) { + return const Text( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + ).paddingInsets(_contentPadding); + }, + ), + ZdsPopOverIconButton( + icon: const Icon(ZdsIcons.sort), + backgroundColor: ZdsColors.red, + popOverBuilder: (context) { + return Container( + child: const Text('Lorem ipsum dolor sit amet.') + .textStyle(Theme.of(context).textTheme.bodyLarge!.copyWith(color: ZdsColors.white)), + ).paddingInsets(_contentPadding); + }, + ), + ], + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ZdsButton.text( + key: button1Key, + child: const Text('Tap me'), + onTap: () { + _showPopover(key: button1Key); + }, + ), + ZdsButton.text( + key: button2Key, + child: const Text('Tap me'), + onTap: () { + _showPopover(key: button2Key); + }, + ), + ], + ), + Column( + children: [ + ZdsButton.text( + key: button3Key, + child: const Text('Tap me'), + onTap: () { + _showPopover(key: button3Key); + }, + ), + const SizedBox(height: 60), + ZdsButton.text( + key: button4Key, + child: const Text('Tap me'), + onTap: () { + _showPopover(key: button4Key); + }, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ZdsButton.text( + key: button5Key, + child: const Text('Tap me'), + onTap: () { + _showPopover(key: button5Key); + }, + ), + ZdsButton.text( + key: button6Key, + child: const Text('Tap me'), + onTap: () { + _showPopover(key: button6Key); + }, + ), + ], + ) + ], + ).paddingInsets(_contentPadding), + ); + } +} diff --git a/example/lib/pages/components/profile.dart b/example/lib/pages/components/profile.dart new file mode 100644 index 0000000..a8b1aa1 --- /dev/null +++ b/example/lib/pages/components/profile.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ProfileDemo extends StatelessWidget { + const ProfileDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(ZdsIcons.close), + onPressed: () { + Navigator.maybePop(context); + }, + ), + title: const Text('Close'), + ), + body: Column( + children: [ + ZdsProfile( + avatar: ZdsAvatar( + image: Image.network( + 'https://www.zebra.com/content/dam/zebra_dam/global/graphics/logos/zebra-logo-black-stacked.png', + ), + backgroundColor: ZdsColors.white, + ), + nameText: const Text('Jason Davis'), + jobTitleText: const Text('Store Manager'), + action: ZdsButton.text( + onTap: () {}, + child: Row( + children: [ + const Icon( + ZdsIcons.edit, + size: 18, + ).paddingOnly(right: 4), + const Text('Edit'), + ], + ), + ), + ).padding(24), + ], + ), + ); + } +} diff --git a/example/lib/pages/components/properties_list.dart b/example/lib/pages/components/properties_list.dart new file mode 100644 index 0000000..41b6e77 --- /dev/null +++ b/example/lib/pages/components/properties_list.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class PropertiesListDemo extends StatelessWidget { + const PropertiesListDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(14), + child: ZdsCard( + child: ZdsPropertiesList( + title: Text('4 # Walks'), + properties: { + 'Points Scored': '380', + 'Total Points': '700', + 'Score (%)': '42.12', + 'Passed (%)': '25.00', + }, + ), + ), + ), + Padding( + padding: EdgeInsets.all(14), + child: ZdsCard( + child: ZdsPropertiesList( + direction: ZdsPropertiesListDirection.vertical, + properties: { + 'Walk Description': 'A monthly store walk that Field Leadership should conduct with their stores.', + 'Start Date': '04/19/2021 03:35 PM', + 'End Date': '04/19/2021 11:59 PM', + 'Assign Unit': 'SR-ROC-Rockford IL.00102', + 'Type of Visit': 'Ad hoc', + 'Claim Date': '04/19/2021 03:35 PM', + 'User Name': 'Jason', + 'Announced Visit': 'Yes', + 'Walk Unique ID': 'WALK_688', + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/radio_list.dart b/example/lib/pages/components/radio_list.dart new file mode 100644 index 0000000..15b531d --- /dev/null +++ b/example/lib/pages/components/radio_list.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +enum WalkDateRange { + currentWeek, + lastWeek, + currentMonth, + lastMonth, + yearToDate, + lastXDays, + specificDates, +} + +class WalkDate extends ZdsRadioItem { + const WalkDate(String label, WalkDateRange value) : super(label, value); +} + +class RadioListDemo extends StatelessWidget { + const RadioListDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView( + children: [ + ZdsRadioList( + initialValue: const WalkDate('Current Month', WalkDateRange.currentMonth), + items: const [ + WalkDate('Current Week', WalkDateRange.currentWeek), + WalkDate('Last Week', WalkDateRange.lastWeek), + WalkDate('Current Month', WalkDateRange.currentMonth), + WalkDate('Last Month', WalkDateRange.lastMonth), + WalkDate('YTD', WalkDateRange.yearToDate), + WalkDate('Last X days', WalkDateRange.lastXDays), + WalkDate('Specific Dates', WalkDateRange.specificDates), + ], + onChange: (item) => debugPrint(item is WalkDate ? item.label : ''), + ).space(), + ZdsCard( + padding: EdgeInsets.zero, + child: ZdsExpansionTile( + initiallyExpanded: true, + title: const Text('Walk Date'), + contentPadding: EdgeInsets.zero, + child: ZdsRadioList( + items: const [ + WalkDate('Current Week', WalkDateRange.currentWeek), + WalkDate('Last Week', WalkDateRange.lastWeek), + WalkDate('Current Month', WalkDateRange.currentMonth), + WalkDate('Last Month', WalkDateRange.lastMonth), + WalkDate('YTD', WalkDateRange.yearToDate), + WalkDate('Last X days', WalkDateRange.lastXDays), + WalkDate('Specific Dates', WalkDateRange.specificDates), + ], + onChange: (item) => debugPrint(item is WalkDate ? item.label : ''), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/search.dart b/example/lib/pages/components/search.dart new file mode 100644 index 0000000..e51fb36 --- /dev/null +++ b/example/lib/pages/components/search.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class SearchDemo extends StatefulWidget { + const SearchDemo({Key? key}) : super(key: key); + + @override + _SearchDemoState createState() => _SearchDemoState(); +} + +class _SearchDemoState extends State { + final TextEditingController _clearButtonFieldController = TextEditingController(); + + bool _showSearchAppBarOverlay = false; + bool _showSuffixClearButton = false; + + void _cancel() { + setState(() { + _showSearchAppBarOverlay = false; + }); + + Navigator.of(context).pop(); + } + + void _toggleSearchOverlay() { + setState(() { + _showSearchAppBarOverlay = !_showSearchAppBarOverlay; + }); + } + + void _showSearchOverlay() { + setState(() { + _showSearchAppBarOverlay = true; + }); + } + + @override + void initState() { + super.initState(); + + _clearButtonFieldController.addListener(() { + setState(() { + _showSuffixClearButton = _clearButtonFieldController.text.isNotEmpty; + }); + }); + } + + @override + Widget build(BuildContext context) { + final clearButton = _showSuffixClearButton + ? IconButton( + icon: Icon( + ZdsIcons.close_circle, + color: ZetaColors.of(context).textSubtle, + ), + onPressed: _clearButtonFieldController.clear, + ) + : null; + + return Scaffold( + appBar: ZdsSearchAppBar( + leading: IconButton( + icon: Icon( + ZdsIcons.search_advance, + color: Theme.of(context).primaryColor, + ), + onPressed: _toggleSearchOverlay, + ), + trailing: ZdsButton.text( + onTap: _cancel, + child: const Text('Cancel'), + ), + hintText: 'Search in the App Bar', + showOverlay: _showSearchAppBarOverlay, + overlayBuilder: (_) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('no results found...').padding(5), + GestureDetector( + onTap: () {}, + child: Row( + children: [ + Icon( + ZdsIcons.search_advance, + color: Theme.of(context).primaryColor, + ).padding(5), + const Text('Try smart search').textStyle( + Theme.of(context).textTheme.titleSmall!.copyWith(color: Theme.of(context).primaryColor), + ) + ], + ), + ) + ], + ).padding(10), + onSubmit: (_) => _showSearchOverlay(), + ), + body: SingleChildScrollView( + child: Column( + children: [ + const Divider(), + Container( + color: Theme.of(context).colorScheme.surface, + padding: const EdgeInsets.all(8), + child: ZdsSearchField( + controller: _clearButtonFieldController, + hintText: 'Search with clear button', + suffixIcon: clearButton, + variant: ZdsSearchFieldVariant.outlined, + ), + ), + const SizedBox(height: 20), + Container( + color: Theme.of(context).colorScheme.surface, + padding: const EdgeInsets.all(8), + child: const ZdsSearchField( + hintText: 'Search', + initValue: 'search data', + variant: ZdsSearchFieldVariant.outlined, + ), + ), + const SizedBox(height: 20), + const Padding( + padding: EdgeInsets.all(8), + child: ZdsSearchField(hintText: 'Search'), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/shake_example.dart b/example/lib/pages/components/shake_example.dart new file mode 100644 index 0000000..d831b67 --- /dev/null +++ b/example/lib/pages/components/shake_example.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ShakeExample extends StatefulWidget { + const ShakeExample({Key? key}) : super(key: key); + + @override + State createState() => _ShakeExampleState(); +} + +class _ShakeExampleState extends State { + late final _shakeOnce = GlobalKey(); + late final _shakeTwice = GlobalKey(); + late final _shakeThrice = GlobalKey(); + + bool isDanger = false; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + children: [ + Expanded( + child: ZdsList( + children: [ + Column( + children: [ + ZdsShakeAnimation( + key: _shakeOnce, + shakeCount: 1, + shakeOffset: 5, + onAnimationUpdate: (status) { + setState(() { + isDanger = status == AnimationStatus.reverse || status == AnimationStatus.forward; + }); + }, + shakeDuration: const Duration(milliseconds: 500), + child: ZdsButton.filled( + isDangerButton: isDanger, + child: const Text('Shake once'), + onTap: () { + _shakeOnce.currentState?.shake(); + }, + ), + ), + ZdsShakeAnimation( + key: _shakeTwice, + shakeCount: 2, + shakeOffset: 10, + shakeDuration: const Duration(milliseconds: 300), + child: ZdsButton.outlined( + child: const Text('Shake Twice'), + onTap: () { + _shakeTwice.currentState?.shake(); + }, + ), + ), + ZdsShakeAnimation( + key: _shakeThrice, + shakeOffset: 20, + shakeDuration: const Duration(milliseconds: 250), + child: ZdsButton.filled( + child: const Text('Shake Thrice'), + onTap: () { + _shakeThrice.currentState?.shake(); + }, + ), + ), + ].divide(const SizedBox(height: 40)).toList(growable: false), + ) + ], + ), + ), + ZdsButton.filled( + child: const Text('Shake All'), + onTap: () { + _shakeOnce.currentState?.shake(); + _shakeTwice.currentState?.shake(); + _shakeThrice.currentState?.shake(); + }, + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/components/sheet_header.dart b/example/lib/pages/components/sheet_header.dart new file mode 100644 index 0000000..8d79562 --- /dev/null +++ b/example/lib/pages/components/sheet_header.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class SheetHeaderDemo extends StatelessWidget { + const SheetHeaderDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ZdsButton.filled( + child: const Text('Show Sheet Header'), + onTap: () => showBottomSheet(context), + ), + Container( + color: Theme.of(context).colorScheme.onSurface, + child: ZdsSheetHeader( + headerText: 'Groups List', + leading: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(ZdsIcons.close), + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + ), + trailing: ZdsButton.text( + child: Text( + 'New Group', + style: + Theme.of(context).textTheme.titleSmall!.copyWith(color: Theme.of(context).colorScheme.secondary), + ), + onTap: () {}, + ), + ), + ), + Container( + color: Theme.of(context).colorScheme.onSurface, + child: ZdsSheetHeader( + headerText: 'Filter List', + leading: IconButton(onPressed: () => Navigator.of(context).pop(), icon: const Icon(ZdsIcons.back)), + trailing: ZdsButton.text( + child: Icon( + ZdsIcons.filter, + color: Theme.of(context).colorScheme.secondary, + size: 24, + ), + onTap: () {}, + ), + ), + ), + ].divide(const SizedBox(height: 20)).toList(), + ), + ); + } + + void showBottomSheet(BuildContext context) { + showZdsBottomSheet( + context: context, + headerBuilder: (context) => ZdsSheetHeader( + headerText: 'Select Priority', + leading: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(ZdsIcons.close), + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + ), + ), + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ZdsListTile( + leading: const Text('Urgent'), trailing: ZdsIndex(color: ZdsColors.red, child: const Text('U'))), + ZdsListTile( + leading: const Text('High'), + trailing: ZdsIndex(color: ZdsColors.orange, child: const Text('1')), + ), + ZdsListTile( + leading: const Text('Medium'), + trailing: ZdsIndex(color: ZdsColors.teal, child: const Text('2')), + ), + ZdsListTile( + leading: const Text('Low'), + trailing: ZdsIndex( + color: ZdsColors.green, + child: const Text('3'), + ), + ), + ], + ).paddingOnly(left: 16, right: 16); + }, + ); + } +} diff --git a/example/lib/pages/components/slidable_list_tile.dart b/example/lib/pages/components/slidable_list_tile.dart new file mode 100644 index 0000000..82f7061 --- /dev/null +++ b/example/lib/pages/components/slidable_list_tile.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class SlidableListTileDemo extends StatelessWidget { + const SlidableListTileDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(8), + child: ListView( + children: [ + ZdsSlidableListTile( + onTap: () => debugPrint('Main tile tap'), + width: MediaQuery.of(context).size.width, + actions: [ + ZdsSlidableAction(label: 'More'), + ZdsSlidableAction( + icon: ZdsIcons.sign_out, + label: 'Exit', + onPressed: (_) => debugPrint('Exit'), + backgroundColor: Theme.of(context).colorScheme.error, + ) + ], + child: const _ExampleContent( + leading: CircleAvatar(child: Text('JC')), + title: 'Title', + subtitle: 'Subtitle', + trailing: Text('Trailing'), + ), + ), + const SizedBox(height: 12), + ZdsSlidableListTile( + slideButtonWidth: 120, + backgroundColor: const Color(0xFFFFE440), + width: MediaQuery.of(context).size.width, + leadingActions: [ + ZdsSlidableAction( + icon: Icons.restaurant, + label: 'Kadabra', + backgroundColor: const Color(0xFF6D534E), + foregroundColor: Colors.white, + ), + ZdsSlidableAction( + icon: Icons.flatware, + label: 'Alakazam', + backgroundColor: const Color(0xFFC0C0C0), + ) + ], + actions: [ + ZdsSlidableAction( + icon: Icons.restaurant, + label: 'Kadabra', + backgroundColor: const Color(0xFF6D534E), + foregroundColor: Colors.white, + ), + ZdsSlidableAction( + icon: Icons.flatware, + label: 'Alakazam', + backgroundColor: const Color(0xFFC0C0C0), + ) + ], + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [Text('Abra'), Icon(Icons.no_meals, color: Colors.black)], + ), + ), + const SizedBox(height: 12), + SizedBox( + height: 55, + child: ZdsSlidableListTile( + slideButtonWidth: 120, + backgroundColor: const Color(0xFFFFE440), + width: MediaQuery.of(context).size.width, + leadingActions: [ + ZdsSlidableAction( + label: 'Kadabra', + backgroundColor: const Color(0xFF6D534E), + foregroundColor: Colors.white, + ), + ZdsSlidableAction( + label: 'Alakazam', + backgroundColor: const Color(0xFFC0C0C0), + ) + ], + actions: [ + ZdsSlidableAction( + icon: Icons.restaurant, + label: 'Kadabra', + backgroundColor: const Color(0xFF6D534E), + foregroundColor: Colors.white, + ), + ZdsSlidableAction( + icon: Icons.flatware, + label: 'Alakazam', + backgroundColor: const Color(0xFF6D534E), + foregroundColor: Colors.white, + ) + ], + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [Text('A compressed list tile'), Icon(Icons.no_meals, color: Colors.black)], + ), + ), + ), + const SizedBox(height: 12), + SizedBox( + height: 60, + child: ZdsSlidableListTile( + slideButtonWidth: 120, + backgroundColor: ZdsColors.red, + width: MediaQuery.of(context).size.width, + actions: [ + ZdsSlidableAction( + label: 'Give away', + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: ZdsColors.white, + ), + ZdsSlidableAction( + label: 'Direct swap', + backgroundColor: const Color(0xff7D3CBC), + foregroundColor: ZdsColors.white, + ), + ZdsSlidableAction( + label: 'Swap with anyone', + backgroundColor: Theme.of(context).colorScheme.secondaryContainer, + foregroundColor: ZdsColors.white, + textOverflow: TextOverflow.visible, + ) + ], + child: const Center(child: Text('A list tile with 3 slidable action buttons')), + ), + ) + ], + ), + ), + ); + } +} + +class _ExampleContent extends StatelessWidget { + const _ExampleContent({ + Key? key, + required this.leading, + required this.title, + required this.subtitle, + required this.trailing, + }) : super(key: key); + + final Widget? leading; + final String? title; + final String? subtitle; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 10, 16, 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (leading != null) leading!, + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 6), + if (title != null) + Text( + title ?? '', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyLarge, + ), + if (subtitle != null) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + subtitle ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + ), + if (trailing != null) trailing! + ], + ), + ); + } +} diff --git a/example/lib/pages/components/split_navigator.dart b/example/lib/pages/components/split_navigator.dart new file mode 100644 index 0000000..dd920d5 --- /dev/null +++ b/example/lib/pages/components/split_navigator.dart @@ -0,0 +1,131 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class SplitNavigatorDemo extends StatefulWidget { + const SplitNavigatorDemo({Key? key}) : super(key: key); + + @override + State createState() => _SplitNavigatorDemoState(); +} + +class _SplitNavigatorDemoState extends State { + int _currentIndex = 0; + int get currentIndex => _currentIndex; + + set currentIndex(int currentIndex) { + if (_currentIndex == currentIndex) return; + setState(() { + _currentIndex = currentIndex; + }); + } + + int selectedKey = -1; + + @override + Widget build(BuildContext context) { + final items = [ + const ZdsNavItem(label: 'Tab 1', icon: Icon(ZdsIcons.task)), + const ZdsNavItem(label: 'Tab 2', icon: Icon(ZdsIcons.category)) + ]; + + return Scaffold( + body: Row( + children: [ + AnimatedSize( + duration: const Duration(milliseconds: 150), + child: context.isTablet() + ? ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 56), + child: Scaffold( + body: ZdsVerticalNav( + barWidth: 56, + items: items, + currentIndex: currentIndex, + onTap: onTap, + ), + ), + ) + : const SizedBox.shrink(), + ), + Expanded( + child: ZdsSplitNavigator( + shouldSplit: kIsWeb || (context.isTablet() && context.isLandscape()), + maxPrimaryWidth: 480, + emptyBuilder: (context) { + return const Scaffold( + body: ZdsEmpty(message: Text('Nothing selected!')), + ); + }, + primaryWidget: Scaffold( + appBar: AppBar( + titleSpacing: 0, + centerTitle: true, + title: const Text('Primary'), + ), + body: SafeArea( + right: false, + child: ZdsList.builder( + itemCount: 20, + itemBuilder: (context, index) { + return ZdsListTile( + title: Text('Feed ${index + 1}'), + onTap: () { + final route = ZdsNoAnimationPageRouteBuilder( + builder: (context) { + return FeedPage(feedTitle: 'Feed ${index + 1}'); + }, + ); + + ZdsSplitNavigator.pushDetails(context, route); + }, + ); + }, + ), + ), + ), + ), + ) + ], + ), + bottomNavigationBar: context.isTablet() + ? null + : ZdsBottomTabBar( + currentIndex: currentIndex, + onTap: onTap, + items: items, + ), + ); + } + + void onTap(int index) { + currentIndex = index; + } +} + +class FeedPage extends StatelessWidget { + const FeedPage({Key? key, required this.feedTitle}) : super(key: key); + + final String feedTitle; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(feedTitle)), + body: Center( + child: ZdsButton.filled( + child: const Text('Next'), + onTap: () { + Navigator.of(context).push( + ZdsSplitPageRouteBuilder( + builder: (context) { + return FeedPage(feedTitle: "$feedTitle>${feedTitle.split(">").first}"); + }, + ), + ); + }, + ), + ), + ); + } +} diff --git a/example/lib/pages/components/stats_card.dart b/example/lib/pages/components/stats_card.dart new file mode 100644 index 0000000..7ecd75d --- /dev/null +++ b/example/lib/pages/components/stats_card.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class StatsCardDemo extends StatelessWidget { + const StatsCardDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + child: Column( + children: [ + ZdsStatCard( + stats: const [ + ZdsStat(value: 72.3, description: 'Par'), + ], + title: 'Course Details', + subtitle: 'Really really long subtitle', + ).padding(8), + ZdsStatCard( + stats: [ + const ZdsStat(value: 40, description: 'Total Hours'), + ZdsStat( + value: 32.4, + description: 'Remaining', + color: ZdsColors.green, + ) + ], + title: 'Medical', + subtitle: 'Accrual: 01/22/2022', + ).padding(8), + ZdsStatCard( + stats: [ + const ZdsStat(value: 2.25, description: 'Total Days'), + const ZdsStat(value: 1, description: 'Used'), + ZdsStat( + value: 1.75, + description: 'Remaining', + color: ZdsColors.green, + ) + ], + title: 'Floating Holidays', + ).padding(8), + ZdsStatCard( + stats: [ + const ZdsStat(value: 10, description: 'Total Hours'), + const ZdsStat(value: 2, description: 'Used'), + const ZdsStat(value: 3, description: 'Scheduled'), + ZdsStat( + value: 5, + description: 'Remaining', + color: ZdsColors.green, + ) + ], + title: 'Vacation', + ).padding(8), + // Title-less card + ZdsStatCard( + subtitle: 'Subtitle', + stats: const [ + ZdsStat(value: 7, description: 'Carbon'), + ZdsStat(value: 16, description: 'Hydrogen'), + ZdsStat(value: 1, description: 'Nitrogen'), + ZdsStat(value: 2, description: 'Oxygen') + ], + ).padding(8), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/tab_bar.dart b/example/lib/pages/components/tab_bar.dart new file mode 100644 index 0000000..48abca5 --- /dev/null +++ b/example/lib/pages/components/tab_bar.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class TabBarDemo extends StatefulWidget { + const TabBarDemo({Key? key}) : super(key: key); + + @override + _TabBarDemoState createState() => _TabBarDemoState(); +} + +extension demo on ZdsTab { + ZdsTab get withoutIcon { + return ZdsTab(child: child, label: label, key: key); + } + + ZdsTab get withoutText { + return ZdsTab(child: child, icon: icon, key: key); + } +} + +class _TabBarDemoState extends State with SingleTickerProviderStateMixin { + TabController? controller; + + @override + void initState() { + super.initState(); + controller = TabController(vsync: this, length: 3); + } + + @override + void dispose() { + controller!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + const items2 = const [ + ZdsTab( + icon: Icon(ZdsIcons.details), + label: 'Details', + ), + ZdsTab( + icon: Icon(ZdsIcons.edit), + label: 'Edit', + ), + ZdsTab( + icon: Icon(ZdsIcons.delete), + label: 'Discard', + ), + ZdsTab( + icon: Icon(ZdsIcons.unclaim), + label: 'Unclaim', + ), + ZdsTab( + icon: Icon(ZdsIcons.history), + label: 'History', + ), + ]; + return Scaffold( + appBar: AppBar( + elevation: 0, + ), + body: DefaultTabController( + length: 5, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Colors: primary, basic, surface', style: Theme.of(context).textTheme.displaySmall), + Text('ZdsTabBar with icons', style: Theme.of(context).textTheme.displaySmall), + ZdsTabBar(color: ZdsTabBarColor.primary, tabs: items2), + ZdsTabBar(color: ZdsTabBarColor.basic, tabs: items2), + ZdsTabBar(color: ZdsTabBarColor.surface, tabs: items2), + Text('ZdsResponsiveTabBar with icons', style: Theme.of(context).textTheme.displaySmall), + SizedBox( + width: 345, + child: Column( + children: [ + ZdsResponsiveTabBar(color: ZdsTabBarColor.primary, tabs: items2), + ZdsResponsiveTabBar(color: ZdsTabBarColor.basic, tabs: items2), + ZdsResponsiveTabBar(color: ZdsTabBarColor.surface, tabs: items2), + ], + ), + ), + Text('ZdsTabBar without icons', style: Theme.of(context).textTheme.displaySmall), + ZdsTabBar(color: ZdsTabBarColor.primary, tabs: items2.map((e) => e.withoutIcon).toList()), + ZdsTabBar(color: ZdsTabBarColor.basic, tabs: items2.map((e) => e.withoutIcon).toList()), + ZdsTabBar(color: ZdsTabBarColor.surface, tabs: items2.map((e) => e.withoutIcon).toList()), + Text('ZdsResponsiveTabBar without icons', style: Theme.of(context).textTheme.displaySmall), + SizedBox( + width: 345, + child: Column( + children: [ + ZdsResponsiveTabBar(color: ZdsTabBarColor.primary, tabs: items2.map((e) => e.withoutIcon).toList()), + ZdsResponsiveTabBar(color: ZdsTabBarColor.basic, tabs: items2.map((e) => e.withoutIcon).toList()), + ZdsResponsiveTabBar(color: ZdsTabBarColor.surface, tabs: items2.map((e) => e.withoutIcon).toList()), + ], + ), + ), + Text('ZdsTabBar without text', style: Theme.of(context).textTheme.displaySmall), + ZdsTabBar(color: ZdsTabBarColor.primary, tabs: items2.map((e) => e.withoutText).toList()), + ZdsTabBar(color: ZdsTabBarColor.basic, tabs: items2.map((e) => e.withoutText).toList()), + ZdsTabBar(color: ZdsTabBarColor.surface, tabs: items2.map((e) => e.withoutText).toList()), + Text('ZdsResponsiveTabBar without text', style: Theme.of(context).textTheme.displaySmall), + SizedBox( + width: 245, + child: Column( + children: [ + ZdsResponsiveTabBar(color: ZdsTabBarColor.primary, tabs: items2.map((e) => e.withoutText).toList()), + ZdsResponsiveTabBar(color: ZdsTabBarColor.basic, tabs: items2.map((e) => e.withoutText).toList()), + ZdsResponsiveTabBar(color: ZdsTabBarColor.surface, tabs: items2.map((e) => e.withoutText).toList()), + ], + ), + ), + ].divide(const SizedBox(height: 40)).toList(), + ), + ), + ), + ); + } +} diff --git a/example/lib/pages/components/tag.dart b/example/lib/pages/components/tag.dart new file mode 100644 index 0000000..fa0d83c --- /dev/null +++ b/example/lib/pages/components/tag.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class TagDemo extends StatelessWidget { + const TagDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: Column( + children: [ + const ZdsListTile( + trailing: ZdsTag( + child: Text('Incomplete'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + color: ZdsTagColor.primary, + child: const Text('Pending Review'), + ), + ), + const SizedBox(height: 20), + ZdsListTile( + trailing: ZdsTag( + rounded: true, + prefix: const Text('U'), + color: ZdsTagColor.error, + child: const Text('Urgent'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + rounded: true, + prefix: const Text('1'), + color: ZdsTagColor.alert, + child: const Text('High Priority'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + rounded: true, + prefix: const Text('2'), + color: ZdsTagColor.secondary, + child: const Text('Medium Priority'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + rounded: true, + prefix: const Text('3'), + color: ZdsTagColor.success, + child: const Text('Low Priority'), + ), + ), + const SizedBox(height: 20), + const ZdsListTile( + trailing: ZdsTag( + filled: true, + child: Text('Default'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + filled: true, + color: ZdsTagColor.error, + child: const Text('Urgent'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + filled: true, + color: ZdsTagColor.alert, + child: const Text('High Priority'), + ), + ), + const ZdsListTile( + trailing: ZdsTag( + filled: true, + child: Text('Primary'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + filled: true, + color: ZdsTagColor.primary, + child: const Text('Medium Priority'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + filled: true, + color: ZdsTagColor.success, + child: const Text('Approved'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + filled: true, + customColor: ZdsColors.blueGrey, + child: const Text('Custom onClose'), + onClose: () {}, + ), + ), + // The following two tags are meant to show tags that enable users' big font settings + const ZdsListTile( + trailing: ZdsTag( + unrestrictedSize: true, + child: Text('Unbounded'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + unrestrictedSize: true, + onClose: () {}, + child: const Text('Unbound+btn'), + ), + ), + // New Tag variants from https://jira.zebra.com/browse/ZU-92 - 30/03/2022 J + ZdsListTile( + trailing: ZdsTag( + rectangular: true, + color: ZdsTagColor.secondary, + child: const Text('Give away'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + rectangular: true, + color: ZdsTagColor.secondary, + child: const Text('Additional'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + rectangular: true, + color: ZdsTagColor.success, + prefix: const Icon(Icons.check, size: 18), + child: const Text('Approved'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + rectangular: true, + color: ZdsTagColor.secondary, + prefix: const Icon(Icons.hourglass_bottom, size: 18), + child: const Text('Pending'), + ), + ), + ZdsListTile( + trailing: ZdsTag( + rectangular: true, + color: ZdsTagColor.error, + prefix: const Icon(Icons.close, size: 18), + child: const Text('Declined'), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/lib/pages/components/tag_list.dart b/example/lib/pages/components/tag_list.dart new file mode 100644 index 0000000..abe48f6 --- /dev/null +++ b/example/lib/pages/components/tag_list.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class TagListDemo extends StatelessWidget { + const TagListDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Text('Horizontal').padding(8), + ZdsCard( + padding: EdgeInsets.zero, + child: ZdsTagsList( + direction: Axis.horizontal, + items: [ + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('GM - Store'), + onClose: () {}, + ), + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('Store Message - Clothing'), + onClose: () {}, + ), + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('Team Leader - Clothing'), + onClose: () {}, + ), + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('GM - Second'), + onClose: () {}, + ), + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('Team Leader - Store'), + onClose: () {}, + ), + ], + ), + ), + const Text('Vertical').padding(8), + ZdsCard( + padding: EdgeInsets.zero, + child: ZdsTagsList( + items: [ + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('GM - Store'), + onClose: () {}, + ), + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('Store Message - Clothing'), + onClose: () {}, + ), + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('Team Leader - Clothing'), + onClose: () {}, + ), + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('GM - Second'), + onClose: () {}, + ), + ZdsTag( + customColor: Theme.of(context).colorScheme.primary, + rounded: true, + child: const Text('Team Leader - Store'), + onClose: () {}, + ), + ], + ), + ), + ], + ); + } +} diff --git a/example/lib/pages/components/text_field.dart b/example/lib/pages/components/text_field.dart new file mode 100644 index 0000000..601ac25 --- /dev/null +++ b/example/lib/pages/components/text_field.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class TextFieldDemo extends StatefulWidget { + const TextFieldDemo({Key? key}) : super(key: key); + + @override + _TextFieldDemoState createState() => _TextFieldDemoState(); +} + +class _TextFieldDemoState extends State { + String? error; + final noteController = TextEditingController(); + String? dropdownVal; + @override + void initState() { + noteController.text = 'Lets get an early jump on the seasonal plannogram for this week. for this week.'; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + ZdsButton.filled( + child: const Text('change dropdown'), + onTap: () => setState(() { + dropdownVal = 'P'; + }), + ), + const SizedBox(height: 16), + ZdsDropdownList( + options: [ + ZdsDropdownListItem(name: 'Approved', value: 'A'), + ZdsDropdownListItem(name: 'Pending', value: 'P'), + ZdsDropdownListItem(name: 'Declined', value: 'D'), + ZdsDropdownListItem(name: 'Approved', value: 'K'), + ], + hint: 'hint', + value: dropdownVal, + onChange: (String option) { + setState(() { + dropdownVal = option; + }); + debugPrint(option); + }, + ), + const SizedBox(height: 20), + ZdsCard( + child: ZdsResizableTextArea( + controller: noteController, + label: 'Responder Notes', + maxLines: 100, + footerText: + '09/09/22 @ 04:51pm by: Ann Korgaard by: Ann Korgaard by: Ann Korgaard by: Ann Korgaard', + ), + ), + ZdsCard( + child: ZdsResizableTextArea( + textInputAction: TextInputAction.done, + controller: noteController, + label: 'Text Area ', + maxLines: 100, + footerText: + '09/09/22 @ 04:51pm by: Ann Korgaard by: Ann Korgaard by: Ann Korgaard by: Ann Korgaard', + ), + ), + const SizedBox(height: 20), + TextField( + decoration: ZdsInputDecoration( + labelText: 'Edit Filter Name', + counterText: 'Character left: 255', + ), + ), + TextField( + maxLines: 10, + decoration: ZdsInputDecoration( + labelText: 'Message', + counterText: 'Character left: 255', + ), + ), + TextFormField( + initialValue: 'Weekly Task', + decoration: ZdsInputDecoration( + labelText: 'Title', + ), + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: TextFormField( + initialValue: '04/06/2021', + decoration: ZdsInputDecoration( + labelText: 'Start date', + ), + ), + ), + Expanded( + child: TextFormField( + initialValue: '04/06/2021', + decoration: ZdsInputDecoration( + labelText: 'Due date', + ), + ), + ), + ].divide(const SizedBox(width: 10)).toList(), + ), + TextFormField( + decoration: ZdsInputDecoration.withNoLabel(errorText: error), + ), + TextFormField( + decoration: ZdsInputDecoration.withNoLabel(), + ), + ZdsButton.muted( + child: const Text('Validate'), + onTap: () { + setState(() { + if (error == null) { + error = ''; + } else { + error = null; + } + }); + }, + ), + ], + ).content(), + ); + } +} diff --git a/example/lib/pages/components/toast.dart b/example/lib/pages/components/toast.dart new file mode 100644 index 0000000..939af4e --- /dev/null +++ b/example/lib/pages/components/toast.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ToastDemo extends StatefulWidget { + const ToastDemo({Key? key}) : super(key: key); + + @override + State createState() => _ToastDemoState(); +} + +class _ToastDemoState extends State { + bool showPersistentToast = true; + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Scaffold( + appBar: AppBar(), + body: SizedBox.expand( + child: Column( + children: [ + ZdsButton.muted( + child: const Text('green toast'), + onTap: () { + showToast(context, color: ZdsToastColors.success); + }, + ), + ZdsButton( + isDangerButton: true, + child: const Text('red toast'), + onTap: () { + showToast(context, color: ZdsToastColors.error); + }, + ), + ZdsButton( + child: const Text('blue toast'), + onTap: () { + showToast(context, color: ZdsToastColors.primary); + }, + ), + ZdsButton.muted( + child: const Text('orange toast'), + onTap: () { + showToast(context, color: ZdsToastColors.warning); + }, + ), + ZdsButton.muted( + child: const Text('longer toast'), + onTap: () { + showLongerToast( + context, + ); + }, + ), + ZdsButton.muted( + child: const Text('Dark toast'), + onTap: () { + showToast(context, color: ZdsToastColors.dark); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Show persistent toast'), + Switch(value: showPersistentToast, onChanged: (bool b) => setState(() => showPersistentToast = b)), + ], + ), + ].divide(const SizedBox(height: 10)).toList(), + ), + ), + bottomNavigationBar: PreferredSizeColumn( + children: [ + if (showPersistentToast) + const ZdsToast( + rounded: false, + title: Text('Persistent default toast'), + ), + ], + ), + ), + ); + } + + void showToast(BuildContext context, {ZdsToastColors? color}) { + ScaffoldMessenger.of(context).showZdsToast( + ZdsToast( + title: const Text('Project launched successfully'), + leading: const Icon(ZdsIcons.check_circle), + actions: [ + IconButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + icon: const Icon(ZdsIcons.close), + ), + ], + color: color, + ), + ); + } + + void showLongerToast(BuildContext context) { + ScaffoldMessenger.of(context).showZdsToast( + ZdsToast( + title: const Text( + 'Project launched successfully very very very very very very very very very very very long message', + ), + leading: const Icon(ZdsIcons.check_circle), + multiLine: true, + actions: [ + IconButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + icon: const Icon(ZdsIcons.close), + ), + ], + ), + ); + } +} + +class PreferredSizeColumn extends StatelessWidget implements PreferredSizeWidget { + final List children; + + const PreferredSizeColumn({Key? key, required this.children}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: children, + ); + } + + @override + Size get preferredSize => + Size.fromHeight(children.map((child) => child.preferredSize.height).reduce((value, element) => value + element)); +} diff --git a/example/lib/pages/components/toolbar.dart b/example/lib/pages/components/toolbar.dart new file mode 100644 index 0000000..d6d9301 --- /dev/null +++ b/example/lib/pages/components/toolbar.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ToolBarDemo extends StatefulWidget { + const ToolBarDemo({Key? key}) : super(key: key); + + @override + State createState() => _ToolBarDemoState(); +} + +class _ToolBarDemoState extends State { + DateTimeRange? r = DateTimeRange(start: DateTime.now(), end: DateTime.now().add(const Duration(days: 7))); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + ), + body: SingleChildScrollView( + child: Column( + children: [ + ZdsToolbar( + title: DateRange( + emptyLabel: 'Select range', + isWeekMode: true, + actions: [ + ZdsButton.text( + child: Text(' View', style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)), + onTap: () {}, + ), + ], + ), + ), + ZdsToolbar( + title: DateRange( + emptyLabel: 'Select range', + isSelectable: false, + initialDateRange: r, + onChange: (r1) { + setState(() => r = r1); + }, + actions: [ + ZdsButton.text( + child: Text('Fiscal View', style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)), + onTap: () {}, + ), + ], + ), + ), + const SizedBox(height: 80), + ZdsToolbar( + title: DateRange( + emptyLabel: 'Select range', + initialDateRange: r, + onChange: (r1) => setState(() => r = r1), + actions: [ + ZdsButton.text( + child: Text('Fiscal View', style: TextStyle(color: Theme.of(context).colorScheme.onPrimary)), + onTap: () {}, + ), + ], + ), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(ZdsIcons.filter), + ), + ], + child: const DefaultTabController( + length: 4, + child: ZdsTabBar( + color: ZdsTabBarColor.primary, + tabs: [ + ZdsTab( + label: 'All', + ), + ZdsTab( + label: 'New', + ), + ZdsTab( + label: 'Urgent', + ), + ZdsTab( + label: 'Overdue', + ), + ], + ), + ), + ), + ZdsToolbar( + backgroundColor: Theme.of(context).colorScheme.surface, + title: DateRange( + textStyle: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.primary), + initialDateRange: DateTimeRange( + start: DateTime(2022, 05, 12), + end: DateTime(2022, 05, 18), + ), + isSelectable: false, + ), + ), + ZdsToolbar( + title: const Text('SR-CHI-Downers Grove IL.00103'), + subtitle: const Text('04/01/2021 - 04/30/2021'), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(ZdsIcons.filter), + ), + ], + child: Container( + color: Theme.of(context).colorScheme.primary, + height: 400, + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/pages/components/vertical_nav.dart b/example/lib/pages/components/vertical_nav.dart new file mode 100644 index 0000000..d5e24a3 --- /dev/null +++ b/example/lib/pages/components/vertical_nav.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class VerticalNavDemo extends StatefulWidget { + const VerticalNavDemo({Key? key}) : super(key: key); + + @override + State createState() => _VerticalNavDemoState(); +} + +class _VerticalNavDemoState extends State { + int selectedIndex = 0; + + @override + Widget build(BuildContext context) { + final verticalNav = ZdsVerticalNav( + currentIndex: selectedIndex, + onTap: (index) => setState(() { + selectedIndex = index; + }), + items: const [ + ZdsNavItem(label: 'user', semanticLabel: 'user', icon: Icon(ZdsIcons.user)), + ZdsNavItem(label: 'calendar', semanticLabel: 'calendar', icon: Icon(ZdsIcons.calendar)), + ZdsNavItem(label: 'vacation', semanticLabel: 'vacation', icon: Icon(ZdsIcons.vacation)), + ZdsNavItem(label: 'star', semanticLabel: 'star', icon: Icon(ZdsIcons.star)), + ZdsNavItem(label: 'more', semanticLabel: 'more', icon: Icon(ZdsIcons.more_hori)), + ], + actions: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Icon( + ZdsIcons.pin, + size: 24, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Icon( + ZdsIcons.task, + size: 24, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Icon( + ZdsIcons.chat, + size: 24, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Icon( + ZdsIcons.walk, + size: 24, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Icon( + ZdsIcons.grid, + size: 24, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], + ); + + return Scaffold( + body: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + verticalNav, + SizedBox( + height: 500, + child: verticalNav, + ), + const SizedBox(), + ], + ), + ); + } +} diff --git a/example/lib/pages/theme/colors.dart b/example/lib/pages/theme/colors.dart new file mode 100644 index 0000000..66cb21c --- /dev/null +++ b/example/lib/pages/theme/colors.dart @@ -0,0 +1,516 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class ColorExample { + final String name; + final Color color; + + ColorExample(this.color, this.name); +} + +class ColorSwatchExample { + final String name; + final ZetaColorSwatch color; + + ColorSwatchExample(this.color, this.name); +} + +class ColorsDemo extends StatelessWidget { + const ColorsDemo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final primaryColors = [ + { + 'color': Theme.of(context).colorScheme.primary, + 'name': 'Primary', + 'subtitle': 'Theme.of(context).colorScheme.primary', + }, + { + 'color': Theme.of(context).colorScheme.secondary, + 'name': 'Secondary', + 'subtitle': 'Theme.of(context).colorScheme.secondary', + }, + { + 'color': Theme.of(context).colorScheme.primaryContainer, + 'name': 'Tertiary', + 'subtitle': 'Theme.of(context).colorScheme.primaryContainer', + }, + { + 'color': Theme.of(context).colorScheme.secondaryContainer, + 'name': 'Quaternary', + 'subtitle': 'Theme.of(context).colorScheme.secondaryContainer', + }, + ]; + final List> greys = [ + { + 'color': ZdsColors.black, + 'name': 'Black', + 'subtitle': 'ZdsColors.black', + }, + { + 'color': ZdsColors.darkGrey, + 'name': 'Grey 1', + 'subtitle': 'ZdsColors.darkGrey', + }, + { + 'color': ZdsColors.blueGrey, + 'name': 'Grey 2', + 'subtitle': 'ZdsColors.blueGrey', + }, + { + 'color': ZdsColors.lightGrey, + 'name': 'Grey 3', + 'subtitle': 'ZdsColors.lightGrey', + }, + { + 'color': ZdsColors.white, + 'name': 'White', + 'subtitle': 'ZdsColors.white', + }, + ]; + final otherColors = [ + { + 'color': ZdsColors.red, + 'name': 'Red', + 'subtitle': 'ZdsColors.red', + }, + { + 'color': ZdsColors.green, + 'name': 'Green', + 'subtitle': 'ZdsColors.green', + }, + { + 'color': ZdsColors.yellow, + 'name': 'Yellow', + 'subtitle': 'ZdsColors.yellow', + }, + { + 'color': ZdsColors.orange, + 'name': 'Orange', + 'subtitle': 'ZdsColors.orange', + }, + ]; + final theme = [ + { + 'color': Theme.of(context).colorScheme.primary, + 'name': 'primary', + 'subtitle': 'Theme.of(context).colorScheme.primary', + }, + { + 'color': Theme.of(context).colorScheme.onPrimary, + 'name': 'onPrimary', + 'subtitle': 'Theme.of(context).colorScheme.onPrimary', + }, + { + 'color': Theme.of(context).colorScheme.primaryContainer, + 'name': 'primaryContainer', + 'subtitle': 'Theme.of(context).colorScheme.primaryContainer', + }, + { + 'color': Theme.of(context).colorScheme.onPrimaryContainer, + 'name': 'onPrimaryContainer', + 'subtitle': 'Theme.of(context).colorScheme.onPrimaryContainer', + }, + { + 'color': Theme.of(context).colorScheme.secondary, + 'name': 'secondary', + 'subtitle': 'Theme.of(context).colorScheme.secondary', + }, + { + 'color': Theme.of(context).colorScheme.onSecondary, + 'name': 'onSecondary', + 'subtitle': 'Theme.of(context).colorScheme.onSecondary', + }, + { + 'color': Theme.of(context).colorScheme.secondaryContainer, + 'name': 'secondaryContainer', + 'subtitle': 'Theme.of(context).colorScheme.secondaryContainer', + }, + { + 'color': Theme.of(context).colorScheme.onSecondaryContainer, + 'name': 'onSecondaryContainer', + 'subtitle': 'Theme.of(context).colorScheme.onSecondaryContainer', + }, + { + 'color': Theme.of(context).colorScheme.surface, + 'name': 'surface', + 'subtitle': 'Theme.of(context).colorScheme.surface', + }, + { + 'color': Theme.of(context).colorScheme.onSurface, + 'name': 'onSurface', + 'subtitle': 'Theme.of(context).colorScheme.onSurface', + }, + { + 'color': Theme.of(context).colorScheme.background, + 'name': 'background', + 'subtitle': 'Theme.of(context).colorScheme.background', + }, + { + 'color': Theme.of(context).colorScheme.onBackground, + 'name': 'onBackground', + 'subtitle': 'Theme.of(context).colorScheme.onBackground', + }, + { + 'color': Theme.of(context).colorScheme.error, + 'name': 'error', + 'subtitle': 'Theme.of(context).colorScheme.error', + }, + { + 'color': Theme.of(context).colorScheme.onError, + 'name': 'onError', + 'subtitle': 'Theme.of(context).colorScheme.onError', + }, + ]; + + final List zeta = ZetaColors.of(context).rainbow.map((e) => ColorSwatchExample(e, '')).toList(); + + return LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('ZDS Colors', style: Theme.of(context).textTheme.displayLarge).paddingOnly(left: 16), + _ColorRow(colors: primaryColors, title: 'Theme colors'), + _ColorRow(colors: greys, title: 'Greys'), + _ColorRow(colors: otherColors, title: 'Other colors'), + const _PrimarySwatches(), + const _OtherSwatches(), + _ColorRow(colors: theme, title: 'All theme ColorScheme colors'), + const _Spacer(), + const _Spacer(), + Text('Zeta Colors', style: Theme.of(context).textTheme.displayLarge).paddingOnly(left: 16), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: zeta + .map( + (e) => Column( + children: List.generate( + 10, + (index) => _ColorBoxZeta( + color: e.color[(index + 1) * 10]!, + text: ((index + 1) * 10).toString(), + compact: true, + screenWidth: constraints.maxWidth, + ), + ), + ), + ) + .toList(), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _ColorBoxZeta extends StatelessWidget { + final Color color; + final String text; + final bool compact; + final double screenWidth; + + const _ColorBoxZeta( + {Key? key, required this.color, required this.text, required this.screenWidth, this.compact = false}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: compact ? 100 : 150, + width: (screenWidth * (compact ? 0.25 : 0.5)).clamp(1, 300), + color: color, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: computeForeground(color)), + ), + Text( + color.toHexNoAlpha().toUpperCase(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: computeForeground(color)), + ), + ], + ).padding(16), + ); + } +} + +class _OtherSwatches extends StatelessWidget { + const _OtherSwatches({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const _Spacer(), + Row( + children: [ + const _Spacer(), + Text( + 'Other Swatches', + style: Theme.of(context).textTheme.displayMedium, + ), + ], + ), + const _Spacer(), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Spacer(), + _Swatch( + swatch: ZdsColors.redSwatch, + values: [ + {'val': 'fair', 'display': 'fair'}, + {'val': 'light', 'display': 'light'}, + {'val': 'medium', 'display': 'medium'}, + {'val': 'dark', 'display': 'dark'}, + ], + headColor: ZdsColors.red, + headText: 'Red', + ), + _Spacer(), + _Swatch( + swatch: ZdsColors.greenSwatch, + values: [ + {'val': 'light', 'display': 'light'}, + {'val': 'medium', 'display': 'medium'}, + {'val': 'dark', 'display': 'dark'}, + ], + headColor: ZdsColors.green, + headText: 'Green', + ), + _Spacer(), + ], + ), + ), + const _Spacer(), + const _Spacer(), + ], + ); + } +} + +class _PrimarySwatches extends StatelessWidget { + const _PrimarySwatches({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + const _Spacer(), + Text( + 'Primary Swatches', + style: Theme.of(context).textTheme.displayMedium, + ), + ], + ), + const _Spacer(), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _Spacer(), + _Swatch( + swatch: ZdsColors.primarySwatch(context), + values: List.generate(9, (i) => {'val': (i + 1) * 100, 'display': '${(i + 1) * 10}%'}), + headColor: Theme.of(context).colorScheme.primary, + headText: 'Primary', + ), + const _Spacer(), + _Swatch( + swatch: ZdsColors.secondarySwatch(context), + values: List.generate(9, (i) => {'val': (i + 1) * 100, 'display': '${(i + 1) * 10}%'}), + headColor: Theme.of(context).colorScheme.secondary, + headText: 'Secondary', + ), + const _Spacer(), + _Swatch( + swatch: ZdsColors.greyWarmSwatch, + values: [ + const {'val': 50, 'display': 50}, + ...List.generate(12, (i) => {'val': (i + 1) * 100, 'display': (i + 1) * 100}) + ], + headColor: ZdsColors.darkGrey, + headText: 'Warm grey', + ), + const _Spacer(), + _Swatch( + swatch: ZdsColors.greyCoolSwatch, + values: [ + const {'val': 50, 'display': 50}, + ...List.generate(12, (i) => {'val': (i + 1) * 100, 'display': (i + 1) * 100}) + ], + headColor: ZdsColors.blueGrey, + headText: 'Cool grey', + ), + const _Spacer(), + ], + ), + ), + const _Spacer(), + const _Spacer(), + ], + ); + } +} + +class _ColorRow extends StatelessWidget { + final List> colors; + final String title; + const _ColorRow({Key? key, required this.colors, required this.title}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const _Spacer(), + Row( + children: [ + const _Spacer(), + Text( + title, + style: Theme.of(context).textTheme.displayMedium, + ), + ], + ), + const _Spacer(), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: (colors + .map((e) => _ColorBox( + color: e['color'] as Color, + text: e['name'] as String, + subtitle: e['subtitle'] as String, + )) + .toList()) + .divide(const _Spacer()) + .toList() + ..insert(0, const _Spacer()), + ), + ), + const _Spacer(), + const _Spacer(), + ], + ); + } +} + +class _Spacer extends StatelessWidget { + const _Spacer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const SizedBox(height: 20, width: 20); + } +} + +class _ColorBox extends StatelessWidget { + final Color color; + final String text; + final String subtitle; + const _ColorBox({Key? key, required this.color, required this.text, required this.subtitle}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 150, + width: 330, + color: color, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + style: Theme.of(context).textTheme.displaySmall?.copyWith(color: computeForeground(color)), + ), + Text( + color.toHexNoAlpha().toUpperCase(), + style: Theme.of(context).textTheme.titleLarge?.copyWith(color: computeForeground(color)), + ), + Text( + subtitle, + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: computeForeground(color)), + ), + ], + ).padding(16), + ); + } +} + +class _Swatch extends StatelessWidget { + final ColorSwatch swatch; + final Color headColor; + final String headText; + final List values; + + const _Swatch({Key? key, required this.swatch, required this.values, required this.headColor, required this.headText}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: 122, + width: 260, + color: headColor, + padding: const EdgeInsets.all(20), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + headText, + style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: computeForeground(headColor)), + ), + const Expanded(child: SizedBox()), + DefaultTextStyle( + style: Theme.of(context).textTheme.titleLarge!.copyWith(color: computeForeground(headColor)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [Text('100%'), Text(headColor.toHexNoAlpha().toUpperCase())], + ), + ) + ]), + ), + ...values.map((Map element) { + final Color color = swatch[element['val']] ?? Colors.black; + return Container( + height: 34, + width: 260, + color: color, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + element['display'].toString(), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: computeForeground(color)), + ), + Text( + color.toHexNoAlpha().toUpperCase(), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: computeForeground(color)), + ), + ], + ).paddingOnly(left: 20, right: 20), + ); + }).toList() + ], + ); + } +} diff --git a/example/lib/pages/theme/text.dart b/example/lib/pages/theme/text.dart new file mode 100644 index 0000000..6e25809 --- /dev/null +++ b/example/lib/pages/theme/text.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +class TextDemo extends StatelessWidget { + const TextDemo({Key? key}) : super(key: key); + +//?TODO: Integrate and use Zeta text themes + @override + Widget build(BuildContext context) { + final display = [ + { + 'header': 'Display Large', + 'textStyle': Theme.of(context).textTheme.displayLarge, + }, + { + 'header': 'Display Medium', + 'textStyle': Theme.of(context).textTheme.displayMedium, + }, + { + 'header': 'Display Small', + 'textStyle': Theme.of(context).textTheme.displaySmall, + }, + ]; + final headline = [ + { + 'header': 'Headline Large', + 'textStyle': Theme.of(context).textTheme.headlineLarge, + }, + { + 'header': 'Headline Medium', + 'textStyle': Theme.of(context).textTheme.headlineMedium, + }, + { + 'header': 'Headline Small', + 'textStyle': Theme.of(context).textTheme.headlineSmall, + }, + ]; + final body = [ + { + 'header': 'Body Large', + 'textStyle': Theme.of(context).textTheme.bodyLarge, + }, + { + 'header': 'Body Medium', + 'textStyle': Theme.of(context).textTheme.bodyMedium, + }, + { + 'header': 'Body Small', + 'textStyle': Theme.of(context).textTheme.bodySmall, + }, + ]; + + final title = [ + { + 'header': 'Title Large', + 'textStyle': Theme.of(context).textTheme.titleLarge, + }, + { + 'header': 'Title Medium', + 'textStyle': Theme.of(context).textTheme.titleMedium, + }, + { + 'header': 'Title Small', + 'textStyle': Theme.of(context).textTheme.titleSmall, + }, + ]; + final label = [ + { + 'header': 'Label Large', + 'textStyle': Theme.of(context).textTheme.labelLarge, + }, + { + 'header': 'Label Medium', + 'textStyle': Theme.of(context).textTheme.labelMedium, + }, + { + 'header': 'Label Small', + 'textStyle': Theme.of(context).textTheme.labelSmall, + }, + ]; + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _FontGroup(fonts: display), + _FontGroup(fonts: headline), + _FontGroup(fonts: body), + _FontGroup(fonts: title), + _FontGroup(fonts: label), + ], + ), + ), + ); + } +} + +class _FontGroup extends StatelessWidget { + final List fonts; + + const _FontGroup({Key? key, required this.fonts}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...fonts + .map((e) { + final TextStyle style = e['textStyle'] as TextStyle; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(e['header'].toString(), style: style), + Text( + 'Font: ${style.fontFamily?.split('/').last}\nSize: ${style.fontSize?.toInt()}\nLine height: ${(style.height ?? 1 * style.fontSize!).toInt()}', + style: TextStyle(color: ZetaColors.of(context).cool.shade70), + ), + const _Spacer(), + ], + ); + }) + .toList() + .divide(const _Spacer()), + const _Spacer(), + const Divider(), + const _Spacer(), + ], + ); + } +} + +class _Spacer extends StatelessWidget { + const _Spacer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const SizedBox(height: 20, width: 20); + } +} diff --git a/example/lib/pages/utils/color_utils.dart b/example/lib/pages/utils/color_utils.dart new file mode 100644 index 0000000..c761a05 --- /dev/null +++ b/example/lib/pages/utils/color_utils.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +class ColorUtilsDemo extends StatefulWidget { + const ColorUtilsDemo({Key? key}) : super(key: key); + + @override + State createState() => _ColorUtilsDemoState(); +} + +class _ColorUtilsDemoState extends State { + final TextEditingController controller = TextEditingController(); + late Color color; + + @override + void initState() { + super.initState(); + + color = getRandomColorFromTheme(DateTime.now()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: ZdsInputDecoration(labelText: 'Input text as seed'), + controller: controller, + onSubmitted: (value) => setState(() => color = getRandomColorFromTheme(value)), + ), + ZdsButton.filled( + child: const Text('Generate random color'), + onTap: () => setState(() => color = getRandomColorFromTheme(controller.text)), + ), + const SizedBox( + height: 16, + ), + Container( + color: color, + height: 200, + width: 200, + padding: const EdgeInsets.all(16), + child: Center( + child: Text( + '#${color.toString().split('0xff').last.replaceAll(')', '').toUpperCase()}', + style: Theme.of(context).textTheme.displayMedium!.copyWith(color: ZdsColors.white), + ), + ), + ), + ], + ).paddingOnly(left: 24, right: 24), + ), + ); + } +} diff --git a/example/lib/routes.dart b/example/lib/routes.dart new file mode 100644 index 0000000..18e128e --- /dev/null +++ b/example/lib/routes.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; + +import 'home.dart'; +import 'pages/components/app_bar.dart'; +import 'pages/assets/animations.dart'; +import 'pages/assets/icons.dart'; +import 'pages/assets/images.dart'; +import 'pages/components/big_toggle_button.dart'; +import 'pages/components/block_table.dart'; +import 'pages/components/bottom_bar.dart'; +import 'pages/components/bottom_sheet.dart'; +import 'pages/components/bottom_tab_bar.dart'; +import 'pages/components/bottom_tab_scaffold.dart'; +import 'pages/components/button.dart'; +import 'pages/components/calendar.dart'; +import 'pages/components/card.dart'; +import 'pages/components/card_actions.dart'; +import 'pages/components/date_picker.dart'; +import 'pages/components/day_picker_demo.dart'; +import 'pages/components/empty_list_view.dart'; +import 'pages/components/empty_view.dart'; +import 'pages/components/expandable.dart'; +import 'pages/components/expansion_tile.dart'; +import 'pages/components/file_picker.dart'; +import 'pages/components/icon_text_button.dart'; +import 'pages/components/image_picker.dart'; +import 'pages/components/index.dart'; +import 'pages/components/infinite_list.dart'; +import 'pages/components/information_bar.dart'; +import 'pages/components/list.dart'; +import 'pages/components/list_tile.dart'; +import 'pages/components/modal.dart'; +import 'pages/components/navigation_menu.dart'; +import 'pages/components/popover.dart'; +import 'pages/components/profile.dart'; +import 'pages/components/properties_list.dart'; +import 'pages/components/radio_list.dart'; +import 'pages/components/search.dart'; +import 'pages/components/shake_example.dart'; +import 'pages/components/sheet_header.dart'; +import 'pages/components/slidable_list_tile.dart'; +import 'pages/components/split_navigator.dart'; +import 'pages/components/stats_card.dart'; +import 'pages/components/tab_bar.dart'; +import 'pages/components/tag.dart'; +import 'pages/components/tag_list.dart'; +import 'pages/components/text_field.dart'; +import 'pages/theme/colors.dart'; +import 'pages/theme/text.dart'; +import 'pages/components/toast.dart'; +import 'pages/components/toolbar.dart'; +import 'pages/utils/color_utils.dart'; +import 'pages/components/vertical_nav.dart'; + +final kRoutes = { + 'Components': [ + const DemoRoute( + title: 'Buttons', + child: ButtonDemo(), + ), + const DemoRoute( + title: 'Block Table', + child: BlockTableDemo(), + ), + const DemoRoute( + title: 'Calendar', + child: CalendarDemo(), + ), + const DemoRoute( + title: 'Cards', + child: CardDemo(), + ), + const DemoRoute( + title: 'Card Actions', + child: CardActionsDemo(), + ), + const DemoRoute( + title: 'File Picker', + child: FilePickerDemo(), + ), + const DemoRoute( + title: 'Lists', + child: ListDemo(), + ), + const DemoRoute( + title: 'List tiles', + child: ListTileDemo(), + ), + const DemoRoute( + title: 'Slidable list tiles', + child: SlidableListTileDemo(), + ), + const DemoRoute( + title: 'Bottom bar', + child: BottomBarDemo(), + ), + const DemoRoute( + title: 'Bottom tab bar', + child: BottomTabBarDemo(), + ), + const DemoRoute( + title: 'Index component', + child: IndexDemo(), + ), + const DemoRoute( + title: 'Tags', + child: TagDemo(), + ), + const DemoRoute( + title: 'Search', + wrapper: false, + child: SearchDemo(), + ), + const DemoRoute( + title: 'Tab bar', + wrapper: false, + child: TabBarDemo(), + ), + const DemoRoute( + title: 'Toolbar', + wrapper: false, + child: ToolBarDemo(), + ), + const DemoRoute( + title: 'App bar', + wrapper: false, + child: AppBarDemo(), + ), + const DemoRoute( + title: 'Properties List', + child: PropertiesListDemo(), + ), + const DemoRoute( + title: 'Empty', + child: EmptyViewDemo(), + ), + const DemoRoute( + title: 'Expansion Tile', + child: ExpansionTileDemo(), + ), + const DemoRoute( + title: 'Bottom Sheet', + child: BottomSheetDemo(), + ), + const DemoRoute( + title: 'Modal', + child: ModalDemo(), + ), + const DemoRoute( + title: 'Radio List', + child: RadioListDemo(), + ), + const DemoRoute( + title: 'Stats card', + child: StatsCardDemo(), + ), + const DemoRoute( + title: 'Information bar', + child: InformationBarDemo(), + ), + const DemoRoute( + title: 'Toast', + wrapper: false, + child: ToastDemo(), + ), + const DemoRoute( + title: 'Text Field', + child: TextFieldDemo(), + ), + const DemoRoute( + title: 'Empty list view', + child: EmptyListView(), + ), + const DemoRoute( + title: 'Expandable', + child: ExpandableDemo(), + ), + const DemoRoute( + title: 'Navigation menu', + child: NavigationMenuDemo(), + ), + const DemoRoute( + title: 'Tag list', + child: TagListDemo(), + ), + const DemoRoute( + title: 'Big Toggle Button', + child: BigToggleButtonDemo(), + ), + const DemoRoute( + title: 'Profile', + wrapper: false, + child: ProfileDemo(), + ), + const DemoRoute( + title: 'PopOver', + wrapper: false, + child: PopOverDemo(), + ), + const DemoRoute( + title: 'ImagePicker', + child: ImagePickerDemo(), + ), + const DemoRoute( + title: 'IconTextButton', + child: IconTextButtonDemo(), + ), + const DemoRoute( + title: 'Date pickers', + child: DatePickerDemo(), + ), + const DemoRoute( + title: 'Days picker', + child: DayPickerDemo(), + ), + const DemoRoute(title: 'Infinite list', child: InfiniteListDemo()), + const DemoRoute(title: 'Bottom Tab Bar Scaffold', child: BottomTabScaffoldDemo()), + const DemoRoute(title: 'Sheet Headers', child: SheetHeaderDemo()), + const DemoRoute(title: 'Vertical Navigation', child: VerticalNavDemo()), + const DemoRoute(wrapper: false, title: 'Split Navigation', child: SplitNavigatorDemo()), + ], + 'Theme': [ + const DemoRoute(title: 'Text', child: TextDemo()), + const DemoRoute(title: 'Colors', child: ColorsDemo()), + ], + 'Assets': [ + const DemoRoute(title: 'Animations', child: AnimationsDemo()), + const DemoRoute(title: 'Images', child: ImagesDemo()), + const DemoRoute(title: 'Icons', child: IconsDemo()) + ], + 'Utils': [ + const DemoRoute(title: 'Colors', child: ColorUtilsDemo()), + const DemoRoute(title: 'Shake Animation', child: ShakeExample()), + ], +}; + +final kFlattRoutes = kRoutes.keys.expand((key) => kRoutes[key]!).toList(); + +const kHomeRoute = DemoRoute( + title: 'zds_flutter Demo', + routeName: Navigator.defaultRouteName, + wrapper: false, + child: HomePage(), +); + +final kAllRoutes = { + Navigator.defaultRouteName: (context) => kHomeRoute, + for (DemoRoute route in kFlattRoutes) route.routeName: (context) => route, +}; + +class DemoRoute extends StatelessWidget { + final bool wrapper; + final String title; + final Widget child; + final String? _routeName; + + String get routeName => _routeName ?? '/${child.runtimeType}'; + + const DemoRoute({Key? key, String? routeName, this.wrapper = true, required this.title, required this.child}) + : _routeName = routeName, + super(key: key); + + @override + Widget build(BuildContext context) { + if (!wrapper) { + return child; + } + return Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: child, + ); + } +} diff --git a/example/linux/.gitignore b/example/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt new file mode 100644 index 0000000..d67bd4e --- /dev/null +++ b/example/linux/CMakeLists.txt @@ -0,0 +1,139 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..7299b5c --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..786ff5c --- /dev/null +++ b/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/linux/main.cc b/example/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc new file mode 100644 index 0000000..0ba8f43 --- /dev/null +++ b/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/example/linux/my_application.h b/example/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/example/macos/.gitignore b/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..c93f1fa --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,22 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_selector_macos +import image_editor_common +import path_provider_foundation +import sqflite +import url_launcher_macos +import video_compress + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + ImageEditorPlugin.register(with: registry.registrar(forPlugin: "ImageEditorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) +} diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 0000000..b52666a --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 0000000..3de18c4 --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,63 @@ +PODS: + - file_selector_macos (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - image_editor_common (1.0.0): + - Flutter + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite (0.0.2): + - FlutterMacOS + - FMDB (>= 2.7.5) + - url_launcher_macos (0.0.1): + - FlutterMacOS + - video_compress (0.3.0): + - FlutterMacOS + +DEPENDENCIES: + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - image_editor_common (from `Flutter/ephemeral/.symlinks/plugins/image_editor_common/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`) + +SPEC REPOS: + trunk: + - FMDB + +EXTERNAL SOURCES: + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos + FlutterMacOS: + :path: Flutter/ephemeral + image_editor_common: + :path: Flutter/ephemeral/.symlinks/plugins/image_editor_common/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + video_compress: + :path: Flutter/ephemeral/.symlinks/plugins/video_compress/macos + +SPEC CHECKSUMS: + file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + image_editor_common: 1b11f59fad8909bafcdaa0f31cc9373425b58600 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f + +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 + +COCOAPODS: 1.12.1 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..7fd9730 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,794 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 29666A48E0746A8F43E5F1A1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D378C5C936653DBBC1228D2F /* Pods_RunnerTests.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4E2DDD2FE9AF92E37C6AC131 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D1DE25DC559C693961F556 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 05D1DE25DC559C693961F556 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 080E8B4371495D57BEFB8951 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 1D280AB165145E5660CBC9E2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 79A57FF7A45B475C2A8C6313 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 85B7DA7DC0B8A2F69B379C94 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + CD5B7ABAD27E2B25762937E3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + D0B9D83427CF00600F5310B6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + D378C5C936653DBBC1228D2F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 29666A48E0746A8F43E5F1A1 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4E2DDD2FE9AF92E37C6AC131 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 899CBA0828B908BA3E1F7AB8 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 899CBA0828B908BA3E1F7AB8 /* Pods */ = { + isa = PBXGroup; + children = ( + 1D280AB165145E5660CBC9E2 /* Pods-Runner.debug.xcconfig */, + 85B7DA7DC0B8A2F69B379C94 /* Pods-Runner.release.xcconfig */, + CD5B7ABAD27E2B25762937E3 /* Pods-Runner.profile.xcconfig */, + D0B9D83427CF00600F5310B6 /* Pods-RunnerTests.debug.xcconfig */, + 79A57FF7A45B475C2A8C6313 /* Pods-RunnerTests.release.xcconfig */, + 080E8B4371495D57BEFB8951 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 05D1DE25DC559C693961F556 /* Pods_Runner.framework */, + D378C5C936653DBBC1228D2F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + BFDD682F310FE1BEDA5A496D /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 8B8A796660CBF63049EC3BAF /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 74F13BF3EE80E516567F1853 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 74F13BF3EE80E516567F1853 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8B8A796660CBF63049EC3BAF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + BFDD682F310FE1BEDA5A496D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0B9D83427CF00600F5310B6 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 79A57FF7A45B475C2A8C6313 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 080E8B4371495D57BEFB8951 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..397f3d3 --- /dev/null +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..dda192b --- /dev/null +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..08c3ab1 --- /dev/null +++ b/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..5418c9f --- /dev/null +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..bc02bb7 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: zds_flutter_example + +publish_to: "none" +version: 1.0.0+1 + +environment: + sdk: ">=3.0.1 <4.0.0" + +dependencies: + flutter: + sdk: flutter + intl: ^0.18.0 + lottie: ^2.1.0 + table_calendar: ^3.0.9 + zds_flutter: + path: ../ + +dev_dependencies: + flutter_lints: ^2.0.1 + flutter_test: + sdk: flutter + +dependency_overrides: + http: ^1.0.0 + +flutter: + uses-material-design: true diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..d7baaaf --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + zds_flutter example + + + + + + + + + + \ No newline at end of file diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..8c01291 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/example/windows/.gitignore b/example/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt new file mode 100644 index 0000000..c09389c --- /dev/null +++ b/example/windows/CMakeLists.txt @@ -0,0 +1,102 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..930d207 --- /dev/null +++ b/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..043a96f --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..a95e267 --- /dev/null +++ b/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc new file mode 100644 index 0000000..aecaa2b --- /dev/null +++ b/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp new file mode 100644 index 0000000..a61bf80 --- /dev/null +++ b/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/example/windows/runner/resources/app_icon.ico differ diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp new file mode 100644 index 0000000..b2b0873 --- /dev/null +++ b/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/lib/assets/animations/approval_stamped.json b/lib/assets/animations/approval_stamped.json new file mode 100644 index 0000000..741f95a --- /dev/null +++ b/lib/assets/animations/approval_stamped.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":59,"w":1080,"h":1080,"nm":"O/RequestApprovalStampingB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"tick","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.08,969.656,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[61.254,-37.766],[-14.012,37.766],[-61.254,-9.475]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":33.212,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.211],"y":[1]},"o":{"x":[0.464],"y":[0]},"t":20,"s":[100]},{"t":32,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"stamp and hand","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[20]},{"t":20,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.68,"y":0.507},"o":{"x":0.228,"y":0},"t":0,"s":[708.991,511.844,0],"to":[-35.908,-29.177,0],"ti":[16.606,-28.068,0]},{"i":{"x":0.928,"y":0.698},"o":{"x":0.272,"y":0.062},"t":16,"s":[622.974,511.475,0],"to":[-16.606,28.068,0],"ti":[0.39,-39.935,0]},{"t":20,"s":[611.991,678.094,0]}],"ix":2},"a":{"a":0,"k":[619.491,617.094,0],"ix":1},"s":{"a":0,"k":[90,90,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[58.995,27.338],[0,0],[31.747,16.318],[26.907,13.963],[14.317,-4.751],[44.054,-17.777],[1.393,-1.443],[8.822,-22.162],[6.285,-13.725],[6.334,-19.737],[-1.518,-2.21],[-12.57,-0.713],[-6.496,6.431],[-9.502,22.745],[-7.826,11.722],[0,0],[-0.002,0.001],[-31.716,-4.365],[-4.446,-8.371],[-6.28,-15.847],[-2.946,-6.975],[9.126,-0.326],[31.714,-1.062],[0,0],[10.453,-7.931],[0.606,-0.5],[-1.03,-23.209],[-2.668,-3.982],[-31.137,-6.941],[-56.731,-0.909],[-57.599,25.985],[-16.404,14.176],[0,0]],"o":[[0,0],[-32.182,-16.77],[-26.503,-13.623],[-15.748,-8.174],[-24.796,8.227],[-69.12,27.891],[-11.225,20.628],[-5.668,14.239],[-3.124,6.819],[-0.757,2.358],[5.447,7.931],[15.438,0.869],[11.306,-11.195],[7.614,-18.225],[0,0],[0,0],[26.953,-14.872],[10.63,1.463],[8.197,15.434],[2.767,6.982],[5.111,12.101],[-32.256,1.148],[0,0],[-14.425,0.481],[-0.645,0.49],[-8.769,7.237],[0.191,4.307],[18.975,28.322],[55.222,12.31],[63.927,1.024],[20.516,-9.258],[0,0],[-9.427,-4.234]],"v":[[205.237,-107.191],[204.871,-107.37],[108.114,-157.333],[27.298,-199.007],[-17.383,-204.095],[-132.987,-161.14],[-214.19,-122.781],[-243.358,-57.077],[-261.173,-14.388],[-296.501,89.901],[-295.415,96.419],[-260.28,111.153],[-219.108,100.722],[-187.53,41.003],[-164.898,-6.159],[-163.71,-7.939],[-161.834,-8.973],[-63.787,-40.739],[-43.805,-17.17],[-22.835,30.497],[-14.262,51.673],[-37.495,70.098],[-134.259,73.405],[-146.231,73.806],[-184.258,86.666],[-186.13,88.144],[-197.592,126.74],[-193.172,139.177],[-105.437,183.067],[62.657,206.686],[240.533,164.223],[296.893,132.017],[296.893,-64.937]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[558.553,360.916],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-32.236,1.147],[-1.252,0.319],[1.95,4.92],[7.74,14.572],[2.831,4.323],[0.667,0],[19.308,-14.472],[10.478,-24.356],[-10.952,0.366]],"o":[[31.705,-1.061],[2.104,-0.075],[-2.052,-5.021],[-6.103,-15.401],[-2.674,-5.036],[-0.678,-0.021],[-24.198,0],[-22.121,16.581],[9.481,-3.954],[0,0]],"v":[[-29.431,35.972],[67.295,32.667],[72.252,32.063],[66.254,17.085],[46.1,-28.813],[37.819,-42.89],[35.802,-42.922],[-32.117,-19.781],[-72.252,42.922],[-41.403,36.373]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[453.137,380.797],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.181,3.37],[17.085,0],[48.452,9.233],[23.097,7.279],[9.849,4.729],[0.663,4.56],[-16.463,-20.861],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[-123.486,156.478]],"o":[[-17.793,2.466],[-49.326,0],[-23.769,-4.529],[-10.406,-3.28],[-3.842,-1.845],[3.137,21.567],[123.486,156.478],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[2.629,-3.332]],"v":[[162.527,-103.067],[110.071,-99.63],[-37.069,-113.563],[-107.865,-130.646],[-138.376,-142.75],[-162.527,-157.111],[-135.658,-93.012],[-135.658,157.111],[-37.804,157.111],[8.877,157.111],[9.836,157.111],[10.796,157.111],[57.476,157.111],[155.331,157.111],[155.331,-93.012]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[529.244,683.205],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[55.101,195],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[906.184,393.479],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[125.567,100.789],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-41.644,0],[0,0],[0,-41.644]],"o":[[0,0],[0,0],[0,-41.644],[0,0],[41.644,0],[0,0]],"v":[[291.066,103.011],[-291.066,103.011],[-291.066,-27.607],[-215.663,-103.011],[215.663,-103.011],[291.066,-27.607]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[539.08,962.989],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":4}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/check.json b/lib/assets/animations/check.json new file mode 100644 index 0000000..455535a --- /dev/null +++ b/lib/assets/animations/check.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":111,"w":1080,"h":1080,"nm":"O/TaskCompleteGeneralB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":17,"ty":4,"nm":"Tick","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[545.098,559.587,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[189.216,-116.662],[-43.285,116.662],[-189.216,-29.268]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":102,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.211],"y":[1]},"o":{"x":[0.464],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,545.391,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[790.068,790.068],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/check_circle.json b/lib/assets/animations/check_circle.json new file mode 100644 index 0000000..203035e --- /dev/null +++ b/lib/assets/animations/check_circle.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":60,"w":1080,"h":1080,"nm":"O/TaskCompleteB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Tick 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[385.139,702.064,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[116.29,-71.699],[-26.602,71.699],[-116.29,-17.988]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":60,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.211],"y":[1]},"o":{"x":[0.464],"y":[0]},"t":1,"s":[100]},{"t":21,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":161,"st":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Tick 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[385.139,427.665,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[116.29,-71.699],[-26.602,71.699],[-116.29,-17.988]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":60,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.211],"y":[1]},"o":{"x":[0.464],"y":[0]},"t":1,"s":[100]},{"t":21,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":161,"st":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Line 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[711.762,702.064,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-109.404,0],[109.404,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":60,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":160,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Line 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[711.762,427.665,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-109.404,0],[109.404,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":60,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":160,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Paper","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[545.008,554.103,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[105,105,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.727,0],[0,0],[0,22.727],[0,0],[-22.727,0],[0,0],[0,-22.727],[0,0]],"o":[[0,0],[-22.727,0],[0,0],[0,-22.727],[0,0],[22.727,0],[0,0],[0,22.727]],"v":[[359.876,401.026],[-359.876,401.026],[-401.026,359.876],[-401.026,-359.876],[-359.876,-401.026],[359.876,-401.026],[401.026,-359.876],[401.026,359.876]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":160,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/check_glimmer.json b/lib/assets/animations/check_glimmer.json new file mode 100644 index 0000000..2052479 --- /dev/null +++ b/lib/assets/animations/check_glimmer.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":111,"w":1080,"h":1080,"nm":"O/TaskCompleteGeneralB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Blink 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[538,1047.021,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":29,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":48,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":70,"s":[50,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":92,"s":[100,100,100]},{"t":111,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[61.447,61.447],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Blink 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[507.276,31.608,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":18,"s":[200,200,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":49,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":72,"s":[150,150,100]},{"i":{"x":[0.895,0.895,0.895],"y":[1,1,1]},"o":{"x":[0.097,0.097,0.097],"y":[0,0,0]},"t":92,"s":[50,50,100]},{"t":111,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":162,"st":12,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Blink 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[125.8,826.813,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":20,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":44,"s":[200,200,100]},{"i":{"x":[0.527,0.527,0.527],"y":[0.744,0.744,-37.327]},"o":{"x":[0.107,0.107,0.107],"y":[0,0,0]},"t":63,"s":[50,50,100]},{"i":{"x":[0.905,0.905,0.905],"y":[1,1,1]},"o":{"x":[0.42,0.42,0.42],"y":[-0.227,-0.227,34.042]},"t":87,"s":[100,100,100]},{"t":111,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Blink 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[852.78,937.846,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":12,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":37,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":58,"s":[150,150,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":77,"s":[50,50,100]},{"t":111,"s":[150,150,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":157,"st":7,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Blink 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[62.027,374.995,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":14,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":32,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":48,"s":[200,200,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":66,"s":[50,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":91,"s":[150,150,100]},{"t":111,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":157,"st":7,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Blink 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1045.706,503.94,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":12,"s":[120,120,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":35,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":60,"s":[200,200,100]},{"i":{"x":[0.517,0.517,0.517],"y":[1.632,1.632,16.447]},"o":{"x":[0.12,0.12,0.12],"y":[0,0,0]},"t":78,"s":[100,100,100]},{"i":{"x":[0.848,0.848,0.848],"y":[1,1,1]},"o":{"x":[0.367,0.367,0.367],"y":[0.165,0.165,-14.084]},"t":93,"s":[120,120,100]},{"t":111,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Blink 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1003.736,746.508,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":13,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":28,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":53,"s":[50,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.102,0.102,0.102],"y":[0,0,0]},"t":86,"s":[200,200,100]},{"t":111,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":157,"st":7,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Blink 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.456,609.547,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":20,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":40,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":60,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":78,"s":[50,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":93,"s":[110,110,100]},{"t":111,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[74.975,74.975],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Blink 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[968.687,275.938,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":17,"s":[100,100,100]},{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":48,"s":[30,30,100]},{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":79,"s":[150,150,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":93,"s":[100,100,100]},{"t":111,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[63.234,63.234],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":156,"st":6,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Blink 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[819.711,113.955,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":37,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":57,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":77,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":97,"s":[50,50,100]},{"t":111,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[44.284,44.284],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":167,"st":17,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Blink 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.345,953.012,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":6,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":35,"s":[50,50,100]},{"i":{"x":[0.578,0.578,0.578],"y":[0.802,0.802,-8.88]},"o":{"x":[0.096,0.096,0.096],"y":[0,0,0]},"t":58,"s":[150,150,100]},{"i":{"x":[0.692,0.692,0.692],"y":[1.005,1.005,-24.54]},"o":{"x":[0.351,0.351,0.351],"y":[-0.306,-0.306,15.285]},"t":72,"s":[100,100,100]},{"i":{"x":[0.88,0.88,0.88],"y":[1,1,1]},"o":{"x":[0.495,0.495,0.495],"y":[0.004,0.004,20.523]},"t":98,"s":[150,150,100]},{"t":111,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[44.284,44.284],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":161,"st":11,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Blink 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[217.77,142.966,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":6,"s":[30,30,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":29,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":65,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":84,"s":[150,150,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":101,"s":[100,100,100]},{"t":111,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[49.316,49.316],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":154,"st":4,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Tick","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[545.098,559.587,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[189.216,-116.662],[-43.285,116.662],[-189.216,-29.268]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":102,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.211],"y":[1]},"o":{"x":[0.464],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,545.391,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[790.068,790.068],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/check_ripple.json b/lib/assets/animations/check_ripple.json new file mode 100644 index 0000000..de1f526 --- /dev/null +++ b/lib/assets/animations/check_ripple.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":90,"w":1080,"h":1080,"nm":"I-TaskCompleteRippleB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"tick ","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.697,1.941,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[25.865,-15.947],[-5.917,15.947],[-25.865,-4.001]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"main circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,166.667]},"t":0,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,150.645]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":36,"s":[1000,1000,100]},{"t":50,"s":[960,960,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[108,108],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"ripple 2","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[0,0]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.33,0.33],"y":[0,0]},"t":29,"s":[1080,1080]},{"t":50,"s":[1000,1000]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"ripple 1","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[0,0]},{"i":{"x":[0.25,0.25],"y":[1,1]},"o":{"x":[0.33,0.33],"y":[0,0]},"t":20,"s":[1080,1080]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.33,0.33],"y":[0,0]},"t":30,"s":[1080,1080]},{"t":50,"s":[1000,1000]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.238235294118,0.23431374045,0.240196093391,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/thumbs_up.json b/lib/assets/animations/thumbs_up.json new file mode 100644 index 0000000..6045f4d --- /dev/null +++ b/lib/assets/animations/thumbs_up.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":60,"w":1080,"h":1080,"nm":"O/RequestApprovalB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ball","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[303.311,322.64,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[49.686,49.686],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[20,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Ring","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[504.389,607.188,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-76.799,-203.27],[-63.114,23.846],[76.799,203.27],[63.114,-23.846]],"o":[[76.799,203.27],[63.114,-23.846],[-76.799,-203.27],[-63.114,23.846]],"v":[[-114.278,43.176],[139.057,368.053],[114.278,-43.176],[-139.057,-368.053]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-5.556,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"hand","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.25],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.67],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":10,"s":[-10]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.33],"y":[0]},"t":23,"s":[10]},{"t":29,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.05,"y":0},"t":0,"s":[496.502,611.432,0],"to":[6.255,-31,0],"ti":[-36.255,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[545.646,545,0],"to":[36.255,0,0],"ti":[0.361,-21.834,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[591.289,609.406,0],"to":[-0.361,21.834,0],"ti":[36.255,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[543.481,676.002,0],"to":[-36.255,0,0],"ti":[7.83,10.762,0]},{"t":29,"s":[496.502,611.432,0]}],"ix":2},"a":{"a":0,"k":[507.914,605.384,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[55.315,0],[41.373,0.649],[-0.423,3.134],[40.337,56.39],[0,0],[23.872,-38.836],[10.607,-11.426],[5.548,-5.708],[25.183,-23.699],[0,-17.526],[0,0],[-11.695,-30.956],[-40.294,-58.173],[0,0],[0,0],[0,0],[-6.506,46.467],[0,0]],"o":[[0,0],[-5.494,-0.086],[1.664,-12.333],[-68.505,-95.769],[-8.007,67.727],[-3.476,5.655],[-4.679,5.04],[-32.014,32.932],[-12.763,12.011],[0,0],[8.732,30.077],[31.68,83.848],[0,0],[0,0],[0,0],[46.92,0],[0,0],[0,-55.316]],"v":[[182.045,-97.898],[4.395,-99.846],[0,-110.232],[25.599,-330.933],[-76.962,-350.407],[-123.699,-188.126],[-145.75,-161.691],[-161.172,-145.499],[-262.208,-47.153],[-282.204,-0.873],[-282.204,69.24],[-251.502,161.07],[-140.815,379.207],[21.453,379.207],[43.485,379.207],[147.596,379.207],[240.771,298.169],[282.204,2.26]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[627.426,483.406],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.248,9.225],[0,0],[-51.387,0],[0,0],[30.071,79.59]],"o":[[0,0],[0,51.387],[0,0],[-38.239,-57.85],[-3.499,-9.261]],"v":[[-57.417,-119.049],[-57.417,26.004],[35.628,119.049],[57.417,119.049],[-47.296,-91.312]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[402.639,743.564],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Arm","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[261.904,647.481,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[68.949,217.131],[-68.949,217.131],[-68.949,-217.131],[68.949,-217.131]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/thumbs_up_approved.json b/lib/assets/animations/thumbs_up_approved.json new file mode 100644 index 0000000..de47a41 --- /dev/null +++ b/lib/assets/animations/thumbs_up_approved.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":60,"w":1080,"h":1080,"nm":"I-Request ApprovalB","ddd":0,"assets":[],"fonts":{"list":[{"fName":"Helvetica","fFamily":"Helvetica","fStyle":"Regular","ascent":71.8994140625}]},"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"thumb","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.67],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":10,"s":[-10]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":23,"s":[10]},{"t":29,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.05,"y":0},"t":0,"s":[166.502,546.432,0],"to":[0,0,0],"ti":[-7.465,0.338,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[184.354,520,0],"to":[7.465,-0.338,0],"ti":[-1.521,-8.5,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[211.289,544.406,0],"to":[1.521,8.5,0],"ti":[7.465,-0.338,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":23,"s":[193.481,571.002,0],"to":[-7.465,0.338,0],"ti":[4.497,4.095,0]},{"t":29,"s":[166.502,546.432,0]}],"ix":2},"a":{"a":0,"k":[168.252,550.779,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[12.164,38.306],[-12.164,38.306],[-12.164,-38.306],[12.164,-38.306]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115.099,579.372],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[9.759,0],[7.299,0.115],[-0.075,0.553],[7.116,9.948],[0,0],[4.211,-6.851],[6.958,-6.548],[0,-3.092],[0,0],[-9.066,0],[0,0],[-1.148,8.198],[0,0]],"o":[[0,0],[-0.969,-0.015],[0.294,-2.176],[-12.086,-16.895],[-1.413,11.948],[-2.445,3.977],[-2.252,2.119],[0,0],[0,9.066],[0,0],[8.277,0],[0,0],[0,-9.759]],"v":[[32.116,-17.271],[0.775,-17.614],[0,-19.447],[4.516,-58.382],[-13.577,-61.818],[-21.823,-33.189],[-46.258,-8.319],[-49.786,-0.154],[-49.786,50.484],[-33.371,66.899],[26.038,66.899],[42.476,52.602],[49.786,0.399]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[183.784,550.779],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":5,"nm":"approved","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[642.6,561.483,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"t":{"d":{"k":[{"s":{"sz":[700.334655761719,154.544189453125],"ps":[-348.167327880859,-73.272102355957],"s":161.887512207031,"f":"Helvetica","t":"Approved","j":0,"tr":0,"lh":194.265014648438,"ls":0,"fc":[0.286,0.282,0.29]},"t":0}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0],"ix":2}},"a":[]},"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,551.227,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[9.216,0],[0,0],[0,9.216],[0,0],[-9.216,0],[0,0],[0,-9.216],[0,0]],"o":[[0,0],[-9.216,0],[0,0],[0,-9.216],[0,0],[9.216,0],[0,0],[0,9.216]],"v":[[523.313,150.022],[-523.313,150.022],[-540,133.335],[-540,-133.335],[-523.313,-150.022],[523.313,-150.022],[540,-133.335],[540,133.335]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.843137254902,0.835294117647,0.847058823529,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[],"chars":[{"ch":"A","size":161.887512207031,"style":"Regular","w":66.7,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[21.973,-29.395],[33.545,-61.084],[44.434,-29.395]],"c":true},"ix":2},"nm":"A","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[1.465,0],[11.426,0],[19.189,-21.484],[47.559,-21.484],[54.834,0],[65.479,0],[39.453,-71.729],[28.467,-71.729]],"c":true},"ix":2},"nm":"A","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"A","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Helvetica"},{"ch":"p","size":161.887512207031,"style":"Regular","w":55.62,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.115,0],[2.286,5.404],[0,4.037],[-1.208,3.255],[-6.108,0],[-2.287,-5.762],[0,-4.166],[2.727,-3.434]],"o":[[-6.076,0],[-1.208,-2.832],[0,-5.013],[2.254,-6.087],[6.075,0],[1.208,2.995],[0,6.836],[-2.728,3.435]],"v":[[28.516,-5.908],[15.973,-14.014],[14.16,-24.316],[15.973,-36.719],[28.516,-45.85],[41.058,-37.207],[42.871,-26.465],[38.78,-11.06]],"c":true},"ix":2},"nm":"p","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-1.569,-0.977],[-3.954,0],[-3.791,3.092],[0,11.394],[4.144,4.541],[5.94,0],[2.969,-1.953],[1.762,-2.376],[0,0],[0,0]],"o":[[0,0],[0,0],[1.928,2.377],[2.679,1.725],[5.032,0],[5.98,-4.883],[0,-8.43],[-4.145,-4.541],[-4.015,0],[-2.089,1.302],[0,0],[0,0],[0,0]],"v":[[5.713,20.85],[14.502,20.85],[14.502,-5.859],[19.747,-0.83],[29.698,1.758],[42.934,-2.881],[51.904,-27.295],[45.687,-46.753],[30.56,-53.564],[20.084,-50.635],[14.307,-45.117],[14.307,-52.051],[5.713,-52.051]],"c":true},"ix":2},"nm":"p","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"p","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Helvetica"},{"ch":"r","size":161.887512207031,"style":"Regular","w":33.3,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-2.377,2.849],[-4.427,0],[-0.439,-0.032],[-0.521,-0.098],[0,0],[0.391,0.033],[0.163,0],[2.669,-2.522],[0.684,-1.758],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-3.711],[2.376,-2.848],[0.52,0],[0.439,0.033],[0,0],[-0.945,-0.098],[-0.391,-0.032],[-3.484,0],[-2.67,2.523],[0,0],[0,0],[0,0]],"v":[[6.689,0],[15.479,0],[15.479,-30.078],[19.043,-39.917],[29.248,-44.189],[30.688,-44.141],[32.129,-43.945],[32.129,-53.223],[30.127,-53.418],[29.297,-53.467],[20.068,-49.683],[15.039,-43.262],[15.039,-52.295],[6.689,-52.295]],"c":true},"ix":2},"nm":"r","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"r","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Helvetica"},{"ch":"o","size":161.887512207031,"style":"Regular","w":55.62,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.761,0],[2.414,3.706],[0,5.56],[-2.414,4.097],[-5.312,0],[-2.446,-4.812],[0,-4.877],[2.141,-4.405]],"o":[[-5.247,0],[-2.414,-3.706],[0,-5.787],[2.414,-4.097],[5.987,0],[1.545,3.056],[0,5.397],[-2.141,4.405]],"v":[[27.026,-5.713],[15.535,-11.272],[11.914,-25.172],[15.535,-39.997],[27.122,-46.143],[39.772,-38.924],[42.09,-27.025],[38.879,-12.321]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[6.691,0],[4.428,-5.203],[0,-8.781],[-4.202,-4.862],[-7.144,0],[-4.073,5.496],[0,8.424],[4.622,4.488]],"o":[[-7.467,0],[-4.428,5.204],[0,8.196],[4.202,4.862],[8.566,0],[4.073,-5.496],[0,-8.716],[-4.623,-4.488]],"v":[[27.366,-53.809],[9.523,-46.003],[2.881,-25.025],[9.184,-5.437],[26.202,1.855],[45.16,-6.389],[51.27,-27.27],[44.336,-47.076]],"c":true},"ix":2},"nm":"o","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"o","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Helvetica"},{"ch":"v","size":161.887512207031,"style":"Regular","w":50,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[0.562,-52.295],[19.849,0],[29.214,0],[48.901,-52.295],[39.282,-52.295],[24.731,-9.717],[10.767,-52.295]],"c":true},"ix":2},"nm":"v","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"v","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Helvetica"},{"ch":"e","size":161.887512207031,"style":"Regular","w":55.62,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[3.688,0],[4.529,-5.241],[0,-8.398],[-4.497,-4.736],[-6.47,0],[-2.1,0.52],[-2.65,2.605],[-1.286,2.361],[-0.228,1.921],[0,0],[1.518,-1.765],[4.554,0],[2.325,3.215],[0.162,5.321],[0,0],[0.517,2.409],[1.747,2.637],[3.461,1.742]],"o":[[-7.312,0],[-4.53,5.241],[0,8.529],[4.497,4.736],[2.649,0],[3.909,-0.912],[1.582,-1.497],[1.286,-2.36],[0,0],[-0.633,2.322],[-2.713,3.041],[-4.877,0],[-2.325,-3.215],[0,0],[0,-5.273],[-0.583,-3.516],[-1.812,-2.766],[-3.462,-1.741]],"v":[[28.022,-53.467],[10.261,-45.605],[3.467,-25.146],[10.211,-5.249],[26.661,1.855],[33.784,1.074],[43.622,-4.199],[47.925,-9.985],[50.195,-16.406],[41.553,-16.406],[38.326,-10.275],[27.425,-5.713],[16.621,-10.535],[12.891,-23.34],[50.928,-23.34],[50.151,-34.863],[46.657,-44.092],[38.747,-50.854]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-2.711,2.914],[-4.002,0],[-2.389,-4.231],[-0.356,-3.809]],"o":[[0.161,-4.492],[2.711,-2.913],[5.584,0],[1.291,2.279],[0,0]],"v":[[13.086,-30.322],[17.395,-41.431],[27.466,-45.801],[39.425,-39.453],[41.895,-30.322]],"c":true},"ix":2},"nm":"e","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"e","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Helvetica"},{"ch":"d","size":161.887512207031,"style":"Regular","w":55.62,"data":{"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,5.599],[-2.737,3.386],[-4.405,0],[-2.689,-3.174],[0,-6.608],[2.607,-3.499],[4.048,0],[2.364,3.777]],"o":[[0,-6.575],[2.737,-3.385],[3.951,0],[2.688,3.174],[0,6.543],[-2.608,3.5],[-5.215,0],[-2.364,-3.776]],"v":[[12.012,-25.537],[16.116,-40.479],[26.829,-45.557],[36.788,-40.796],[40.82,-26.123],[36.909,-11.06],[26.926,-5.811],[15.558,-11.475]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[3.989,0],[3.875,-5.582],[0,-7.617],[-4.248,-4.899],[-5.804,0],[-2.789,1.432],[-2.043,3.223],[0,0],[0,0],[0,0],[0,0],[0,0],[1.556,0.977]],"o":[[-7.199,0],[-3.875,5.583],[0,8.138],[4.248,4.9],[3.6,0],[2.789,-1.432],[0,0],[0,0],[0,0],[0,0],[0,0],[-1.978,-2.441],[-2.692,-1.692]],"v":[[25.157,-53.223],[8.546,-44.849],[2.734,-25.049],[9.106,-5.493],[24.185,1.855],[33.768,-0.293],[41.016,-7.275],[41.016,0],[48.926,0],[48.926,-71.973],[40.479,-71.973],[40.479,-45.557],[35.177,-50.684]],"c":true},"ix":2},"nm":"d","mn":"ADBE Vector Shape - Group","hd":false}],"nm":"d","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}]},"fFamily":"Helvetica"}]} \ No newline at end of file diff --git a/lib/assets/animations/time_approved.json b/lib/assets/animations/time_approved.json new file mode 100644 index 0000000..428e374 --- /dev/null +++ b/lib/assets/animations/time_approved.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":138,"w":1080,"h":1080,"nm":"O/TimecardB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":13,"ty":4,"nm":"thumb","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"t":34,"s":[100]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.67],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":22,"s":[-10]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":40,"s":[10]},{"t":45,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.05,"y":0},"t":22,"s":[546.502,520.432,0],"to":[0,0,0],"ti":[-9.131,0.338,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[575.646,494,0],"to":[9.131,-0.338,0],"ti":[0.361,-8.5,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":34,"s":[601.289,518.406,0],"to":[-0.361,8.5,0],"ti":[9.131,-0.338,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[573.481,545.002,0],"to":[-9.131,0.338,0],"ti":[4.497,4.095,0]},{"t":45,"s":[546.502,520.432,0]}],"ix":2},"a":{"a":0,"k":[540,521.406,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":22,"s":[90,90,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":34,"s":[103,103,100]},{"t":45,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[35.793,112.717],[-35.793,112.717],[-35.793,-112.717],[35.793,-112.717]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[383.593,605.542],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[28.715,0],[21.477,0.337],[-0.219,1.627],[20.94,29.273],[0,0],[12.392,-20.161],[20.475,-19.268],[0,-9.098],[0,0],[-26.676,0],[0,0],[-3.378,24.122],[0,0]],"o":[[0,0],[-2.852,-0.045],[0.864,-6.402],[-35.562,-49.715],[-4.157,35.158],[-7.194,11.703],[-6.626,6.235],[0,0],[0,26.676],[0,0],[24.357,0],[0,0],[0,-28.716]],"v":[[94.504,-50.821],[2.281,-51.832],[0,-57.223],[13.289,-171.794],[-39.953,-181.903],[-64.215,-97.66],[-136.117,-24.478],[-146.497,-0.453],[-146.497,148.552],[-98.196,196.854],[76.62,196.854],[124.989,154.786],[146.497,1.173]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[585.703,521.406],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":150,"st":-6,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.33],"y":[0]},"t":18,"s":[0]},{"t":25,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[536,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":22,"s":[80,80,100]},{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":25,"s":[100,100,100]},{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":38,"s":[105,105,100]},{"t":45,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[815,815],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":151,"st":-32,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Clock","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.878,17.878],[-17.878,-17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[747.663,747.663],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[25.284,0],[-25.284,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[833.68,540],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.878,-17.878],[-17.878,17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[747.663,332.337],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-25.284],[0,25.284]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[540,246.32],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.878,-17.878],[17.878,17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[332.337,332.337],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-25.284,0],[25.284,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[246.32,540],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.878,17.878],[17.878,-17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[332.337,747.663],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,25.284],[0,-25.284]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[540,833.679],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Clock inner border","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[659.675,659.675],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Clock centre","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[542,544,0],"ix":2},"a":{"a":0,"k":[2,4,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[68.119,68.119],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":3,"ty":"el","s":{"a":0,"k":[642.293,642.293],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":560,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[3.834,-2.219],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[55.088,55.088],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.017],"y":[0.975]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":30,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Clock outer border","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[790.068,790.068],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.843137254902,0.835294117647,0.847058823529,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/time_approved_box.json b/lib/assets/animations/time_approved_box.json new file mode 100644 index 0000000..c2e0105 --- /dev/null +++ b/lib/assets/animations/time_approved_box.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":85,"w":1080,"h":1080,"nm":"I-TimcardB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"tick","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[544.398,552.419,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[163.257,-100.657],[-37.346,100.657],[-163.257,-25.253]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":88.006,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.211],"y":[1]},"o":{"x":[0.464],"y":[0]},"t":28,"s":[100]},{"t":48,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":28,"op":168,"st":18,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.057],"y":[0]},"t":20,"s":[0]},{"t":28,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540.171,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[681.675,681.675],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":180,"st":30,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Clock","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.878,17.878],[-17.878,-17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[747.663,747.663],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[25.284,0],[-25.284,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[833.68,540],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.878,-17.878],[-17.878,17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[747.663,332.337],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-25.284],[0,25.284]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[540,246.32],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.878,-17.878],[17.878,17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[332.337,332.337],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-25.284,0],[25.284,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[246.32,540],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.878,17.878],[17.878,-17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[332.337,747.663],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,25.284],[0,-25.284]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[540,833.679],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":55,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Clock centre","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[542,544,0],"ix":2},"a":{"a":0,"k":[2,4,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[68.119,68.119],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":55,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Clock inner border","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[659.675,659.675],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":55,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Swiping","sr":1,"ks":{"o":{"a":0,"k":51,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":3,"ty":"el","s":{"a":0,"k":[659.675,659.675],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[100]},{"t":30,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.137254901961,0.137254901961,0.138235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":560,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[52.479,52.479],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":55,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"clock background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[659.675,659.675],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.843137254902,0.835294117647,0.847058823529,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":55,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Square","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.829,540.171,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[1079.658,1079.658],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274522543,0.282352954149,0.290196090937,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":56,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/time_approved_glimmer.json b/lib/assets/animations/time_approved_glimmer.json new file mode 100644 index 0000000..f28d8ad --- /dev/null +++ b/lib/assets/animations/time_approved_glimmer.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":138,"w":1080,"h":1080,"nm":"O/TimecardB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Blink 12","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,559.021,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":56,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":75,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":97,"s":[50,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":119,"s":[100,100,100]},{"t":138,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[61.447,61.447],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":177,"st":27,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Blink 11","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[19.276,-456.392,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":45,"s":[200,200,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":76,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":99,"s":[150,150,100]},{"i":{"x":[0.895,0.895,0.895],"y":[1,1,1]},"o":{"x":[0.097,0.097,0.097],"y":[0,0,0]},"t":119,"s":[50,50,100]},{"t":138,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":189,"st":39,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Blink 10","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-362.2,338.813,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":47,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":71,"s":[200,200,100]},{"i":{"x":[0.527,0.527,0.527],"y":[0.744,0.744,-37.327]},"o":{"x":[0.107,0.107,0.107],"y":[0,0,0]},"t":90,"s":[50,50,100]},{"i":{"x":[0.905,0.905,0.905],"y":[1,1,1]},"o":{"x":[0.42,0.42,0.42],"y":[-0.227,-0.227,34.042]},"t":114,"s":[100,100,100]},{"t":138,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":177,"st":27,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Blink 9","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[364.78,449.846,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":39,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":64,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":85,"s":[150,150,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":104,"s":[50,50,100]},{"t":138,"s":[150,150,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":184,"st":34,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Blink 8","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-425.973,-113.005,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":41,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":59,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":75,"s":[200,200,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":93,"s":[50,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":118,"s":[150,150,100]},{"t":138,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":184,"st":34,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Blink 7","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[557.706,15.94,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":39,"s":[120,120,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":62,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":87,"s":[200,200,100]},{"i":{"x":[0.517,0.517,0.517],"y":[1.632,1.632,16.447]},"o":{"x":[0.12,0.12,0.12],"y":[0,0,0]},"t":105,"s":[100,100,100]},{"i":{"x":[0.848,0.848,0.848],"y":[1,1,1]},"o":{"x":[0.367,0.367,0.367],"y":[0.165,0.165,-14.084]},"t":120,"s":[120,120,100]},{"t":138,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":177,"st":27,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Blink 6","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[515.736,258.508,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":40,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":55,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":80,"s":[50,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.102,0.102,0.102],"y":[0,0,0]},"t":113,"s":[200,200,100]},{"t":138,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30.332,30.332],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":184,"st":34,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Blink 5","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-447.544,121.547,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":47,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":67,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":87,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":105,"s":[50,50,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":120,"s":[110,110,100]},{"t":138,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[74.975,74.975],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":177,"st":27,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Blink 4","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[480.687,-212.062,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":27,"s":[50,50,100]},{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":44,"s":[100,100,100]},{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":75,"s":[30,30,100]},{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":106,"s":[150,150,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":120,"s":[100,100,100]},{"t":138,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[63.234,63.234],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":183,"st":33,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Blink 3","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[331.711,-374.045,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":64,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":84,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":104,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":124,"s":[50,50,100]},{"t":138,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[44.284,44.284],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":194,"st":44,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Blink 2","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-237.655,465.012,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[100,100,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":33,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":62,"s":[50,50,100]},{"i":{"x":[0.578,0.578,0.578],"y":[0.802,0.802,-8.88]},"o":{"x":[0.096,0.096,0.096],"y":[0,0,0]},"t":85,"s":[150,150,100]},{"i":{"x":[0.692,0.692,0.692],"y":[1.005,1.005,-24.54]},"o":{"x":[0.351,0.351,0.351],"y":[-0.306,-0.306,15.285]},"t":99,"s":[100,100,100]},{"i":{"x":[0.88,0.88,0.88],"y":[1,1,1]},"o":{"x":[0.495,0.495,0.495],"y":[0.004,0.004,20.523]},"t":125,"s":[150,150,100]},{"t":138,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[44.284,44.284],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":188,"st":38,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Blink 1","parent":15,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-270.23,-345.034,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":27,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":33,"s":[30,30,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":56,"s":[150,150,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":92,"s":[50,50,100]},{"i":{"x":[0.9,0.9,0.9],"y":[1,1,1]},"o":{"x":[0.1,0.1,0.1],"y":[0,0,0]},"t":111,"s":[150,150,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":128,"s":[100,100,100]},{"t":138,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[49.316,49.316],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.283333333333,0.279411794625,0.28725493188,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":181,"st":31,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"thumb","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":22,"s":[0]},{"t":34,"s":[100]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.67],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":22,"s":[-10]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":40,"s":[10]},{"t":45,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.05,"y":0},"t":22,"s":[546.502,520.432,0],"to":[0,0,0],"ti":[-9.131,0.338,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[575.646,494,0],"to":[9.131,-0.338,0],"ti":[0.361,-8.5,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":34,"s":[601.289,518.406,0],"to":[-0.361,8.5,0],"ti":[9.131,-0.338,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":40,"s":[573.481,545.002,0],"to":[-9.131,0.338,0],"ti":[4.497,4.095,0]},{"t":45,"s":[546.502,520.432,0]}],"ix":2},"a":{"a":0,"k":[540,521.406,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":22,"s":[90,90,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":34,"s":[103,103,100]},{"t":45,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[35.793,112.717],[-35.793,112.717],[-35.793,-112.717],[35.793,-112.717]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[383.593,605.542],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[28.715,0],[21.477,0.337],[-0.219,1.627],[20.94,29.273],[0,0],[12.392,-20.161],[20.475,-19.268],[0,-9.098],[0,0],[-26.676,0],[0,0],[-3.378,24.122],[0,0]],"o":[[0,0],[-2.852,-0.045],[0.864,-6.402],[-35.562,-49.715],[-4.157,35.158],[-7.194,11.703],[-6.626,6.235],[0,0],[0,26.676],[0,0],[24.357,0],[0,0],[0,-28.716]],"v":[[94.504,-50.821],[2.281,-51.832],[0,-57.223],[13.289,-171.794],[-39.953,-181.903],[-64.215,-97.66],[-136.117,-24.478],[-146.497,-0.453],[-146.497,148.552],[-98.196,196.854],[76.62,196.854],[124.989,154.786],[146.497,1.173]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[585.703,521.406],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":150,"st":-6,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.33],"y":[0]},"t":18,"s":[0]},{"t":25,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[536,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":22,"s":[80,80,100]},{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":25,"s":[100,100,100]},{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.33,0.33,0.33],"y":[0,0,0]},"t":38,"s":[105,105,100]},{"t":45,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[815,815],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":151,"st":-32,"bm":0},{"ddd":0,"ind":15,"ty":3,"nm":"sparkings and circles","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540.5,0],"ix":2},"a":{"a":0,"k":[50,50.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,16.667]},"t":18,"s":[0,0,100]},{"t":40,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Clock","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.878,17.878],[-17.878,-17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[747.663,747.663],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[25.284,0],[-25.284,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[833.68,540],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[17.878,-17.878],[-17.878,17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[747.663,332.337],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-25.284],[0,25.284]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[540,246.32],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.878,-17.878],[17.878,17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[332.337,332.337],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-25.284,0],[25.284,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[246.32,540],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-17.878,17.878],[17.878,-17.878]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[332.337,747.663],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,25.284],[0,-25.284]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[540,833.679],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Clock inner border","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[659.675,659.675],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Clock centre","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[542,544,0],"ix":2},"a":{"a":0,"k":[2,4,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[68.119,68.119],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":50,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":3,"ty":"el","s":{"a":0,"k":[642.293,642.293],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":560,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[3.834,-2.219],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[55.088,55.088],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.017],"y":[0.975]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":30,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Clock outer border","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[790.068,790.068],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.843137254902,0.835294117647,0.847058823529,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/timecard_tapping.json b/lib/assets/animations/timecard_tapping.json new file mode 100644 index 0000000..2045410 --- /dev/null +++ b/lib/assets/animations/timecard_tapping.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":89,"w":1080,"h":1080,"nm":"O/TimecardTappingB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"hand","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.33],"y":[0]},"t":5,"s":[20]},{"i":{"x":[0.67],"y":[1]},"o":{"x":[0.05],"y":[0]},"t":20,"s":[-10]},{"t":28,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.33,"y":0},"t":5,"s":[136.216,571.365,0],"to":[0,-4.167,0],"ti":[0,4.167,0]},{"t":28,"s":[136.216,546.365,0]}],"ix":2},"a":{"a":0,"k":[-243,6,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[-7.92,1.468],[-23.885,10.638],[0.62,17.763],[10.047,6.436],[9.979,0.083],[0.217,-0.002],[0.042,0],[0.628,-0.029],[24.87,-1.086],[0.228,-0.005],[0,0],[0,0],[0,0],[0,0],[-0.824,3.921],[19.358,-8.199],[0,0],[0,0],[-24.361,-1.544],[-27.665,6.072]],"o":[[0,0],[0,0],[0,0],[0,0],[9.143,-2.086],[12.004,-2.224],[19.635,-8.745],[-0.61,-17.461],[-7.78,-4.984],[-0.217,0],[-0.042,0],[-0.628,0.008],[-24.868,1.139],[-0.228,0.01],[0,0],[0,0],[0,0],[0,0],[0,-4.163],[-20.441,0.641],[-5.389,3.219],[0,0],[0,0],[28.42,1.801],[0,0]],"v":[[36.574,137.489],[48.823,134.684],[48.998,134.644],[49.149,134.61],[49.191,134.6],[74.89,129.076],[137.468,109.611],[179.758,68.95],[165.681,37.382],[138.549,29.648],[137.898,29.645],[137.772,29.647],[135.886,29.701],[61.279,33.074],[60.594,33.096],[51.072,34.047],[37.799,35.371],[37.799,22.033],[37.799,-134.867],[39.063,-147.012],[-21.067,-136.94],[-179.77,-77.128],[-179.769,99.414],[-47.658,146.63],[36.588,137.541]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":115,"st":-65,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"tick","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[301.187,2.863,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[49.611,-30.588],[-11.349,30.588],[-49.611,-7.674]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":26.899,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.211],"y":[1]},"o":{"x":[0.464],"y":[0]},"t":28,"s":[100]},{"t":40,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":125,"st":5,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"tick circle","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[299.85,-0.859,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.75,0.75,0.75],"y":[1,1,1]},"o":{"x":[0.25,0.25,0.25],"y":[0,0,0]},"t":20,"s":[0,0,100]},{"t":28,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[207.151,207.151],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":125,"st":5,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"tick card","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[299.85,-0.365,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[25.9,0],[0,0],[0,-25.899],[0,0],[0,0],[0,0],[-22.14,1.015],[0,0],[-1.065,0],[-9.852,-6.312],[-0.634,-18.147],[33.495,-14.918],[12.528,-2.321],[9.035,-2.061],[0.104,-0.024],[-21.636,0],[0,0],[0,25.9],[0,0]],"o":[[0,0],[-25.9,0],[0,0],[0,0],[0,0],[22.142,-0.967],[0,0],[1.065,-0.049],[12.472,0],[15.491,9.925],[0.687,19.66],[-24.914,11.097],[-7.688,1.425],[-0.104,0.024],[5.41,19.923],[0,0],[25.9,0],[0,0],[0,-25.899]],"v":[[203.082,-181.397],[-203.082,-181.397],[-249.977,-134.502],[-249.977,22.398],[-239.892,21.391],[-239.556,21.377],[-173.132,18.379],[-164.975,18.005],[-161.78,17.932],[-127.656,27.581],[-108.026,68.894],[-157.47,121.005],[-222.76,141.313],[-248.016,146.746],[-248.331,146.818],[-203.082,181.397],[203.082,181.397],[249.977,134.502],[249.977,-134.502]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":120,"st":-60,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"arm","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-218.344,11.128,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[51.831,176.513],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":22,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":120,"st":-60,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"clock","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[679.383,539.295,0],"ix":2},"a":{"a":0,"k":[687.066,967.578,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.361,7.361],[-7.361,-7.361]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[754.2,1034.711],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[10.41,0],[-10.41,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[782.006,967.578],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[7.361,-7.361],[-7.361,7.361]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[754.199,900.445],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-10.41],[0,10.41]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[687.066,872.638],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-7.361,-7.361],[7.361,7.361]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[619.933,900.445],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-10.41,0],[10.41,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[592.126,967.578],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-7.361,7.361],[7.361,-7.361]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[619.933,1034.711],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,10.41],[0,-10.41]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":18,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[687.066,1062.519],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"","np":1,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"clock cent","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[679.383,539.295,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[25,25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"clock circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540.367,540.423,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[207,207],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[139.016,-1.128],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"clock card","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[679.066,539.506,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[27.575,0],[0,0],[0,27.575],[0,0],[-27.575,0],[0,0],[0,-27.575],[0,0]],"o":[[0,0],[-27.575,0],[0,0],[0,-27.575],[0,0],[27.575,0],[0,0],[0,27.575]],"v":[[200.048,181.397],[-200.048,181.397],[-249.977,131.468],[-249.977,-131.468],[-200.048,-181.397],[200.048,-181.397],[249.977,-131.468],[249.977,131.468]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.286274509804,0.282352941176,0.290196078431,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/animations/two_checks.json b/lib/assets/animations/two_checks.json new file mode 100644 index 0000000..d0f5b7d --- /dev/null +++ b/lib/assets/animations/two_checks.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.0.2","a":"","k":"","d":"","tc":"#FFFFFF"},"fr":30,"ip":0,"op":80,"w":1080,"h":1080,"nm":"I-TaskCompleteB","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"tick","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[546.969,559.406,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[258.653,-159.474],[-59.169,159.474],[-258.653,-40.009]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":140,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.211],"y":[1]},"o":{"x":[0.464],"y":[0]},"t":27,"s":[100]},{"t":47,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":27,"op":167,"st":17,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.25],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":18,"s":[0]},{"t":27,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.67,0.67,0.67],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,2.083]},"t":18,"s":[70,70,100]},{"t":27,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[1080,1080],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.280392156863,0.276470588235,0.28431372549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":18,"op":168,"st":18,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"ring","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.462,540,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":3,"ty":"el","s":{"a":0,"k":[1009.11,1009.11],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.278431372549,0.274509803922,0.282352941176,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":69.452,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.086],"y":[0]},"t":0,"s":[100]},{"t":30,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/lib/assets/fonts/selection.json b/lib/assets/fonts/selection.json new file mode 100644 index 0000000..099d700 --- /dev/null +++ b/lib/assets/fonts/selection.json @@ -0,0 +1,6800 @@ +{ + "IcoMoonType": "selection", + "icons": [ + { + "icon": { + "paths": [ + "M384 479.573v-159.573c0-58.88 47.787-106.667 106.667-106.667s106.667 47.787 106.667 106.667v159.573c51.627-34.56 85.333-93.013 85.333-159.573 0-106.24-85.76-192-192-192s-192 85.76-192 192c0 66.56 33.707 125.013 85.333 159.573zM803.84 677.12l-193.707-96.427c-7.253-2.987-14.933-4.693-23.040-4.693h-32.427v-256c0-35.413-28.587-64-64-64s-64 28.587-64 64v458.24c-153.6-32.427-151.040-32-156.587-32-13.227 0-25.173 5.547-33.707 14.080l-33.707 34.133 210.773 210.773c11.52 11.52 27.733 18.773 45.227 18.773h289.707c32 0 56.747-23.467 61.44-54.613l32-224.853c0.427-2.987 0.853-5.973 0.853-8.533 0-26.453-16.213-49.493-38.827-58.88z" + ], + "attrs": [], + "tags": [ + "action" + ], + "defaultCode": 59821, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 570, + "id": 1, + "name": "action", + "prevSize": 32, + "code": 59821 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 0 + }, + { + "icon": { + "paths": [ + "M810.667 554.667h-256v256h-85.333v-256h-256v-85.333h256v-256h85.333v256h256v85.333z" + ], + "attrs": [], + "tags": [ + "add" + ], + "defaultCode": 59651, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 571, + "id": 2, + "name": "add", + "prevSize": 32, + "code": 59651 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 1 + }, + { + "icon": { + "paths": [ + "M341.333 469.333h341.333v85.333h-341.333v-85.333zM857.6 512h81.067c0-117.759-95.573-213.332-213.333-213.332h-170.667v81.067h170.667c72.96 0 132.267 59.305 132.267 132.265zM166.4 512c0-72.96 59.307-132.265 132.267-132.265h170.667v-81.067h-170.667c-117.76 0-213.333 95.573-213.333 213.332s95.573 213.333 213.333 213.333h170.667v-81.067h-170.667c-72.96 0-132.267-59.307-132.267-132.267zM810.667 512h-85.333v128h-128v85.333h128v128h85.333v-128h128v-85.333h-128v-128z" + ], + "attrs": [], + "tags": [ + "add link" + ], + "defaultCode": 59649, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 572, + "id": 3, + "name": "add-link", + "prevSize": 32, + "code": 59650 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 2 + }, + { + "icon": { + "paths": [ + "M938.667 221.013l-486.827 487.253-180.907-180.907 60.16-60.16 120.747 120.747 426.667-426.667 60.16 59.733zM512 853.333c-188.16 0-341.333-153.173-341.333-341.333s153.173-341.333 341.333-341.333c66.987 0 129.707 19.627 182.613 53.333l61.867-61.867c-71.723-49.996-157.052-76.801-244.48-76.8-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c73.813 0 143.36-18.773 203.947-52.053l-64-64c-42.667 19.627-90.027 30.72-139.947 30.72zM810.667 640h-128v85.333h128v128h85.333v-128h128v-85.333h-128v-128h-85.333v128z" + ], + "attrs": [], + "tags": [ + "add task" + ], + "defaultCode": 59724, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 573, + "id": 4, + "name": "add-task", + "prevSize": 32, + "code": 59724 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 3 + }, + { + "icon": { + "paths": [ + "M341.333 426.667h-128v-128h-85.333v128h-128v85.333h128v128h85.333v-128h128v-85.333zM768 469.333c70.827 0 127.573-57.173 127.573-128s-56.747-128-127.573-128c-13.653 0-26.88 2.133-38.827 5.973 24.32 34.56 38.4 76.373 38.4 122.027s-14.507 87.040-38.4 122.027c11.947 3.84 25.173 5.973 38.827 5.973zM554.667 469.333c70.827 0 127.573-57.173 127.573-128s-56.747-128-127.573-128c-70.827 0-128 57.173-128 128s57.173 128 128 128zM837.12 561.493c35.413 31.147 58.88 70.827 58.88 121.173v85.333h128v-85.333c0-65.707-101.12-106.24-186.88-121.173zM554.667 554.667c-85.333 0-256 42.667-256 128v85.333h512v-85.333c0-85.333-170.667-128-256-128z" + ], + "attrs": [], + "tags": [ + "add user" + ], + "defaultCode": 59650, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 574, + "id": 5, + "name": "add-user", + "prevSize": 32, + "code": 59652 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 4 + }, + { + "icon": { + "paths": [ + "M512 960c-129.707 0-234.667-104.96-234.667-234.667v-448c0-94.293 76.373-170.667 170.667-170.667s170.667 76.373 170.667 170.667v362.667c0 58.88-47.787 106.667-106.667 106.667s-106.667-47.787-106.667-106.667v-320h85.333v323.84c0 23.467 42.667 23.467 42.667 0v-366.507c0-46.933-38.4-85.333-85.333-85.333s-85.333 38.4-85.333 85.333v448c0 82.347 66.987 149.333 149.333 149.333s149.333-66.987 149.333-149.333v-405.333h85.333v405.333c0 129.707-104.96 234.667-234.667 234.667z" + ], + "attrs": [], + "tags": [ + "attachment" + ], + "defaultCode": 59652, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 575, + "id": 6, + "name": "attachment", + "prevSize": 32, + "code": 59653 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 5 + }, + { + "icon": { + "paths": [ + "M128 384v256h170.667l213.333 213.333v-682.667l-213.333 213.333h-170.667zM704 512c0-75.52-43.52-140.373-106.667-171.947v343.467c63.147-31.147 106.667-96 106.667-171.52zM597.333 137.813v87.893c123.307 36.693 213.333 151.040 213.333 286.293s-90.027 249.6-213.333 286.293v87.893c171.093-38.827 298.667-191.573 298.667-374.187s-127.573-335.36-298.667-374.187z" + ], + "attrs": [], + "tags": [ + "audio" + ], + "defaultCode": 59823, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 576, + "id": 7, + "name": "audio", + "prevSize": 32, + "code": 59823 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 6 + }, + { + "icon": { + "paths": [ + "M597.333 85.333h-341.333c-46.933 0-85.333 38.4-85.333 85.333v682.667c0 46.933 38.4 85.333 85.333 85.333h512c46.933 0 85.333-38.4 85.333-85.333v-512l-256-256zM256 853.333v-682.667h298.667v213.333h213.333v469.333h-512zM682.667 469.333h-170.667v165.547c-15.36-10.24-33.707-16.213-53.333-16.213-52.907 0-96 43.093-96 96s43.093 96 96 96c52.907 0 96-43.093 96-96v-160h128v-85.333z" + ], + "attrs": [], + "tags": [ + "audio-file" + ], + "defaultCode": 59653, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 577, + "id": 8, + "name": "audio-file", + "prevSize": 32, + "code": 59654 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 7 + }, + { + "icon": { + "paths": [ + "M517.419 166.699c-20.821-64.171-83.755-97.067-139.648-74.027-31.445 12.331-54.827 39.467-66.56 74.027h-152.747l-0.043 0c-20.617 0.247-39.067 9.505-51.669 24.106-13.739 15.403-21.419 36.352-21.419 58.155v575.744l0-0.038c-0.002 0.251-0.003 0.501-0.003 0.752 0 22.019 8.113 42.144 21.422 57.441 13.739 15.403 32.299 24.064 51.712 24.064h231.936c-9.088-25.984-14.805-53.589-16.64-82.219h-215.296v-575.744h73.088v82.219h365.568v-82.219h73.088v247.808l-0.577 0.018c4.009-0.154 8.038-0.231 12.085-0.231 21.812 0 43.097 2.253 61.623 6.187v-253.781c0-21.845-7.723-42.752-21.419-58.155-13.739-15.445-32.299-24.107-51.712-24.107h-152.789zM472.917 577.963h-241.365v82.219h177.707l0.81-1.633c16.361-30.171 37.59-57.319 62.849-80.586zM440.192 178.731c6.827 7.765 10.667 18.176 10.667 29.099s-3.84 21.376-10.667 29.056l-0.038 0.045c-6.258 7.266-15.484 11.903-25.818 12.030-9.685 0-19.029-4.352-25.856-12.075s-10.667-18.133-10.667-29.056c0-10.923 3.84-21.376 10.667-29.099l0.037-0.044c6.265-7.251 15.491-11.873 25.819-11.988l0.020 0c10.308 0.115 19.533 4.737 25.836 12.032zM231.552 413.44h365.568v82.261h-365.568v-82.261z", + "M611.627 618.325c12.617 0.786 23.286 8.402 28.414 19.167l0.087 0.204 91.819 196.693 51.413-82.261c6.057-9.569 16.585-15.829 28.575-15.829 0.004 0 0.008 0 0.012 0l101.205-0c18.616 0 33.707 15.091 33.707 33.707s-15.091 33.707-33.707 33.707v0h-82.517l-74.411 119.083c-6.053 9.587-16.592 15.861-28.595 15.861-13.438 0-25.040-7.864-30.454-19.241l-0.087-0.204-91.819-196.693-51.413 82.261c-6.057 9.569-16.585 15.829-28.575 15.829-0.004 0-0.008-0-0.012-0l-101.205 0c-18.616 0-33.707-15.091-33.707-33.707s15.091-33.707 33.707-33.707h82.517l74.411-119.083c6.055-9.58 16.589-15.849 28.587-15.849 0.72 0 1.435 0.023 2.144 0.067l-0.097-0.005z" + ], + "attrs": [], + "tags": [ + "audit-activity" + ], + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 578, + "id": 9, + "name": "audit-activity", + "prevSize": 32, + "code": 59648 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 8 + }, + { + "icon": { + "paths": [ + "M853.333 469.333h-519.253l238.507-238.507-60.587-60.16-341.333 341.333 341.333 341.333 60.16-60.16-238.080-238.507h519.253v-85.333z" + ], + "attrs": [], + "tags": [ + "back" + ], + "defaultCode": 59654, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 579, + "id": 10, + "name": "back", + "prevSize": 32, + "code": 59655 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 9 + }, + { + "icon": { + "paths": [ + "M853.333 128h-682.667v426.667c0 94.293 76.373 170.667 170.667 170.667h256c94.293 0 170.667-76.373 170.667-170.667v-128h85.333c47.36 0 85.333-38.4 85.333-85.333v-128c0-47.36-37.973-85.333-85.333-85.333zM853.333 341.333h-85.333v-128h85.333v128zM170.667 810.667h682.667v85.333h-682.667v-85.333z" + ], + "attrs": [], + "tags": [ + "break" + ], + "defaultCode": 59824, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 580, + "id": 11, + "name": "break-", + "prevSize": 32, + "code": 59824 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 10 + }, + { + "icon": { + "paths": [ + "M768 469.333v85.333h170.667v-85.333h-170.667zM682.667 751.36c40.96 30.293 94.293 70.4 136.533 101.973 17.067-22.613 34.133-45.653 51.2-68.267-42.24-31.573-95.573-71.68-136.533-102.4-17.067 23.040-34.133 46.080-51.2 68.693zM870.4 238.933c-17.067-22.613-34.133-45.653-51.2-68.267-42.24 31.573-95.573 71.68-136.533 102.4 17.067 22.613 34.133 45.653 51.2 68.267 40.96-30.72 94.293-70.4 136.533-102.4zM170.667 384c-46.933 0-85.333 38.4-85.333 85.333v85.333c0 46.933 38.4 85.333 85.333 85.333h42.667v170.667h85.333v-170.667h42.667l213.333 128v-512l-213.333 128h-170.667zM661.333 512c0-56.747-24.747-107.947-64-142.933v285.44c39.253-34.56 64-85.76 64-142.507z" + ], + "attrs": [], + "tags": [ + "broadcast" + ], + "defaultCode": 59825, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 581, + "id": 12, + "name": "broadcast", + "prevSize": 32, + "code": 59825 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 11 + }, + { + "icon": { + "paths": [ + "M512 298.667v-170.667h-426.667v768h853.333v-597.333h-426.667zM256 810.667h-85.333v-85.333h85.333v85.333zM256 640h-85.333v-85.333h85.333v85.333zM256 469.333h-85.333v-85.333h85.333v85.333zM256 298.667h-85.333v-85.333h85.333v85.333zM426.667 810.667h-85.333v-85.333h85.333v85.333zM426.667 640h-85.333v-85.333h85.333v85.333zM426.667 469.333h-85.333v-85.333h85.333v85.333zM426.667 298.667h-85.333v-85.333h85.333v85.333zM853.333 810.667h-341.333v-85.333h85.333v-85.333h-85.333v-85.333h85.333v-85.333h-85.333v-85.333h341.333v426.667zM768 469.333h-85.333v85.333h85.333v-85.333zM768 640h-85.333v85.333h85.333v-85.333z" + ], + "attrs": [], + "tags": [ + "building" + ], + "defaultCode": 59826, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 582, + "id": 13, + "name": "building", + "prevSize": 32, + "code": 59826 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 12 + }, + { + "icon": { + "paths": [ + "M832 170.667h-42.667v-85.333h-85.333v85.333h-341.333v-85.333h-85.333v85.333h-42.667c-47.128 0-85.333 38.205-85.333 85.333v0 597.333c0 46.933 38.4 85.333 85.333 85.333h597.333c47.128 0 85.333-38.205 85.333-85.333v0-597.333c0-47.128-38.205-85.333-85.333-85.333v0zM832 853.333h-597.333v-469.333h597.333v469.333z" + ], + "attrs": [], + "tags": [ + "calendar" + ], + "defaultCode": 59759, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 583, + "id": 14, + "name": "calendar", + "prevSize": 32, + "code": 59759 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 13 + }, + { + "icon": { + "paths": [ + "M384 469.333h-85.333v85.333h85.333v-85.333zM554.667 469.333h-85.333v85.333h85.333v-85.333zM725.333 469.333h-85.333v85.333h85.333v-85.333zM810.667 170.667h-42.667v-85.333h-85.333v85.333h-341.333v-85.333h-85.333v85.333h-42.667c-47.36 0-84.907 38.4-84.907 85.333l-0.427 597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 853.333h-597.333v-469.333h597.333v469.333z" + ], + "attrs": [], + "tags": [ + "calendar-3-day" + ], + "defaultCode": 59827, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 584, + "id": 15, + "name": "calendar-3-day", + "prevSize": 32, + "code": 59827 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 14 + }, + { + "icon": { + "paths": [ + "M768 128h42.667c46.933 0 85.333 38.4 85.333 85.333v299.597c-25.835-16.542-54.596-28.915-85.333-36.177v-135.42h-597.333v469.333h263.42c7.262 30.737 19.635 59.499 36.177 85.333h-299.597c-47.36 0-85.333-38.4-85.333-85.333l0.427-597.333c0-46.933 37.547-85.333 84.907-85.333h42.667v-85.333h85.333v85.333h341.333v-85.333h85.333v85.333z", + "M725.333 512c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333zM682.667 832l-106.667-106.667 30.080-30.080 76.587 76.373 161.92-161.92 30.080 30.293-192 192z" + ], + "attrs": [], + "tags": [ + "calendar-available" + ], + "defaultCode": 59828, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 585, + "id": 16, + "name": "calendar-available", + "prevSize": 32, + "code": 59828 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 15 + }, + { + "icon": { + "paths": [ + "M384 469.333h-85.333v85.333h85.333v-85.333zM810.667 170.667h-42.667v-85.333h-85.333v85.333h-341.333v-85.333h-85.333v85.333h-42.667c-47.36 0-84.907 38.4-84.907 85.333l-0.427 597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 853.333h-597.333v-469.333h597.333v469.333z" + ], + "attrs": [], + "tags": [ + "calendar-day" + ], + "defaultCode": 59829, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 586, + "id": 17, + "name": "calendar-day", + "prevSize": 32, + "code": 59829 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 16 + }, + { + "icon": { + "paths": [ + "M768 128h42.667c46.933 0 85.333 38.4 85.333 85.333v299.597c-25.835-16.542-54.596-28.915-85.333-36.177v-135.42h-597.333v469.333h263.42c7.262 30.737 19.635 59.499 36.177 85.333h-299.597c-47.36 0-85.333-38.4-85.333-85.333l0.427-597.333c0-46.933 37.547-85.333 84.907-85.333h42.667v-85.333h85.333v85.333h341.333v-85.333h85.333v85.333z", + "M597.333 824.9v71.1h71.1l209.702-209.702-71.1-71.1-209.702 209.702zM933.12 631.313c7.394-7.394 7.394-19.341 0-26.735l-44.365-44.365c-7.394-7.394-19.341-7.394-26.735 0l-34.697 34.697 71.1 71.1 34.697-34.697z" + ], + "attrs": [], + "tags": [ + "calendar-edit" + ], + "defaultCode": 59830, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 587, + "id": 18, + "name": "calendar-edit", + "prevSize": 32, + "code": 59830 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 17 + }, + { + "icon": { + "paths": [ + "M810.667 170.667h-42.667v-85.333h-85.333v85.333h-341.333v-85.333h-85.333v85.333h-42.667c-47.36 0-84.907 38.4-84.907 85.333l-0.427 597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 853.333h-597.333v-469.333h597.333v469.333z", + "M298.667 554.667v-85.333h426.667v85.333h-426.667z", + "M298.667 725.333v-85.333h298.667v85.333h-298.667z" + ], + "attrs": [], + "tags": [ + "calendar-month" + ], + "defaultCode": 59831, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 588, + "id": 19, + "name": "calendar-month", + "prevSize": 32, + "code": 59831 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 18 + }, + { + "icon": { + "paths": [ + "M768 170.667h42.667c46.933 0 85.333 38.4 85.333 85.333v384h-85.333v-256h-597.333v469.333h298.667v85.333h-298.667c-47.36 0-85.333-38.4-85.333-85.333l0.427-597.333c0-46.933 37.547-85.333 84.907-85.333h42.667v-85.333h85.333v85.333h341.333v-85.333h85.333v85.333z", + "M1024.004 731.26l-64 64.004c-85.299-85.299-256.034-89.161-341.333-3.861-30.298 30.298-55.317 85.559-64 124.352h80.282c7.334-24.704 21.035-48.055 40.525-67.546 63.876-63.876 167.701-63.876 231.582 0l-57.899 57.894 174.844 0.004v-174.848z" + ], + "attrs": [], + "tags": [ + "calendar-rollover" + ], + "defaultCode": 59832, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 589, + "id": 20, + "name": "calendar-rollover", + "prevSize": 32, + "code": 59832 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 19 + }, + { + "icon": { + "paths": [ + "M810.667 85.333h-42.667v-85.333h-85.333v85.333h-341.333v-85.333h-85.333v85.333h-42.667c-47.128 0-85.333 38.205-85.333 85.333v0 597.333c0 46.933 38.4 85.333 85.333 85.333h597.333c47.128 0 85.333-38.205 85.333-85.333v0-597.333c0-47.128-38.205-85.333-85.333-85.333v0zM810.667 768h-597.333v-469.333h597.333v469.333zM298.667 384h213.333v213.333h-213.333v-213.333z" + ], + "attrs": [], + "tags": [ + "calendar-today" + ], + "defaultCode": 59749, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 590, + "id": 21, + "name": "calendar-today", + "prevSize": 32, + "code": 59749 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 20 + }, + { + "icon": { + "paths": [ + "M810.667 128h-42.667v-85.333h-85.333v85.333h-341.333v-85.333h-85.333v85.333h-42.667c-47.36 0-84.907 38.4-84.907 85.333l-0.427 597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 810.667h-597.333v-469.333h597.333v469.333z", + "M619.968 682.351l91.802-91.802-91.802-91.802v75.507h-229.499v-75.507l-91.8 91.802 91.8 91.802v-75.507h229.499v75.507z" + ], + "attrs": [], + "tags": [ + "calendar-trade" + ], + "defaultCode": 59834, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 591, + "id": 22, + "name": "calendar-trade", + "prevSize": 32, + "code": 59834 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 21 + }, + { + "icon": { + "paths": [ + "M768 128h42.667c47.128 0 85.333 38.205 85.333 85.333v0 298.667c-24.227-16.692-53.008-28.683-84.075-33.957l-1.258-0.177v-136.533h-597.333v469.333h264.533c4.267 29.867 17.067 59.733 34.133 85.333h-298.667c-47.128 0-85.333-38.205-85.333-85.333v0-597.333c0-46.933 38.4-85.333 85.333-85.333h42.667v-85.333h85.333v85.333h341.333v-85.333h85.333v85.333z", + "M725.333 512c-117.821 0-213.333 95.513-213.333 213.333s95.513 213.333 213.333 213.333v0c117.821 0 213.333-95.513 213.333-213.333s-95.513-213.333-213.333-213.333v0zM832 802.133l-29.867 29.867-76.8-76.8-76.8 76.8-29.867-29.867 76.8-76.8-76.8-76.8 29.867-29.867 76.8 76.8 76.8-76.8 29.867 29.867-76.8 76.8 76.8 76.8z" + ], + "attrs": [], + "tags": [ + "calendar-unavailable" + ], + "defaultCode": 59756, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 592, + "id": 23, + "name": "calendar-unavailable", + "prevSize": 32, + "code": 59756 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 22 + }, + { + "icon": { + "paths": [ + "M810.667 170.667h-42.667v-85.333h-85.333v85.333h-341.333v-85.333h-85.333v85.333h-42.667c-47.36 0-84.907 38.4-84.907 85.333l-0.427 597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 853.333h-597.333v-469.333h597.333v469.333z", + "M298.667 554.667v-85.333h426.667v85.333h-426.667z" + ], + "attrs": [], + "tags": [ + "calendar-week" + ], + "defaultCode": 59836, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 593, + "id": 24, + "name": "calendar-week", + "prevSize": 32, + "code": 59836 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 23 + }, + { + "icon": { + "paths": [ + "M384 426.667h-85.333v85.333h85.333v-85.333zM554.667 426.667h-85.333v85.333h85.333v-85.333zM725.333 426.667h-85.333v85.333h85.333v-85.333zM810.667 170.667h-42.667v-85.333h-85.333v85.333h-341.333v-85.333h-85.333v85.333h-42.667c-47.36 0-84.907 38.4-84.907 85.333l-0.427 597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 853.333h-597.333v-469.333h597.333v469.333z", + "M298.667 576h85.333v85.333h-85.333v-85.333z", + "M469.333 576h85.333v85.333h-85.333v-85.333z", + "M640 576h85.333v85.333h-85.333v-85.333z", + "M298.667 725.333h85.333v85.333h-85.333v-85.333z", + "M469.333 725.333h85.333v85.333h-85.333v-85.333z", + "M640 725.333h85.333v85.333h-85.333v-85.333z" + ], + "attrs": [], + "tags": [ + "calendar-year" + ], + "defaultCode": 59837, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 594, + "id": 25, + "name": "calendar-year", + "prevSize": 32, + "code": 59837 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 24 + }, + { + "icon": { + "paths": [ + "M853.76 656.213c-52.48 0-103.253-8.533-150.613-23.893-14.933-5.12-31.573-1.28-43.093 10.24l-66.987 84.053c-120.747-57.6-233.813-166.4-293.973-291.413l83.2-70.827c11.52-11.947 14.933-28.587 10.24-43.52-15.787-47.36-23.893-98.133-23.893-150.613 0-23.040-19.2-42.24-42.24-42.24h-147.627c-23.040 0-50.773 10.24-50.773 42.24 0 396.373 329.813 725.76 725.76 725.76 30.293 0 42.24-26.88 42.24-50.347v-147.2c0-23.040-19.2-42.24-42.24-42.24z" + ], + "attrs": [], + "tags": [ + "call" + ], + "defaultCode": 59839, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 595, + "id": 26, + "name": "call", + "prevSize": 32, + "code": 59839 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 25 + }, + { + "icon": { + "paths": [ + "M512 648.533c75.405 0 136.533-61.129 136.533-136.533s-61.129-136.533-136.533-136.533c-75.405 0-136.534 61.129-136.534 136.533s61.129 136.533 136.534 136.533z", + "M384 85.333l-78.080 85.333h-135.253c-46.933 0-85.333 38.4-85.333 85.333v512c0 46.933 38.4 85.333 85.333 85.333h682.667c46.933 0 85.333-38.4 85.333-85.333v-512c0-46.933-38.4-85.333-85.333-85.333h-135.253l-78.080-85.333h-256zM512 725.333c-117.76 0-213.333-95.573-213.333-213.333s95.573-213.333 213.333-213.333c117.76 0 213.333 95.573 213.333 213.333s-95.573 213.333-213.333 213.333z" + ], + "attrs": [], + "tags": [ + "camera" + ], + "defaultCode": 59741, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 596, + "id": 27, + "name": "camera", + "prevSize": 32, + "code": 59741 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 26 + }, + { + "icon": { + "paths": [ + "M682.667 298.667h-42.667l-42.667-42.667h-170.667l-42.667 42.667h-42.667c-46.933 0-85.333 38.4-85.333 85.333v256c0 46.933 38.4 85.333 85.333 85.333h341.333c46.933 0 85.333-38.4 85.333-85.333v-256c0-46.933-38.4-85.333-85.333-85.333zM512 597.333c-46.933 0-85.333-38.4-85.333-85.333s38.4-85.333 85.333-85.333c46.933 0 85.333 38.4 85.333 85.333s-38.4 85.333-85.333 85.333z", + "M365.654 21.76l191.146 191.147v-125.867c201.387 20.053 361.813 180.48 381.867 381.866h85.333c-28.16-340.053-363.093-536.746-658.346-447.146z", + "M467.2 936.96c-201.387-20.053-361.813-180.48-381.867-381.867h-85.333c28.16 340.053 363.093 536.747 658.347 447.147l-191.147-191.147v125.867z" + ], + "attrs": [], + "tags": [ + "camera-switch" + ], + "defaultCode": 59740, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 597, + "id": 28, + "name": "camera-switch", + "prevSize": 32, + "code": 59740 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 27 + }, + { + "icon": { + "paths": [ + "M340.102 303.695l-1.361 170.966h269.032v-98.122c0-14.818 12.024-26.842 26.842-26.842h249.919c14.822 0 26.842 12.024 26.842 26.842v249.919c0 14.822-12.024 26.842-26.842 26.842h-249.919c-14.818 0-26.842-12.024-26.842-26.842v-98.118h-269.447l-2.512 317.224 264.263-0.008v-98.214c0-14.818 12.020-26.842 26.842-26.842h249.915c14.822 0 26.846 12.024 26.846 26.842v249.919c0 14.818-12.024 26.842-26.846 26.842h-249.915c-14.822 0-26.842-12.024-26.842-26.842v-98.235l-291.414 0.013c-14.763-0.114-26.643-12.189-26.533-26.948l4.51-568.385h-97.975c-14.82 0-26.842-12.024-26.842-26.842v-249.919c0-14.82 12.023-26.842 26.842-26.842h249.916c14.822 0 26.846 12.024 26.846 26.842v249.919c0 14.818-12.024 26.842-26.846 26.842l-98.481-0.008zM411.729 53.779h-196.238v196.237h196.238v-196.237zM849.98 774.187h-196.23v196.23h196.23v-196.23zM857.704 403.389h-196.238v196.238h196.238v-196.238z" + ], + "width": 1056, + "attrs": [], + "tags": [ + "category" + ], + "defaultCode": 59655, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 598, + "id": 29, + "name": "category", + "prevSize": 32, + "code": 59656 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 28 + }, + { + "icon": { + "paths": [ + "M853.333 298.667h-213.333v-128c0-46.933-38.4-85.333-85.333-85.333h-85.333c-46.933 0-85.333 38.4-85.333 85.333v128h-213.333c-46.933 0-85.333 38.4-85.333 85.333v469.333c0 46.933 38.4 85.333 85.333 85.333h682.667c46.933 0 85.333-38.4 85.333-85.333v-469.333c0-46.933-38.4-85.333-85.333-85.333zM384 512c35.413 0 64 28.587 64 64s-28.587 64-64 64c-35.413 0-64-28.587-64-64s28.587-64 64-64zM512 768h-256v-32c0-42.667 85.333-64 128-64s128 21.333 128 64v32zM554.667 384h-85.333v-213.333h85.333v213.333zM768 704h-170.667v-64h170.667v64zM768 576h-170.667v-64h170.667v64z" + ], + "attrs": [], + "tags": [ + "certified" + ], + "defaultCode": 59840, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 599, + "id": 30, + "name": "certified", + "prevSize": 32, + "code": 59840 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 29 + }, + { + "icon": { + "paths": [ + "M301.767 683.938l109.95-248.324c5.318-12.011 14.648-21.642 26.282-27.132l240.567-113.497c27.75-13.092 56.35 16.413 43.665 45.075l-109.948 248.326c-5.321 12.011-14.647 21.641-26.283 27.132l-240.568 113.498c-27.767 13.090-56.35-16.414-43.666-45.077zM549.632 550.848c20.783-21.453 20.783-56.243 0-77.696s-54.481-21.453-75.264 0c-20.783 21.453-20.783 56.243 0 77.696s54.481 21.453 75.264 0zM98.667 512c0-235.647 185.050-426.667 413.333-426.667s413.333 191.020 413.333 426.667c0 235.648-185.050 426.667-413.333 426.667s-413.333-191.019-413.333-426.667zM845.333 512c0-189.729-149.534-344.086-333.333-344.086s-333.333 154.357-333.333 344.086c0 189.73 149.533 344.085 333.333 344.085s333.333-154.355 333.333-344.085z" + ], + "attrs": [], + "tags": [ + "channel" + ], + "defaultCode": 59656, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 600, + "id": 31, + "name": "channel", + "prevSize": 32, + "code": 59657 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 30 + }, + { + "icon": { + "paths": [ + "M640 170.667v298.667h-419.413l-49.92 49.92v-348.587h469.333zM682.667 85.333h-554.667c-23.467 0-42.667 19.2-42.667 42.667v597.333l170.667-170.667h426.667c23.467 0 42.667-19.2 42.667-42.667v-384c0-23.467-19.2-42.667-42.667-42.667zM896 256h-85.333v384h-554.667v85.333c0 23.467 19.2 42.667 42.667 42.667h469.333l170.667 170.667v-640c0-23.467-19.2-42.667-42.667-42.667z" + ], + "attrs": [], + "tags": [ + "chat" + ], + "defaultCode": 59847, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 601, + "id": 32, + "name": "chat", + "prevSize": 32, + "code": 59847 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 31 + }, + { + "icon": { + "paths": [ + "M785.067 170.667h-546.133c-37.547 0-68.267 30.72-68.267 68.267v614.4l136.533-136.533h477.867c37.547 0 68.267-30.72 68.267-68.267v-409.6c0-37.547-30.72-68.267-68.267-68.267zM785.067 648.533h-477.867l-68.267 68.267v-477.867h546.133v409.6z", + "M616.017 400.842c22.366 0 40.422-18.055 40.422-40.421s-18.057-40.421-40.422-40.421c-22.366 0-40.422 18.055-40.422 40.421s18.057 40.421 40.422 40.421zM427.383 400.842c22.37 0 40.422-18.055 40.422-40.421s-18.052-40.421-40.422-40.421c-22.365 0-40.419 18.055-40.419 40.421s18.055 40.421 40.419 40.421zM521.702 576c62.784 0 116.143-39.343 137.698-94.315h-275.401c21.558 54.972 74.914 94.315 137.702 94.315z" + ], + "attrs": [], + "tags": [ + "chat-bot" + ], + "defaultCode": 59841, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 602, + "id": 33, + "name": "chat-bot", + "prevSize": 32, + "code": 59841 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 32 + }, + { + "icon": { + "paths": [ + "M785.067 170.667h-546.133c-37.547 0-68.267 30.72-68.267 68.267v614.4l136.533-136.533h477.867c37.547 0 68.267-30.72 68.267-68.267v-409.6c0-37.547-30.72-68.267-68.267-68.267zM785.067 648.533h-477.867l-68.267 68.267v-477.867h546.133v409.6z" + ], + "attrs": [], + "tags": [ + "chat-message" + ], + "defaultCode": 59845, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 603, + "id": 34, + "name": "chat-message", + "prevSize": 32, + "code": 59845 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 33 + }, + { + "icon": { + "paths": [ + "M455.1 644.023l58.871-59.78 21.321 21.619 344.461-349.862 58.914 59.736-403.375 409.597-80.192-81.31zM279.291 605.862l344.462-349.862 58.914 59.736-403.376 409.597-195.083-198.4 58.914-59.733 136.169 138.662z" + ], + "attrs": [], + "tags": [ + "chat-message-read" + ], + "defaultCode": 59842, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 604, + "id": 35, + "name": "chat-message-read", + "prevSize": 32, + "code": 59842 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 34 + }, + { + "icon": { + "paths": [ + "M819.703 606.993h-22.144l-7.846-7.569c27.469-31.953 44.006-73.438 44.006-118.562 0-100.628-81.566-182.195-182.191-182.195-100.629 0-182.195 81.567-182.195 182.195 0 100.625 81.566 182.191 182.195 182.191 45.124 0 86.609-16.538 118.562-44.006l7.569 7.846v22.144l140.147 139.87 41.766-41.766-139.87-140.147zM651.529 606.993c-69.794 0-126.135-56.337-126.135-126.131 0-69.796 56.341-126.136 126.135-126.136s126.131 56.34 126.131 126.136c0 69.794-56.337 126.131-126.131 126.131z", + "M238.933 170.667h546.133c37.547 0 68.267 30.72 68.267 68.267v62.758c-13.935-1.993-28.181-3.025-42.667-3.025-8.623 0-17.161 0.366-25.6 1.082v-60.815h-546.133v477.867l68.267-68.267h209.173c4.117 23.842 11.063 46.716 20.48 68.267h-229.653l-136.533 136.533v-614.4c0-37.547 30.72-68.267 68.267-68.267z" + ], + "attrs": [], + "tags": [ + "chat-search" + ], + "defaultCode": 59846, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 606, + "id": 37, + "name": "chat-search", + "prevSize": 32, + "code": 59846 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 35 + }, + { + "icon": { + "paths": [ + "M785.067 170.667h-546.133c-37.547 0-68.267 30.72-68.267 68.267v614.4l136.533-136.533h477.867c37.547 0 68.267-30.72 68.267-68.267v-409.6c0-37.547-30.72-68.267-68.267-68.267zM785.067 648.533h-477.867l-68.267 68.267v-477.867h546.133v409.6z" + ], + "attrs": [], + "tags": [ + "chat-unread active" + ], + "defaultCode": 59657, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 607, + "id": 38, + "name": "chat-unread-active", + "prevSize": 32, + "code": 59658 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 36 + }, + { + "icon": { + "paths": [ + "M384.007 689.92l-177.92-177.92-60.587 60.16 238.507 238.507 512.002-512-60.16-60.16-451.842 451.413z" + ], + "attrs": [], + "tags": [ + "check" + ], + "defaultCode": 59659, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 608, + "id": 39, + "name": "check", + "prevSize": 32, + "code": 59659 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 37 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM426.667 725.333l-213.333-213.333 60.16-60.16 153.173 152.747 323.84-323.84 60.16 60.587-384 384z" + ], + "attrs": [], + "tags": [ + "check circle" + ], + "defaultCode": 59658, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 609, + "id": 40, + "name": "check-circle", + "prevSize": 32, + "code": 59660 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 38 + }, + { + "icon": { + "paths": [ + "M303.577 353.921l-60.16 60.16 256.001 255.999 256-255.999-60.16-60.16-195.84 195.412-195.841-195.412z" + ], + "attrs": [], + "tags": [ + "chevron-down" + ], + "defaultCode": 59660, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 610, + "id": 41, + "name": "chevron-down", + "prevSize": 32, + "code": 59661 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 39 + }, + { + "icon": { + "paths": [ + "M678.827 316.16l-60.16-60.16-256 256 256 256 60.16-60.16-195.413-195.84 195.413-195.84z" + ], + "width": 1067, + "attrs": [], + "tags": [ + "chevron-left" + ], + "defaultCode": 59661, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 611, + "id": 42, + "name": "chevron-left", + "prevSize": 32, + "code": 59662 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 40 + }, + { + "icon": { + "paths": [ + "M426.671 256l-60.16 60.16 195.414 195.84-195.414 195.84 60.16 60.16 256-256-256-256z" + ], + "attrs": [], + "tags": [ + "chevron-right" + ], + "defaultCode": 59662, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 612, + "id": 43, + "name": "chevron-right", + "prevSize": 32, + "code": 59663 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 41 + }, + { + "icon": { + "paths": [ + "M695.258 670.080l60.16-60.16-256-255.998-256.001 255.998 60.16 60.16 195.841-195.413 195.84 195.413z" + ], + "attrs": [], + "tags": [ + "chevron-up" + ], + "defaultCode": 59663, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 613, + "id": 44, + "name": "chevron-up", + "prevSize": 32, + "code": 59664 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 42 + }, + { + "icon": { + "paths": [ + "M938.667 128h-640c-29.44 0-52.48 14.933-67.84 37.547l-230.827 346.453 230.827 346.027c15.36 22.613 38.4 37.973 67.84 37.973h640c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 665.173l-60.16 60.16-153.173-153.173-153.173 153.173-60.16-60.16 153.173-153.173-153.173-153.173 60.16-60.16 153.173 153.173 153.173-153.173 60.16 60.16-153.173 153.173 153.173 153.173z" + ], + "attrs": [], + "tags": [ + "clear" + ], + "defaultCode": 59747, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 614, + "id": 45, + "name": "clear", + "prevSize": 32, + "code": 59747 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 43 + }, + { + "icon": { + "paths": [ + "M777.481 169.752h-158.528c-21.619-66.56-86.852-100.693-144.879-76.8-32.614 12.8-56.888 40.96-69.025 76.8h-158.53c-20.117 0-39.41 8.99-53.635 24.994s-22.217 37.708-22.217 60.34v597.335c0 22.63 7.991 44.335 22.217 60.339s33.518 24.994 53.635 24.994h530.962c20.117 0 39.411-8.99 53.636-24.994s22.217-37.709 22.217-60.339v-597.335c0-22.632-7.991-44.337-22.217-60.34s-33.519-24.994-53.636-24.994zM512 169.752c10.057 0 19.703 4.495 26.816 12.497s11.11 18.854 11.11 30.17c0 11.316-3.998 22.168-11.11 30.17s-16.759 12.497-26.816 12.497c-10.057 0-19.703-4.495-26.816-12.497s-11.11-18.854-11.11-30.17c0-11.316 3.998-22.168 11.11-30.17s16.759-12.497 26.816-12.497zM322.371 340.419h379.257v-85.333h75.853v597.335h-530.962v-597.335h75.852v85.333zM701.628 511.087h-379.257v-85.335h379.257v85.335zM625.779 681.754h-303.409v-85.333h303.409v85.333z" + ], + "attrs": [], + "tags": [ + "clipboard" + ], + "defaultCode": 59717, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 615, + "id": 46, + "name": "clipboard", + "prevSize": 32, + "code": 59717 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 44 + }, + { + "icon": { + "paths": [ + "M511.573 85.333c-235.52 0-426.24 191.147-426.24 426.667s190.72 426.667 426.24 426.667c235.947 0 427.093-191.147 427.093-426.667s-191.147-426.667-427.093-426.667zM512 853.333c-188.587 0-341.333-152.747-341.333-341.333s152.747-341.333 341.333-341.333c188.587 0 341.333 152.747 341.333 341.333s-152.747 341.333-341.333 341.333z", + "M533.333 298.667h-64v256l224 134.4 32-52.48-192-113.92v-224z" + ], + "attrs": [], + "tags": [ + "clock" + ], + "defaultCode": 59864, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 616, + "id": 47, + "name": "clock", + "prevSize": 32, + "code": 59864 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 45 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667 0 25.024-2.159 49.545-6.298 73.391-25.122-13.602-52.774-23.121-82.069-27.674 2.001-14.955 3.034-30.217 3.034-45.717 0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333 15.501 0 30.763-1.033 45.717-3.034 4.548 29.278 14.059 56.913 27.648 82.022-23.97 4.169-48.627 6.345-73.792 6.345-235.52 0-426.24-191.147-426.24-426.667z", + "M810.667 597.333c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333zM768 917.333l-106.667-106.667 30.080-30.080 76.587 76.373 161.92-161.92 30.080 30.293-192 192z" + ], + "attrs": [], + "tags": [ + "clock-available" + ], + "defaultCode": 59848, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 617, + "id": 48, + "name": "clock-available", + "prevSize": 32, + "code": 59848 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 46 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667 0 25.024-2.159 49.545-6.298 73.391-25.122-13.602-52.774-23.121-82.069-27.674 2.001-14.955 3.034-30.217 3.034-45.717 0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333 15.501 0 30.763-1.033 45.717-3.034 4.548 29.278 14.059 56.913 27.648 82.022-23.97 4.169-48.627 6.345-73.792 6.345-235.52 0-426.24-191.147-426.24-426.667z", + "M779.025 1012.54c-56.401 0-107.089-34.428-128.021-86.848l-52.42-131.657c-5.363-13.666 7.437-27.332 21.453-22.835l13.666 4.497c9.685 3.115 17.647 10.556 21.453 20.070l24.393 61.069h12.975v-203.277c0-11.938 9.69-21.628 21.623-21.628 11.938 0 21.628 9.69 21.628 21.628v151.377h17.301v-185.98c0-11.934 9.685-21.623 21.623-21.623s21.623 9.69 21.623 21.623v185.98h17.301v-160.026c0-11.938 9.69-21.628 21.628-21.628 11.934 0 21.623 9.69 21.623 21.628v160.026h17.301v-108.126c0-11.938 9.685-21.628 21.623-21.628s21.628 9.69 21.628 21.628v177.327c0 76.467-61.935 138.402-138.402 138.402z" + ], + "attrs": [], + "tags": [ + "clock-bid" + ], + "defaultCode": 59850, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 618, + "id": 49, + "name": "clock-bid", + "prevSize": 32, + "code": 59850 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 47 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667 0 25.024-2.159 49.545-6.298 73.391-25.122-13.602-52.774-23.121-82.069-27.674 2.001-14.955 3.034-30.217 3.034-45.717 0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333 15.501 0 30.763-1.033 45.717-3.034 4.548 29.278 14.059 56.913 27.648 82.022-23.97 4.169-48.627 6.345-73.792 6.345-235.52 0-426.24-191.147-426.24-426.667z", + "M640 910.234v71.1h71.1l209.702-209.702-71.1-71.1-209.702 209.702zM975.787 716.646c7.394-7.394 7.394-19.341 0-26.735l-44.365-44.365c-7.394-7.394-19.341-7.394-26.735 0l-34.697 34.697 71.1 71.1 34.697-34.697z" + ], + "attrs": [], + "tags": [ + "clock-edit" + ], + "defaultCode": 59852, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 619, + "id": 50, + "name": "clock-edit", + "prevSize": 32, + "code": 59852 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 48 + }, + { + "icon": { + "paths": [ + "M487.505 24.518c-268.115 0-487.482 219.367-487.482 487.482s219.367 487.482 487.482 487.482 487.482-219.367 487.482-487.482-219.367-487.482-487.482-487.482zM660.561 697.243l-199.056-122.277c-14.625-8.938-23.156-24.374-23.156-41.436v-228.709c0.407-20.311 17.061-36.561 36.968-36.561 20.311 0 36.561 16.25 36.561 36.561v216.929l186.868 112.528c17.875 10.969 23.562 33.716 12.999 51.185-6.736 10.495-18.346 17.351-31.557 17.351-7.266 0-14.047-2.074-19.785-5.661l0.157 0.091z" + ], + "width": 975, + "attrs": [], + "tags": [ + "clock filled" + ], + "defaultCode": 59764, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 620, + "id": 51, + "name": "clock-filled", + "prevSize": 32, + "code": 59787 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 49 + }, + { + "icon": { + "paths": [ + "M469.333 173.653v-86.187c-85.758 8.533-163.838 42.667-226.985 94.293l60.587 61.013c47.36-36.693 104.107-61.44 166.398-69.12zM242.775 302.933l-61.013-60.587c-51.627 63.147-85.76 141.227-94.293 226.987h86.187c7.68-62.293 32.427-119.040 69.12-166.4zM173.655 554.667h-86.187c8.533 85.76 42.667 163.84 94.293 226.987l61.013-61.013c-36.693-46.933-61.44-103.68-69.12-165.973zM242.349 842.24c63.147 51.627 141.653 85.76 226.985 94.293v-86.187c-62.291-7.68-119.038-32.427-166.398-69.12l-60.587 61.013zM938.667 512c0 220.16-167.253 401.92-381.867 424.533v-86.187c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.187c214.613 22.613 381.867 204.373 381.867 424.533z", + "M533.333 298.667h-64v256l224 134.4 32-52.48-192-113.92v-224z" + ], + "attrs": [], + "tags": [ + "clock-flexible" + ], + "defaultCode": 59853, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 621, + "id": 52, + "name": "clock-flexible", + "prevSize": 32, + "code": 59853 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 50 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M511.573 85.333c-235.52 0-426.24 191.147-426.24 426.667s190.72 426.667 426.24 426.667c25.165 0 49.822-2.176 73.792-6.345-13.589-25.109-23.1-52.745-27.648-82.022-14.955 2.001-30.217 3.034-45.717 3.034-188.587 0-341.333-152.747-341.333-341.333s152.747-341.333 341.333-341.333c188.587 0 341.333 152.747 341.333 341.333 0 15.501-1.033 30.763-3.034 45.717 29.295 4.553 56.947 14.071 82.069 27.674 4.139-23.846 6.298-48.367 6.298-73.391 0-235.52-191.147-426.667-427.093-426.667z", + "M810.667 1024c117.82 0 213.333-95.514 213.333-213.333s-95.514-213.333-213.333-213.333c-117.82 0-213.333 95.514-213.333 213.333s95.514 213.333 213.333 213.333zM682.667 853.333c23.565 0 42.667-19.102 42.667-42.667s-19.102-42.667-42.667-42.667c-23.565 0-42.667 19.102-42.667 42.667s19.102 42.667 42.667 42.667zM853.333 810.667c0 23.565-19.102 42.667-42.667 42.667s-42.667-19.102-42.667-42.667c0-23.565 19.102-42.667 42.667-42.667s42.667 19.102 42.667 42.667zM938.667 853.333c23.565 0 42.667-19.102 42.667-42.667s-19.102-42.667-42.667-42.667c-23.565 0-42.667 19.102-42.667 42.667s19.102 42.667 42.667 42.667z" + ], + "attrs": [], + "tags": [ + "clock-in-progress" + ], + "defaultCode": 59854, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 622, + "id": 53, + "name": "clock-in-progress", + "prevSize": 32, + "code": 59854 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 51 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667 0 25.024-2.159 49.545-6.298 73.391-25.122-13.602-52.774-23.121-82.069-27.674 2.001-14.955 3.034-30.217 3.034-45.717 0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333 15.501 0 30.763-1.033 45.717-3.034 4.548 29.278 14.059 56.913 27.648 82.022-23.97 4.169-48.627 6.345-73.792 6.345-235.52 0-426.24-191.147-426.24-426.667z", + "M810.667 597.333c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333zM832 917.333h-42.667v-128h42.667v128zM832 746.667h-42.667v-42.667h42.667v42.667z" + ], + "attrs": [], + "tags": [ + "clock-info" + ], + "defaultCode": 59855, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 623, + "id": 54, + "name": "clock-info", + "prevSize": 32, + "code": 59855 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 52 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667 0 25.024-2.159 49.545-6.298 73.391-25.122-13.602-52.774-23.121-82.069-27.674 2.001-14.955 3.034-30.217 3.034-45.717 0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333 15.501 0 30.763-1.033 45.717-3.034 4.548 29.278 14.059 56.913 27.648 82.022-23.97 4.169-48.627 6.345-73.792 6.345-235.52 0-426.24-191.147-426.24-426.667z", + "M810.667 597.333c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333zM832 960h-42.667v-42.667h42.667v42.667zM876.16 794.667l-19.2 19.627c-15.36 15.573-24.96 28.373-24.96 60.373h-42.667v-10.667c0-23.467 9.6-44.8 24.96-60.373l26.453-26.88c7.893-7.68 12.587-18.347 12.587-30.080 0-23.467-19.2-42.667-42.667-42.667s-42.667 19.2-42.667 42.667h-42.667c0-47.147 38.187-85.333 85.333-85.333s85.333 38.187 85.333 85.333c0 18.773-7.68 35.84-19.84 48z" + ], + "attrs": [], + "tags": [ + "clock-missed" + ], + "defaultCode": 59856, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 624, + "id": 55, + "name": "clock-missed", + "prevSize": 32, + "code": 59856 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 53 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667 0 25.024-2.159 49.545-6.298 73.391-25.122-13.602-52.774-23.121-82.069-27.674 2.001-14.955 3.034-30.217 3.034-45.717 0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333 15.501 0 30.763-1.033 45.717-3.034 4.548 29.278 14.059 56.913 27.648 82.022-23.97 4.169-48.627 6.345-73.792 6.345-235.52 0-426.24-191.147-426.24-426.667z", + "M810.667 597.333c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333z" + ], + "attrs": [], + "tags": [ + "clock-on" + ], + "defaultCode": 59857, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 625, + "id": 56, + "name": "clock-on", + "prevSize": 32, + "code": 59857 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 54 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M768 490.667l128 149.333 128-149.333h-256z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667h-85.333c0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333v85.333c-0.141 0-0.286 0-0.427 0-235.52 0-426.24-191.147-426.24-426.667z" + ], + "attrs": [], + "tags": [ + "clock-rollover" + ], + "defaultCode": 59858, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 626, + "id": 57, + "name": "clock-rollover", + "prevSize": 32, + "code": 59858 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 55 + }, + { + "icon": { + "paths": [ + "M692.907 331.093c-49.92-49.92-115.2-75.093-180.907-75.093v256l-180.907 180.907c99.84 99.84 261.973 99.84 362.24 0 99.84-99.84 99.84-261.973-0.427-361.813zM512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM512 853.333c-188.587 0-341.333-152.747-341.333-341.333s152.747-341.333 341.333-341.333c188.587 0 341.333 152.747 341.333 341.333s-152.747 341.333-341.333 341.333z" + ], + "attrs": [], + "tags": [ + "clock-segment" + ], + "defaultCode": 59859, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 627, + "id": 58, + "name": "clock-segment", + "prevSize": 32, + "code": 59859 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 56 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M810.667 1024c117.82 0 213.333-95.514 213.333-213.333s-95.514-213.333-213.333-213.333c-117.82 0-213.333 95.514-213.333 213.333s95.514 213.333 213.333 213.333zM896 810.667l-128 85.333v-170.667l128 85.333z", + "M511.573 85.333c-235.52 0-426.24 191.147-426.24 426.667s190.72 426.667 426.24 426.667c25.165 0 49.822-2.176 73.792-6.345-13.589-25.109-23.1-52.745-27.648-82.022-14.955 2.001-30.217 3.034-45.717 3.034-188.587 0-341.333-152.747-341.333-341.333s152.747-341.333 341.333-341.333c188.587 0 341.333 152.747 341.333 341.333 0 15.501-1.033 30.763-3.034 45.717 29.295 4.553 56.947 14.071 82.069 27.674 4.139-23.846 6.298-48.367 6.298-73.391 0-235.52-191.147-426.667-427.093-426.667z" + ], + "attrs": [], + "tags": [ + "clock-start" + ], + "defaultCode": 59860, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 628, + "id": 59, + "name": "clock-start", + "prevSize": 32, + "code": 59860 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 57 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M810.667 1024c-117.82 0-213.333-95.514-213.333-213.333s95.514-213.333 213.333-213.333c117.82 0 213.333 95.514 213.333 213.333s-95.514 213.333-213.333 213.333zM896 725.333h-170.667v170.667h170.667v-170.667z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667 0 25.024-2.159 49.545-6.298 73.391-25.122-13.602-52.774-23.121-82.069-27.674 2.001-14.955 3.034-30.217 3.034-45.717 0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333 15.501 0 30.763-1.033 45.717-3.034 4.548 29.278 14.059 56.913 27.648 82.022-23.97 4.169-48.627 6.345-73.792 6.345-235.52 0-426.24-191.147-426.24-426.667z" + ], + "attrs": [], + "tags": [ + "clock-stop" + ], + "defaultCode": 59861, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 629, + "id": 60, + "name": "clock-stop", + "prevSize": 32, + "code": 59861 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 58 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h64v224l131.149 77.815c-17.566 12.241-33.523 26.624-47.497 42.778l-147.652-88.593v-256z", + "M746.24 597.333v74.667h235.093v64h-235.093v74.667l-106.24-106.667 106.24-106.667z", + "M640 853.333h235.093v-74.667l106.24 106.667-106.24 106.667v-74.667h-235.093v-64z", + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667 0 25.024-2.159 49.545-6.298 73.391-25.122-13.602-52.774-23.121-82.069-27.674 2.001-14.955 3.034-30.217 3.034-45.717 0-188.587-152.747-341.333-341.333-341.333s-341.333 152.747-341.333 341.333c0 188.587 152.747 341.333 341.333 341.333 15.501 0 30.763-1.033 45.717-3.034 4.548 29.278 14.059 56.913 27.648 82.022-23.97 4.169-48.627 6.345-73.792 6.345-235.52 0-426.24-191.147-426.24-426.667z" + ], + "attrs": [], + "tags": [ + "clock-switch" + ], + "defaultCode": 59862, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 630, + "id": 61, + "name": "clock-switch", + "prevSize": 32, + "code": 59862 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 59 + }, + { + "icon": { + "paths": [ + "M640 42.667h-256v85.333h256v-85.333zM469.333 597.333h85.333v-256h-85.333v256zM811.947 315.307l60.587-60.587c-18.347-21.76-38.4-42.24-60.16-60.16l-60.587 60.587c-66.133-52.907-149.333-84.48-239.787-84.48-212.053 0-384 171.947-384 384s171.52 384 384 384c212.48 0 384-171.947 384-384 0-90.453-31.573-173.653-84.053-239.36zM512 853.333c-165.12 0-298.667-133.547-298.667-298.667s133.547-298.667 298.667-298.667c165.12 0 298.667 133.547 298.667 298.667s-133.547 298.667-298.667 298.667z" + ], + "attrs": [], + "tags": [ + "clock-timer" + ], + "defaultCode": 59863, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 631, + "id": 62, + "name": "clock-timer", + "prevSize": 32, + "code": 59863 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 60 + }, + { + "icon": { + "paths": [ + "M896 204.227l-77.35-76.227-306.65 302.208-306.651-302.208-77.349 76.227 306.65 302.209-306.65 302.204 77.349 76.228 306.651-302.208 306.65 302.208 77.35-76.228-306.65-302.204 306.65-302.209z" + ], + "attrs": [], + "tags": [ + "close" + ], + "defaultCode": 59664, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 632, + "id": 63, + "name": "close", + "prevSize": 32, + "code": 59665 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 61 + }, + { + "icon": { + "paths": [ + "M554.667 938.667c235.639 0 426.667-191.027 426.667-426.667 0-235.642-191.027-426.667-426.667-426.667-235.642 0-426.667 191.025-426.667 426.667 0 235.639 191.025 426.667 426.667 426.667zM725.030 298.667l42.97 42.971-170.364 170.362 170.364 170.364-42.97 42.97-170.364-170.364-170.362 170.364-42.971-42.97 170.364-170.364-170.364-170.362 42.971-42.971 170.362 170.364 170.364-170.364z" + ], + "attrs": [], + "tags": [ + "close circle" + ], + "defaultCode": 59716, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 633, + "id": 64, + "name": "close-circle", + "prevSize": 32, + "code": 59716 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 62 + }, + { + "icon": { + "paths": [ + "M825.6 428.373c-29.013-147.2-158.293-257.707-313.6-257.707-123.307 0-230.4 69.973-283.733 172.373-128.427 13.653-228.267 122.453-228.267 254.293 0 141.227 114.773 256 256 256h554.667c117.76 0 213.333-95.573 213.333-213.333 0-112.64-87.467-203.947-198.4-211.627zM725.333 554.667l-213.333 213.333-213.333-213.333h128v-170.667h170.667v170.667h128z" + ], + "attrs": [], + "tags": [ + "cloud-download" + ], + "defaultCode": 59865, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 634, + "id": 65, + "name": "cloud-download", + "prevSize": 32, + "code": 59865 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 63 + }, + { + "icon": { + "paths": [ + "M1024 640c0-112.64-87.467-203.947-198.4-211.627-29.013-147.2-158.293-257.707-313.6-257.707-56.747 0-109.654 15.36-155.734 41.387l63.573 63.573c28.588-12.373 59.308-19.627 92.161-19.627 129.707 0 234.667 104.96 234.667 234.667v21.333h64c70.827 0 128 57.173 128 128 0 42.24-20.48 78.933-51.627 102.4l60.16 60.16c46.507-39.253 76.8-96.853 76.8-162.56zM158.292 194.56c-16.64 16.64-16.64 43.52 0 60.16l87.893 87.893h-17.92c-139.947 14.933-245.76 142.507-225.707 289.707 17.067 128.853 133.547 221.013 262.827 221.013h491.094l55.040 55.040c16.64 16.64 43.52 16.64 60.16 0s16.64-43.52 0-60.16l-653.228-653.653c-16.64-16.64-43.52-16.64-60.16 0zM255.999 768c-94.293 0-170.667-76.373-170.667-170.667s76.373-170.667 170.667-170.667h73.813l341.334 341.333h-415.148z" + ], + "attrs": [], + "tags": [ + "cloud-off" + ], + "defaultCode": 59748, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 635, + "id": 66, + "name": "cloud-off", + "prevSize": 32, + "code": 59748 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 64 + }, + { + "icon": { + "paths": [ + "M825.6 428.373c-29.013-147.2-158.293-257.707-313.6-257.707-123.307 0-230.4 69.973-283.733 172.373-128.427 13.653-228.267 122.453-228.267 254.293 0 141.227 114.773 256 256 256h554.667c117.76 0 213.333-95.573 213.333-213.333 0-112.64-87.467-203.947-198.4-211.627zM597.333 554.667v170.667h-170.667v-170.667h-128l213.333-213.333 213.333 213.333h-128z" + ], + "attrs": [], + "tags": [ + "cloud-upload" + ], + "defaultCode": 59866, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 636, + "id": 67, + "name": "cloud-upload", + "prevSize": 32, + "code": 59866 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 65 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.699 0-426.667 190.968-426.667 426.667s190.968 426.667 426.667 426.667c235.699 0 426.667-190.967 426.667-426.667s-190.967-426.667-426.667-426.667zM512 856.085c-189.763 0-344.086-154.321-344.086-344.085s154.323-344.086 344.086-344.086c189.764 0 344.085 154.323 344.085 344.086s-154.321 344.085-344.085 344.085zM512 608.346c-57.805 0-112.172 25.459-149.333 69.85-14.624 17.549-12.215 43.524 5.333 58.15 17.548 14.622 43.527 12.386 58.15-5.163 42.667-51.098 129.033-51.098 171.699 0 13.935 16.687 39.915 20.471 58.15 5.163 17.549-14.626 19.785-40.602 5.333-58.15-37.163-44.39-91.529-69.85-149.333-69.85zM429.419 484.471c17.719 0 34.236-11.524 39.569-29.419 6.541-21.85-5.85-44.902-27.699-51.44l-137.632-41.29c-22.022-6.71-44.903 5.849-51.441 27.699s5.85 44.901 27.699 51.442l48.516 14.622c-5.333 8.431-9.118 17.894-9.118 28.561 0 30.451 24.602 55.053 55.054 55.053s55.053-24.777 55.053-55.228zM771.785 390.022c-6.537-21.85-29.419-34.237-51.439-27.699l-137.634 41.29c-21.85 6.537-34.24 29.59-27.699 51.44 5.333 17.894 21.85 29.419 39.569 29.419 0 30.451 24.602 55.057 55.053 55.057s55.053-24.606 55.053-55.057c0-10.667-3.785-20.126-9.118-28.557l48.516-14.626c21.85-6.366 34.236-29.417 27.699-51.267z" + ], + "attrs": [], + "tags": [ + "complaint" + ], + "defaultCode": 59665, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 637, + "id": 68, + "name": "complaint", + "prevSize": 32, + "code": 59666 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 66 + }, + { + "icon": { + "paths": [ + "M682.667 42.667h-512c-46.933 0-85.333 38.4-85.333 85.333v597.333h85.333v-597.333h512v-85.333zM810.667 213.333h-469.333c-46.933 0-85.333 38.4-85.333 85.333v597.333c0 46.933 38.4 85.333 85.333 85.333h469.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 896h-469.333v-597.333h469.333v597.333z" + ], + "attrs": [], + "tags": [ + "copy" + ], + "defaultCode": 59750, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 638, + "id": 69, + "name": "copy", + "prevSize": 32, + "code": 59750 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 67 + }, + { + "icon": { + "paths": [ + "M128 554.667h341.333v-426.667h-341.333v426.667zM128 896h341.333v-256h-341.333v256zM554.667 896h341.333v-426.667h-341.333v426.667zM554.667 128v256h341.333v-256h-341.333z" + ], + "attrs": [], + "tags": [ + "dashboard" + ], + "defaultCode": 59751, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 639, + "id": 70, + "name": "dashboard", + "prevSize": 32, + "code": 59751 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 68 + }, + { + "icon": { + "paths": [ + "M256 810.667c0 46.933 38.4 85.333 85.333 85.333h341.333c46.933 0 85.333-38.4 85.333-85.333v-512h-512v512zM810.667 170.667h-149.333l-42.667-42.667h-213.333l-42.667 42.667h-149.333v85.333h597.333v-85.333z" + ], + "attrs": [], + "tags": [ + "delete" + ], + "defaultCode": 59667, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 641, + "id": 72, + "name": "delete", + "prevSize": 32, + "code": 59667 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 69 + }, + { + "icon": { + "paths": [ + "M128 554.664h85.333v-85.334h-85.333v85.334zM128 725.331h85.333v-85.334h-85.333v85.334zM128 383.997h85.333v-85.333h-85.333v85.333zM298.667 554.664h597.333v-85.334h-597.333v85.334zM298.667 725.331h597.333v-85.334h-597.333v85.334zM298.667 298.664v85.333h597.333v-85.333h-597.333z" + ], + "attrs": [], + "tags": [ + "details" + ], + "defaultCode": 59668, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 642, + "id": 73, + "name": "details", + "prevSize": 32, + "code": 59668 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 70 + }, + { + "icon": { + "paths": [ + "M512 810.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM256 42.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM256 298.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM256 554.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM768 213.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM512 554.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM768 554.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM768 298.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM512 298.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM512 42.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333z" + ], + "attrs": [], + "tags": [ + "dialpad" + ], + "defaultCode": 59867, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 643, + "id": 74, + "name": "dialpad", + "prevSize": 32, + "code": 59867 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 71 + }, + { + "icon": { + "paths": [ + "M810.667 384h-170.667v-256h-256v256h-170.667l298.667 298.667 298.667-298.667zM213.333 768v85.333h597.333v-85.333h-597.333z" + ], + "attrs": [], + "tags": [ + "download" + ], + "defaultCode": 59868, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 644, + "id": 75, + "name": "download", + "prevSize": 32, + "code": 59868 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 72 + }, + { + "icon": { + "paths": [ + "M128 736.004v160h160l471.893-471.895-160-160-471.893 471.895zM883.627 300.376c16.64-16.64 16.64-43.52 0-60.16l-99.84-99.84c-16.64-16.64-43.52-16.64-60.16 0l-78.080 78.080 160 160 78.080-78.080z" + ], + "attrs": [], + "tags": [ + "edit" + ], + "defaultCode": 59670, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 645, + "id": 76, + "name": "edit", + "prevSize": 32, + "code": 59670 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 73 + }, + { + "icon": { + "paths": [ + "M750.080 266.667l75.52 75.52-212.48 212.48h-75.52v-75.52l212.48-212.48zM889.6 247.893l-45.227-45.227c-8.533-8.533-21.76-8.533-30.293 0l-36.267 36.267 75.52 75.52 36.267-36.267c8.533-8.533 8.533-22.187 0-30.293zM768 520.533v204.8h85.333v85.333h-682.667v-85.333h85.333v-298.667c0-119.040 81.493-219.307 192-247.467v-29.867c0-35.413 28.587-64 64-64s64 28.587 64 64v29.867c34.987 8.96 66.987 25.173 94.293 46.507l-218.027 218.027v196.267h196.267l119.467-119.467zM426.667 853.333h170.667c0 46.933-38.4 85.333-85.333 85.333s-85.333-38.4-85.333-85.333z" + ], + "attrs": [], + "tags": [ + "edit-notifications" + ], + "defaultCode": 59725, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 646, + "id": 77, + "name": "edit-notifications", + "prevSize": 32, + "code": 59725 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 74 + }, + { + "icon": { + "paths": [ + "M785.067 452.267c-78.933-68.693-181.333-110.933-294.4-110.933-198.398 0-366.078 129.28-424.958 308.053l100.693 33.28c44.8-136.107 172.8-234.667 324.265-234.667 83.2 0 159.147 30.72 218.453 80.213l-154.453 154.453h384v-384l-153.6 153.6z" + ], + "attrs": [], + "tags": [ + "edit-redo" + ], + "defaultCode": 59752, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 647, + "id": 78, + "name": "edit-redo", + "prevSize": 32, + "code": 59752 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 75 + }, + { + "icon": { + "paths": [ + "M533.333 341.333c-113.067 0-215.467 42.24-294.4 110.933l-153.6-153.6v384h384l-154.453-154.453c59.307-49.493 134.827-80.213 218.453-80.213 151.040 0 279.467 98.56 324.267 234.667l101.12-33.28c-59.307-178.773-226.987-308.053-425.387-308.053z" + ], + "attrs": [], + "tags": [ + "edit-undo" + ], + "defaultCode": 59753, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 648, + "id": 79, + "name": "edit-undo", + "prevSize": 32, + "code": 59753 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 76 + }, + { + "icon": { + "paths": [ + "M853.333 170.667h-682.667c-46.933 0-84.907 38.4-84.907 85.333l-0.427 512c0 46.933 38.4 85.333 85.333 85.333h682.667c46.933 0 85.333-38.4 85.333-85.333v-512c0-46.933-38.4-85.333-85.333-85.333zM853.333 341.333l-341.333 213.333-341.333-213.333v-85.333l341.333 213.333 341.333-213.333v85.333z" + ], + "attrs": [], + "tags": [ + "email" + ], + "defaultCode": 59872, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 649, + "id": 80, + "name": "email", + "prevSize": 32, + "code": 59872 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 77 + }, + { + "icon": { + "paths": [ + "M597.333 384v-170.667l298.667 298.667-298.667 298.667v-174.933c-213.333 0-362.667 68.267-469.333 217.6 42.667-213.333 170.667-426.667 469.333-469.333z" + ], + "attrs": [], + "tags": [ + "email-forward" + ], + "defaultCode": 59869, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 650, + "id": 81, + "name": "email-forward", + "prevSize": 32, + "code": 59869 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 78 + }, + { + "icon": { + "paths": [ + "M426.667 384v-170.667l-298.667 298.667 298.667 298.667v-174.933c213.333 0 362.667 68.267 469.333 217.6-42.667-213.333-170.667-426.667-469.333-469.333z" + ], + "attrs": [], + "tags": [ + "email-reply" + ], + "defaultCode": 59871, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 651, + "id": 82, + "name": "email-reply", + "prevSize": 32, + "code": 59871 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 79 + }, + { + "icon": { + "paths": [ + "M298.667 341.333v-128l-298.667 298.667 298.667 298.667v-128l-170.667-170.667 170.667-170.667zM554.667 384v-170.667l-298.667 298.667 298.667 298.667v-174.933c213.333 0 362.667 68.267 469.333 217.6-42.667-213.333-170.667-426.667-469.333-469.333z" + ], + "attrs": [], + "tags": [ + "email-reply-all" + ], + "defaultCode": 59870, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 652, + "id": 83, + "name": "email-reply-all", + "prevSize": 32, + "code": 59870 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 80 + }, + { + "icon": { + "paths": [ + "M119.893 119.893l-60.587 60.16 96.853 96.853c-44.372 65.848-70.827 146.966-70.827 234.263 0 0.292 0 0.584 0.001 0.875l-0-0.045c0 235.52 191.147 426.667 426.667 426.667 87.040 0 167.68-26.027 235.093-70.827l96.853 96.853 60.16-60.587-784.213-784.213zM512 853.333c-188.16 0-341.333-153.173-341.333-341.333 0-63.147 17.493-122.027 47.787-173.227l466.347 466.773c-49.283 30.017-108.886 47.787-172.64 47.787-0.056 0-0.112-0-0.169-0l0.009 0zM512 170.667c188.16 0 341.333 153.173 341.333 341.333 0 63.147-17.493 122.027-47.787 172.8l61.867 61.867c44.626-65.427 71.256-146.225 71.256-233.242 0-0.501-0.001-1.002-0.003-1.502l0 0.078c0-235.52-191.147-426.667-426.667-426.667-87.040 0-167.68 26.027-235.093 70.827l61.867 61.867c51.2-29.867 110.080-47.36 173.227-47.36z" + ], + "attrs": [], + "tags": [ + "empty" + ], + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 653, + "id": 84, + "name": "empty", + "prevSize": 32, + "code": 59802 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 81 + }, + { + "icon": { + "paths": [ + "M634.88 430.080c0 32.768-12.288 61.44-36.864 86.016s-53.248 36.864-86.016 36.864c-32.768 0-61.44-12.288-86.016-36.864s-36.864-53.248-36.864-86.016c0-32.768 12.288-61.44 36.864-86.016s53.248-32.768 86.016-32.768c32.768 0 61.44 12.288 86.016 32.768s36.864 49.152 36.864 86.016z", + "M167.936 61.44v61.44h688.128v-61.44h-688.128zM167.936 901.12v61.44h688.128v-61.44h-688.128zM913.408 204.8c-12.288-12.288-28.672-20.48-40.96-20.48h-716.8c-16.384 0-32.768 4.096-40.96 16.384-12.288 12.288-20.48 28.672-20.48 45.056v532.48c0 16.384 8.192 32.768 20.48 40.96s28.672 20.48 40.96 20.48h712.704c16.384 0 32.768-8.192 40.96-20.48s20.48-24.576 20.48-40.96v-532.48c0-16.384-8.192-32.768-16.384-40.96zM868.352 778.24h-90.112c-32.768-40.96-73.728-73.728-122.88-98.304-49.152-20.48-98.304-32.768-143.36-32.768s-94.208 12.288-143.36 32.768c-49.152 20.48-90.112 53.248-122.88 98.304h-90.112v-532.48h712.704v532.48z" + ], + "attrs": [], + "tags": [ + "ess" + ], + "defaultCode": 59727, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 654, + "id": 85, + "name": "ess", + "prevSize": 32, + "code": 59727 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 82 + }, + { + "icon": { + "paths": [ + "M877.714 18.286c-12.108 0.010-23.066 4.922-31 12.857l-815.571 815.571c-7.923 7.939-12.822 18.897-12.822 31s4.899 23.061 12.823 31.001l-0.001-0.001c7.939 7.923 18.897 12.822 31 12.822s23.061-4.899 31.001-12.823l-0.001 0.001 815.571-815.571c7.923-7.939 12.822-18.897 12.822-31s-4.899-23.061-12.823-31.001l0.001 0.001c-7.934-7.936-18.892-12.847-30.998-12.857l-0.002-0zM936.286 321.857c-0.032-0-0.070-0-0.108-0-12.12 0-23.093 4.913-31.035 12.857l-570.429 570.429c-7.965 7.946-12.893 18.933-12.893 31.071s4.928 23.126 12.893 31.071l0.001 0.001c7.939 7.923 18.897 12.822 31 12.822s23.061-4.899 31.001-12.823l-0.001 0.001 570.571-570.571c7.923-7.939 12.822-18.897 12.822-31s-4.899-23.061-12.823-31.001l0.001 0.001c-7.934-7.936-18.892-12.847-30.998-12.857l-0.002-0z" + ], + "attrs": [], + "tags": [ + "expand" + ], + "defaultCode": 59666, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 655, + "id": 86, + "name": "expand", + "prevSize": 32, + "code": 59669 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 83 + }, + { + "icon": { + "paths": [ + "M810.667 128h-597.333c-47.36 0-85.333 38.4-85.333 85.333v597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-37.973-85.333-85.333-85.333zM810.667 810.667h-597.333v-512h597.333v512zM576 554.667c0 35.413-28.587 64-64 64s-64-28.587-64-64c0-35.413 28.587-64 64-64s64 28.587 64 64zM512 384c-116.48 0-215.893 70.827-256 170.667 40.107 99.84 139.52 170.667 256 170.667s215.893-70.827 256-170.667c-40.107-99.84-139.52-170.667-256-170.667zM512 661.333c-58.88 0-106.667-47.787-106.667-106.667s47.787-106.667 106.667-106.667c58.88 0 106.667 47.787 106.667 106.667s-47.787 106.667-106.667 106.667z" + ], + "attrs": [], + "tags": [ + "eye-preview" + ], + "defaultCode": 59754, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 656, + "id": 87, + "name": "eye-preview", + "prevSize": 32, + "code": 59754 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 84 + }, + { + "icon": { + "paths": [ + "M512 192c-213.333 0-395.52 132.693-469.333 320 73.813 187.307 256 320 469.333 320s395.52-132.693 469.333-320c-73.813-187.307-256-320-469.333-320zM512 725.333c-117.76 0-213.333-95.573-213.333-213.333s95.573-213.333 213.333-213.333c117.76 0 213.333 95.573 213.333 213.333s-95.573 213.333-213.333 213.333zM512 384c-70.827 0-128 57.173-128 128s57.173 128 128 128c70.827 0 128-57.173 128-128s-57.173-128-128-128z" + ], + "attrs": [], + "tags": [ + "eye-view" + ], + "defaultCode": 59755, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 657, + "id": 88, + "name": "eye-view", + "prevSize": 32, + "code": 59755 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 85 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-o" + ], + "defaultCode": 61462, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file", + "id": 94, + "order": 663, + "prevSize": 32, + "code": 61462 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 86 + }, + { + "icon": { + "paths": [ + "M365.714 219.429v-73.143h-73.143v73.143h73.143zM438.857 292.571v-73.143h-73.143v73.143h73.143zM365.714 365.714v-73.143h-73.143v73.143h73.143zM438.857 438.857v-73.143h-73.143v73.143h73.143zM838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-73.143v73.143h-73.143v-73.143h-292.571v877.714h731.429zM446.286 538.857c48.571 164 61.143 199.429 61.143 199.429 2.857 9.714 4.571 19.429 4.571 29.714 0 63.429-61.714 109.714-146.286 109.714s-146.286-46.286-146.286-109.714c0-10.286 1.714-20 4.571-29.714 0 0 12-35.429 68.571-226.286v-73.143h73.143v73.143h45.143c16.571 0 30.857 10.857 35.429 26.857zM365.714 804.571c40.571 0 73.143-16.571 73.143-36.571s-32.571-36.571-73.143-36.571-73.143 16.571-73.143 36.571 32.571 36.571 73.143 36.571z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-archive-o", + "file-zip-o" + ], + "defaultCode": 61894, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-archive", + "id": 89, + "order": 658, + "prevSize": 32, + "code": 61894 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 87 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM354.286 485.714c6.857 2.857 11.429 9.714 11.429 17.143v310.857c0 7.429-4.571 14.286-11.429 17.143-2.286 0.571-4.571 1.143-6.857 1.143-4.571 0-9.143-1.714-13.143-5.143l-94.857-95.429h-74.857c-10.286 0-18.286-8-18.286-18.286v-109.714c0-10.286 8-18.286 18.286-18.286h74.857l94.857-95.429c5.714-5.143 13.143-6.857 20-4zM592.571 879.429c10.857 0 21.143-4.571 28.571-13.714 47.429-58.286 73.714-132 73.714-207.429s-26.286-149.143-73.714-207.429c-12.571-16-36-18.286-51.429-5.714-16 13.143-18.286 36-5.143 52 37.143 45.714 57.143 101.714 57.143 161.143s-20 115.429-57.143 161.143c-13.143 16-10.857 38.857 5.143 51.429 6.857 5.714 14.857 8.571 22.857 8.571zM472 794.857c9.714 0 19.429-4 26.857-11.429 32-34.286 49.714-78.286 49.714-125.143s-17.714-90.857-49.714-125.143c-13.714-14.857-37.143-15.429-52-1.714-14.286 13.714-15.429 37.143-1.143 52 18.857 20.571 29.714 46.857 29.714 74.857s-10.857 54.286-29.714 74.857c-14.286 14.857-13.143 38.286 1.143 52 7.429 6.286 16.571 9.714 25.143 9.714z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-audio-o", + "file-sound-o" + ], + "defaultCode": 61895, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-audio", + "id": 90, + "order": 659, + "prevSize": 32, + "code": 61895 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 88 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM274.286 438.857c6.286-8 17.714-9.714 25.714-3.429l29.143 21.714c8 6.286 9.714 17.714 3.429 25.714l-104 138.857 104 138.857c6.286 8 4.571 19.429-3.429 25.714l-29.143 21.714c-8 6.286-19.429 4.571-25.714-3.429l-129.143-172c-4.571-6.286-4.571-15.429 0-21.714zM732.571 610.857c4.571 6.286 4.571 15.429 0 21.714l-129.143 172c-6.286 8-17.714 9.714-25.714 3.429l-29.143-21.714c-8-6.286-9.714-17.714-3.429-25.714l104-138.857-104-138.857c-6.286-8-4.571-19.429 3.429-25.714l29.143-21.714c8-6.286 19.429-4.571 25.714 3.429zM378.286 874.286c-10.286-1.714-16.571-11.429-14.857-21.143l78.857-474.857c1.714-10.286 11.429-16.571 21.143-14.857l36 5.714c10.286 1.714 16.571 11.429 14.857 21.143l-78.857 474.857c-1.714 10.286-11.429 16.571-21.143 14.857z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-code-o" + ], + "defaultCode": 61897, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-code", + "id": 91, + "order": 660, + "prevSize": 32, + "code": 61897 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 89 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM245.143 817.143v60.571h160.571v-60.571h-42.857l58.857-92c6.857-10.857 10.286-19.429 12-19.429h1.143c0.571 2.286 1.714 4 2.857 5.714 2.286 4.571 5.714 8 9.714 13.714l61.143 92h-43.429v60.571h166.286v-60.571h-38.857l-109.714-156 111.429-161.143h38.286v-61.143h-159.429v61.143h42.286l-58.857 90.857c-6.857 10.857-12 19.429-12 18.857h-1.143c-0.571-2.286-1.714-4-2.857-5.714-2.286-4-5.143-8-9.714-13.143l-60.571-90.857h43.429v-61.143h-165.714v61.143h38.857l108 155.429-110.857 161.714h-38.857z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-excel-o" + ], + "defaultCode": 61891, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-excel", + "id": 92, + "order": 661, + "prevSize": 32, + "code": 61891 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 90 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM731.429 694.857v182.857h-585.143v-109.714l109.714-109.714 73.143 73.143 219.429-219.429zM256 585.143c-60.571 0-109.714-49.143-109.714-109.714s49.143-109.714 109.714-109.714 109.714 49.143 109.714 109.714-49.143 109.714-109.714 109.714z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-image-o", + "file-photo-o", + "file-picture-o" + ], + "defaultCode": 61893, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-image", + "id": 93, + "order": 662, + "prevSize": 32, + "code": 61893 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 91 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM510.857 612c14.286 11.429 30.286 21.714 48 32 24-2.857 46.286-4 66.857-4 38.286 0 86.857 4.571 101.143 28 4 5.714 7.429 16 1.143 29.714-0.571 0.571-1.143 1.714-1.714 2.286v0.571c-1.714 10.286-10.286 21.714-40.571 21.714-36.571 0-92-16.571-140-41.714-79.429 8.571-162.857 26.286-224 47.429-58.857 100.571-104 149.714-138.286 149.714-5.714 0-10.857-1.143-16-4l-13.714-6.857c-1.714-0.571-2.286-1.714-3.429-2.857-2.857-2.857-5.143-9.143-3.429-20.571 5.714-26.286 36.571-70.286 107.429-107.429 4.571-2.857 10.286-1.143 13.143 3.429 0.571 0.571 1.143 1.714 1.143 2.286 17.714-29.143 38.286-66.286 61.143-112.571 25.714-51.429 45.714-101.714 59.429-149.714-18.286-62.286-24-126.286-13.714-164 4-14.286 12.571-22.857 24-22.857h12.571c8.571 0 15.429 2.857 20 8.571 6.857 8 8.571 20.571 5.143 38.857-0.571 1.714-1.143 3.429-2.286 4.571 0.571 1.714 0.571 2.857 0.571 4.571v17.143c-0.571 36-1.143 70.286-8 109.714 20 60 49.714 108.571 83.429 136zM181.714 846.857c17.143-8 41.714-32.571 78.286-90.286-42.857 33.143-69.714 70.857-78.286 90.286zM409.143 321.143c-5.714 16-5.714 43.429-1.143 75.429 1.714-9.143 2.857-17.714 4-25.143 1.143-9.714 2.857-17.714 4-24.571 0.571-1.714 1.143-2.857 2.286-4.571-0.571-0.571-0.571-1.714-1.143-2.857-0.571-10.286-4-16.571-7.429-20.571 0 1.143-0.571 1.714-0.571 2.286zM338.286 698.857c50.286-20 106.286-36 162.286-46.286-5.714-4.571-11.429-8.571-16.571-13.143-28-24.571-53.143-58.857-72.571-100.571-10.857 34.857-26.857 72-47.429 112.571-8.571 16-17.143 32-25.714 47.429zM707.429 689.714c-2.857-2.857-17.714-13.714-80-13.714 28 10.286 53.714 16 70.857 16 5.143 0 8 0 10.286-0.571 0-0.571-0.571-1.143-1.143-1.714z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-pdf-o" + ], + "defaultCode": 61889, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-pdf", + "id": 95, + "order": 664, + "prevSize": 32, + "code": 61889 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 92 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM237.714 817.143v60.571h186.857v-60.571h-53.143v-95.429h78.286c24.571 0 46.857-1.143 67.429-8.571 51.429-17.714 83.429-70.857 83.429-133.143s-30.857-110.286-78.286-130.286c-21.714-8.571-48-10.857-74.286-10.857h-210.286v61.143h52.571v317.143h-52.571zM439.429 657.143h-68v-153.143h68.571c20 0 35.429 3.429 47.429 10.286 20.571 12 32 35.429 32 65.714 0 32-11.429 56.571-35.429 68.571-12 5.714-26.857 8.571-44.571 8.571z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-powerpoint-o" + ], + "defaultCode": 61892, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-powerpoint", + "id": 96, + "order": 665, + "prevSize": 32, + "code": 61892 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 93 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM219.429 457.143c0-10.286 8-18.286 18.286-18.286h402.286c10.286 0 18.286 8 18.286 18.286v36.571c0 10.286-8 18.286-18.286 18.286h-402.286c-10.286 0-18.286-8-18.286-18.286v-36.571zM640 585.143c10.286 0 18.286 8 18.286 18.286v36.571c0 10.286-8 18.286-18.286 18.286h-402.286c-10.286 0-18.286-8-18.286-18.286v-36.571c0-10.286 8-18.286 18.286-18.286h402.286zM640 731.429c10.286 0 18.286 8 18.286 18.286v36.571c0 10.286-8 18.286-18.286 18.286h-402.286c-10.286 0-18.286-8-18.286-18.286v-36.571c0-10.286 8-18.286 18.286-18.286h402.286z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-text-o" + ], + "defaultCode": 61686, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-text", + "id": 97, + "order": 666, + "prevSize": 32, + "code": 61686 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 94 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM438.857 438.857c40 0 73.143 33.143 73.143 73.143v219.429c0 40-33.143 73.143-73.143 73.143h-219.429c-40 0-73.143-33.143-73.143-73.143v-219.429c0-40 33.143-73.143 73.143-73.143h219.429zM720 440c6.857 2.857 11.429 9.714 11.429 17.143v329.143c0 7.429-4.571 14.286-11.429 17.143-2.286 0.571-4.571 1.143-6.857 1.143-4.571 0-9.714-1.714-13.143-5.143l-151.429-152v-51.429l151.429-152c3.429-3.429 8.571-5.143 13.143-5.143 2.286 0 4.571 0.571 6.857 1.143z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-movie-o", + "file-video-o" + ], + "defaultCode": 61896, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-video", + "id": 98, + "order": 667, + "prevSize": 32, + "code": 61896 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 95 + }, + { + "icon": { + "paths": [ + "M838.857 217.143c21.143 21.143 38.857 63.429 38.857 93.714v658.286c0 30.286-24.571 54.857-54.857 54.857h-768c-30.286 0-54.857-24.571-54.857-54.857v-914.286c0-30.286 24.571-54.857 54.857-54.857h512c30.286 0 72.571 17.714 93.714 38.857zM585.143 77.714v214.857h214.857c-3.429-9.714-8.571-19.429-12.571-23.429l-178.857-178.857c-4-4-13.714-9.143-23.429-12.571zM804.571 950.857v-585.143h-237.714c-30.286 0-54.857-24.571-54.857-54.857v-237.714h-438.857v877.714h731.429zM133.143 438.857v61.143h40l93.714 377.714h90.857l73.143-277.143c2.857-8.571 4.571-17.143 5.714-26.286 0.571-4.571 1.143-9.143 1.143-13.714h2.286l1.714 13.714c1.714 8 2.286 17.143 5.143 26.286l73.143 277.143h90.857l93.714-377.714h40v-61.143h-171.429v61.143h51.429l-56.571 250.286c-2.286 9.143-3.429 18.857-4 26.286l-1.143 12h-2.286c0-3.429-1.143-8-1.714-12-1.714-7.429-2.857-17.143-5.143-26.286l-82.286-311.429h-65.143l-82.286 311.429c-2.286 9.143-2.857 18.857-4.571 26.286l-2.286 12h-2.286l-1.143-12c-0.571-7.429-1.714-17.143-4-26.286l-56.571-250.286h51.429v-61.143h-171.429z" + ], + "width": 877.7142857142857, + "attrs": [], + "tags": [ + "file-word-o" + ], + "defaultCode": 61890, + "grid": 16 + }, + "attrs": [], + "properties": { + "name": "file-word", + "id": 99, + "order": 668, + "prevSize": 32, + "code": 61890 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 96 + }, + { + "icon": { + "paths": [ + "M426.667 768h170.667v-85.333h-170.667v85.333zM128 256v85.333h768v-85.333h-768zM256 554.667h512v-85.333h-512v85.333z" + ], + "attrs": [], + "tags": [ + "filter" + ], + "defaultCode": 59671, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 669, + "id": 100, + "name": "filter", + "prevSize": 32, + "code": 59671 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 97 + }, + { + "icon": { + "paths": [ + "M614.4 256l-17.067-85.333h-384v725.333h85.333v-298.667h238.933l17.067 85.333h298.667v-426.667h-238.933z" + ], + "attrs": [], + "tags": [ + "flag" + ], + "defaultCode": 59757, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 670, + "id": 101, + "name": "flag", + "prevSize": 32, + "code": 59757 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 98 + }, + { + "icon": { + "paths": [ + "M85.333 725.333l341.333-234.667-341.333-234.667v469.333z", + "M938.667 725.333v-469.333l-341.333 234.667 341.333 234.667z", + "M469.333 256h85.333v512h-85.333v-512z" + ], + "attrs": [], + "tags": [ + "flip" + ], + "defaultCode": 59758, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 671, + "id": 102, + "name": "flip", + "prevSize": 32, + "code": 59758 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 99 + }, + { + "icon": { + "paths": [ + "M426.667 170.667h-256c-46.933 0-84.907 38.4-84.907 85.333l-0.427 512c0 46.933 38.4 85.333 85.333 85.333h682.667c46.933 0 85.333-38.4 85.333-85.333v-426.667c0-46.933-38.4-85.333-85.333-85.333h-341.333l-85.333-85.333z" + ], + "attrs": [], + "tags": [ + "folder" + ], + "defaultCode": 59873, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 672, + "id": 103, + "name": "folder", + "prevSize": 32, + "code": 59873 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 100 + }, + { + "icon": { + "paths": [ + "M791.275 93.091h-558.547c-12.345 0-24.184 4.904-32.913 13.633s-13.633 20.568-13.633 32.913v744.728c0 12.343 4.904 24.183 13.633 32.913s20.568 13.632 32.913 13.632h558.547c12.343 0 24.183-4.902 32.913-13.632 8.725-8.73 13.632-20.57 13.632-32.913v-744.728c0-12.344-4.907-24.183-13.632-32.913-8.73-8.729-20.57-13.633-32.913-13.633zM744.73 837.82h-465.456v-651.638h465.456v651.638zM651.635 279.273h-279.271v93.091h279.271v-93.091zM651.635 651.635h-279.271v93.094h279.271v-93.094zM651.635 465.455h-279.271v93.090h279.271v-93.090z" + ], + "attrs": [], + "tags": [ + "form" + ], + "defaultCode": 59672, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 673, + "id": 104, + "name": "form", + "prevSize": 32, + "code": 59672 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 101 + }, + { + "icon": { + "paths": [ + "M576 661.333h-149.333v-128h149.333c16.973 0 33.254 6.741 45.257 18.743s18.743 28.284 18.743 45.257c0 16.973-6.741 33.254-18.743 45.257s-28.284 18.743-45.257 18.743zM426.667 277.333h128c16.973 0 33.254 6.743 45.257 18.745s18.743 28.281 18.743 45.255c0 16.974-6.741 33.253-18.743 45.255s-28.284 18.745-45.257 18.745h-128v-128zM665.6 460.373c41.387-29.013 70.4-76.373 70.4-119.040 0-96.427-74.667-170.667-170.667-170.667h-266.667v597.333h300.373c89.6 0 158.293-72.533 158.293-161.707 0-64.853-36.693-120.32-91.733-145.92z" + ], + "attrs": [], + "tags": [ + "mdi-format-bold" + ], + "defaultCode": 59683, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 717, + "id": 148, + "name": "format-bold", + "prevSize": 32, + "code": 59683 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 102 + }, + { + "icon": { + "paths": [ + "M426.667 170.667v128h94.293l-145.92 341.333h-119.040v128h341.333v-128h-94.293l145.92-341.333h119.040v-128h-341.333z" + ], + "attrs": [], + "tags": [ + "mdi-format-italic" + ], + "defaultCode": 59684, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 718, + "id": 149, + "name": "format-italic", + "prevSize": 32, + "code": 59684 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 103 + }, + { + "icon": { + "paths": [ + "M170.667 469.333h519.253l-238.507-238.507 60.587-60.16 341.333 341.333-341.333 341.333-60.16-60.16 238.080-238.507h-519.253v-85.333z" + ], + "attrs": [], + "tags": [ + "forward" + ], + "defaultCode": 59760, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 674, + "id": 105, + "name": "forward", + "prevSize": 32, + "code": 59760 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 104 + }, + { + "icon": { + "paths": [ + "M298.667 597.333h-85.333v213.333h213.333v-85.333h-128v-128zM213.333 426.667h85.333v-128h128v-85.333h-213.333v213.333zM725.333 725.333h-128v85.333h213.333v-213.333h-85.333v128zM597.333 213.333v85.333h128v128h85.333v-213.333h-213.333z" + ], + "attrs": [], + "tags": [ + "fullscreen" + ], + "defaultCode": 59762, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 675, + "id": 106, + "name": "fullscreen", + "prevSize": 32, + "code": 59762 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 105 + }, + { + "icon": { + "paths": [ + "M213.333 682.667h128v128h85.333v-213.333h-213.333v85.333zM341.333 341.333h-128v85.333h213.333v-213.333h-85.333v128zM597.333 810.667h85.333v-128h128v-85.333h-213.333v213.333zM682.667 341.333v-128h-85.333v213.333h213.333v-85.333h-128z" + ], + "attrs": [], + "tags": [ + "fullscreen-exit" + ], + "defaultCode": 59761, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 676, + "id": 107, + "name": "fullscreen-exit", + "prevSize": 32, + "code": 59761 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 106 + }, + { + "icon": { + "paths": [ + "M95.718 56.889h764.528c13.643 0 24.607 11.938 24.607 25.338v156.413c0 0.523-0.070 0.976-0.135 1.397-0.057 0.363-0.11 0.701-0.11 1.040 0 0.122-0.061 0.306-0.121 0.488-0.061 0.183-0.122 0.366-0.122 0.488 0 0.243-0.061 0.425-0.121 0.609-0.061 0.183-0.122 0.366-0.122 0.609 0 0.245-0.061 0.427-0.122 0.609s-0.121 0.366-0.121 0.61c0 0.122-0.061 0.304-0.122 0.486-0.061 0.183-0.122 0.366-0.122 0.488-0.122 0.243-0.182 0.427-0.243 0.61-0.061 0.182-0.122 0.366-0.243 0.609 0 0.284-0.084 0.486-0.154 0.653-0.048 0.118-0.091 0.22-0.091 0.321 0 0.357-0.131 0.583-0.296 0.87-0.061 0.104-0.125 0.218-0.191 0.347-0.094 0.188-0.152 0.338-0.201 0.468-0.080 0.206-0.138 0.357-0.287 0.508-0.122 0.122-0.245 0.306-0.367 0.488s-0.243 0.364-0.364 0.486c0 0.243-0.243 0.731-0.486 0.974-0.245 0.243-0.488 0.488-0.731 0.974-0.245 0.488-0.488 0.731-0.731 0.974-0.243 0.485-0.485 0.728-0.727 0.971l-0.004 0.004-0.488 0.486-170.094 185.732c-27.719 0.562-54.303 5.751-79.009 14.831l176.011-191.792h-653.918l247.29 269.461c4.142 4.385 6.579 9.99 6.579 16.080v340.359l97.256-83.154c8.26 14.218 17.917 27.526 28.783 39.731l-134.811 114.809c-4.385 3.898-9.988 5.847-15.835 5.847-3.655 0-7.066-0.731-10.233-2.193-8.528-4.142-13.888-12.669-13.888-22.171v-383.726l-278.475-303.569c0-0.151-0.093-0.209-0.222-0.289-0.079-0.050-0.172-0.107-0.265-0.199-0.244-0.488-0.487-0.731-0.731-0.974-0.122-0.122-0.244-0.306-0.365-0.488s-0.244-0.366-0.366-0.488c-0.122-0.122-0.244-0.306-0.367-0.488s-0.243-0.364-0.364-0.486c-0.122-0.122-0.183-0.304-0.244-0.488-0.061-0.182-0.122-0.366-0.244-0.486 0-0.151-0.093-0.302-0.222-0.511-0.079-0.128-0.172-0.279-0.265-0.464-0.093-0.186-0.15-0.337-0.199-0.465-0.080-0.208-0.137-0.358-0.288-0.509-0.122-0.243-0.183-0.427-0.244-0.609s-0.122-0.366-0.244-0.61c0-0.286-0.084-0.488-0.153-0.654-0.049-0.118-0.091-0.219-0.091-0.32-0.137-0.137-0.197-0.35-0.266-0.597-0.054-0.193-0.114-0.408-0.221-0.622 0-0.122-0.061-0.304-0.122-0.486-0.061-0.183-0.122-0.366-0.122-0.488 0-0.245-0.061-0.427-0.122-0.61-0.061-0.182-0.122-0.364-0.122-0.607s-0.061-0.427-0.122-0.609c-0.061-0.183-0.122-0.367-0.122-0.61 0-0.122-0.061-0.304-0.122-0.488-0.061-0.182-0.122-0.366-0.122-0.486-0.244-0.974-0.244-1.705-0.244-2.436v-156.414c0-13.644 11.207-25.582 24.607-25.582zM119.838 215.252h716.288v-109.636h-716.288v109.636z", + "M776.070 587.071l-63.101 63.101-63.101-63.101-34.352 34.352 63.101 63.101-63.101 63.103 34.352 34.352 63.101-63.103 63.101 63.103 34.354-34.352-63.103-63.103 63.103-63.101-34.354-34.352zM712.969 440.889c-134.73 0-243.635 108.905-243.635 243.635s108.905 243.635 243.635 243.635 243.635-108.904 243.635-243.635c0-134.73-108.905-243.635-243.635-243.635zM712.969 879.433c-107.443 0-194.908-87.465-194.908-194.908s87.465-194.908 194.908-194.908c107.443 0 194.908 87.465 194.908 194.908s-87.465 194.908-194.908 194.908z" + ], + "attrs": [], + "tags": [ + "funnel" + ], + "defaultCode": 59673, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 677, + "id": 108, + "name": "funnel", + "prevSize": 32, + "codes": [ + 59673 + ], + "code": 59673 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 107 + }, + { + "icon": { + "paths": [ + "M511.573 85.333c-235.52 0-426.24 191.147-426.24 426.667s190.72 426.667 426.24 426.667c235.947 0 427.093-191.147 427.093-426.667s-191.147-426.667-427.093-426.667zM807.253 341.333h-125.867c-13.653-53.333-33.28-104.533-58.88-151.893 78.507 26.88 143.787 81.493 184.747 151.893zM512 172.373c35.413 51.2 63.147 107.947 81.493 168.96h-162.987c18.347-61.013 46.080-117.76 81.493-168.96zM181.76 597.333c-6.827-27.307-11.093-55.893-11.093-85.333s4.267-58.027 11.093-85.333h144.213c-3.413 28.16-5.973 56.32-5.973 85.333s2.56 57.173 5.973 85.333h-144.213zM216.747 682.667h125.867c13.653 53.333 33.28 104.533 58.88 151.893-78.507-26.88-143.787-81.067-184.747-151.893zM342.613 341.333h-125.867c40.96-70.827 106.24-125.013 184.747-151.893-25.6 47.36-45.227 98.56-58.88 151.893zM512 851.627c-35.413-51.2-63.147-107.947-81.493-168.96h162.987c-18.347 61.013-46.080 117.76-81.493 168.96zM611.84 597.333h-199.68c-3.84-28.16-6.827-56.32-6.827-85.333s2.987-57.6 6.827-85.333h199.68c3.84 27.733 6.827 56.32 6.827 85.333s-2.987 57.173-6.827 85.333zM622.507 834.56c25.6-47.36 45.227-98.56 58.88-151.893h125.867c-40.96 70.4-106.24 125.013-184.747 151.893zM698.027 597.333c3.413-28.16 5.973-56.32 5.973-85.333s-2.56-57.173-5.973-85.333h144.213c6.827 27.307 11.093 55.893 11.093 85.333s-4.267 58.027-11.093 85.333h-144.213z" + ], + "attrs": [], + "tags": [ + "globe" + ], + "defaultCode": 59874, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 678, + "id": 109, + "name": "globe", + "prevSize": 32, + "code": 59874 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 108 + }, + { + "icon": { + "paths": [ + "M125.995 161.707v702.293c0 34.987 13.995 68.48 38.827 93.227 24.789 24.704 58.453 38.613 93.568 38.656h639.573v-966.016h-639.531c-36.517 0.029-69.581 14.791-93.574 38.662l0.006-0.006c-23.944 23.826-38.777 56.789-38.827 93.217l-0 0.009zM865.664 963.712h-607.275c-0.008 0-0.017 0-0.026 0-27.618 0-52.628-11.166-70.761-29.23l0.003 0.003c-18.076-18.007-29.263-42.918-29.269-70.441l-0-0.001v-586.283c24.365 28.058 60.093 45.696 99.941 45.696 0.039 0 0.079-0 0.118-0l607.269 0zM865.664 291.285h-607.275c-0.020 0-0.045 0-0.069 0-27.597 0-52.589-11.149-70.72-29.188l0.004 0.004c-18.076-18.007-29.263-42.918-29.269-70.441l-0-0.001v-29.952c0-26.453 10.539-51.797 29.269-70.485 18.126-18.035 43.119-29.184 70.715-29.184 0.024 0 0.048 0 0.072 0l607.271-0z", + "M258.389 2.987c-42.24 0.043-82.773 16.811-112.64 46.507-28.811 28.683-46.659 68.358-46.72 112.202l-0 0.012v702.293c0.068 43.862 17.932 83.538 46.756 112.206l0.007 0.007c29.867 29.696 70.4 46.464 112.597 46.507h666.539v-1019.733zM258.389 88.832h580.352v175.701h-580.352c-40.28-0.048-72.937-32.615-73.131-72.856l-0-0.018v-29.952c0.218-40.241 32.866-72.784 73.126-72.832l0.005-0zM185.259 330.752c22.528 11.819 47.061 19.627 73.131 19.627h580.352v586.496h-580.352c-40.28-0.048-72.937-32.615-73.131-72.856l-0-0.018z", + "M234.155 177.365c0.024 8.912 7.255 16.128 16.171 16.128 0 0 0-0 0-0l523.477 0c8.566-0.454 15.34-7.511 15.34-16.149s-6.774-15.695-15.3-16.148l-0.040-0.002h-523.52c-8.912 0.024-16.128 7.255-16.128 16.171 0 0 0 0 0 0l-0-0z", + "M250.325 134.229c-23.703 0.239-42.855 19.39-43.093 43.071l-0 0.023c0 11.435 4.565 22.4 12.629 30.464v0.128c8.107 8.021 19.029 12.501 30.464 12.501h523.477c23.721-0.215 42.897-19.375 43.136-43.071l0-0.023c0-23.467-19.627-43.093-43.093-43.093zM397.952 819.029c9.563-0.033 17.676-6.205 20.605-14.78l0.046-0.154 26.027-79.019c2.994-8.724 11.123-14.884 20.692-14.891l91.222-0c9.376 0.229 17.272 6.32 20.178 14.737l0.046 0.154 26.837 79.189c3.016 8.636 11.093 14.72 20.59 14.72 0.036 0 0.073-0 0.109-0l-0.006 0h1.621c0.133 0.003 0.291 0.005 0.449 0.005 12.065 0 21.845-9.78 21.845-21.845 0-2.665-0.477-5.219-1.351-7.58l0.049 0.151-102.528-301.653c-3.072-8.875-11.307-35.584-20.693-35.584h-22.613c-9.387 0-17.664 26.88-20.736 35.584l-103.125 301.653c-0.812 2.197-1.282 4.734-1.282 7.382 0 12.088 9.8 21.888 21.888 21.888 0.031 0 0.062-0 0.092-0l-0.005 0zM468.395 646.656l24.107-71.253c6.613-21.035 12.288-42.069 17.451-62.549h0.981c5.12 20.011 10.24 40.533 17.92 63.061l24.107 70.571c0.721 2.075 1.137 4.466 1.137 6.955 0 12.059-9.772 21.837-21.829 21.845l-43.094 0c-12.065-0-21.845-9.781-21.845-21.845 0-2.41 0.39-4.728 1.111-6.896l-0.044 0.154z", + "M501.035 425.557c-11.406 0.035-21.49 5.659-27.663 14.276l-0.070 0.103c-4.48 5.632-6.997 10.667-9.387 15.701-4.608 10.027-7.851 20.139-8.96 23.467v0.128l-103.168 301.44c-1.8 4.884-2.841 10.525-2.841 16.409 0 10.58 3.366 20.373 9.085 28.368l-0.1-0.148v0.085c9.173 12.971 24.235 20.48 40.107 20.48 21.282-0.061 39.355-13.733 45.976-32.767l0.104-0.343v-0.128l24.832-75.435h83.371l25.685 75.776v0.085c6.897 19.29 25.011 32.846 46.292 32.853l0.982 0c0.277 0.006 0.603 0.009 0.93 0.009 26.957 0 48.811-21.853 48.811-48.811 0-5.894-1.045-11.544-2.959-16.775l0.109 0.339-102.4-301.355c-1.28-3.584-4.309-13.653-8.96-23.68-2.629-6.026-5.773-11.223-9.5-15.936l0.114 0.149c-6.232-8.695-16.306-14.293-27.687-14.293-0.016 0-0.032 0-0.049 0l0.003-0zM512.384 479.445c0.853 2.005 1.621 4.139 2.432 6.4h-4.779c0.853-2.261 1.493-4.395 2.347-6.443zM510.592 606.080l14.549 42.283h-28.928zM574.891 661.461l15.787 46.421c-7.552-10.027-16.085-18.859-28.587-20.992 2.816-2.475 7.509-1.877 9.728-4.992 4.267-5.931 1.749-13.483 3.072-20.48zM447.445 667.776c1.536 4.693-0.768 10.155 2.133 14.251 2.432 3.2 7.253 2.688 10.24 5.205-11.051 1.707-17.792 10.069-25.173 18.091z" + ], + "attrs": [], + "tags": [ + "glossary" + ], + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 679, + "id": 110, + "name": "glossary", + "prevSize": 32, + "code": 59649 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 109 + }, + { + "icon": { + "paths": [ + "M170.667 341.333h170.667v-170.667h-170.667v170.667zM426.667 853.333h170.667v-170.667h-170.667v170.667zM170.667 853.333h170.667v-170.667h-170.667v170.667zM170.667 597.333h170.667v-170.667h-170.667v170.667zM426.667 597.333h170.667v-170.667h-170.667v170.667zM682.667 170.667v170.667h170.667v-170.667h-170.667zM426.667 341.333h170.667v-170.667h-170.667v170.667zM682.667 597.333h170.667v-170.667h-170.667v170.667zM682.667 853.333h170.667v-170.667h-170.667v170.667z" + ], + "attrs": [], + "tags": [ + "grid" + ], + "defaultCode": 59763, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 680, + "id": 111, + "name": "grid", + "prevSize": 32, + "code": 59763 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 110 + }, + { + "icon": { + "paths": [ + "M464 632v-215.46c0-26.505-21.495-48-48-48h-72c-53.025 0-96 42.975-96 96.002v119.458c0 53.026 42.975 96 96 96h72c26.505 0 48-21.495 48-48zM392 608h-48c-13.23 0-24-10.769-24-24v-119.458c0-13.231 10.77-24 24-24h48v167.458zM512 128c-214.23 0-377.13 178.245-384 384v24c0 13.261 10.74 24 24 24h24c13.26 0 24-10.739 24-24v-24c0-172.035 139.965-312 312-312 172.036 0 312 139.965 312 312h-0.179c0.119 3.644 0.179 248.58 0.179 248.58 0 35.025-28.395 63.42-63.42 63.42h-152.58c0-39.765-32.235-72-72-72h-48c-39.765 0-72 32.235-72 72s32.235 72 72 72h272.58c74.79 0 135.42-60.629 135.42-135.42v-248.58c-6.869-205.755-169.771-384-384-384zM680 680c53.026 0 96-42.974 96-96v-119.458c0-53.027-42.974-96.002-96-96.002h-72c-26.505 0-48 21.495-48 48v215.46c0 26.505 21.495 48 48 48h72zM632 440.542h48c13.231 0 24 10.769 24 24v119.458c0 13.231-10.769 24-24 24h-48v-167.458z" + ], + "attrs": [], + "tags": [ + "headset" + ], + "defaultCode": 59875, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 681, + "id": 112, + "name": "headset", + "prevSize": 32, + "code": 59875 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 111 + }, + { + "icon": { + "paths": [ + "M554.656 128c-212.053 0-384 171.947-384 384h-128l165.974 165.971 2.986 5.978 172.374-171.949h-128c0-165.12 133.546-298.667 298.666-298.667s298.669 133.547 298.669 298.667c0 165.12-133.549 298.669-298.669 298.669-82.346 0-157.013-33.709-210.773-87.898l-60.587 60.589c69.547 69.549 165.12 112.64 271.36 112.64 212.051 0 384-171.949 384-384 0-212.053-171.949-384-384-384zM511.99 341.333v213.334l182.615 108.373 30.72-51.627-149.335-88.746v-181.334h-64z" + ], + "attrs": [], + "tags": [ + "history" + ], + "defaultCode": 59674, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 682, + "id": 113, + "name": "history", + "prevSize": 32, + "code": 59674 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 112 + }, + { + "icon": { + "paths": [ + "M426.667 853.333v-256h170.667v256h213.333v-341.333h128l-426.667-384-426.667 384h128v341.333h213.333z" + ], + "attrs": [], + "tags": [ + "home" + ], + "defaultCode": 59765, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 683, + "id": 114, + "name": "home", + "prevSize": 32, + "code": 59765 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 113 + }, + { + "icon": { + "paths": [ + "M807.198 462.785c0 32.964 0 65.437 0 98.4 59.040 0 135.792 0 196.801 0 0-32.963 0-65.435 0-98.4-61.009 0-137.761 0-196.801 0z", + "M708.794 788.013c47.232 34.933 108.732 81.18 157.44 117.589 19.68-26.077 39.361-52.645 59.040-78.721-48.708-36.408-110.208-82.656-157.44-118.080-19.68 26.568-39.36 53.136-59.040 79.212z", + "M925.274 197.12c-19.679-26.077-39.36-52.645-59.040-78.721-48.708 36.408-110.208 82.656-157.44 118.080 19.68 26.076 39.36 52.644 59.040 78.721 47.232-35.424 108.732-81.18 157.44-118.080z", + "M118.399 364.393c-54.12 0-98.4 44.28-98.4 98.4v98.401c0 54.12 44.28 98.4 98.4 98.4h49.201v196.801h98.4v-196.801h49.201l246 147.6v-590.402l-246 147.6h-196.801zM365.877 448.525l96.924-58.057v243.049l-96.924-58.057-23.616-14.268h-223.861v-98.401h223.861l23.616-14.268z", + "M684.201 511.986c0-65.435-28.536-124.476-73.8-164.82v329.149c45.264-39.852 73.8-98.892 73.8-164.329z" + ], + "attrs": [], + "tags": [ + "horn" + ], + "defaultCode": 59669, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 684, + "id": 115, + "name": "horn", + "prevSize": 32, + "code": 59675 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 114 + }, + { + "icon": { + "paths": [ + "M768 938.667l-0.427-256-170.24-170.667 170.24-171.093 0.427-255.573h-512v256l170.667 170.667-170.667 170.24v256.427h512zM341.333 320v-149.333h341.333v149.333l-170.667 170.667-170.667-170.667z" + ], + "attrs": [], + "tags": [ + "hourglass" + ], + "defaultCode": 59876, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 685, + "id": 116, + "name": "hourglass", + "prevSize": 32, + "code": 59876 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 115 + }, + { + "icon": { + "paths": [ + "M565.333 714.24c0 29.44-22.613 53.76-53.333 53.76-29.867 0-53.76-23.893-53.76-53.76 0-30.293 23.893-53.333 53.76-53.333 30.293 0 53.333 23.467 53.333 53.333zM511.573 256c-75.52 0-127.147 49.067-146.347 106.24l69.973 29.44c9.387-28.587 31.573-63.147 76.8-63.147 69.12 0 82.773 64.853 58.453 99.413-23.040 32.853-62.72 55.040-83.627 92.16-16.64 29.44-13.227 63.573-13.227 84.48h77.653c0-39.68 2.987-47.787 9.387-60.16 16.64-30.72 47.36-45.227 79.787-92.587 29.013-42.667 17.92-100.693-0.853-131.413-21.76-35.84-64.853-64.427-128-64.427zM810.667 213.333h-597.333v597.333h597.333v-597.333zM810.667 128c46.933 0 85.333 38.4 85.333 85.333v597.333c0 46.933-38.4 85.333-85.333 85.333h-597.333c-46.933 0-85.333-38.4-85.333-85.333v-597.333c0-46.933 38.4-85.333 85.333-85.333h597.333z" + ], + "attrs": [], + "tags": [ + "how do I" + ], + "defaultCode": 59675, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 686, + "id": 117, + "name": "how-do-I", + "prevSize": 32, + "code": 59676 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 116 + }, + { + "icon": { + "paths": [ + "M896 810.667v-597.333c0-46.933-38.4-85.333-85.333-85.333h-597.333c-46.933 0-85.333 38.4-85.333 85.333v597.333c0 46.933 38.4 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333zM362.667 576l106.667 128.427 149.333-192.427 192 256h-597.333l149.333-192z" + ], + "attrs": [], + "tags": [ + "image" + ], + "defaultCode": 59766, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 691, + "id": 122, + "name": "image", + "prevSize": 32, + "code": 59766 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 117 + }, + { + "icon": { + "paths": [ + "M810.667 512v298.667h-597.333v-298.667h-85.333v298.667c0 46.933 38.4 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-298.667h-85.333zM554.667 540.587l110.507-110.080 60.16 60.16-213.333 213.333-213.333-213.333 60.16-60.16 110.507 110.080v-412.587h85.333v412.587z" + ], + "attrs": [], + "tags": [ + "import" + ], + "defaultCode": 59877, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 692, + "id": 123, + "name": "import", + "prevSize": 32, + "code": 59877 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 118 + }, + { + "icon": { + "paths": [ + "M810.667 128h-597.76c-47.36 0-84.48 37.973-84.48 85.333l-0.427 597.333c0 46.933 37.547 85.333 84.907 85.333h597.76c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-47.36-38.4-85.333-85.333-85.333zM810.667 640h-170.667c0 70.827-57.6 128-128 128s-128-57.173-128-128h-171.093v-426.667h597.76v426.667z" + ], + "attrs": [], + "tags": [ + "inbox" + ], + "defaultCode": 59878, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 693, + "id": 124, + "name": "inbox", + "prevSize": 32, + "code": 59878 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 119 + }, + { + "icon": { + "paths": [ + "M512 938.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667zM469.333 298.667h85.333v256h-85.333v-256zM469.333 640h85.333v85.333h-85.333v-85.333z" + ], + "attrs": [], + "tags": [ + "indicator-alert" + ], + "defaultCode": 59767, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 695, + "id": 126, + "name": "indicator-alert", + "prevSize": 32, + "code": 59767 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 120 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.947 0-426.667 190.72-426.667 426.667s190.72 426.667 426.667 426.667c235.947 0 426.667-190.72 426.667-426.667s-190.72-426.667-426.667-426.667zM725.333 665.173l-60.16 60.16-153.173-153.173-153.173 153.173-60.16-60.16 153.173-153.173-153.173-153.173 60.16-60.16 153.173 153.173 153.173-153.173 60.16 60.16-153.173 153.173 153.173 153.173z" + ], + "attrs": [], + "tags": [ + "indicator-cancel" + ], + "defaultCode": 59768, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 696, + "id": 127, + "name": "indicator-cancel", + "prevSize": 32, + "code": 59768 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 121 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM426.667 725.333l-213.333-213.333 60.16-60.16 153.173 152.747 323.84-323.84 60.16 60.587-384 384z" + ], + "attrs": [], + "tags": [ + "indicator-check" + ], + "defaultCode": 59769, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 697, + "id": 128, + "name": "indicator-check", + "prevSize": 32, + "code": 59769 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 122 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM554.667 810.667h-85.333v-85.333h85.333v85.333zM642.987 480l-38.4 39.253c-30.72 31.147-49.92 56.747-49.92 120.747h-85.333v-21.333c0-46.933 19.2-89.6 49.92-120.747l52.907-53.76c15.787-15.36 25.173-36.693 25.173-60.16 0-46.933-38.4-85.333-85.333-85.333s-85.333 38.4-85.333 85.333h-85.333c0-94.293 76.373-170.667 170.667-170.667s170.667 76.373 170.667 170.667c0 37.547-15.36 71.68-39.68 96z" + ], + "attrs": [], + "tags": [ + "indicator-help" + ], + "defaultCode": 59770, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 698, + "id": 129, + "name": "indicator-help", + "prevSize": 32, + "code": 59770 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 123 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM554.667 725.333h-85.333v-256h85.333v256zM554.667 384h-85.333v-85.333h85.333v85.333z" + ], + "attrs": [], + "tags": [ + "indicator-info" + ], + "defaultCode": 59771, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 699, + "id": 130, + "name": "indicator-info", + "prevSize": 32, + "code": 59771 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 124 + }, + { + "icon": { + "paths": [ + "M469.333 298.667h85.333v85.333h-85.333v-85.333zM469.333 469.333h85.333v256h-85.333v-256zM512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM512 853.333c-188.16 0-341.333-153.173-341.333-341.333s153.173-341.333 341.333-341.333c188.16 0 341.333 153.173 341.333 341.333s-153.173 341.333-341.333 341.333z" + ], + "attrs": [], + "tags": [ + "info" + ], + "defaultCode": 59681, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 700, + "id": 131, + "name": "info", + "prevSize": 32, + "code": 59681 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 125 + }, + { + "icon": { + "paths": [ + "M853.333 213.333h-682.667c-46.933 0-84.907 38.4-84.907 85.333l-0.427 426.667c0 46.933 38.4 85.333 85.333 85.333h682.667c46.933 0 85.333-38.4 85.333-85.333v-426.667c0-46.933-38.4-85.333-85.333-85.333zM469.333 341.333h85.333v85.333h-85.333v-85.333zM469.333 469.333h85.333v85.333h-85.333v-85.333zM341.333 341.333h85.333v85.333h-85.333v-85.333zM341.333 469.333h85.333v85.333h-85.333v-85.333zM298.667 554.667h-85.333v-85.333h85.333v85.333zM298.667 426.667h-85.333v-85.333h85.333v85.333zM682.667 725.333h-341.333v-85.333h341.333v85.333zM682.667 554.667h-85.333v-85.333h85.333v85.333zM682.667 426.667h-85.333v-85.333h85.333v85.333zM810.667 554.667h-85.333v-85.333h85.333v85.333zM810.667 426.667h-85.333v-85.333h85.333v85.333z" + ], + "attrs": [], + "tags": [ + "keyboard" + ], + "defaultCode": 59879, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 701, + "id": 132, + "name": "keyboard", + "prevSize": 32, + "code": 59879 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 126 + }, + { + "icon": { + "paths": [ + "M810.667 810.667h-597.333v-597.333h298.667v-85.333h-298.667c-47.36 0-85.333 38.4-85.333 85.333v597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-298.667h-85.333v298.667zM597.333 128v85.333h153.173l-419.413 419.413 60.16 60.16 419.413-419.413v153.173h85.333v-298.667h-298.667z" + ], + "attrs": [], + "tags": [ + "launch" + ], + "defaultCode": 59682, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 702, + "id": 133, + "name": "launch", + "prevSize": 32, + "code": 59682 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 127 + }, + { + "icon": { + "paths": [ + "M384 896c0 21.333 17.067 42.667 42.667 42.667h170.667c25.6 0 42.667-21.333 42.667-42.667v-42.667h-256v42.667zM512 85.333c-166.4 0-298.667 132.267-298.667 298.667 0 102.4 51.2 192 128 243.2v98.133c0 21.333 17.067 42.667 42.667 42.667h256c25.6 0 42.667-21.333 42.667-42.667v-98.133c76.8-55.467 128-145.067 128-243.2 0-166.4-132.267-298.667-298.667-298.667z" + ], + "attrs": [], + "tags": [ + "lightbulb" + ], + "defaultCode": 59880, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 703, + "id": 134, + "name": "lightbulb", + "prevSize": 32, + "code": 59880 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 128 + }, + { + "icon": { + "paths": [ + "M166.4 512c0-72.96 59.307-132.267 132.267-132.267h170.667v-81.067h-170.667c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333h170.667v-81.067h-170.667c-72.96 0-132.267-59.307-132.267-132.267zM341.333 554.667h341.333v-85.333h-341.333v85.333zM725.333 298.667h-170.667v81.067h170.667c72.96 0 132.267 59.307 132.267 132.267s-59.307 132.267-132.267 132.267h-170.667v81.067h170.667c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333z" + ], + "attrs": [], + "tags": [ + "link" + ], + "defaultCode": 59773, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 704, + "id": 135, + "name": "link", + "prevSize": 32, + "code": 59773 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 129 + }, + { + "icon": { + "paths": [ + "M341.333 469.333h341.333v85.333h-341.333v-85.333zM857.6 512h81.067c0-117.759-95.573-213.332-213.333-213.332h-170.667v81.067h170.667c72.96 0 132.267 59.305 132.267 132.265zM166.4 512c0-72.96 59.307-132.265 132.267-132.265h170.667v-81.067h-170.667c-117.76 0-213.333 95.573-213.333 213.332s95.573 213.333 213.333 213.333h170.667v-81.067h-170.667c-72.96 0-132.267-59.307-132.267-132.267zM810.667 512h-85.333v128h-128v85.333h128v128h85.333v-128h128v-85.333h-128v-128z" + ], + "attrs": [], + "tags": [ + "link-add" + ], + "defaultCode": 59772, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 705, + "id": 136, + "name": "link-add", + "prevSize": 32, + "code": 59772 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 130 + }, + { + "icon": { + "paths": [ + "M170.667 448c-35.413 0-64 28.587-64 64s28.587 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64zM170.667 192c-35.413 0-64 28.587-64 64s28.587 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64zM170.667 704c-35.413 0-64 29.013-64 64s29.013 64 64 64c34.987 0 64-29.013 64-64s-28.587-64-64-64zM298.667 810.667h597.333v-85.333h-597.333v85.333zM298.667 554.667h597.333v-85.333h-597.333v85.333zM298.667 213.333v85.333h597.333v-85.333h-597.333z" + ], + "attrs": [], + "tags": [ + "list-bullet" + ], + "defaultCode": 59775, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 706, + "id": 137, + "name": "list-bullet", + "prevSize": 32, + "code": 59775 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 131 + }, + { + "icon": { + "paths": [ + "M213.333 128h597.333c46.933 0 85.333 38.4 85.333 85.333v597.333c0 46.933-38.4 85.333-85.333 85.333h-597.333c-46.933 0-85.333-38.4-85.333-85.333v-597.333c0-46.933 38.4-85.333 85.333-85.333zM213.333 213.333v597.333h597.333v-597.333h-597.333zM341.333 384c23.564 0 42.667-19.103 42.667-42.667s-19.103-42.667-42.667-42.667c-23.564 0-42.667 19.103-42.667 42.667s19.103 42.667 42.667 42.667zM512 384h170.667c23.467 0 42.667-19.2 42.667-42.667s-19.2-42.667-42.667-42.667h-170.667c-23.467 0-42.667 19.2-42.667 42.667s19.2 42.667 42.667 42.667zM512 554.667h170.667c23.467 0 42.667-19.2 42.667-42.667s-19.2-42.667-42.667-42.667h-170.667c-23.467 0-42.667 19.2-42.667 42.667s19.2 42.667 42.667 42.667zM512 725.333h170.667c23.467 0 42.667-19.2 42.667-42.667s-19.2-42.667-42.667-42.667h-170.667c-23.467 0-42.667 19.2-42.667 42.667s19.2 42.667 42.667 42.667zM384 512c0 23.565-19.103 42.667-42.667 42.667s-42.667-19.102-42.667-42.667c0-23.565 19.103-42.667 42.667-42.667s42.667 19.102 42.667 42.667zM341.333 725.333c23.564 0 42.667-19.102 42.667-42.667s-19.103-42.667-42.667-42.667c-23.564 0-42.667 19.102-42.667 42.667s19.103 42.667 42.667 42.667z" + ], + "attrs": [], + "tags": [ + "list-bullet-contained" + ], + "defaultCode": 59774, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 707, + "id": 138, + "name": "list-bullet-contained", + "prevSize": 32, + "code": 59774 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 132 + }, + { + "icon": { + "paths": [ + "M170.667 448c-35.413 0-64 28.587-64 64s28.587 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64zM170.667 192c-35.413 0-64 28.587-64 64s28.587 64 64 64c35.413 0 64-28.587 64-64s-28.587-64-64-64zM170.667 704c-35.413 0-64 29.013-64 64s29.013 64 64 64c34.987 0 64-29.013 64-64s-28.587-64-64-64zM341.333 810.667h512c23.467 0 42.667-19.2 42.667-42.667s-19.2-42.667-42.667-42.667h-512c-23.467 0-42.667 19.2-42.667 42.667s19.2 42.667 42.667 42.667zM341.333 554.667h512c23.467 0 42.667-19.2 42.667-42.667s-19.2-42.667-42.667-42.667h-512c-23.467 0-42.667 19.2-42.667 42.667s19.2 42.667 42.667 42.667zM298.667 256c0 23.467 19.2 42.667 42.667 42.667h512c23.467 0 42.667-19.2 42.667-42.667s-19.2-42.667-42.667-42.667h-512c-23.467 0-42.667 19.2-42.667 42.667z" + ], + "attrs": [], + "tags": [ + "ic-round-format-list-bulleted" + ], + "defaultCode": 59678, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 689, + "id": 120, + "name": "list-bulleted", + "prevSize": 32, + "code": 59679 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 133 + }, + { + "icon": { + "paths": [ + "M128 554.667h85.333v-85.333h-85.333v85.333zM128 725.333h85.333v-85.333h-85.333v85.333zM128 384h85.333v-85.333h-85.333v85.333zM298.667 554.667h597.333v-85.333h-597.333v85.333zM298.667 725.333h597.333v-85.333h-597.333v85.333zM298.667 298.667v85.333h597.333v-85.333h-597.333z" + ], + "attrs": [], + "tags": [ + "list-details" + ], + "defaultCode": 59776, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 708, + "id": 139, + "name": "list-details", + "prevSize": 32, + "code": 59776 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 134 + }, + { + "icon": { + "paths": [ + "M170.667 298.667h682.667v85.333h-682.667v-85.333zM170.667 554.667h682.667v-85.333h-682.667v85.333zM170.667 725.333h298.667v-85.333h-298.667v85.333zM170.667 896h298.667v-85.333h-298.667v85.333zM657.493 775.253l-60.16-60.587-60.16 60.16 120.32 121.173 195.84-195.413-60.587-60.587-135.253 135.253zM170.667 128v85.333h682.667v-85.333h-682.667z" + ], + "attrs": [], + "tags": [ + "list-feedback" + ], + "defaultCode": 59777, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 709, + "id": 140, + "name": "list-feedback", + "prevSize": 32, + "code": 59777 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 135 + }, + { + "icon": { + "paths": [ + "M791.275 93.091h-558.547c-12.345 0-24.184 4.904-32.913 13.633s-13.633 20.568-13.633 32.913v744.728c0 12.343 4.904 24.183 13.633 32.913s20.568 13.632 32.913 13.632h558.547c12.343 0 24.183-4.902 32.913-13.632 8.725-8.73 13.632-20.57 13.632-32.913v-744.728c0-12.344-4.907-24.183-13.632-32.913-8.73-8.729-20.57-13.633-32.913-13.633zM744.73 837.82h-465.456v-651.638h465.456v651.638zM651.635 279.273h-279.271v93.091h279.271v-93.091zM651.635 651.635h-279.271v93.094h279.271v-93.094zM651.635 465.455h-279.271v93.090h279.271v-93.090z" + ], + "attrs": [], + "tags": [ + "list-form" + ], + "defaultCode": 59778, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 710, + "id": 141, + "name": "list-form", + "prevSize": 32, + "code": 59778 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 136 + }, + { + "icon": { + "paths": [ + "M85.333 725.333h85.333v21.333h-42.667v42.667h42.667v21.333h-85.333v42.667h128v-170.667h-128v42.667zM128 341.333h42.667v-170.667h-85.333v42.667h42.667v128zM85.333 469.333h76.8l-76.8 89.6v38.4h128v-42.667h-76.8l76.8-89.6v-38.4h-128v42.667zM298.667 213.333v85.333h597.333v-85.333h-597.333zM298.667 810.667h597.333v-85.333h-597.333v85.333zM298.667 554.667h597.333v-85.333h-597.333v85.333z" + ], + "attrs": [], + "tags": [ + "ic-sharp-format-list-numbered" + ], + "defaultCode": 59679, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 690, + "id": 121, + "name": "list-numbered", + "prevSize": 32, + "code": 59680 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 137 + }, + { + "icon": { + "paths": [ + "M938.667 298.666h-384v85.333h384v-85.333zM938.667 640h-384v85.333h384v-85.333zM236.373 469.333l-151.040-151.040 60.16-60.16 90.453 90.453 180.907-180.907 60.16 60.16-240.64 241.494zM236.373 810.667l-151.040-151.040 60.16-60.16 90.453 90.453 180.907-180.907 60.16 60.16-240.64 241.493z" + ], + "attrs": [], + "tags": [ + "list-task" + ], + "defaultCode": 59779, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 711, + "id": 142, + "name": "list-task", + "prevSize": 32, + "code": 59779 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 138 + }, + { + "icon": { + "paths": [ + "M512 85.333c-179.2 0-341.333 137.387-341.333 349.867 0 141.653 113.92 309.333 341.333 503.467 227.413-194.133 341.333-361.813 341.333-503.467 0-212.48-162.133-349.867-341.333-349.867zM512 512c-46.933 0-85.333-38.4-85.333-85.333s38.4-85.333 85.333-85.333c46.933 0 85.333 38.4 85.333 85.333s-38.4 85.333-85.333 85.333z" + ], + "attrs": [], + "tags": [ + "location" + ], + "defaultCode": 59882, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 712, + "id": 143, + "name": "location", + "prevSize": 32, + "code": 59882 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 139 + }, + { + "icon": { + "paths": [ + "M597.333 426.667v-287.573c-27.733-7.253-56.32-11.093-85.333-11.093-179.2 0-341.333 137.387-341.333 349.867 0 141.653 113.92 309.333 341.333 503.467 227.413-194.133 341.333-361.813 341.333-503.467 0-17.493-1.707-34.56-3.84-51.2h-252.16zM512 554.667c-46.933 0-85.333-38.4-85.333-85.333s38.4-85.333 85.333-85.333c46.933 0 85.333 38.4 85.333 85.333s-38.4 85.333-85.333 85.333z", + "M961.707 122.88l-60.587-60.587-90.453 90.88-90.453-90.88-60.587 60.587 90.88 90.453-90.88 90.453 60.587 60.587 90.453-90.88 90.453 90.88 60.587-60.587-90.88-90.453 90.88-90.453z" + ], + "attrs": [], + "tags": [ + "location-off" + ], + "defaultCode": 59881, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 713, + "id": 144, + "name": "location-off", + "prevSize": 32, + "code": 59881 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 140 + }, + { + "icon": { + "paths": [ + "M768 341.333h-42.667v-85.333c0-117.76-95.573-213.333-213.333-213.333s-213.333 95.573-213.333 213.333v85.333h-42.667c-46.933 0-85.333 38.4-85.333 85.333v426.667c0 46.933 38.4 85.333 85.333 85.333h512c46.933 0 85.333-38.4 85.333-85.333v-426.667c0-46.933-38.4-85.333-85.333-85.333zM512 725.333c-46.933 0-85.333-38.4-85.333-85.333s38.4-85.333 85.333-85.333c46.933 0 85.333 38.4 85.333 85.333s-38.4 85.333-85.333 85.333zM644.267 341.333h-264.533v-85.333c0-72.96 59.307-132.267 132.267-132.267s132.267 59.307 132.267 132.267v85.333z" + ], + "attrs": [], + "tags": [ + "lock" + ], + "defaultCode": 59781, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 714, + "id": 145, + "name": "lock", + "prevSize": 32, + "code": 59781 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 141 + }, + { + "icon": { + "paths": [ + "M512 725.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM768 341.333h-42.667v-85.333c0-117.76-95.573-213.333-213.333-213.333s-213.333 95.573-213.333 213.333h81.067c0-72.96 59.307-132.267 132.267-132.267s132.267 59.307 132.267 132.267v85.333h-388.267c-46.933 0-85.333 38.4-85.333 85.333v426.667c0 46.933 38.4 85.333 85.333 85.333h512c46.933 0 85.333-38.4 85.333-85.333v-426.667c0-46.933-38.4-85.333-85.333-85.333zM768 853.333h-512v-426.667h512v426.667z" + ], + "attrs": [], + "tags": [ + "lock-undo" + ], + "defaultCode": 59780, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 715, + "id": 146, + "name": "lock-undo", + "prevSize": 32, + "code": 59780 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 142 + }, + { + "icon": { + "paths": [ + "M674.304 573.359l-90.509 90.509 255.838 255.838 90.509-90.509-255.838-255.838z", + "M742.4 443.2c82.347 0 149.333-66.987 149.333-149.333 0-24.747-6.827-47.787-17.493-68.267l-115.2 115.2-63.573-63.573 115.2-115.2c-20.48-10.667-43.52-17.493-68.267-17.493-82.347 0-149.333 66.987-149.333 149.333 0 17.493 3.413 34.133 8.96 49.493l-78.933 78.933-75.947-75.947 30.293-30.293-60.16-60.16 90.453-90.453c-49.92-49.92-130.987-49.92-180.907 0l-151.040 151.040 60.16 60.16h-120.32l-30.293 30.293 151.040 151.040 30.293-30.293v-120.747l60.16 60.16 30.293-30.293 75.947 75.947-316.16 316.16 90.453 90.453 485.547-485.12c15.36 5.547 32 8.96 49.493 8.96z" + ], + "attrs": [], + "tags": [ + "maintenance" + ], + "defaultCode": 59883, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 716, + "id": 147, + "name": "maintenance", + "prevSize": 32, + "code": 59883 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 143 + }, + { + "icon": { + "paths": [ + "M469.333 384h-85.333v-298.667h-85.333v298.667h-85.333v-298.667h-85.333v298.667c0 90.453 70.827 163.84 160 169.387v385.28h106.667v-385.28c89.173-5.547 160-78.933 160-169.387v-298.667h-85.333v298.667zM682.667 256v341.333h106.667v341.333h106.667v-853.333c-117.76 0-213.333 95.573-213.333 170.667z" + ], + "attrs": [], + "tags": [ + "meal" + ], + "defaultCode": 59884, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 721, + "id": 152, + "name": "meal", + "prevSize": 32, + "code": 59884 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 144 + }, + { + "icon": { + "paths": [ + "M128 768h768v-85.333h-768v85.333zM128 554.667h768v-85.333h-768v85.333zM128 256v85.333h768v-85.333h-768z" + ], + "attrs": [], + "tags": [ + "menu" + ], + "defaultCode": 59687, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 722, + "id": 153, + "name": "menu", + "prevSize": 32, + "code": 59687 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 145 + }, + { + "icon": { + "paths": [ + "M455.092 644.024l58.869-59.781 21.32 21.617 344.463-349.86 58.911 59.736-403.374 409.599-80.188-81.311zM279.282 605.86l344.462-349.86 58.911 59.736-403.373 409.599-195.082-198.4 58.914-59.737 136.168 138.662z" + ], + "attrs": [], + "tags": [ + "message read" + ], + "defaultCode": 59688, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 723, + "id": 154, + "name": "message-read", + "prevSize": 32, + "code": 59688 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 146 + }, + { + "icon": { + "paths": [ + "M512 597.333c70.827 0 127.573-57.173 127.573-128l0.427-256c0-70.827-57.173-128-128-128s-128 57.173-128 128v256c0 70.827 57.173 128 128 128zM738.133 469.333c0 128-108.373 217.6-226.133 217.6s-226.133-89.6-226.133-217.6h-72.533c0 145.493 116.053 265.813 256 286.72v139.947h85.333v-139.947c139.947-20.48 256-140.8 256-286.72h-72.533z" + ], + "attrs": [], + "tags": [ + "mic" + ], + "defaultCode": 59885, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 724, + "id": 155, + "name": "mic", + "prevSize": 32, + "code": 59885 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 147 + }, + { + "icon": { + "paths": [ + "M810.667 554.667h-597.333v-85.333h597.333v85.333z" + ], + "attrs": [], + "tags": [ + "minus" + ], + "defaultCode": 59783, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 725, + "id": 156, + "name": "minus", + "prevSize": 32, + "code": 59783 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 148 + }, + { + "icon": { + "paths": [ + "M512.427 512c0-152.321 93.867-282.455 226.56-335.788 37.973-15.36 32-72.107-8.107-81.067-46.933-10.24-96.853-12.8-148.48-5.973-192.425 25.6-346.452 183.893-366.505 376.748-26.453 256.427 173.653 472.747 424.532 472.747 31.147 0 61.013-3.413 90.453-9.813 40.533-8.96 46.933-65.28 8.533-81.067-137.387-55.040-227.413-188.16-226.987-335.787z" + ], + "attrs": [], + "tags": [ + "mode-dark" + ], + "defaultCode": 59784, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 726, + "id": 157, + "name": "mode-dark", + "prevSize": 32, + "code": 59784 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 149 + }, + { + "icon": { + "paths": [ + "M512 298.667c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333zM85.333 554.667h85.333c23.467 0 42.667-19.2 42.667-42.667s-19.2-42.667-42.667-42.667h-85.333c-23.467 0-42.667 19.2-42.667 42.667s19.2 42.667 42.667 42.667zM853.333 554.667h85.333c23.467 0 42.667-19.2 42.667-42.667s-19.2-42.667-42.667-42.667h-85.333c-23.467 0-42.667 19.2-42.667 42.667s19.2 42.667 42.667 42.667zM469.333 85.333v85.333c0 23.467 19.2 42.667 42.667 42.667s42.667-19.2 42.667-42.667v-85.333c0-23.467-19.2-42.667-42.667-42.667s-42.667 19.2-42.667 42.667zM469.333 853.333v85.333c0 23.467 19.2 42.667 42.667 42.667s42.667-19.2 42.667-42.667v-85.333c0-23.467-19.2-42.667-42.667-42.667s-42.667 19.2-42.667 42.667zM255.573 195.413c-16.64-16.64-43.947-16.64-60.16 0-16.64 16.64-16.64 43.947 0 60.16l45.227 45.227c16.64 16.64 43.947 16.64 60.16 0s16.64-43.947 0-60.16l-45.227-45.227zM783.36 723.2c-16.64-16.64-43.947-16.64-60.16 0-16.64 16.64-16.64 43.947 0 60.16l45.227 45.227c16.64 16.64 43.947 16.64 60.16 0 16.64-16.64 16.64-43.947 0-60.16l-45.227-45.227zM828.587 255.573c16.64-16.64 16.64-43.947 0-60.16-16.64-16.64-43.947-16.64-60.16 0l-45.227 45.227c-16.64 16.64-16.64 43.947 0 60.16s43.947 16.64 60.16 0l45.227-45.227zM300.8 783.36c16.64-16.64 16.64-43.947 0-60.16-16.64-16.64-43.947-16.64-60.16 0l-45.227 45.227c-16.64 16.64-16.64 43.947 0 60.16s43.947 16.64 60.16 0l45.227-45.227z" + ], + "attrs": [], + "tags": [ + "mode-light" + ], + "defaultCode": 59785, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 727, + "id": 158, + "name": "mode-light", + "prevSize": 32, + "code": 59785 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 150 + }, + { + "icon": { + "paths": [ + "M503.466 465.067c-96.853-25.174-128-51.2-128-91.734 0-46.506 43.093-78.933 115.2-78.933 75.947 0 104.107 36.267 106.667 89.6h94.296c-2.989-73.387-47.789-140.8-136.963-162.56v-93.44h-128v92.16c-82.773 17.92-149.333 71.68-149.333 154.027 0 98.56 81.493 147.626 200.533 176.213 106.667 25.6 128 63.147 128 102.829 0 29.44-20.907 76.371-115.2 76.371-87.893 0-122.453-39.251-127.147-89.6h-93.866c5.12 93.44 75.093 145.92 157.013 163.411v92.589h128v-91.731c83.2-15.789 149.334-64 149.334-151.469 0-121.173-103.681-162.56-200.534-187.733z" + ], + "attrs": [], + "tags": [ + "money" + ], + "defaultCode": 59689, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 728, + "id": 159, + "name": "money", + "prevSize": 32, + "code": 59689 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 151 + }, + { + "icon": { + "paths": [ + "M682.667 512c0 46.933 38.4 85.333 85.333 85.333s85.333-38.4 85.333-85.333c0-46.933-38.4-85.333-85.333-85.333s-85.333 38.4-85.333 85.333zM597.333 512c0-46.933-38.4-85.333-85.333-85.333s-85.333 38.4-85.333 85.333c0 46.933 38.4 85.333 85.333 85.333s85.333-38.4 85.333-85.333zM341.333 512c0-46.933-38.4-85.333-85.333-85.333s-85.333 38.4-85.333 85.333c0 46.933 38.4 85.333 85.333 85.333s85.333-38.4 85.333-85.333z" + ], + "attrs": [], + "tags": [ + "more-hori" + ], + "defaultCode": 59786, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 729, + "id": 160, + "name": "more-hori", + "prevSize": 32, + "code": 59786 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 152 + }, + { + "icon": { + "paths": [ + "M512 341.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM512 426.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM512 682.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333z" + ], + "attrs": [], + "tags": [ + "more vert" + ], + "defaultCode": 59690, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 730, + "id": 161, + "name": "more-vert", + "prevSize": 32, + "code": 59690 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 153 + }, + { + "icon": { + "paths": [ + "M386.763 128l-91.429 160h-103.334c-47.128 0-85.333 38.205-85.333 85.333v469.333c0 47.13 38.205 85.333 85.333 85.333h682.667c47.13 0 85.333-38.204 85.333-85.333v-469.333c0-47.128-38.204-85.333-85.333-85.333h-103.334l-91.43-160h-293.139zM697.621 288h-328.575l54.857-96h218.861l54.857 96zM192 352h682.667c11.78 0 21.333 9.551 21.333 21.333 0 106.039-85.961 192-192 192h-341.333c-106.039 0-192-85.961-192-192 0-11.782 9.551-21.333 21.333-21.333zM170.667 542.665c44.716 50.667 109.172 83.494 181.333 86.451v64.218h64v-64h234.667v64h64v-64.218c72.162-2.957 136.619-35.785 181.333-86.451v300.002c0 11.78-9.553 21.333-21.333 21.333h-682.667c-11.782 0-21.333-9.553-21.333-21.333v-300.002z" + ], + "width": 1067, + "attrs": [], + "tags": [ + "mywork" + ], + "defaultCode": 59728, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 731, + "id": 162, + "name": "mywork", + "prevSize": 32, + "code": 59728 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 154 + }, + { + "icon": { + "paths": [ + "M301.767 683.938l109.95-248.324c5.318-12.011 14.648-21.642 26.282-27.132l240.567-113.497c27.75-13.092 56.35 16.413 43.665 45.075l-109.948 248.326c-5.321 12.011-14.647 21.641-26.283 27.132l-240.568 113.498c-27.767 13.090-56.35-16.414-43.666-45.077zM549.632 550.848c20.783-21.453 20.783-56.243 0-77.696s-54.481-21.453-75.264 0c-20.783 21.453-20.783 56.243 0 77.696s54.481 21.453 75.264 0zM98.667 512c0-235.647 185.050-426.667 413.333-426.667s413.333 191.020 413.333 426.667c0 235.648-185.050 426.667-413.333 426.667s-413.333-191.019-413.333-426.667zM845.333 512c0-189.729-149.534-344.086-333.333-344.086s-333.333 154.357-333.333 344.086c0 189.73 149.533 344.085 333.333 344.085s333.333-154.355 333.333-344.085z" + ], + "attrs": [], + "tags": [ + "navigate" + ], + "defaultCode": 59886, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 732, + "id": 163, + "name": "navigate", + "prevSize": 32, + "code": 59886 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 155 + }, + { + "icon": { + "paths": [ + "M870.071 465.967c13.154 22.272 20.595 46.033 20.595 74.756 0 66.022-55.825 128.371-128.73 128.371h-54.72c7.381 19.217 13.274 42.197 13.274 69.811 0 109.188-56.533 157.094-142.903 157.094-92.412 0-87.138-142.4-107.639-162.901-34.121-34.121-74.423-99.669-103.141-125.099h-148.14c-26.51 0-48-21.491-48-48v-360c0-26.51 21.49-48 48-48h96c22.339 0 41.112 15.261 46.467 35.925 66.762-1.501 112.59-59.91 266.702-59.91 10.833 0 22.831-0.015 33.331-0.015 115.674 0 167.979 59.134 169.408 142.995 19.981 27.637 30.451 64.683 26.014 100.485 14.78 27.678 20.493 60.516 13.483 94.487zM777.446 385.22c18.842-31.695 1.89-74.115-20.911-86.355 11.55-73.17-26.411-98.85-79.68-98.85h-56.73c-107.456 0-177.041 56.73-257.459 56.73v279.255h16.38c42.54 0 101.969 106.334 141.811 146.189 42.539 42.543 28.365 113.446 56.73 141.811 70.903 0 70.903-49.472 70.903-85.094 0-58.756-42.539-85.082-42.539-141.811h155.985c31.663 0 56.593-28.365 56.73-56.73 0.137-28.348-19.23-56.717-33.404-56.717 20.233-21.833 24.555-67.852-7.817-98.428zM302.667 248c0-19.883-16.117-36-36-36s-36 16.117-36 36c0 19.883 16.117 36 36 36s36-16.117 36-36z" + ], + "attrs": [], + "tags": [ + "negative" + ], + "defaultCode": 59691, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 733, + "id": 164, + "name": "negative", + "prevSize": 32, + "code": 59691 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 156 + }, + { + "icon": { + "paths": [ + "M213.333 128c-47.36 0-85.333 37.973-85.333 85.333v597.333c0 22.63 8.99 44.335 24.994 60.339s37.708 24.994 60.34 24.994h597.333c22.63 0 44.335-8.99 60.339-24.994s24.994-37.709 24.994-60.339v-298.667h-85.333v298.667h-597.333v-597.333h298.667v-85.333h-298.667zM758.613 170.667c-7.68 0.065-15.027 3.127-20.48 8.533l-52.053 51.627 106.667 106.667 52.053-51.627c11.093-11.093 11.093-29.867 0-40.533l-66.133-66.133c-5.547-5.547-12.8-8.533-20.053-8.533zM655.787 261.12l-314.453 314.88v106.667h106.667l314.453-314.88-106.667-106.667z" + ], + "attrs": [], + "tags": [ + "new message" + ], + "defaultCode": 59692, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 734, + "id": 165, + "name": "new-message", + "prevSize": 32, + "code": 59692 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 157 + }, + { + "icon": { + "paths": [ + "M341.333 682.667h341.333v85.333h-341.333v-85.333zM341.333 512h341.333v85.333h-341.333v-85.333zM597.333 85.333h-341.333c-46.933 0-85.333 38.4-85.333 85.333v682.667c0 46.933 37.973 85.333 84.907 85.333h512.427c46.933 0 85.333-38.4 85.333-85.333v-512l-256-256zM768 853.333h-512v-682.667h298.667v213.333h213.333v469.333z" + ], + "attrs": [], + "tags": [ + "note" + ], + "defaultCode": 59788, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 735, + "id": 166, + "name": "note", + "prevSize": 32, + "code": 59788 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 158 + }, + { + "icon": { + "paths": [ + "M511.999 938.669c46.933 0 85.333-38.4 85.333-85.338h-170.666c0 46.938 37.973 85.338 85.333 85.338zM768 682.669v-213.335c0-130.987-69.971-240.64-192.001-269.653v-29.014c0-35.413-28.587-64-64-64s-64 28.587-64 64v29.014c-122.453 29.013-192 138.24-192 269.653v213.335l-85.333 85.331v42.669h682.665v-42.669l-85.331-85.331z" + ], + "attrs": [], + "tags": [ + "notification" + ], + "defaultCode": 59693, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 736, + "id": 167, + "name": "notification", + "prevSize": 32, + "code": 59693 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 159 + }, + { + "icon": { + "paths": [ + "M286.72 116.511l-62.415-74.606-224.305 187.733 62.903 74.606 223.817-187.733zM975.238 230.125l-224.305-188.221-62.903 74.606 224.305 188.22 62.903-74.606zM487.619 146.255c-242.347 0-438.857 196.511-438.857 438.859s196.023 438.857 438.857 438.857c242.347 0 438.857-196.51 438.857-438.857s-196.51-438.859-438.857-438.859zM487.619 926.447c-188.709 0-341.333-152.625-341.333-341.333s152.625-341.335 341.333-341.335c188.709 0 341.333 152.625 341.333 341.335s-152.625 341.333-341.333 341.333zM341.333 487.589h177.006l-177.006 204.801v87.771h292.571v-97.524h-177.006l177.006-204.801v-87.772h-292.571v97.524z" + ], + "width": 975, + "attrs": [], + "tags": [ + "notification-silence" + ], + "defaultCode": 59743, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 737, + "id": 168, + "name": "notification-silence", + "prevSize": 32, + "code": 59743 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 160 + }, + { + "icon": { + "paths": [ + "M853.333 797.44l-628.48-648.533-54.187 54.187 119.467 119.467v0.427c-22.187 42.24-34.133 92.16-34.133 145.92v213.333l-85.333 85.333v42.667h585.813l85.333 85.333 54.187-54.187-42.667-43.947zM512 938.667c47.36 0 85.333-37.973 85.333-85.333h-170.667c0 47.36 37.973 85.333 85.333 85.333zM768 626.347v-157.013c0-131.413-69.973-240.64-192-269.653v-29.013c0-35.413-28.587-64-64-64s-64 28.587-64 64v29.013c-6.4 1.28-12.373 3.413-17.92 5.12-4.267 1.28-8.533 2.987-12.8 4.693h-0.427c-0.427 0-0.427 0-0.853 0.427-9.813 3.84-19.627 8.533-29.013 13.227 0 0-0.427 0-0.427 0.427l381.44 402.773z" + ], + "attrs": [], + "tags": [ + "notifications-off" + ], + "defaultCode": 59720, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 738, + "id": 169, + "name": "notifications-off", + "prevSize": 32, + "code": 59720 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 161 + }, + { + "icon": { + "paths": [ + "M512 938.667c46.933 0 85.333-38.4 85.333-85.333h-170.667c0 46.933 38.4 85.333 85.333 85.333zM512 277.333c106.24 0 170.667 86.187 170.667 192v4.267l85.333 85.333v-89.6c0-130.987-69.547-240.64-192-269.653v-29.013c0-35.413-28.587-64-64-64s-64 28.587-64 64v29.013c-10.24 2.56-20.053 6.4-29.44 9.813l69.973 69.973c7.68-0.853 15.36-2.133 23.467-2.133zM230.827 142.933l-60.16 60.16 119.893 119.893c-22.187 42.667-34.56 92.587-34.56 146.347v213.333l-85.333 85.333v42.667h607.573l74.24 74.24 60.16-60.16-681.813-681.813zM682.667 725.333h-341.333v-256c0-29.013 5.12-56.32 14.507-81.067l326.827 326.827v10.24z" + ], + "attrs": [], + "tags": [ + "notifications-off-outlined" + ], + "defaultCode": 59680, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 739, + "id": 170, + "name": "notifications-off-outlined", + "prevSize": 32, + "code": 59695 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 162 + }, + { + "icon": { + "paths": [ + "M672.001 483.557v-227.557h-227.557l93.584 93.584-284.443 284.443-93.584-93.584v227.557h227.557l-93.584-93.584 284.443-284.443z" + ], + "width": 832, + "attrs": [], + "tags": [ + "open-in-full-black-24dp" + ], + "defaultCode": 59742, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 740, + "id": 171, + "name": "open-in-small", + "prevSize": 32, + "code": 59742 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 163 + }, + { + "icon": { + "paths": [ + "M810.667 810.667h-597.333v-597.333h298.667v-85.333h-298.667c-47.36 0-85.333 38.4-85.333 85.333v597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-298.667h-85.333v298.667zM597.333 128v85.333h153.173l-419.413 419.413 60.16 60.16 419.413-419.413v153.173h85.333v-298.667h-298.667z" + ], + "attrs": [], + "tags": [ + "open-new" + ], + "defaultCode": 59889, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 741, + "id": 172, + "name": "open-new", + "prevSize": 32, + "code": 59889 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 164 + }, + { + "icon": { + "paths": [ + "M716.373 247.467l-119.040-119.467h298.667v298.667l-119.040-119.467-174.507 174.507-60.16-60.16 174.080-174.080zM810.667 512v177.92l85.333 85.333v-263.253h-85.333zM843.947 964.693l-68.693-68.693h-561.92c-47.36 0-85.333-38.4-85.333-85.333v-561.92l-68.693-68.693 60.16-60.16 784.213 784.213-59.733 60.587zM689.92 810.667l-208.213-208.213-67.84 67.84-60.16-60.16 67.84-67.84-208.213-208.213v476.587h476.586zM334.080 213.333h177.92v-85.333h-263.253l85.333 85.333z" + ], + "attrs": [], + "tags": [ + "open-new-off" + ], + "defaultCode": 59888, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 742, + "id": 173, + "name": "open-new-off", + "prevSize": 32, + "code": 59888 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 165 + }, + { + "icon": { + "paths": [ + "M455.091 21.734c-90.522 8.499-176.256 41.421-247.091 94.925-82.15 62.054-141.517 144.666-173.875 241.946-30.874 92.851-34.099 192.026-9.344 288.998 8.704 34.125 18.816 60.851 36.71 97.050 74.035 149.581 215.424 250.214 378.701 269.568 25.062 2.97 80.256 3.43 104.781 0.87 81.436-8.47 155.036-36.153 218.047-78.402l-1.727 1.090c27.648-18.15 50.739-37.197 76.621-63.181 40.96-41.062 67.686-78.387 93.107-129.946 16.384-33.254 23.45-50.842 32.973-82.381 22.195-73.446 26.291-164.096 10.982-241.843-17.062-83.456-52.293-156.766-101.481-218.455l0.796 1.034c-15.77-19.789-58.086-61.773-78.899-78.259-68.659-54.4-148.992-88.448-237.517-100.685-22.298-3.098-80.589-4.403-102.784-2.33zM530.816 71.578c10.522 0.922 21.581 2.125 24.55 2.688l5.402 0.973v124.467l-148.915-0.845-0.461-60.16-0.435-60.134 10.419-1.766c38.118-6.477 74.957-8.218 109.44-5.222zM609.254 85.53c29.739 8.073 55.51 18.172 79.855 30.659l-2.287-1.065 23.706 11.648v72.909h-136.448v-121.933l7.066 0.998c3.891 0.563 16.538 3.61 28.109 6.784zM397.696 140.621v59.11l-127.283-0.87-0.922-63.898 11.264-6.554c28.262-16.41 60.006-30.029 95.309-40.858 10.522-3.226 19.686-5.888 20.378-5.939s1.254 26.522 1.254 59.008zM737.766 144.614c20.71 13.978 62.285 48.742 64 53.53 0.333 0.922-15.206 1.536-38.682 1.536h-39.245v-31.616c0-17.382 0.41-31.616 0.896-31.616 0.512 0 6.374 3.686 13.030 8.166zM257.536 171.802l-0.435 27.059-32.87 0.435c-19.866 0.282-32.87-0.179-32.87-1.126 0-4.173 59.418-53.325 64.512-53.376 1.664-0.026 2.022 5.965 1.664 27.008zM257.101 287.872v75.725h-126.464l-0.461-45.21-0.435-45.21 7.731-11.366c8.064-11.853 28.723-37.965 36.352-45.926l4.403-4.608 78.874 0.87v75.725zM397.696 287.898v76.57l-127.283-0.87-0.435-74.061c-0.256-40.73-0.077-74.982 0.358-76.134 0.614-1.613 14.976-2.074 64.077-2.074h63.283v76.57zM560.768 287.898v76.544l-148.915-0.845-0.435-74.061c-0.256-40.73-0.077-74.982 0.358-76.134 0.614-1.613 17.152-2.074 74.88-2.074h74.112v76.57zM710.528 287.872v76.544h-136.448v-153.088h136.448v76.544zM824.909 222.566c17.613 19.968 37.581 48.282 50.918 72.192l6.17 11.059-0.461 28.877-0.435 28.902-157.261 0.845v-153.114h91.187l9.882 11.238zM117.76 329.882l-0.435 33.715-15.411 0.461c-12.134 0.384-15.386 0-15.386-1.792 0-5.786 27.622-66.074 30.259-66.074 0.794 0 1.229 15.155 0.973 33.69zM901.862 349.363c2.765 6.707 5.018 12.826 5.018 13.619s-2.611 1.434-5.837 1.434h-5.811v-14.413c0-7.936 0.358-14.080 0.794-13.619 0.435 0.435 3.072 6.272 5.837 12.979zM117.76 442.214l0.435 65.306h-60.339l1.126-16.23c2.458-35.046 12.723-89.856 20.736-110.694l1.766-4.634 35.84 0.947 0.435 65.306zM710.528 441.805v65.715h-136.448v-131.456h136.448v65.741zM881.536 442.214l0.435 65.306h-158.131v-131.482l157.261 0.87 0.435 65.306zM911.514 378.138c2.355 2.816 9.344 28.134 13.696 49.51 4.659 22.912 8.294 52.608 8.294 67.738v12.134h-38.272v-131.456h7.296c3.994 0 8.038 0.947 8.986 2.074zM118.144 596.608v75.776l-33.792-0.973-3.174-9.139c-11.162-32.23-20.275-81.331-22.323-120.218l-1.126-21.222h60.416v75.776zM257.92 596.557v75.699h-128.128v-151.424h128.128v75.725zM397.696 596.557v75.699h-128.128v-151.424h128.128v75.725zM560.768 596.557v75.699h-149.76v-151.424h149.76v75.725zM710.528 596.557v75.699h-136.448v-151.424h136.448v75.725zM881.92 596.557v75.699h-158.080v-151.424h158.080v75.725zM934.195 535.398c-2.688 48.845-9.139 84.992-22.554 126.464l-3.379 10.394h-13.030v-151.424h39.782l-0.819 14.566zM118.093 714.266c-0.051 32.102-0.077 32.128-10.163 12.339-5.069-9.933-18.074-38.912-18.074-40.269 0-0.435 6.374-0.768 14.157-0.768h14.131l-0.051 28.698zM257.536 750.874l-0.435 65.331-88.602 0.87-10.624-13.44c-5.547-7.001-11.903-15.512-18.043-24.186l-1.311-1.951-8.73-12.672v-79.258h128.179l-0.435 65.306zM397.696 751.309v65.766l-127.283-0.87-0.461-65.331-0.435-65.306h128.179v65.741zM560.768 751.309v65.715h-149.76v-131.456h149.76v65.741zM881.92 708.89c0 23.142-0.051 23.398-5.325 32.87-12.851 23.040-36.506 57.139-48.486 69.862l-5.094 5.402h-99.174v-131.456h158.080v23.322zM901.837 687.642c-0.764 3.318-1.855 6.238-3.272 8.962l0.098-0.207-3.149 6.656-0.154-8.755c-0.102-7.654 0.282-8.73 3.2-8.73 1.843 0 3.302 0.947 3.277 2.074zM257.536 861.542c0.256 17.613-0.179 32.026-0.973 32.026-6.144 0-75.187-58.24-75.187-63.411 0-0.922 15.462-1.357 37.862-1.101l37.862 0.461 0.435 32.026zM397.696 891.904c0 57.523-0.256 63.232-2.739 63.232-7.552 0-52.506-15.386-74.624-25.549-18.381-8.422-43.904-22.093-47.846-25.626-2.611-2.304-2.918-6.374-2.918-37.837 0-19.379 0.512-35.738 1.101-36.352 0.614-0.589 29.44-1.101 64.077-1.101h62.95v63.232zM560.768 895.846v67.149l-17.357 1.946c-33.843 3.814-88.55 2.125-120.755-3.712l-10.803-1.946-0.461-63.232c-0.23-34.765-0.077-64.154 0.384-65.306 0.614-1.613 17.050-2.074 74.88-2.074h74.112v67.174zM710.144 869.862l-0.435 41.165-23.398 11.674c-33.126 16.486-77.568 31.923-105.165 36.531l-7.066 1.178v-131.738h136.499l-0.435 41.19zM809.37 832.205c-19.456 21.632-59.366 54.477-83.456 68.659-1.638 0.973-2.074-6.579-2.074-35.482v-36.71h88.704l-3.174 3.533z" + ], + "width": 998, + "attrs": [], + "tags": [ + "open-shift" + ], + "defaultCode": 59746, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 743, + "id": 174, + "name": "open-shift", + "prevSize": 32, + "code": 59746 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 166 + }, + { + "icon": { + "paths": [ + "M860.16 593.92h-73.728v-40.96c0-40.96-32.768-73.728-73.728-73.728h-172.032v-49.152h73.728c32.768 0 61.44-28.672 61.44-61.44v-204.8c0-32.768-28.672-61.44-61.44-61.44h-204.8c-32.768 0-61.44 28.672-61.44 61.44v204.8c0 32.768 28.672 61.44 61.44 61.44h73.728v49.152h-176.128c-40.96 0-73.728 32.768-73.728 73.728v40.96h-69.632c-32.768 0-61.44 28.672-61.44 61.44v204.8c0 32.768 28.672 61.44 61.44 61.44h204.8c32.768 0 61.44-28.672 61.44-61.44v-204.8c0-32.768-28.672-61.44-61.44-61.44h-69.632v-40.96c0-4.096 4.096-8.192 8.192-8.192h409.6c4.096 0 8.192 4.096 8.192 8.192v40.96h-73.728c-32.768 0-61.44 28.672-61.44 61.44v204.8c0 32.768 28.672 61.44 61.44 61.44h204.8c32.768 0 61.44-28.672 61.44-61.44v-204.8c4.096-32.768-24.576-61.44-57.344-61.44zM368.64 655.36v204.8h-204.8v-204.8h204.8zM409.6 368.64v-204.8h204.8v204.8h-204.8zM860.16 860.16h-204.8v-204.8h204.8v204.8z" + ], + "attrs": [], + "tags": [ + "org" + ], + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 744, + "id": 175, + "name": "org", + "prevSize": 32, + "code": 59805 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 167 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.093 0-426.667 191.573-426.667 426.667s191.573 426.667 426.667 426.667c58.88 0 106.667-47.787 106.667-106.667 0-26.027-9.813-51.2-27.307-71.253-3.413-4.267-5.547-8.96-5.547-14.080 0-11.947 9.387-21.333 21.333-21.333h75.52c141.227 0 256-114.773 256-256 0-211.627-191.573-384-426.667-384zM746.667 554.667c-35.413 0-64-28.587-64-64s28.587-64 64-64c35.413 0 64 28.587 64 64s-28.587 64-64 64zM618.667 384c-35.413 0-64-28.587-64-64s28.587-64 64-64c35.413 0 64 28.587 64 64s-28.587 64-64 64zM213.333 490.667c0-35.413 28.587-64 64-64s64 28.587 64 64c0 35.413-28.587 64-64 64s-64-28.587-64-64zM469.333 320c0 35.413-28.587 64-64 64s-64-28.587-64-64c0-35.413 28.587-64 64-64s64 28.587 64 64z" + ], + "attrs": [], + "tags": [ + "palette" + ], + "defaultCode": 59890, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 745, + "id": 176, + "name": "palette", + "prevSize": 32, + "code": 59890 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 168 + }, + { + "icon": { + "paths": [ + "M597.333 85.333h-341.333c-46.933 0-84.907 38.4-84.907 85.333l-0.427 682.667c0 46.933 37.973 85.333 84.907 85.333h512.427c46.933 0 85.333-38.4 85.333-85.333v-512l-256-256zM554.667 384v-234.667l234.667 234.667h-234.667z" + ], + "attrs": [], + "tags": [ + "paper" + ], + "defaultCode": 59789, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 746, + "id": 177, + "name": "paper", + "prevSize": 32, + "code": 59789 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 169 + }, + { + "icon": { + "paths": [ + "M472.747 87.253c-72.777 7.153-138.707 31.396-195.334 68.631l1.627-1.005c-26.097 16.873-48.76 35.564-69.059 56.554l-0.103 0.107c-76.764 76.183-124.283 181.739-124.283 298.394 0 5.528 0.107 11.030 0.318 16.506l-0.024-0.788c3.029 90.411 31.531 171.008 86.187 244.053 16.939 22.613 54.272 60.757 75.392 77.099 61.525 47.531 127.445 75.819 204.8 87.893 27.605 4.309 91.861 4.309 119.467 0 95.488-14.933 175.104-54.912 241.579-121.387s106.453-146.091 121.387-241.579c4.309-27.605 4.309-91.861 0-119.467-12.075-77.397-40.448-143.445-87.893-204.8-14.379-18.56-51.541-55.723-70.272-70.229-88.192-68.224-194.304-99.669-303.787-89.984zM543.317 172.416c80.56 7.432 151.864 42.097 205.726 94.577l-0.073-0.070c64.183 62 104.035 148.833 104.035 244.973 0 54.378-12.749 105.778-35.422 151.375l0.891-1.979c-18.642 38.173-42.775 70.691-71.813 98.063l-0.166 0.155c-24.593 23.856-53.119 43.843-84.478 58.861l-1.922 0.829c-43.256 21.499-94.207 34.080-148.096 34.080s-104.84-12.581-150.075-34.966l1.979 0.886c-45.384-21.996-83.54-52.209-113.979-89.035l-0.453-0.565c-85.291-101.931-102.699-247.381-43.989-366.933 56.012-113.512 170.416-190.5 302.933-191.785l0.171-0.001c8.917-0.085 24.533 0.597 34.731 1.536zM256 512v42.667h512v-85.333h-512v42.667z" + ], + "attrs": [], + "tags": [ + "partial approval" + ], + "defaultCode": 59738, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 747, + "id": 178, + "name": "partially-approved", + "prevSize": 32, + "code": 59738 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 170 + }, + { + "icon": { + "paths": [ + "M548.237 594.202l-45.376 23.684c-46.626 24.883-105.667 13.039-140.112-28.237l-22.318-26.761c-18.902-22.771-17.365-55.735 3.587-76.629l139.033-139.033-27.388-16.112c-4.382-2.562-9.391-3.928-14.46-3.928h-103.391l14.973-29.947c7.060-14.063 1.309-31.142-12.753-38.202l-85.4-42.7c-14.006-7.003-31.086-1.309-38.145 12.753l-170.801 341.599c-7.060 14.063-1.309 31.142 12.753 38.204l85.4 42.701c4.042 2.048 8.369 2.961 12.696 2.961 10.476 0 20.496-5.807 25.506-15.714l9.679-19.302 220.731 163.23c25.223 18.615 55.281 27.895 85.285 27.895 30.519 0 60.919-9.621 86.14-28.809l99.068-76.062-134.707-111.59z", + "M950.583 570.692l-170.803-341.601c-7.057-14.12-24.081-19.756-38.199-12.753l-85.402 42.7c-14.063 7.060-19.755 24.14-12.753 38.202l15.031 29.947h-74.867l-199.325 199.268 22.261 26.756c17.194 20.612 46.516 26.645 69.8 14.293l79.194-41.28 173.935 144.102 72.644-56.026 12.241 24.482c5.009 9.963 15.087 15.714 25.451 15.714 4.271 0 8.597-0.969 12.753-3.017l85.402-42.701c14.003-6.946 19.639-24.026 12.638-38.089z" + ], + "attrs": [], + "tags": [ + "partner" + ], + "defaultCode": 59891, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 748, + "id": 179, + "name": "partner", + "prevSize": 32, + "code": 59891 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 171 + }, + { + "icon": { + "paths": [ + "M588.8 512h38.4v-115.2h-38.4v115.2zM435.2 435.2h38.4v-38.4h-38.4v38.4zM819.2 128h-460.8c-42.24 0-76.8 34.56-76.8 76.8v460.8c0 42.24 34.56 76.8 76.8 76.8h460.8c42.24 0 76.8-34.56 76.8-76.8v-460.8c0-42.24-34.56-76.8-76.8-76.8zM512 435.2c0 21.12-17.28 38.4-38.4 38.4h-38.4v57.6c0 10.752-8.448 19.2-19.2 19.2s-19.2-8.448-19.2-19.2v-153.6c0-10.752 8.448-19.2 19.2-19.2h57.6c21.12 0 38.4 17.28 38.4 38.4v38.4zM665.6 512c0 21.12-17.28 38.4-38.4 38.4h-57.6c-10.752 0-19.2-8.448-19.2-19.2v-153.6c0-10.752 8.448-19.2 19.2-19.2h57.6c21.12 0 38.4 17.28 38.4 38.4v115.2zM761.6 396.8h-19.2v38.4h19.2c10.752 0 19.2 8.448 19.2 19.2s-8.448 19.2-19.2 19.2h-19.2v57.6c0 10.752-8.448 19.2-19.2 19.2s-19.2-8.448-19.2-19.2v-153.6c0-10.752 8.448-19.2 19.2-19.2h38.4c10.752 0 19.2 8.448 19.2 19.2s-8.448 19.2-19.2 19.2zM166.4 281.6c-21.12 0-38.4 17.28-38.4 38.4v499.2c0 42.24 34.56 76.8 76.8 76.8h499.2c21.12 0 38.4-17.28 38.4-38.4s-17.28-38.4-38.4-38.4h-499.2v-499.2c0-21.12-17.28-38.4-38.4-38.4z" + ], + "attrs": [], + "tags": [ + "PDF" + ], + "defaultCode": 59694, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 569, + "id": 0, + "name": "pdf", + "prevSize": 32, + "code": 59694 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 172 + }, + { + "icon": { + "paths": [ + "M810.667 128h-178.347c-17.92-49.493-64.853-85.333-120.32-85.333s-102.4 35.84-120.32 85.333h-178.347c-46.933 0-85.333 38.4-85.333 85.333v597.333c0 46.933 38.4 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM512 128c23.467 0 42.667 19.2 42.667 42.667s-19.2 42.667-42.667 42.667c-23.467 0-42.667-19.2-42.667-42.667s19.2-42.667 42.667-42.667zM512 298.667c70.827 0 128 57.173 128 128s-57.173 128-128 128c-70.827 0-128-57.173-128-128s57.173-128 128-128zM768 810.667h-512v-59.733c0-85.333 170.667-132.267 256-132.267s256 46.933 256 132.267v59.733z" + ], + "attrs": [], + "tags": [ + "person-assign" + ], + "defaultCode": 59892, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 750, + "id": 181, + "name": "person-assign", + "prevSize": 32, + "code": 59892 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 174 + }, + { + "icon": { + "paths": [ + "M469.333 512c94.255 0 170.667-76.412 170.667-170.667 0-94.257-76.412-170.667-170.667-170.667-94.257 0-170.667 76.41-170.667 170.667 0 94.255 76.41 170.667 170.667 170.667z", + "M497.92 555.52c-9.387-0.427-18.773-0.853-28.587-0.853-103.253 0-199.68 28.587-282.027 77.653-37.547 22.187-59.307 64-59.307 107.947v113.067h395.093c-33.707-48.213-53.76-107.093-53.76-170.667 0-45.653 10.667-88.32 28.587-127.147z", + "M725.12 469.333c-117.76 0-213.12 95.573-213.12 213.333s95.36 213.333 213.12 213.333c117.973 0 213.547-95.573 213.547-213.333s-95.573-213.333-213.547-213.333zM725.333 853.333c-94.293 0-170.667-76.373-170.667-170.667s76.373-170.667 170.667-170.667c94.293 0 170.667 76.373 170.667 170.667s-76.373 170.667-170.667 170.667z", + "M736 576h-32v128l112 67.2 16-26.24-96-56.96v-112z" + ], + "attrs": [], + "tags": [ + "person-clock" + ], + "defaultCode": 59893, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 751, + "id": 182, + "name": "person-clock", + "prevSize": 32, + "code": 59893 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 175 + }, + { + "icon": { + "paths": [ + "M512 512c94.255 0 170.667-76.412 170.667-170.667 0-94.257-76.412-170.667-170.667-170.667-94.257 0-170.667 76.41-170.667 170.667 0 94.255 76.41 170.667 170.667 170.667z", + "M540.587 555.52c-9.387-0.427-18.773-0.853-28.587-0.853-103.253 0-199.68 28.587-282.027 77.653-37.547 22.187-59.307 64-59.307 107.947v113.067h395.093c-33.707-48.213-53.76-107.093-53.76-170.667 0-45.653 10.667-88.32 28.587-127.147z", + "M704 853.333c82.432 0 149.333-66.901 149.333-149.333s-66.901-149.333-149.333-149.333c-82.432 0-149.333 66.901-149.333 149.333s66.901 149.333 149.333 149.333zM689.067 629.333h29.867v89.6h-29.867v-89.6zM689.067 748.8h29.867v29.867h-29.867v-29.867z" + ], + "attrs": [], + "tags": [ + "person-info" + ], + "defaultCode": 59894, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 752, + "id": 183, + "name": "person-info", + "prevSize": 32, + "code": 59894 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 176 + }, + { + "icon": { + "paths": [ + "M512 512c94.255 0 170.667-76.412 170.667-170.667 0-94.257-76.412-170.667-170.667-170.667-94.257 0-170.667 76.41-170.667 170.667 0 94.255 76.41 170.667 170.667 170.667z", + "M540.587 555.52c-9.387-0.427-18.773-0.853-28.587-0.853-103.253 0-199.68 28.587-282.027 77.653-37.547 22.187-59.307 64-59.307 107.947v113.067h395.093c-33.707-48.213-53.76-107.093-53.76-170.667 0-45.653 10.667-88.32 28.587-127.147z", + "M736.969 512l-139.635 104.725v50.428c0 86.106 59.58 166.63 139.635 186.18 80.060-19.55 139.639-100.075 139.639-186.18v-50.428l-139.639-104.725z" + ], + "attrs": [], + "tags": [ + "person-manager" + ], + "defaultCode": 59895, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 753, + "id": 184, + "name": "person-manager", + "prevSize": 32, + "code": 59895 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 177 + }, + { + "icon": { + "paths": [ + "M469.333 512c94.255 0 170.667-76.412 170.667-170.667 0-94.257-76.412-170.667-170.667-170.667-94.257 0-170.667 76.41-170.667 170.667 0 94.255 76.41 170.667 170.667 170.667z", + "M497.92 555.52c-9.387-0.427-18.773-0.853-28.587-0.853-103.253 0-199.68 28.587-282.027 77.653-37.547 22.187-59.307 64-59.307 107.947v113.067h395.093c-33.707-48.213-53.76-107.093-53.76-170.667 0-45.653 10.667-88.32 28.587-127.147z", + "M725.76 512l170.24 170.667-170.24 170.667v-128h-171.093v-85.333h171.093v-128z" + ], + "attrs": [], + "tags": [ + "person-move" + ], + "defaultCode": 59896, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 754, + "id": 185, + "name": "person-move", + "prevSize": 32, + "code": 59896 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 178 + }, + { + "icon": { + "paths": [ + "M703.573 661.333v-74.667l-106.24 106.667 106.24 106.667v-74.667h235.093v-64h-235.093zM832.427 842.667h-235.093v64h235.093v74.667l106.24-106.667-106.24-106.667v74.667zM405.333 234.667c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM245.333 379.733l-117.333 601.6h89.6l74.667-341.333 91.733 85.333v256h85.333v-322.133l-87.467-87.467 25.6-128c55.467 68.267 138.667 110.933 232.533 110.933v-85.333c-78.933 0-147.2-42.667-185.6-104.533l-40.533-68.267c-14.933-25.6-42.667-40.533-72.533-40.533-10.667 0-21.333 2.133-32 6.4l-224 91.733v200.533h85.333v-142.933l74.667-32z" + ], + "attrs": [], + "tags": [ + "person-swap" + ], + "defaultCode": 59897, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 755, + "id": 186, + "name": "person-swap", + "prevSize": 32, + "code": 59897 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 179 + }, + { + "icon": { + "paths": [ + "M576 234.667c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM418.133 379.733l-119.467 601.6h89.6l76.8-341.333 89.6 85.333v256h85.333v-320l-89.6-85.333 25.6-128c55.467 64 140.8 106.667 234.667 106.667v-85.333c-81.067 0-149.333-42.667-183.467-102.4l-42.667-68.267c-17.067-25.6-42.667-42.667-72.533-42.667-12.8 0-21.333 4.267-34.133 4.267l-221.867 93.867v200.533h85.333v-145.067l76.8-29.867z" + ], + "attrs": [], + "tags": [ + "person-walk" + ], + "defaultCode": 59898, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 756, + "id": 187, + "name": "person-walk", + "prevSize": 32, + "code": 59898 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 180 + }, + { + "icon": { + "paths": [ + "M725.333 43.093l-426.667-0.427c-46.933 0-85.333 38.4-85.333 85.333v768c0 46.933 38.4 85.333 85.333 85.333h426.667c46.933 0 85.333-38.4 85.333-85.333v-768c0-46.933-38.4-84.907-85.333-84.907zM725.333 810.667h-426.667v-597.333h426.667v597.333z" + ], + "attrs": [], + "tags": [ + "phone" + ], + "defaultCode": 59899, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 757, + "id": 188, + "name": "phone", + "prevSize": 32, + "code": 59899 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 181 + }, + { + "icon": { + "paths": [ + "M682.643 383.999v-213.333h42.669c23.469 0 42.669-19.2 42.669-42.667s-19.2-42.667-42.669-42.667h-426.666c-23.467 0-42.667 19.2-42.667 42.667s19.2 42.667 42.667 42.667h42.667v213.333c0 70.827-57.174 128-128 128v85.333h254.72v298.668l42.666 42.662 42.667-42.662v-298.668h257.277v-85.333c-70.822 0-128-57.173-128-128z" + ], + "attrs": [], + "tags": [ + "pin" + ], + "defaultCode": 59696, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 758, + "id": 189, + "name": "pin", + "prevSize": 32, + "code": 59697 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 182 + }, + { + "icon": { + "paths": [ + "M675.84 389.12v-204.8h40.96c22.528 0 40.96-18.432 40.96-40.96s-18.432-40.96-40.96-40.96h-409.6c-22.528 0-40.96 18.432-40.96 40.96s18.432 40.96 40.96 40.96h40.96v204.8c0 67.994-54.886 122.88-122.88 122.88v81.92h244.531v286.72l40.96 40.96 40.96-40.96v-286.72h246.989v-81.92c-67.994 0-122.88-54.886-122.88-122.88z" + ], + "attrs": [], + "tags": [ + "pinboard" + ], + "defaultCode": 59729, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 759, + "id": 190, + "name": "pinboard", + "prevSize": 32, + "code": 59729 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 183 + }, + { + "icon": { + "paths": [ + "M810.667 554.667h-256v256h-85.333v-256h-256v-85.333h256v-256h85.333v256h256v85.333z" + ], + "attrs": [], + "tags": [ + "plus" + ], + "defaultCode": 59791, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 760, + "id": 191, + "name": "plus", + "prevSize": 32, + "code": 59791 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 184 + }, + { + "icon": { + "paths": [ + "M481.442 100.731c11.281 13.021 21.5 27.43 30.558 42.164 9.062-14.734 19.281-29.143 30.562-42.164 17.348-20.025 39.535-39.481 66.564-50.045 28.676-11.207 60.809-11.492 92.621 4.415 58.411 29.208 88.239 98.214 57.084 158.231h73.169c35.345 0 64 28.654 64 64s-28.655 64-64 64h-640c-35.346 0-64-28.654-64-64s28.654-64 64-64h73.17c-31.156-60.016-1.329-129.023 57.084-158.231 31.812-15.907 63.943-15.622 92.62-4.415 27.033 10.564 49.216 30.019 66.568 50.045zM342.56 176.86c7.808 12.146 36.154 34.040 107.868 30.324-9.399-17.927-20.796-35.934-33.481-50.576-12.474-14.398-23.867-22.822-33.135-26.443-7.621-2.979-14.506-3.185-23.395 1.259-23.753 11.877-25.943 32.858-17.858 45.435zM607.053 156.608c-12.685 14.642-24.081 32.649-33.481 50.576 71.714 3.715 100.062-18.179 107.87-30.324 8.085-12.577 5.892-33.558-17.86-45.436-8.887-4.445-15.774-4.238-23.394-1.259-9.267 3.621-20.659 12.045-33.135 26.443z", + "M819.2 384c42.24 0 76.8 33.28 76.8 73.954v406.758c0 40.674-34.56 73.954-76.8 73.954h-614.4c-42.24 0-76.8-33.28-76.8-73.954v-406.758c0-40.674 34.56-73.954 76.8-73.954h614.4zM554.667 469.333h-85.333v384h85.333v-384z" + ], + "attrs": [], + "tags": [ + "point-gift" + ], + "defaultCode": 59900, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 761, + "id": 192, + "name": "point-gift", + "prevSize": 32, + "code": 59900 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 185 + }, + { + "icon": { + "paths": [ + "M85.333 512c0-235.52 190.72-426.667 426.24-426.667 235.947 0 427.093 191.147 427.093 426.667s-191.147 426.667-427.093 426.667c-235.52 0-426.24-191.147-426.24-426.667zM481.067 704.64l30.933 28.16 31.774-28.71c109.397-99.413 181.559-164.988 181.559-245.423 0-65.707-51.627-117.333-117.333-117.333-37.12 0-72.747 17.28-96 44.587-23.253-27.307-58.88-44.587-96-44.587-65.707 0-117.333 51.627-117.333 117.333 0 80.572 72.414 146.24 182.131 245.73l0.269 0.243z" + ], + "attrs": [], + "tags": [ + "point-heart" + ], + "defaultCode": 59901, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 762, + "id": 193, + "name": "point-heart", + "prevSize": 32, + "code": 59901 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 186 + }, + { + "icon": { + "paths": [ + "M213.333 469.333h-170.667v469.333h170.667v-469.333z", + "M853.333 725.333h-298.667l-89.173-31.147 14.080-40.107 75.093 28.587h120.32c27.733 0 50.347-22.613 50.347-50.347 0-20.907-13.227-39.68-32.853-47.36l-309.76-115.627h-84.053v384.853l298.667 84.48 341.76-128c-0.427-46.933-38.4-85.333-85.76-85.333z", + "M789.333 128c-105.984 0-192 86.016-192 192s86.016 192 192 192c105.984 0 192-86.016 192-192s-86.016-192-192-192zM806.229 430.592v23.808h-33.6v-24.768c-14.208-3.456-45.888-14.784-57.984-56.832l31.68-12.864c1.152 4.224 11.136 40.128 46.080 40.128 17.856 0 38.016-9.216 38.016-30.912 0-18.432-13.44-28.032-43.776-38.976-21.12-7.488-64.32-19.776-64.32-63.552 0-1.92 0.192-46.080 50.304-56.832v-24.192h33.6v23.808c35.328 6.144 48.192 34.368 51.072 42.816l-30.336 12.864c-2.112-6.72-11.328-25.728-36.48-25.728-13.44 0-34.752 7.104-34.752 26.688 0 18.24 16.512 25.152 50.688 36.48 46.080 15.936 57.792 39.36 57.792 66.24 0 50.496-48 60.096-57.984 61.824z" + ], + "attrs": [], + "tags": [ + "point-money" + ], + "defaultCode": 59902, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 763, + "id": 194, + "name": "point-money", + "prevSize": 32, + "code": 59902 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 187 + }, + { + "icon": { + "paths": [ + "M511.573 85.333c-235.52 0-426.24 191.147-426.24 426.667s190.72 426.667 426.24 426.667c235.947 0 427.093-191.147 427.093-426.667s-191.147-426.667-427.093-426.667zM692.48 768l-180.48-108.8-180.48 108.8 47.787-205.227-159.147-137.813 209.92-17.92 81.92-193.707 81.92 193.28 209.92 17.92-159.147 137.813 47.787 205.653z" + ], + "attrs": [], + "tags": [ + "point-star" + ], + "defaultCode": 59903, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 764, + "id": 195, + "name": "point-star", + "prevSize": 32, + "code": 59903 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 188 + }, + { + "icon": { + "paths": [ + "M384 170.667c-188.587 0-341.333 152.747-341.333 341.333s152.747 341.333 341.333 341.333c188.587 0 341.333-152.747 341.333-341.333s-152.747-341.333-341.333-341.333zM512 448h-85.333v213.333h-85.333v-213.333h-85.333v-64h256v64zM864 160l117.333 53.333-117.333 53.333-53.333 117.333-53.333-117.333-117.333-53.333 117.333-53.333 53.333-117.333 53.333 117.333zM864 757.333l117.333 53.333-117.333 53.333-53.333 117.333-53.333-117.333-117.333-53.333 117.333-53.333 53.333-117.333 53.333 117.333z" + ], + "attrs": [], + "tags": [ + "point-token" + ], + "defaultCode": 59904, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 765, + "id": 196, + "name": "point-token", + "prevSize": 32, + "code": 59904 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 189 + }, + { + "icon": { + "paths": [ + "M870.071 558.033c13.154-22.272 20.595-46.033 20.595-74.756 0-66.024-55.825-128.372-128.73-128.372h-54.72c7.381-19.215 13.274-42.195 13.274-69.81 0-109.191-56.533-157.095-142.903-157.095-92.412 0-87.138 142.4-107.639 162.9-34.121 34.121-74.423 99.671-103.141 125.1h-148.14c-26.51 0-48 21.491-48 48v360c0 26.509 21.49 48 48 48h96c22.339 0 41.112-15.262 46.467-35.925 66.762 1.502 112.59 59.908 266.702 59.908 10.833 0 22.831 0.017 33.331 0.017 115.674 0 167.979-59.136 169.408-142.993 19.981-27.639 30.451-64.687 26.014-100.489 14.78-27.678 20.493-60.514 13.483-94.485zM777.446 638.78c18.842 31.697 1.89 74.116-20.911 86.357 11.55 73.169-26.411 98.846-79.68 98.846h-56.73c-107.456 0-177.041-56.73-257.459-56.73v-279.253h16.38c42.54 0 101.969-106.335 141.811-146.19 42.539-42.54 28.365-113.445 56.73-141.81 70.903 0 70.903 49.47 70.903 85.095 0 58.755-42.539 85.080-42.539 141.811h155.985c31.663 0 56.593 28.365 56.73 56.73 0.137 28.348-19.23 56.717-33.404 56.717 20.233 21.833 24.555 67.853-7.817 98.428zM302.667 776c0 19.883-16.117 35.998-36 35.998s-36-16.115-36-35.998c0-19.883 16.117-35.998 36-35.998s36 16.115 36 35.998z" + ], + "attrs": [], + "tags": [ + "positive" + ], + "defaultCode": 59697, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 766, + "id": 197, + "name": "positive", + "prevSize": 32, + "code": 59698 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 190 + }, + { + "icon": { + "paths": [ + "M810.667 128h-597.333c-22.632 0-44.337 8.99-60.34 24.994s-24.994 37.708-24.994 60.34v597.333c0 22.63 8.99 44.335 24.994 60.339s37.708 24.994 60.34 24.994h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-22.632-8.99-44.337-24.994-60.34s-37.709-24.994-60.339-24.994zM810.667 810.667h-597.333v-512h597.333v512zM576 554.667c0 35.413-28.587 64-64 64s-64-28.587-64-64c0-35.413 28.587-64 64-64s64 28.587 64 64zM512 384c-116.48 0-215.893 70.827-256 170.667 40.107 99.84 139.52 170.667 256 170.667s215.893-70.827 256-170.667c-40.107-99.84-139.52-170.667-256-170.667zM512 661.333c-28.288 0-55.42-11.238-75.426-31.241-20.003-20.006-31.241-47.138-31.241-75.426s11.238-55.42 31.241-75.426c20.006-20.002 47.138-31.241 75.426-31.241s55.42 11.238 75.426 31.241c20.002 20.006 31.241 47.138 31.241 75.426s-11.238 55.42-31.241 75.426c-20.006 20.002-47.138 31.241-75.426 31.241z" + ], + "attrs": [], + "tags": [ + "preview" + ], + "defaultCode": 59698, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 767, + "id": 198, + "name": "preview", + "prevSize": 32, + "code": 59699 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 191 + }, + { + "icon": { + "paths": [ + "M810.667 341.333h-597.333c-70.827 0-128 57.173-128 128v256h170.667v170.667h512v-170.667h170.667v-256c0-70.827-57.173-128-128-128zM682.667 810.667h-341.333v-213.333h341.333v213.333zM810.667 512c-23.467 0-42.667-19.2-42.667-42.667s19.2-42.667 42.667-42.667c23.467 0 42.667 19.2 42.667 42.667s-19.2 42.667-42.667 42.667zM768 128h-512v170.667h512v-170.667z" + ], + "attrs": [], + "tags": [ + "print" + ], + "defaultCode": 59792, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 768, + "id": 199, + "name": "print", + "prevSize": 32, + "code": 59792 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 192 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM512 213.333c70.827 0 128 57.173 128 128s-57.173 128-128 128c-70.827 0-128-57.173-128-128s57.173-128 128-128zM512 819.2c-106.667 0-200.96-54.613-256-137.387 1.28-84.907 170.667-131.413 256-131.413 84.907 0 254.72 46.507 256 131.413-55.040 82.773-149.333 137.387-256 137.387z" + ], + "attrs": [], + "tags": [ + "profile" + ], + "defaultCode": 59905, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 769, + "id": 200, + "name": "profile", + "prevSize": 32, + "code": 59905 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 193 + }, + { + "icon": { + "paths": [ + "M853.331 252.235h-341.331l-85.333-81.569h-256c-46.934 0-84.907 36.706-84.907 81.569l-0.427 489.41c0 44.864 38.4 81.568 85.334 81.568h682.664c46.938 0 85.338-36.704 85.338-81.568v-407.841c0-44.863-38.4-81.569-85.338-81.569zM853.331 741.645h-682.664v-489.41h220.586l85.334 81.569h376.744v407.841zM640 537.725c46.931 0 85.331-36.706 85.331-81.569s-38.4-81.569-85.331-81.569c-46.932 0-85.332 36.706-85.332 81.569s38.4 81.569 85.332 81.569zM469.334 700.864h341.335v-40.787c0-54.242-113.92-81.568-170.669-81.568-56.746 0-170.666 27.326-170.666 81.568v40.787z" + ], + "attrs": [], + "tags": [ + "project" + ], + "defaultCode": 59699, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 770, + "id": 201, + "name": "project", + "prevSize": 32, + "code": 59700 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 194 + }, + { + "icon": { + "paths": [ + "M213.333 170.667v85.333h597.333v-85.333h-597.333zM213.333 597.333h170.667v256h256v-256h170.667l-298.667-298.667-298.667 298.667z" + ], + "attrs": [], + "tags": [ + "publish" + ], + "defaultCode": 59906, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 771, + "id": 202, + "name": "publish", + "prevSize": 32, + "code": 59906 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 195 + }, + { + "icon": { + "paths": [ + "M308.224 757.76c-9.557 0-18.944-4.78-28.16-14.336s-13.824-19.116-13.824-28.672v-100.352h512v-348.16h102.4c9.556 0 18.772 4.779 27.648 14.336s13.312 19.456 13.312 29.696v610.304l-162.816-162.816h-450.56zM102.4 716.8v-571.392c0-9.557 4.437-19.115 13.312-28.672s18.091-14.336 27.648-14.336h531.456c10.24 0 19.796 4.608 28.672 13.824s13.312 18.944 13.312 29.184v364.544c0 9.556-4.436 19.116-13.312 28.672s-18.432 14.336-28.672 14.336h-408.576l-163.84 163.84zM655.36 491.52v-327.68h-491.52v409.6l75.776-81.92h415.744z" + ], + "attrs": [], + "tags": [ + "qchat" + ], + "defaultCode": 59730, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 772, + "id": 203, + "name": "qchat", + "prevSize": 32, + "code": 59730 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 196 + }, + { + "icon": { + "paths": [ + "M449.536 674.816l295.936-295.936-44.032-44.032-251.904 251.904-121.856-121.856-44.032 44.032 165.888 165.888zM204.8 880.64c-16.384 0-30.72-6.144-43.008-18.432s-18.432-26.624-18.432-43.008v-614.4c0-16.384 6.144-30.72 18.432-43.008s26.624-18.432 43.008-18.432h614.4c16.384 0 30.72 6.144 43.008 18.432s18.432 26.624 18.432 43.008v614.4c0 16.384-6.144 30.72-18.432 43.008s-26.624 18.432-43.008 18.432h-614.4zM204.8 819.2h614.4v-614.4h-614.4v614.4z" + ], + "attrs": [], + "tags": [ + "qcheck" + ], + "defaultCode": 59732, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 773, + "id": 204, + "name": "qcheck", + "prevSize": 32, + "code": 59732 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 197 + }, + { + "icon": { + "paths": [ + "M960 821.333c0 64.802-52.531 117.333-117.333 117.333h-618.667c-64.801 0-117.333-52.531-117.333-117.333v-597.334c0-64.801 52.532-117.333 117.333-117.333h203.857c44.442 0 85.069 25.11 104.943 64.86l38.165 54.991c9.033 18.068 27.499 29.482 47.701 29.482h224c64.802 0 117.333 52.532 117.333 117.333v448zM170.667 224v202.667h725.333v-53.333c0-29.455-23.876-53.333-53.333-53.333h-224c-44.442 0-85.069-25.11-104.947-64.861l-38.161-54.991c-9.033-18.068-27.503-29.482-47.701-29.482h-203.857c-29.455 0-53.333 23.878-53.333 53.333zM170.667 490.667v330.667c0 29.457 23.878 53.333 53.333 53.333h618.667c29.457 0 53.333-23.876 53.333-53.333v-330.667h-725.333z" + ], + "width": 1067, + "attrs": [], + "tags": [ + "qdocs" + ], + "defaultCode": 59733, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 774, + "id": 205, + "name": "qdocs", + "prevSize": 32, + "code": 59733 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 198 + }, + { + "icon": { + "paths": [ + "M533.333 896v-75.733l230.4-230.4 75.733 75.733-230.4 230.4h-75.733zM149.333 672v-64h320v64h-320zM885.333 619.733l-75.733-75.733 30.933-30.933c5.687-5.687 13.154-8.533 22.4-8.533s16.713 2.846 22.4 8.533l30.933 30.933c5.687 5.687 8.533 13.154 8.533 22.4s-2.846 16.713-8.533 22.4l-30.933 30.933zM149.333 496v-64h501.333v64h-501.333zM149.333 320v-64h501.333v64h-501.333z" + ], + "width": 1067, + "attrs": [], + "tags": [ + "qforms" + ], + "defaultCode": 59734, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 775, + "id": 206, + "name": "qforms", + "prevSize": 32, + "code": 59734 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 199 + }, + { + "icon": { + "paths": [ + "M102.4 266.24c0-45.243 36.677-81.92 81.92-81.92h655.36c45.244 0 81.92 36.677 81.92 81.92v491.52c0 45.244-36.676 81.92-81.92 81.92h-655.36c-45.243 0-81.92-36.676-81.92-81.92v-491.52zM184.32 245.76c-11.311 0-20.48 9.169-20.48 20.48v57.814l348.16 174.081 348.16-174.080v-57.815c0-11.311-9.171-20.48-20.48-20.48h-655.36zM163.84 392.746v365.014c0 11.309 9.169 20.48 20.48 20.48h655.36c11.309 0 20.48-9.171 20.48-20.48v-365.013l-348.16 174.078-348.16-174.079z" + ], + "attrs": [], + "tags": [ + "qnote" + ], + "defaultCode": 59723, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 776, + "id": 207, + "name": "qnote", + "prevSize": 32, + "code": 59723 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 200 + }, + { + "icon": { + "paths": [ + "M166.4 213.333h648.533c44.587 0 81.067 36 81.067 80v247.774c-38.746-58.274-100.732-99.763-172.727-111.236l91.661-56.538v-80l-324.267 200-324.267-200v80l294.379 181.568c-21.696 37.602-34.112 81.233-34.112 127.765 0 65.566 24.649 125.376 65.186 170.667h-325.453c-44.587 0-81.067-36.002-81.067-80l0.405-480c0-44 36.075-80 80.661-80z", + "M682.453 469.333c-117.76 0-213.12 95.573-213.12 213.333s95.36 213.333 213.12 213.333c117.973 0 213.547-95.573 213.547-213.333s-95.573-213.333-213.547-213.333zM682.667 853.333c-94.293 0-170.667-76.373-170.667-170.667s76.373-170.667 170.667-170.667c94.293 0 170.667 76.373 170.667 170.667s-76.373 170.667-170.667 170.667z", + "M693.333 576h-32v128l112 67.2 16-26.24-96-56.96v-112z" + ], + "attrs": [], + "tags": [ + "qnote-expired" + ], + "defaultCode": 59726, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 777, + "id": 208, + "name": "qnote-expired", + "prevSize": 32, + "code": 59726 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 201 + }, + { + "icon": { + "paths": [ + "M102.4 266.24c0-45.243 36.677-81.92 81.92-81.92h655.36c45.244 0 81.92 36.677 81.92 81.92v491.52c0 45.244-36.676 81.92-81.92 81.92h-655.36c-45.243 0-81.92-36.676-81.92-81.92v-491.52zM184.32 245.76c-11.311 0-20.48 9.169-20.48 20.48v57.814l348.16 174.081 348.16-174.080v-57.815c0-11.311-9.171-20.48-20.48-20.48h-655.36zM163.84 392.746v365.014c0 11.309 9.169 20.48 20.48 20.48h655.36c11.309 0 20.48-9.171 20.48-20.48v-365.013l-348.16 174.078-348.16-174.079z" + ], + "attrs": [], + "tags": [ + "qnotes" + ], + "defaultCode": 59735, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 778, + "id": 209, + "name": "qnotes", + "prevSize": 32, + "code": 59735 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 202 + }, + { + "icon": { + "paths": [ + "M405.333 277.333v128h-128v-128h128zM469.333 213.333h-256v256h256v-256zM405.333 618.667v128h-128v-128h128zM469.333 554.667h-256v256h256v-256zM746.667 277.333v128h-128v-128h128zM810.667 213.333h-256v256h256v-256zM554.667 554.667h64v64h-64v-64zM618.667 618.667h64v64h-64v-64zM682.667 554.667h64v64h-64v-64zM554.667 682.667h64v64h-64v-64zM618.667 746.667h64v64h-64v-64zM682.667 682.667h64v64h-64v-64zM746.667 618.667h64v64h-64v-64zM746.667 746.667h64v64h-64v-64zM938.667 298.667h-85.333v-128h-128v-85.333h213.333v213.333zM938.667 938.667v-213.333h-85.333v128h-128v85.333h213.333zM85.333 938.667h213.333v-85.333h-128v-128h-85.333v213.333zM85.333 85.333v213.333h85.333v-128h128v-85.333h-213.333z" + ], + "attrs": [], + "tags": [ + "qr" + ], + "defaultCode": 59907, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 779, + "id": 210, + "name": "qr", + "prevSize": 32, + "code": 59907 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 203 + }, + { + "icon": { + "paths": [ + "M320 64h-256v256h256v-256zM384 0v0 384h-384v-384h384zM128 128h128v128h-128zM960 64h-256v256h256v-256zM1024 0v0 384h-384v-384h384zM768 128h128v128h-128zM320 704h-256v256h256v-256zM384 640v0 384h-384v-384h384zM128 768h128v128h-128zM448 0h64v64h-64zM512 64h64v64h-64zM448 128h64v64h-64zM512 192h64v64h-64zM448 256h64v64h-64zM512 320h64v64h-64zM448 384h64v64h-64zM448 512h64v64h-64zM512 576h64v64h-64zM448 640h64v64h-64zM512 704h64v64h-64zM448 768h64v64h-64zM512 832h64v64h-64zM448 896h64v64h-64zM512 960h64v64h-64zM960 512h64v64h-64zM64 512h64v64h-64zM128 448h64v64h-64zM0 448h64v64h-64zM256 448h64v64h-64zM320 512h64v64h-64zM384 448h64v64h-64zM576 512h64v64h-64zM640 448h64v64h-64zM704 512h64v64h-64zM768 448h64v64h-64zM832 512h64v64h-64zM896 448h64v64h-64zM960 640h64v64h-64zM576 640h64v64h-64zM640 576h64v64h-64zM704 640h64v64h-64zM832 640h64v64h-64zM896 576h64v64h-64zM960 768h64v64h-64zM576 768h64v64h-64zM640 704h64v64h-64zM768 704h64v64h-64zM832 768h64v64h-64zM896 704h64v64h-64zM960 896h64v64h-64zM640 832h64v64h-64zM704 896h64v64h-64zM768 832h64v64h-64zM832 896h64v64h-64zM640 960h64v64h-64zM768 960h64v64h-64zM896 960h64v64h-64z" + ], + "attrs": [], + "tags": [ + "qrcode" + ], + "defaultCode": 59722, + "grid": 16 + }, + "attrs": [], + "properties": { + "ligatures": "qrcode", + "name": "qrcode", + "order": 780, + "id": 211, + "prevSize": 32, + "code": 59722 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 204 + }, + { + "icon": { + "paths": [ + "M64 503.842c0-242.918 196.924-439.842 439.842-439.842 242.921 0 439.844 196.924 439.844 439.842 0 242.921-196.924 439.844-439.844 439.844-242.918 0-439.842-196.924-439.842-439.844zM503.842 0c-278.263 0-503.842 225.579-503.842 503.842 0 278.266 225.579 503.844 503.842 503.844 278.266 0 503.844-225.579 503.844-503.844 0-278.263-225.579-503.842-503.844-503.842zM534.315 169.293l-28.717-58.404-114.076 232.006-254.788 37.161 184.356 180.375-43.552 254.874 228.060-120.346 228.058 120.346-43.55-254.874 184.356-180.375-254.788-37.161-85.359-173.602zM441.523 386.249l64.075-130.315 71.507 145.429 160.186 23.364-115.921 113.417 27.335 159.966-143.106-75.516-143.106 75.516 27.334-159.966-115.921-113.417 160.186-23.364 7.433-15.115z" + ], + "attrs": [], + "tags": [ + "qstar" + ], + "defaultCode": 59736, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 781, + "id": 212, + "name": "qstar", + "prevSize": 32, + "code": 59736 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 205 + }, + { + "icon": { + "paths": [ + "M170.667 256h-85.333v597.333c0 46.933 38.4 85.333 85.333 85.333h597.333v-85.333h-597.333v-597.333zM853.333 85.333h-512c-46.933 0-85.333 38.4-85.333 85.333v512c0 46.933 38.4 85.333 85.333 85.333h512c46.933 0 85.333-38.4 85.333-85.333v-512c0-46.933-38.4-85.333-85.333-85.333zM810.667 469.333h-170.667v170.667h-85.333v-170.667h-170.667v-85.333h170.667v-170.667h85.333v170.667h170.667v85.333z" + ], + "attrs": [], + "tags": [ + "queue" + ], + "defaultCode": 59793, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 782, + "id": 213, + "name": "queue", + "prevSize": 32, + "code": 59793 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 206 + }, + { + "icon": { + "paths": [ + "M452.608 962.56v-81.92h-247.808c-16.384 0-30.72-6.144-43.008-18.432s-18.432-26.624-18.432-43.008v-614.4c0-16.384 6.144-30.72 18.432-43.008s26.624-18.432 43.008-18.432h247.808v-81.92h61.44v901.12h-61.44zM204.8 776.192h247.808v-283.648l-247.808 283.648zM575.488 880.64v-384l243.712 279.552v-571.392h-243.712v-61.44h243.712c16.384 0 30.72 6.144 43.008 18.432s18.432 26.624 18.432 43.008v614.4c0 16.384-6.144 30.72-18.432 43.008s-26.624 18.432-43.008 18.432h-243.712z" + ], + "attrs": [], + "tags": [ + "qvisual" + ], + "defaultCode": 59737, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 783, + "id": 214, + "name": "qvisual", + "prevSize": 32, + "code": 59737 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 207 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM512 853.333c-188.587 0-341.333-152.747-341.333-341.333s152.747-341.333 341.333-341.333c188.587 0 341.333 152.747 341.333 341.333s-152.747 341.333-341.333 341.333z" + ], + "attrs": [], + "tags": [ + "radio-off" + ], + "defaultCode": 59794, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 784, + "id": 215, + "name": "radio-off", + "prevSize": 32, + "code": 59794 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 208 + }, + { + "icon": { + "paths": [ + "M512 298.667c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333zM512 85.333c-235.52 0-426.667 191.147-426.667 426.667s191.147 426.667 426.667 426.667c235.52 0 426.667-191.147 426.667-426.667s-191.147-426.667-426.667-426.667zM512 853.333c-188.587 0-341.333-152.747-341.333-341.333s152.747-341.333 341.333-341.333c188.587 0 341.333 152.747 341.333 341.333s-152.747 341.333-341.333 341.333z" + ], + "attrs": [], + "tags": [ + "radio-on" + ], + "defaultCode": 59795, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 785, + "id": 216, + "name": "radio-on", + "prevSize": 32, + "code": 59795 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 209 + }, + { + "icon": { + "paths": [ + "M102.4 880.64v-491.52h204.8v491.52h-204.8zM409.6 880.64v-737.28h204.8v737.28h-204.8zM716.8 880.64v-409.6h204.8v409.6h-204.8z" + ], + "attrs": [], + "tags": [ + "rar" + ], + "defaultCode": 59731, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 786, + "id": 217, + "name": "rar", + "prevSize": 32, + "code": 59731 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 210 + }, + { + "icon": { + "paths": [ + "M768 384v-85.333h-85.333v-177.493c-52.48-23.040-110.080-35.84-171.093-35.84-235.52 0-426.24 191.147-426.24 426.667s190.72 426.667 426.24 426.667c235.947 0 427.093-191.147 427.093-426.667 0-44.8-7.253-87.467-20.053-128h-150.613zM661.333 341.333c35.413 0 64 28.587 64 64s-28.587 64-64 64c-35.413 0-64-28.587-64-64s28.587-64 64-64zM362.667 341.333c35.413 0 64 28.587 64 64s-28.587 64-64 64c-35.413 0-64-28.587-64-64s28.587-64 64-64zM512 746.667c-99.413 0-183.893-62.293-218.027-149.333h436.053c-34.133 87.040-118.613 149.333-218.027 149.333zM938.667 128h85.333v85.333h-85.333v85.333h-85.333v-85.333h-85.333v-85.333h85.333v-85.333h85.333v85.333z" + ], + "attrs": [], + "tags": [ + "react-add" + ], + "defaultCode": 59908, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 787, + "id": 218, + "name": "react-add", + "prevSize": 32, + "code": 59908 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 211 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.699 0-426.667 190.968-426.667 426.667s190.968 426.667 426.667 426.667c235.699 0 426.667-190.967 426.667-426.667s-190.967-426.667-426.667-426.667zM512 856.085c-189.763 0-344.086-154.321-344.086-344.085s154.323-344.086 344.086-344.086c189.764 0 344.085 154.323 344.085 344.086s-154.321 344.085-344.085 344.085zM512 608.346c-57.805 0-112.172 25.459-149.333 69.85-14.624 17.549-12.215 43.524 5.333 58.15 17.548 14.622 43.527 12.386 58.15-5.163 42.667-51.098 129.033-51.098 171.699 0 13.935 16.687 39.915 20.471 58.15 5.163 17.549-14.626 19.785-40.602 5.333-58.15-37.163-44.39-91.529-69.85-149.333-69.85zM429.419 484.471c17.719 0 34.236-11.524 39.569-29.419 6.541-21.85-5.85-44.902-27.699-51.44l-137.632-41.29c-22.022-6.71-44.903 5.849-51.441 27.699s5.85 44.901 27.699 51.442l48.516 14.622c-5.333 8.431-9.118 17.894-9.118 28.561 0 30.451 24.602 55.053 55.054 55.053s55.053-24.777 55.053-55.228zM771.785 390.022c-6.537-21.85-29.419-34.237-51.439-27.699l-137.634 41.29c-21.85 6.537-34.24 29.59-27.699 51.44 5.333 17.894 21.85 29.419 39.569 29.419 0 30.451 24.602 55.057 55.053 55.057s55.053-24.606 55.053-55.057c0-10.667-3.785-20.126-9.118-28.557l48.516-14.626c21.85-6.366 34.236-29.417 27.699-51.267z" + ], + "attrs": [], + "tags": [ + "react-angry" + ], + "defaultCode": 59909, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 788, + "id": 219, + "name": "react-angry", + "prevSize": 32, + "code": 59909 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 212 + }, + { + "icon": { + "paths": [ + "M870.071 465.967c13.154 22.272 20.595 46.033 20.595 74.756 0 66.022-55.825 128.371-128.73 128.371h-54.72c7.381 19.217 13.274 42.197 13.274 69.811 0 109.188-56.533 157.094-142.903 157.094-92.412 0-87.138-142.4-107.639-162.901-34.121-34.121-74.423-99.669-103.141-125.099h-148.14c-26.51 0-48-21.491-48-48v-360c0-26.51 21.49-48 48-48h96c22.339 0 41.112 15.261 46.467 35.925 66.762-1.501 112.59-59.91 266.702-59.91 10.833 0 22.831-0.015 33.331-0.015 115.674 0 167.979 59.134 169.408 142.995 19.981 27.637 30.451 64.683 26.014 100.485 14.78 27.678 20.493 60.516 13.483 94.487zM777.446 385.22c18.842-31.695 1.89-74.115-20.911-86.355 11.55-73.17-26.411-98.85-79.68-98.85h-56.73c-107.456 0-177.041 56.73-257.459 56.73v279.255h16.38c42.54 0 101.969 106.334 141.811 146.189 42.539 42.543 28.365 113.446 56.73 141.811 70.903 0 70.903-49.472 70.903-85.094 0-58.756-42.539-85.082-42.539-141.811h155.985c31.663 0 56.593-28.365 56.73-56.73 0.137-28.348-19.23-56.717-33.404-56.717 20.233-21.833 24.555-67.852-7.817-98.428zM302.667 248c0-19.883-16.117-36-36-36s-36 16.117-36 36c0 19.883 16.117 36 36 36s36-16.117 36-36z" + ], + "attrs": [], + "tags": [ + "react-thumbsdown" + ], + "defaultCode": 59910, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 789, + "id": 220, + "name": "react-thumbsdown", + "prevSize": 32, + "code": 59910 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 213 + }, + { + "icon": { + "paths": [ + "M870.071 558.033c13.154-22.272 20.595-46.033 20.595-74.756 0-66.024-55.825-128.372-128.73-128.372h-54.72c7.381-19.215 13.274-42.195 13.274-69.81 0-109.191-56.533-157.095-142.903-157.095-92.412 0-87.138 142.4-107.639 162.9-34.121 34.121-74.423 99.671-103.141 125.1h-148.14c-26.51 0-48 21.491-48 48v360c0 26.509 21.49 48 48 48h96c22.339 0 41.112-15.262 46.467-35.925 66.762 1.502 112.59 59.908 266.702 59.908 10.833 0 22.831 0.017 33.331 0.017 115.674 0 167.979-59.136 169.408-142.993 19.981-27.639 30.451-64.687 26.014-100.489 14.78-27.678 20.493-60.514 13.483-94.485zM777.446 638.78c18.842 31.697 1.89 74.116-20.911 86.357 11.55 73.169-26.411 98.846-79.68 98.846h-56.73c-107.456 0-177.041-56.73-257.459-56.73v-279.253h16.38c42.54 0 101.969-106.335 141.811-146.19 42.539-42.54 28.365-113.445 56.73-141.81 70.903 0 70.903 49.47 70.903 85.095 0 58.755-42.539 85.080-42.539 141.811h155.985c31.663 0 56.593 28.365 56.73 56.73 0.137 28.348-19.23 56.717-33.404 56.717 20.233 21.833 24.555 67.853-7.817 98.428zM302.667 776c0 19.883-16.117 35.998-36 35.998s-36-16.115-36-35.998c0-19.883 16.117-35.998 36-35.998s36 16.115 36 35.998z" + ], + "attrs": [], + "tags": [ + "react-thumbsup" + ], + "defaultCode": 59911, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 790, + "id": 221, + "name": "react-thumbsup", + "prevSize": 32, + "code": 59911 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 214 + }, + { + "icon": { + "paths": [ + "M810.667 128h-178.347c-17.92-49.493-64.853-85.333-120.32-85.333s-102.4 35.84-120.32 85.333h-178.347c-46.933 0-85.333 38.4-85.333 85.333v597.333c0 46.933 38.4 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM512 117.333c9.387 0 17.493 4.267 23.467 10.667 5.12 5.547 8.533 13.227 8.533 21.333 0 17.493-14.507 32-32 32s-32-14.507-32-32c0-8.107 3.413-15.787 8.533-21.333 5.973-6.4 14.080-10.667 23.467-10.667zM810.667 810.667h-597.333v-597.333h597.333v597.333zM512 256c-70.4 0-128 57.6-128 128s57.6 128 128 128c70.4 0 128-57.6 128-128s-57.6-128-128-128zM512 426.667c-23.467 0-42.667-19.2-42.667-42.667s19.2-42.667 42.667-42.667c23.467 0 42.667 19.2 42.667 42.667s-19.2 42.667-42.667 42.667zM256 702.72v65.28h512v-65.28c0-106.667-169.387-152.747-256-152.747s-256 45.653-256 152.747zM354.56 682.667c29.44-23.893 101.547-47.787 157.44-47.787s128.427 23.893 157.44 47.787h-314.88z" + ], + "attrs": [], + "tags": [ + "reassign" + ], + "defaultCode": 59719, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 791, + "id": 222, + "name": "reassign", + "prevSize": 32, + "code": 59719 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 215 + }, + { + "icon": { + "paths": [ + "M810.667 298.667v170.667h-561.92l152.747-153.173-60.16-60.16-256 256 256 256 60.16-60.16-152.747-153.173h647.253v-256h-85.333z" + ], + "attrs": [], + "tags": [ + "recall" + ], + "defaultCode": 59796, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 792, + "id": 223, + "name": "recall", + "prevSize": 32, + "code": 59796 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 216 + }, + { + "icon": { + "paths": [ + "M448 298.667c-73.553 0-144.094 29.219-196.104 81.229s-81.229 122.551-81.229 196.104c0 73.553 29.219 144.094 81.229 196.105s122.551 81.229 196.104 81.229h149.333v-85.333h-149.333c-106.667 0-192-85.333-192-192s85.333-192 192-192h241.92l-131.413 131.84 60.16 60.16 234.667-234.667-234.667-234.667-60.587 60.16 131.84 131.84h-241.92zM768 768h-85.333v85.333h85.333v-85.333z" + ], + "attrs": [], + "tags": [ + "mdi-redo-variant" + ], + "defaultCode": 59685, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 719, + "id": 150, + "name": "redo-variant", + "prevSize": 32, + "code": 59685 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 217 + }, + { + "icon": { + "paths": [ + "M753.067 270.933c-61.867-61.867-146.773-100.267-241.067-100.267-188.586 0-340.906 152.747-340.906 341.333s152.32 341.333 340.906 341.333c159.147 0 291.84-108.8 329.813-256h-88.747c-34.987 99.413-129.707 170.667-241.067 170.667-141.226 0-256-114.773-256-256s114.773-256 256-256c70.827 0 133.973 29.44 180.053 75.947l-137.387 137.387h298.667v-298.667l-100.267 100.267z" + ], + "attrs": [], + "tags": [ + "refresh" + ], + "defaultCode": 59797, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 793, + "id": 224, + "name": "refresh", + "prevSize": 32, + "code": 59797 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 218 + }, + { + "icon": { + "paths": [ + "M383.997 341.334c46.932 0 85.332-38.4 85.332-85.333s-38.4-85.333-85.332-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM383.995 426.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.934 0 85.334-38.4 85.334-85.333s-38.4-85.333-85.334-85.333zM383.995 682.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.934 0 85.334-38.4 85.334-85.333s-38.4-85.333-85.334-85.333z", + "M646.912 341.334c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM646.912 426.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333zM646.912 682.667c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333z" + ], + "attrs": [], + "tags": [ + "reorder" + ], + "defaultCode": 59798, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 794, + "id": 225, + "name": "reorder", + "prevSize": 32, + "code": 59798 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 219 + }, + { + "icon": { + "paths": [ + "M298.667 298.667h426.667v128l170.667-170.667-170.667-170.667v128h-512v256h85.333v-170.667zM725.333 725.333h-426.667v-128l-170.667 170.667 170.667 170.667v-128h512v-256h-85.333v170.667z" + ], + "attrs": [], + "tags": [ + "repeat" + ], + "defaultCode": 59799, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 795, + "id": 226, + "name": "repeat", + "prevSize": 32, + "code": 59799 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 220 + }, + { + "icon": { + "paths": [ + "M213.333 128h597.333c46.933 0 85.333 38.4 85.333 85.333v597.333c0 46.933-38.4 85.333-85.333 85.333h-597.333c-46.933 0-85.333-38.4-85.333-85.333v-597.333c0-46.933 38.4-85.333 85.333-85.333zM213.333 810.667h597.333v-597.333h-597.333v597.333zM384 512h-85.333v213.333h85.333v-213.333zM725.333 298.667h-85.333v426.667h85.333v-426.667zM554.667 597.333h-85.333v128h85.333v-128zM554.667 426.667h-85.333v85.333h85.333v-85.333z" + ], + "attrs": [], + "tags": [ + "report" + ], + "defaultCode": 59700, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 796, + "id": 227, + "name": "report", + "prevSize": 32, + "code": 59701 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 221 + }, + { + "icon": { + "paths": [ + "M512 213.333v-128l-170.667 170.667 170.667 170.667v-128c141.227 0 256 114.773 256 256 0 126.72-92.587 231.68-213.333 252.16v86.187c168.533-20.907 298.667-164.267 298.667-338.347 0-188.587-152.747-341.333-341.333-341.333z", + "M256 554.667c0-70.4 28.587-134.4 75.093-180.907l-60.587-60.587c-61.44 61.867-99.84 147.2-99.84 241.493 0 174.080 130.133 317.44 298.667 338.347v-86.187c-120.747-20.48-213.333-125.44-213.333-252.16z" + ], + "attrs": [], + "tags": [ + "reset" + ], + "defaultCode": 59800, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 797, + "id": 228, + "name": "reset", + "prevSize": 32, + "code": 59800 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 222 + }, + { + "icon": { + "paths": [ + "M853.333 85.333h-682.667c-46.933 0-84.907 38.4-84.907 85.333l-0.427 768 170.667-170.667h597.333c46.933 0 85.333-38.4 85.333-85.333v-512c0-46.933-38.4-85.333-85.333-85.333zM853.333 682.667h-632.747l-49.92 49.92v-561.92h682.667v512zM448 597.333h320v-85.333h-234.667l-85.333 85.333zM612.693 346.88c8.533-8.533 8.533-21.76 0-30.293l-75.52-75.52c-8.533-8.533-21.76-8.533-30.293 0l-250.88 250.88v105.387h105.387l251.307-250.453z" + ], + "attrs": [], + "tags": [ + "review" + ], + "defaultCode": 59701, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 798, + "id": 229, + "name": "review", + "prevSize": 32, + "code": 59702 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 223 + }, + { + "icon": { + "paths": [ + "M562.684 484.868l-83.452-23.834c-2.931-0.845-5.513-2.62-7.356-5.056-1.839-2.441-2.837-5.41-2.842-8.461 0-7.735 6.298-14.033 14.067-14.033h54.63c5.999 0 11.802 1.28 17.101 3.699 8 3.682 17.284 2.85 23.518-3.383l29.201-29.201c8.781-8.783 7.782-23.8-2.586-30.633-15.834-10.45-33.916-16.85-52.966-19.1v-29.533c0-14.733-11.934-26.667-26.667-26.667h-26.667c-14.733 0-26.667 11.933-26.667 26.667v29.267c-50.483 6.033-88.95 51.633-82.2 105.082 4.833 38.251 34.433 68.851 71.516 79.45l83.452 23.834c2.931 0.845 5.513 2.62 7.356 5.056 1.839 2.441 2.837 5.41 2.842 8.461 0 7.735-6.298 14.033-14.067 14.033h-54.63c-5.999 0-11.802-1.28-17.101-3.699-8-3.682-17.284-2.85-23.518 3.383l-29.199 29.201c-8.783 8.781-7.784 23.799 2.583 30.635 15.834 10.449 33.916 16.849 52.967 19.098v29.534c0 14.733 11.934 26.667 26.667 26.667h26.667c14.733 0 26.667-11.934 26.667-26.667v-29.265c50.483-6.033 88.951-51.635 82.202-105.084-4.834-38.251-34.436-68.851-71.518-79.45zM938.667 512c0-60.834-31.334-114-77.833-144.5 11.332-54.833-4.501-114.5-47.168-157.167-42.667-42.833-102.498-58.5-157.167-47.167-30.831-46.667-83.998-77.833-144.499-77.833s-113.833 31.167-144.5 77.833c-54.833-11.334-114.5 4.5-157.167 47.167-42.833 42.667-58.5 102.5-47.167 157.167-46.833 30.833-77.833 84.165-77.833 144.5 0 60.501 31.167 113.835 77.833 144.499-11.334 54.835 4.5 114.5 47.167 157.167 42.667 42.833 102 58.5 157.167 47.168 30.833 46.831 84.165 77.833 144.5 77.833 60.668 0 113.835-31.334 144.499-77.833 55.168 11.332 114.5-4.501 157.167-47.168 42.833-42.667 58.5-102.498 47.168-157.167 46.831-30.831 77.833-84.164 77.833-144.499zM758.165 614.165c15.168 33.169 50.5 91.337-1.165 143.002-46.669 46.665-92.169 24.166-143.002 1.165-12.834 34.334-28.834 100.335-101.999 100.335-75.499 0-91.167-71.334-102-100.335-35.167 16-91.5 50.334-143.167-1.331-53.333-53.333-14-114.833-1.167-143.002-34.333-12.834-100.333-28.834-100.333-101.999 0-75.499 71.334-91.167 100.5-102.167-15.167-33.167-50.5-91.334 1.167-143 53.333-53.333 114.833-14 143-1.167 12.833-34.333 28.835-100.333 102-100.333 75.499 0 91.166 71.334 101.999 100.667 33.169-15.167 91.332-50.5 143.002 1.167 53.333 53.333 13.999 114.833 1.165 143 34.334 12.833 100.335 28.835 100.335 102 0 75.499-71.334 91.166-100.335 101.999z" + ], + "attrs": [], + "tags": [ + "sales" + ], + "defaultCode": 59702, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 799, + "id": 230, + "name": "sales", + "prevSize": 32, + "code": 59703 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 224 + }, + { + "icon": { + "paths": [ + "M725.333 128h-512c-47.36 0-85.333 38.4-85.333 85.333v597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-512l-170.667-170.667zM512 810.667c-70.827 0-128-57.173-128-128s57.173-128 128-128c70.827 0 128 57.173 128 128s-57.173 128-128 128zM640 384h-426.667v-170.667h426.667v170.667z" + ], + "attrs": [], + "tags": [ + "save" + ], + "defaultCode": 59912, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 800, + "id": 231, + "name": "save", + "prevSize": 32, + "code": 59912 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 225 + }, + { + "icon": { + "paths": [ + "M511.574 85.334c-235.52 0-426.24 191.147-426.24 426.667s190.72 426.668 426.24 426.668c235.946 0 427.095-191.149 427.095-426.668s-191.149-426.667-427.095-426.667zM512.001 853.331c-188.587 0-341.333-152.742-341.333-341.331 0-188.587 152.746-341.333 341.333-341.333s341.331 152.746 341.331 341.333c0 188.588-152.742 341.331-341.331 341.331z", + "M533.334 298.666h-64v256l223.997 134.403 32-52.483-191.997-113.92v-224z" + ], + "attrs": [], + "tags": [ + "schedule" + ], + "defaultCode": 59703, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 801, + "id": 232, + "name": "schedule", + "prevSize": 32, + "code": 59704 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 226 + }, + { + "icon": { + "paths": [ + "M196.923 905.846c-15.754 0-29.538-5.908-41.354-17.723s-17.723-25.6-17.723-41.354v-610.462c0-15.754 5.908-29.538 17.723-41.354s25.6-17.723 41.354-17.723h64v-59.077h64v59.077h334.769v-59.077h64v59.077h64c15.754 0 29.538 5.908 41.354 17.723s17.723 25.6 17.723 41.354v610.462c0 15.754-5.908 29.538-17.723 41.354s-25.6 17.723-41.354 17.723h-590.769zM196.923 846.769h590.769v-423.385h-590.769v423.385z" + ], + "width": 985, + "attrs": [], + "tags": [ + "scheduling" + ], + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 802, + "id": 233, + "name": "scheduling", + "prevSize": 32, + "code": 59806 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 227 + }, + { + "icon": { + "paths": [ + "M661.333 597.333h-33.707l-11.947-11.52c41.813-48.64 66.987-111.787 66.987-180.48 0-153.173-124.16-277.333-277.333-277.333s-277.333 124.16-277.333 277.333c0 153.173 124.16 277.333 277.333 277.333 68.693 0 131.84-25.173 180.48-66.987l11.52 11.947v33.707l213.333 212.907 63.573-63.573-212.907-213.333zM405.333 597.333c-106.24 0-192-85.76-192-192s85.76-192 192-192c106.24 0 192 85.76 192 192s-85.76 192-192 192z" + ], + "attrs": [], + "tags": [ + "search" + ], + "defaultCode": 59704, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 803, + "id": 234, + "name": "search", + "prevSize": 32, + "code": 59705 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 228 + }, + { + "icon": { + "paths": [ + "M298.667 384h-213.333v-85.333h213.333v85.333zM298.667 512h-213.333v85.333h213.333v-85.333zM878.507 810.667l-163.413-163.413c-34.133 22.187-74.24 35.413-117.76 35.413-117.76 0-213.333-95.573-213.333-213.333s95.573-213.333 213.333-213.333c117.76 0 213.333 95.573 213.333 213.333 0 43.52-13.227 83.627-35.413 117.333l163.413 163.84-60.16 60.16zM725.333 469.333c0-70.4-57.6-128-128-128s-128 57.6-128 128c0 70.4 57.6 128 128 128s128-57.6 128-128zM85.333 810.667h426.667v-85.333h-426.667v85.333z" + ], + "attrs": [], + "tags": [ + "search-advance" + ], + "defaultCode": 59721, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 804, + "id": 235, + "name": "search-advance", + "prevSize": 32, + "code": 59721 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 229 + }, + { + "icon": { + "paths": [ + "M512 42.667l-384 170.667v256c0 236.8 163.84 458.24 384 512 220.16-53.76 384-275.2 384-512v-256l-384-170.667zM512 511.573h298.667c-22.613 175.787-139.947 332.373-298.667 381.44v-381.013h-298.667v-243.2l298.667-132.693v375.467z" + ], + "attrs": [], + "tags": [ + "security" + ], + "grid": 16, + "defaultCode": 59812 + }, + "attrs": [], + "properties": { + "order": 567, + "id": 280, + "name": "security", + "prevSize": 32, + "code": 59812 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 230 + }, + { + "icon": { + "paths": [ + "M85.76 896l895.573-384-895.573-384-0.427 298.667 640 85.333-640 85.333 0.427 298.667z" + ], + "attrs": [], + "tags": [ + "send" + ], + "defaultCode": 59705, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 805, + "id": 236, + "name": "send", + "prevSize": 32, + "code": 59706 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 231 + }, + { + "icon": { + "paths": [ + "M816.64 551.936c1.536-12.8 2.56-26.112 2.56-39.936s-1.024-27.136-3.072-39.936l86.528-67.584c7.68-6.144 9.728-17.408 5.12-26.112l-81.92-141.824c-5.12-9.216-15.872-12.288-25.088-9.216l-101.888 40.96c-21.504-16.384-44.032-29.696-69.12-39.936l-15.36-108.544c-1.536-10.24-10.24-17.408-20.48-17.408h-163.84c-10.241 0-18.432 7.168-19.968 17.408l-15.36 108.544c-25.088 10.24-48.128 24.064-69.12 39.936l-101.888-40.96c-9.216-3.584-19.968 0-25.088 9.216l-81.92 141.824c-5.12 9.216-3.072 19.968 5.12 26.112l86.528 67.584c-2.048 12.8-3.584 26.624-3.584 39.936s1.024 27.136 3.072 39.936l-86.528 67.584c-7.68 6.144-9.728 17.408-5.12 26.112l81.92 141.824c5.12 9.216 15.872 12.288 25.088 9.216l101.888-40.96c21.504 16.384 44.032 29.696 69.12 39.936l15.36 108.544c2.048 10.24 10.24 17.408 20.48 17.408h163.84c10.24 0 18.944-7.168 19.968-17.408l15.36-108.544c25.088-10.24 48.128-24.064 69.12-39.936l101.888 40.96c9.216 3.584 19.968 0 25.088-9.216l81.92-141.824c5.12-9.216 3.072-19.968-5.12-26.112l-85.504-67.584zM512 665.6c-84.48 0-153.601-69.12-153.601-153.6s69.121-153.6 153.601-153.6c84.48 0 153.6 69.12 153.6 153.6s-69.12 153.6-153.6 153.6z" + ], + "attrs": [], + "tags": [ + "settings-gear" + ], + "defaultCode": 59803, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 806, + "id": 237, + "name": "settings-gear", + "prevSize": 32, + "code": 59803 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 232 + }, + { + "icon": { + "paths": [ + "M128 725.333v85.333h256v-85.333h-256zM128 213.333v85.333h426.667v-85.333h-426.667zM554.667 896v-85.333h341.333v-85.333h-341.333v-85.333h-85.333v256h85.333zM298.667 384v85.333h-170.667v85.333h170.667v85.333h85.333v-256h-85.333zM896 554.667v-85.333h-426.667v85.333h426.667zM640 384h85.333v-85.333h170.667v-85.333h-170.667v-85.333h-85.333v256z" + ], + "attrs": [], + "tags": [ + "settings-tune" + ], + "defaultCode": 59804, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 807, + "id": 238, + "name": "settings-tune", + "prevSize": 32, + "code": 59804 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 233 + }, + { + "icon": { + "paths": [ + "M768 686.080c-32.427 0-61.44 12.8-83.627 32.853l-304.213-177.067c2.133-9.813 3.84-19.627 3.84-29.867s-1.707-20.053-3.84-29.867l300.8-175.36c23.040 21.333 53.333 34.56 87.040 34.56 70.827 0 128-57.173 128-128s-57.173-128-128-128c-70.827 0-128 57.173-128 128 0 10.24 1.707 20.053 3.84 29.867l-300.8 175.36c-23.040-21.333-53.333-34.56-87.040-34.56-70.827 0-128 57.173-128 128s57.173 128 128 128c33.707 0 64-13.227 87.040-34.56l303.787 177.493c-2.133 8.96-3.413 18.347-3.413 27.733 0 68.693 55.893 124.587 124.587 124.587s124.587-55.893 124.587-124.587c0-68.693-55.893-124.587-124.587-124.587z" + ], + "attrs": [], + "tags": [ + "share" + ], + "defaultCode": 59915, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 808, + "id": 239, + "name": "share", + "prevSize": 32, + "code": 59915 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 234 + }, + { + "icon": { + "paths": [ + "M682.667 213.333l-60.587 60.587-67.84-67.84v476.587h-84.48v-476.587l-67.84 67.84-60.587-60.587 170.667-170.667 170.667 170.667zM853.333 426.667v469.333c0 46.933-38.4 85.333-85.333 85.333h-512c-47.36 0-85.333-38.4-85.333-85.333v-469.333c0-47.36 37.973-85.333 85.333-85.333h128v85.333h-128v469.333h512v-469.333h-128v-85.333h128c46.933 0 85.333 37.973 85.333 85.333z" + ], + "attrs": [], + "tags": [ + "share-file" + ], + "defaultCode": 59914, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 809, + "id": 240, + "name": "share-file", + "prevSize": 32, + "code": 59914 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 235 + }, + { + "icon": { + "paths": [ + "M725.333 341.333l-60.16 60.16 67.413 67.84h-348.587v85.333h348.587l-67.413 67.413 60.16 60.587 170.667-170.667-170.667-170.667zM213.333 213.333h298.667v-85.333h-298.667c-46.933 0-85.333 38.4-85.333 85.333v597.333c0 46.933 38.4 85.333 85.333 85.333h298.667v-85.333h-298.667v-597.333z" + ], + "attrs": [], + "tags": [ + "sign out" + ], + "defaultCode": 59706, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 810, + "id": 241, + "name": "sign-out", + "prevSize": 32, + "code": 59707 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 236 + }, + { + "icon": { + "paths": [ + "M512 512c90.485 0 163.84-73.355 163.84-163.84 0-90.486-73.355-163.84-163.84-163.84s-163.84 73.354-163.84 163.84c0 90.485 73.355 163.84 163.84 163.84z", + "M539.443 553.779c-9.011-0.41-18.022-0.819-27.443-0.819-99.123 0-191.693 27.443-270.746 74.547-36.045 21.299-56.934 61.44-56.934 103.629v169.984h379.29c-32.358-46.285-51.61-164.25-51.61-225.28 0-43.827 10.24-84.787 27.443-122.061z", + "M707.49 534.344l-134.050 100.536v48.411c0 82.661 57.197 159.965 134.050 178.733 76.857-18.768 134.054-96.072 134.054-178.733v-48.411l-134.054-100.536z" + ], + "attrs": [], + "tags": [ + "sm" + ], + "defaultCode": 59739, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 811, + "id": 242, + "name": "sm", + "prevSize": 32, + "code": 59739 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 237 + }, + { + "icon": { + "paths": [ + "M576 704l45.248-45.248 114.752 114.752v-645.504h64v645.504l114.752-114.752 45.248 45.248-192 192-192-192z", + "M64 576h448v64h-448v-64z", + "M192 384h320v64h-320v-64z", + "M320 192h192v64h-192v-64z" + ], + "attrs": [], + "tags": [ + "sort" + ], + "defaultCode": 59707, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 812, + "id": 243, + "name": "sort", + "prevSize": 32, + "code": 59708 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 238 + }, + { + "icon": { + "paths": [ + "M480 64c-265.096 0-480 214.904-480 480 0 265.098 214.904 480 480 480 265.098 0 480-214.902 480-480 0-265.096-214.902-480-480-480zM751.59 704c8.58-40.454 13.996-83.392 15.758-128h127.446c-3.336 44.196-13.624 87.114-30.68 128h-112.524zM208.41 384c-8.58 40.454-13.996 83.392-15.758 128h-127.444c3.336-44.194 13.622-87.114 30.678-128h112.524zM686.036 384c9.614 40.962 15.398 83.854 17.28 128h-191.316v-128h174.036zM512 320v-187.338c14.59 4.246 29.044 11.37 43.228 21.37 26.582 18.74 52.012 47.608 73.54 83.486 14.882 24.802 27.752 52.416 38.496 82.484h-155.264zM331.232 237.516c21.528-35.878 46.956-64.748 73.54-83.486 14.182-10 28.638-17.124 43.228-21.37v187.34h-155.264c10.746-30.066 23.616-57.68 38.496-82.484zM448 384v128h-191.314c1.88-44.146 7.666-87.038 17.278-128h174.036zM95.888 704c-17.056-40.886-27.342-83.804-30.678-128h127.444c1.762 44.608 7.178 87.546 15.758 128h-112.524zM256.686 576h191.314v128h-174.036c-9.612-40.96-15.398-83.854-17.278-128zM448 768v187.34c-14.588-4.246-29.044-11.372-43.228-21.37-26.584-18.74-52.014-47.61-73.54-83.486-14.882-24.804-27.75-52.418-38.498-82.484h155.266zM628.768 850.484c-21.528 35.876-46.958 64.746-73.54 83.486-14.184 9.998-28.638 17.124-43.228 21.37v-187.34h155.266c-10.746 30.066-23.616 57.68-38.498 82.484zM512 704v-128h191.314c-1.88 44.146-7.666 87.040-17.28 128h-174.034zM767.348 512c-1.762-44.608-7.178-87.546-15.758-128h112.524c17.056 40.886 27.344 83.806 30.68 128h-127.446zM830.658 320h-95.9c-18.638-58.762-44.376-110.294-75.316-151.428 42.536 20.34 81.058 47.616 114.714 81.272 21.48 21.478 40.362 44.938 56.502 70.156zM185.844 249.844c33.658-33.658 72.18-60.932 114.714-81.272-30.942 41.134-56.676 92.666-75.316 151.428h-95.898c16.138-25.218 35.022-48.678 56.5-70.156zM129.344 768h95.898c18.64 58.762 44.376 110.294 75.318 151.43-42.536-20.34-81.058-47.616-114.714-81.274-21.48-21.478-40.364-44.938-56.502-70.156zM774.156 838.156c-33.656 33.658-72.18 60.934-114.714 81.274 30.942-41.134 56.678-92.668 75.316-151.43h95.9c-16.14 25.218-35.022 48.678-56.502 70.156z" + ], + "attrs": [], + "tags": [ + "sphere", + "globe", + "internet" + ], + "defaultCode": 59849, + "grid": 16 + }, + "attrs": [], + "properties": { + "ligatures": "sphere, globe", + "name": "sphere", + "order": 813, + "id": 244, + "prevSize": 32, + "code": 59849 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 239 + }, + { + "icon": { + "paths": [ + "M511.995 736.851l263.679 159.142-69.971-299.946 232.96-201.813-306.774-26.027-119.893-282.88-119.893 282.88-306.774 26.027 232.96 201.813-69.973 299.946 263.68-159.142z" + ], + "attrs": [], + "tags": [ + "star" + ], + "defaultCode": 59708, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 814, + "id": 245, + "name": "star", + "prevSize": 32, + "code": 59709 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 240 + }, + { + "icon": { + "paths": [ + "M512 687.019l199.155 120.201-52.851-226.551 175.991-152.461-231.646-19.655-90.65-213.881-90.65 213.881-231.647 19.655 175.992 152.461-52.852 226.551 199.157-120.201zM248.32 896l69.973-299.947-232.96-201.813 306.773-26.027 119.893-282.88 119.893 282.88 306.773 26.027-232.96 201.813 69.973 299.947-263.68-159.147-263.68 159.147z" + ], + "attrs": [], + "tags": [ + "start-outlined" + ], + "defaultCode": 59718, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 815, + "id": 246, + "name": "start-outlined", + "prevSize": 32, + "code": 59718 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 241 + }, + { + "icon": { + "paths": [ + "M853.333 170.667h-682.667v85.333h682.667v-85.333zM896 597.333v-85.333l-42.667-213.333h-682.667l-42.667 213.333v85.333h42.667v256h426.667v-256h170.667v256h85.333v-256h42.667zM512 768h-256v-170.667h256v170.667z" + ], + "attrs": [], + "tags": [ + "store" + ], + "defaultCode": 59917, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 816, + "id": 247, + "name": "store", + "prevSize": 32, + "code": 59917 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 242 + }, + { + "icon": { + "paths": [ + "M170.667 170.667v85.333h682.667v-85.333h-682.667zM519.42 853.333h-348.753v-256h-42.667v-85.333l42.667-213.333h682.667l42.667 213.333v21.257c-32.836-13.696-68.868-21.257-106.667-21.257-153.169 0-277.333 124.164-277.333 277.333 0 22.029 2.569 43.456 7.42 64zM256 768h256v-170.667h-256v170.667z", + "M768 618.667v-64l-85.333 85.333 85.333 85.333v-64c70.613 0 128 57.387 128 128 0 21.547-5.333 42.027-14.933 59.733l31.147 31.147c16.64-26.24 26.453-57.387 26.453-90.88 0-94.293-76.373-170.667-170.667-170.667zM768 917.333c-70.613 0-128-57.387-128-128 0-21.547 5.333-42.027 14.933-59.733l-31.147-31.147c-16.64 26.24-26.453 57.387-26.453 90.88 0 94.293 76.373 170.667 170.667 170.667v64l85.333-85.333-85.333-85.333v64z" + ], + "attrs": [], + "tags": [ + "store-swap" + ], + "defaultCode": 59916, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 817, + "id": 248, + "name": "store-swap", + "prevSize": 32, + "code": 59916 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 243 + }, + { + "icon": { + "paths": [ + "M292.267 302.080c0-115.627 110.933-174.080 229.973-174.080 69.973 0 128 20.907 166.4 54.613 32.853 27.733 62.293 73.813 62.293 138.24h-128.427c0-13.227-2.133-25.173-6.4-36.267-12.373-36.693-51.2-54.613-96-54.613-79.36 0-99.84 43.52-99.84 72.533 0 20.48 10.667 37.547 31.573 51.627 16.213 10.667 32.853 20.48 60.16 29.867h-196.693c-8.96-14.507-23.040-37.973-23.040-81.92zM896 512v-85.333h-768v85.333h410.453c49.067 19.2 83.627 32 83.627 84.053 0 42.667-34.56 71.253-97.28 71.253-65.707 0-125.013-23.040-125.013-107.093h-126.72c0 23.467 3.413 48.213 10.24 67.413 34.56 97.707 140.373 140.8 241.92 140.8 96.853 0 226.133-37.973 226.133-172.8 0-12.8-0.427-49.493-20.48-82.773h165.12v-0.853z" + ], + "attrs": [], + "tags": [ + "ic-baseline-strikethrough-s" + ], + "defaultCode": 59677, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 688, + "id": 119, + "name": "strikethrough", + "prevSize": 32, + "code": 59678 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 244 + }, + { + "icon": { + "paths": [ + "M65.142 967.842c-19.515 0.006-36.594-7.304-51.235-21.934s-21.967-31.705-21.974-51.221l-0.186-536.674c-0.006-19.515 7.304-36.594 21.936-51.236s31.704-21.965 51.22-21.973l829.403-0.286c19.515-0.008 36.598 7.304 51.241 21.935 14.636 14.632 21.962 31.705 21.968 51.22l0.189 536.674c0.006 19.515-7.304 36.591-21.934 51.234s-31.705 21.968-51.221 21.976l-829.407 0.285zM65.115 894.659l829.407-0.285-0.189-536.675-829.404 0.286 0.186 536.674zM390.751 807.944l269.496-181.832-269.622-180.421 0.126 362.252zM75.857 211.615l-0.027-73.183 807.45-0.279 0.028 73.183-807.451 0.279zM235.587 65.196l-0.027-73.183 487.887-0.168 0.028 73.183-487.889 0.168zM65.115 894.659v0z" + ], + "attrs": [], + "tags": [ + "subscription" + ], + "grid": 16, + "defaultCode": 59813 + }, + "attrs": [], + "properties": { + "order": 568, + "id": 281, + "name": "subscription", + "prevSize": 32, + "code": 59813 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 245 + }, + { + "icon": { + "paths": [ + "M490.24 682.667c37.547 0 72.533-11.093 101.973-29.867l104.107 104.107 60.587-60.587-104.107-103.68c18.773-29.867 29.867-64.427 29.867-101.973 0-106.24-85.76-192-192-192s-192 85.76-192 192c0 106.24 85.76 192 191.573 192zM490.667 384c28.288 0 55.42 11.238 75.426 31.242 20.002 20.005 31.241 47.137 31.241 75.425s-11.238 55.42-31.241 75.426c-20.006 20.002-47.138 31.241-75.426 31.241s-55.42-11.238-75.425-31.241c-20.004-20.006-31.242-47.138-31.242-75.426s11.238-55.42 31.242-75.425c20.005-20.004 47.137-31.242 75.425-31.242zM853.333 170.667h-682.667c-46.933 0-85.333 38.4-85.333 85.333v512c0 46.933 38.4 85.333 85.333 85.333h682.667c46.933 0 85.333-38.4 85.333-85.333v-512c0-46.933-38.4-85.333-85.333-85.333zM853.333 768h-682.667v-512h682.667v512z" + ], + "attrs": [], + "tags": [ + "summary" + ], + "defaultCode": 59709, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 818, + "id": 249, + "name": "summary", + "prevSize": 32, + "code": 59710 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 246 + }, + { + "icon": { + "paths": [ + "M464 632v-215.46c0-26.505-21.495-48-48-48h-72c-53.025 0-96 42.975-96 96.002v119.458c0 53.026 42.975 96 96 96h72c26.505 0 48-21.495 48-48zM392 608h-48c-13.23 0-24-10.769-24-24v-119.458c0-13.231 10.77-24 24-24h48v167.458zM512 128c-214.23 0-377.13 178.245-384 384v24c0 13.261 10.74 24 24 24h24c13.26 0 24-10.739 24-24v-24c0-172.035 139.965-312 312-312 172.036 0 312 139.965 312 312h-0.179c0.119 3.644 0.179 248.58 0.179 248.58 0 35.025-28.395 63.42-63.42 63.42h-152.58c0-39.765-32.235-72-72-72h-48c-39.765 0-72 32.235-72 72s32.235 72 72 72h272.58c74.79 0 135.42-60.629 135.42-135.42v-248.58c-6.869-205.755-169.771-384-384-384zM680 680c53.026 0 96-42.974 96-96v-119.458c0-53.027-42.974-96.002-96-96.002h-72c-26.505 0-48 21.495-48 48v215.46c0 26.505 21.495 48 48 48h72zM632 440.542h48c13.231 0 24 10.769 24 24v119.458c0 13.231-10.769 24-24 24h-48v-167.458z" + ], + "attrs": [], + "tags": [ + "support" + ], + "defaultCode": 59710, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 819, + "id": 250, + "name": "support", + "prevSize": 32, + "code": 59711 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 247 + }, + { + "icon": { + "paths": [ + "M787.692 420.431h59.077v-203.815c0-17.067-5.581-31.18-16.738-42.338s-25.273-16.738-42.338-16.738h-198.892c-4.596-22.974-15.919-41.846-33.969-56.615s-38.892-22.154-62.523-22.154c-23.631 0-44.473 7.385-62.523 22.154s-29.373 33.641-33.969 56.615h-198.892c-17.067 0-31.18 5.58-42.338 16.738s-16.738 25.272-16.738 42.338v590.769c0 17.065 5.58 31.181 16.738 42.338s25.272 16.738 42.338 16.738h247.138v-59.077h-247.138v-590.769h59.077v88.615h472.615v-88.615h59.077v203.815zM520.369 205.292c-7.55 7.549-16.904 11.323-28.062 11.323s-20.512-3.774-28.062-11.323c-7.55-7.549-11.323-16.903-11.323-28.062s3.773-20.513 11.323-28.062c7.55-7.549 16.904-11.323 28.062-11.323s20.512 3.774 28.062 11.323c7.55 7.549 11.323 16.903 11.323 28.062s-3.773 20.513-11.323 28.062z", + "M649.846 886.15c32.689 0 63.409-6.203 92.16-18.609s53.76-29.243 75.028-50.511c21.268-21.268 38.105-46.277 50.511-75.028s18.609-59.471 18.609-92.16c0-32.685-6.203-63.405-18.609-92.156s-29.243-53.76-50.511-75.028c-21.268-21.268-46.277-38.105-75.028-50.511s-59.471-18.609-92.16-18.609c-32.689 0-63.409 6.203-92.16 18.609s-53.76 29.243-75.028 50.511c-21.268 21.268-38.105 46.277-50.511 75.028s-18.609 59.471-18.609 92.156c0 32.689 6.203 63.409 18.609 92.16s29.243 53.76 50.511 75.028c21.268 21.268 46.277 38.105 75.028 50.511s59.471 18.609 92.16 18.609zM827.077 649.846c0 97.883-79.348 177.231-177.231 177.231s-177.231-79.348-177.231-177.231c0-97.883 79.348-177.231 177.231-177.231s177.231 79.348 177.231 177.231z" + ], + "width": 985, + "attrs": [], + "tags": [ + "surveys" + ], + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 820, + "id": 251, + "name": "surveys", + "prevSize": 32, + "code": 59807 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 248 + }, + { + "icon": { + "paths": [ + "M768 512l170.667-170.667-170.667-170.667v128h-640v85.333h640v128z", + "M256 512l-170.667 170.667 170.667 170.667v-128h640v-85.333h-640v-128z" + ], + "attrs": [], + "tags": [ + "switch" + ], + "defaultCode": 59808, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 821, + "id": 252, + "name": "switch-", + "prevSize": 32, + "code": 59808 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 249 + }, + { + "icon": { + "paths": [ + "M512 170.667v-128l-170.667 170.667 170.667 170.667v-128c141.227 0 256 114.773 256 256 0 43.093-10.667 84.053-29.867 119.467l62.293 62.293c33.28-52.48 52.907-114.773 52.907-181.76 0-188.587-152.747-341.333-341.333-341.333zM512 768c-141.227 0-256-114.773-256-256 0-43.093 10.667-84.053 29.867-119.467l-62.293-62.293c-33.28 52.48-52.907 114.773-52.907 181.76 0 188.587 152.747 341.333 341.333 341.333v128l170.667-170.667-170.667-170.667v128z" + ], + "attrs": [], + "tags": [ + "sync" + ], + "defaultCode": 59809, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 822, + "id": 253, + "name": "sync", + "prevSize": 32, + "code": 59809 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 250 + }, + { + "icon": { + "paths": [ + "M512 170.667v-128l-170.667 170.667 170.667 170.667v-128c141.227 0 256 114.773 256 256 0 43.093-10.667 84.053-29.867 119.467l62.293 62.293c33.28-52.48 52.907-114.773 52.907-181.76 0-188.587-152.747-341.333-341.333-341.333zM512 768c-141.227 0-256-114.773-256-256 0-43.093 10.667-84.053 29.867-119.467l-62.293-62.293c-33.28 52.48-52.907 114.773-52.907 181.76 0 188.587 152.747 341.333 341.333 341.333v128l170.667-170.667-170.667-170.667v128z" + ], + "attrs": [], + "tags": [ + "sync" + ], + "defaultCode": 59648, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 823, + "id": 254, + "name": "synchronize", + "prevSize": 32, + "code": 59712 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 251 + }, + { + "icon": { + "paths": [ + "M915.234 461.901l-353.135-353.136c-15.002-15.003-35.354-23.432-56.567-23.432h-340.198c-44.183 0-80 35.817-80 80v340.198c0 10.505 2.069 20.907 6.089 30.613s9.914 18.526 17.342 25.954l353.136 353.135c31.241 31.241 81.894 31.245 113.139 0l340.194-340.194c31.241-31.245 31.241-81.899 0-113.139zM518.477 858.658l-353.143-353.126v-340.198h340.198l353.135 353.135-340.19 340.19zM405.333 325.333c0 44.183-35.817 80-80 80s-80-35.817-80-80c0-44.183 35.817-80 80-80s80 35.817 80 80z" + ], + "attrs": [], + "tags": [ + "tag" + ], + "defaultCode": 59711, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 824, + "id": 255, + "name": "tag", + "prevSize": 32, + "code": 59713 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 252 + }, + { + "icon": { + "paths": [ + "M938.669 298.666h-384.001v85.333h384.001v-85.333zM938.669 639.999h-384.001v85.332h384.001v-85.332zM236.374 469.333l-151.040-151.040 60.16-60.16 90.454 90.453 180.906-180.906 60.16 60.16-240.64 241.493zM236.374 810.669l-151.040-151.040 60.16-60.163 90.454 90.454 180.906-180.907 60.16 60.16-240.64 241.496z" + ], + "attrs": [], + "tags": [ + "task" + ], + "defaultCode": 59712, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 825, + "id": 256, + "name": "task", + "prevSize": 32, + "code": 59714 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 253 + }, + { + "icon": { + "paths": [ + "M258.133 810.701l-151.467-151.467 59.733-59.733 90.667 90.667 181.333-181.333 59.733 60.8-240 241.067zM258.133 469.367l-151.467-151.468 59.733-59.733 90.667 90.667 181.333-181.333 59.733 60.8-240 241.068zM576 725.367v-85.333h384v85.333h-384zM576 384.033v-85.333h384v85.333h-384z" + ], + "width": 1067, + "attrs": [], + "tags": [ + "taskmanager" + ], + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 826, + "id": 257, + "name": "taskmanager", + "prevSize": 32, + "code": 59810 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 254 + }, + { + "icon": { + "paths": [ + "M960 469.333v-341.333h-298.667v128h-256v-128h-298.667v341.333h298.667v-128h85.333v426.667h170.667v128h298.667v-341.333h-298.667v128h-85.333v-341.333h85.333v128h298.667z" + ], + "attrs": [], + "tags": [ + "tiers" + ], + "defaultCode": 59790, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 827, + "id": 258, + "name": "tiers", + "prevSize": 32, + "code": 59790 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 255 + }, + { + "icon": { + "paths": [ + "M192 170.667h682.667c47.128 0 85.333 38.205 85.333 85.333v0 512c0 47.128-38.205 85.333-85.333 85.333v0h-682.667c-47.128 0-85.333-38.205-85.333-85.333v0-512c0-46.933 38.4-85.333 85.333-85.333zM426.667 725.333c129.603 0 234.667-105.064 234.667-234.667s-105.064-234.667-234.667-234.667v0c-129.603 0-234.667 105.064-234.667 234.667s105.064 234.667 234.667 234.667v0z", + "M448 341.333h-42.667v170.667l145.067 85.333 21.333-34.133-128-76.8v-145.067z" + ], + "attrs": [], + "tags": [ + "timecard" + ], + "defaultCode": 59801, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 828, + "id": 259, + "name": "timecard", + "prevSize": 32, + "code": 59801 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 256 + }, + { + "icon": { + "paths": [ + "M425.967 341.333h-41.967v167.868l146.884 88.132 20.983-34.415-125.9-74.701v-146.884z", + "M853.333 170.667h-682.667c-47.36 0-84.907 37.973-84.907 85.333l-0.427 512c0 47.36 37.973 85.333 85.333 85.333h392.832c-5.751-20.343-8.832-41.813-8.832-64 0-129.604 105.062-234.667 234.667-234.667 56.73 0 108.762 20.13 149.333 53.641v-352.307c0-47.36-37.973-85.333-85.333-85.333zM640 490.667c0 129.604-105.062 234.667-234.667 234.667-129.603 0-234.667-105.062-234.667-234.667 0-129.603 105.064-234.667 234.667-234.667 129.604 0 234.667 105.064 234.667 234.667z", + "M789.333 597.333c-105.984 0-192 86.016-192 192s86.016 192 192 192c105.984 0 192-86.016 192-192s-86.016-192-192-192zM750.933 885.333l-96-96 27.072-27.072 68.928 68.736 145.728-145.728 27.072 27.264-172.8 172.8z" + ], + "attrs": [], + "tags": [ + "timecard-approve" + ], + "defaultCode": 59918, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 829, + "id": 260, + "name": "timecard-approve", + "prevSize": 32, + "code": 59918 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 257 + }, + { + "icon": { + "paths": [ + "M425.967 341.333h-41.967v167.868l146.884 88.132 20.983-34.415-125.9-74.701v-146.884z", + "M853.333 170.667h-682.667c-47.36 0-84.907 37.973-84.907 85.333l-0.427 512c0 47.36 37.973 85.333 85.333 85.333h392.832c-5.751-20.343-8.832-41.813-8.832-64 0-129.604 105.062-234.667 234.667-234.667 56.73 0 108.762 20.13 149.333 53.641v-352.307c0-47.36-37.973-85.333-85.333-85.333zM640 490.667c0 129.604-105.062 234.667-234.667 234.667-129.603 0-234.667-105.062-234.667-234.667 0-129.603 105.064-234.667 234.667-234.667 129.604 0 234.667 105.064 234.667 234.667z", + "M789.333 981.333c105.984 0 192-86.016 192-192s-86.016-192-192-192c-105.984 0-192 86.016-192 192s86.016 192 192 192zM770.133 693.333h38.4v115.2h-38.4v-115.2zM770.133 846.933h38.4v38.4h-38.4v-38.4z" + ], + "attrs": [], + "tags": [ + "timecard-warning" + ], + "defaultCode": 59919, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 830, + "id": 261, + "name": "timecard-warning", + "prevSize": 32, + "code": 59919 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 258 + }, + { + "icon": { + "paths": [ + "M567.296 921.6c-16.384 0-28.16-5.292-35.328-15.872s-8.020-23.724-2.56-39.424l149.504-396.288c4.096-10.924 11.776-20.308 23.040-28.16s23.040-11.776 35.328-11.776c11.604 0 23.212 3.924 34.816 11.776s19.456 17.236 23.552 28.16l149.504 396.288c5.46 15.7 4.608 28.844-2.56 39.424s-19.284 15.872-36.352 15.872c-7.508 0-14.336-2.388-20.48-7.168s-10.58-10.58-13.312-17.408l-34.816-100.352h-198.656l-35.84 100.352c-2.732 6.828-7.508 12.628-14.336 17.408s-13.996 7.168-21.504 7.168zM663.552 724.992h147.456l-71.68-202.752h-4.096l-71.68 202.752zM313.344 370.688c10.923 19.797 22.528 38.059 34.816 54.784 12.288 16.724 26.624 33.964 43.008 51.712 30.036-32.768 54.956-66.388 74.752-100.864 19.796-34.475 36.524-71.168 50.176-110.080h-413.696c-11.605 0-21.333-3.925-29.184-11.776s-11.776-17.579-11.776-29.184c0-11.605 3.925-21.333 11.776-29.184s17.579-11.776 29.184-11.776h245.76v-40.96c0-11.605 3.925-21.333 11.776-29.184s17.579-11.776 29.184-11.776c11.605 0 21.332 3.925 29.184 11.776s11.776 17.579 11.776 29.184v40.96h245.76c11.604 0 21.332 3.925 29.184 11.776s11.776 17.579 11.776 29.184c0 11.605-3.924 21.333-11.776 29.184s-17.58 11.776-29.184 11.776h-77.824c-14.336 48.469-33.792 95.573-58.368 141.312-24.576 45.74-54.956 88.748-91.136 129.024l98.304 100.352-30.72 83.968-126.976-126.976-176.128 176.128c-7.509 7.508-17.067 11.264-28.672 11.264s-21.163-3.756-28.672-11.264c-7.509-7.508-11.264-17.068-11.264-28.672s3.755-21.164 11.264-28.672l178.176-178.176c-18.432-21.164-35.499-42.496-51.2-64s-29.696-44.204-41.984-68.096c-7.509-14.336-7.851-26.965-1.024-37.888s18.773-16.384 35.84-16.384c6.827 0 13.995 2.219 21.504 6.656s12.971 9.728 16.384 15.872z" + ], + "attrs": [], + "tags": [ + "translation" + ], + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 833, + "id": 264, + "name": "translation", + "prevSize": 32, + "code": 59811 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 259 + }, + { + "icon": { + "paths": [ + "M256 810.667c0 46.933 38.4 85.333 85.333 85.333h341.333c46.933 0 85.333-38.4 85.333-85.333v-512h-512v512zM810.667 170.667h-149.333l-42.667-42.667h-213.333l-42.667 42.667h-149.333v85.333h597.333v-85.333z" + ], + "attrs": [], + "tags": [ + "trash" + ], + "defaultCode": 59816, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 834, + "id": 265, + "name": "trash", + "prevSize": 32, + "code": 59816 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 260 + }, + { + "icon": { + "paths": [ + "M512.010 85.336c-234.666 0-426.666 192-426.666 426.667s192 426.666 426.666 426.666c234.665 0 426.665-192 426.665-426.666s-192-426.667-426.665-426.667zM170.677 512.003c0-187.733 153.6-341.333 341.333-341.333 76.8 0 149.334 25.6 209.065 72.533l-477.865 477.866c-46.933-59.731-72.533-132.266-72.533-209.066zM512.010 853.338c-76.8 0-149.333-25.6-209.066-72.538l477.869-477.864c46.931 59.734 72.531 132.267 72.531 209.067 0 187.735-153.6 341.335-341.334 341.335z" + ], + "attrs": [], + "tags": [ + "unclaim" + ], + "defaultCode": 59713, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 835, + "id": 266, + "name": "unclaim", + "prevSize": 32, + "code": 59715 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 261 + }, + { + "icon": { + "paths": [ + "M512 725.333c141.227 0 256-114.773 256-256v-341.333h-106.667v341.333c0 82.347-66.987 149.333-149.333 149.333s-149.333-66.987-149.333-149.333v-341.333h-106.667v341.333c0 141.227 114.773 256 256 256zM213.333 810.667v85.333h597.333v-85.333h-597.333z" + ], + "attrs": [], + "tags": [ + "ic-baseline-format-underlined" + ], + "defaultCode": 59676, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 687, + "id": 118, + "name": "underlined", + "prevSize": 32, + "code": 59677 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 262 + }, + { + "icon": { + "paths": [ + "M576 298.667c73.553 0 144.094 29.219 196.105 81.229s81.229 122.551 81.229 196.104c0 73.553-29.218 144.094-81.229 196.105s-122.551 81.229-196.105 81.229h-149.333v-85.333h149.333c106.667 0 192-85.333 192-192s-85.333-192-192-192h-241.92l131.413 131.84-60.16 60.16-234.667-234.667 234.667-234.667 60.587 60.16-131.84 131.84h241.92zM256 768h85.333v85.333h-85.333v-85.333z" + ], + "attrs": [], + "tags": [ + "mdi-undo-variant" + ], + "defaultCode": 59686, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 720, + "id": 151, + "name": "undo-variant", + "prevSize": 32, + "code": 59686 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 263 + }, + { + "icon": { + "paths": [ + "M511.967 530.115l-362.666-231.464v490.664h411.734c0 9.958 0.886 20.803 2.668 32.538 1.777 11.73 4.086 22.221 6.932 31.462h-421.334c-17.067 0-32-6.574-44.8-19.732-12.8-13.153-19.2-27.909-19.2-44.268v-554.664c0-16.356 6.4-31.111 19.2-44.267s27.733-19.733 44.8-19.733h725.334c16.353 0 31.109 6.578 44.268 19.733 13.153 13.156 19.732 27.911 19.732 44.267v310.399c-7.823-4.265-17.956-8.356-30.403-12.268-12.442-3.912-23.644-6.932-33.597-9.068v-225.064l-362.668 231.464zM511.967 461.85l362.668-227.2h-725.334l362.666 227.2zM824.499 981.315c-55.183 0-102.226-19.2-141.123-57.6s-58.342-85.335-58.342-140.815c0-55.475 19.446-102.764 58.342-141.865s85.939-58.65 141.123-58.65c55.188 0 102.226 19.548 141.123 58.65s58.342 86.39 58.342 141.865c0 55.48-19.446 102.415-58.342 140.815s-85.934 57.6-141.123 57.6zM703.964 799.985h241.070v-42.67h-241.070v42.67z" + ], + "attrs": [], + "tags": [ + "unsubscribe" + ], + "grid": 16, + "defaultCode": 59815 + }, + "attrs": [], + "properties": { + "order": 566, + "id": 279, + "name": "unsubscribe", + "prevSize": 32, + "code": 59815 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 264 + }, + { + "icon": { + "paths": [ + "M896 431.787h-289.28l116.907-120.32c-116.48-115.2-305.067-119.467-421.547-4.267-116.48 115.627-116.48 302.080 0 417.707s305.067 115.627 421.547 0c58.027-57.173 87.040-124.16 87.040-208.64h85.333c0 84.48-37.547 194.133-112.64 268.373-149.76 148.48-392.96 148.48-542.72 0-149.333-148.053-150.613-388.693-0.853-536.747s389.973-148.053 539.733 0l116.48-119.893v303.787zM533.333 341.333v181.333l149.333 88.747-30.72 51.627-182.613-108.373v-213.333h64z" + ], + "attrs": [], + "tags": [ + "update" + ], + "defaultCode": 59818, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 836, + "id": 267, + "name": "update", + "prevSize": 32, + "code": 59818 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 265 + }, + { + "icon": { + "paths": [ + "M384 682.667h256v-256h170.667l-298.667-298.667-298.667 298.667h170.667v256zM213.333 768h597.333v85.333h-597.333v-85.333z" + ], + "attrs": [], + "tags": [ + "upload" + ], + "defaultCode": 59921, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 837, + "id": 268, + "name": "upload", + "prevSize": 32, + "code": 59921 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 266 + }, + { + "icon": { + "paths": [ + "M512 512c94.293 0 170.667-76.373 170.667-170.667s-76.373-170.667-170.667-170.667c-94.293 0-170.667 76.373-170.667 170.667s76.373 170.667 170.667 170.667zM512 597.333c-113.92 0-341.333 57.173-341.333 170.667v85.333h682.667v-85.333c0-113.493-227.413-170.667-341.333-170.667z" + ], + "attrs": [], + "tags": [ + "user" + ], + "defaultCode": 59925, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 838, + "id": 269, + "name": "user", + "prevSize": 32, + "code": 59925 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 267 + }, + { + "icon": { + "paths": [ + "M938.667 384v-85.333h-85.333v85.333h-85.333v85.333h85.333v85.333h85.333v-85.333h85.333v-85.333h-85.333z", + "M341.333 512c94.293 0 170.667-76.373 170.667-170.667s-76.373-170.667-170.667-170.667c-94.293 0-170.667 76.373-170.667 170.667s76.373 170.667 170.667 170.667z", + "M341.333 554.667c-113.92 0-341.333 57.173-341.333 170.667v128h682.667v-128c0-113.493-227.413-170.667-341.333-170.667z", + "M533.76 172.8c39.253 45.227 63.573 104.107 63.573 168.533s-24.32 123.307-63.573 168.533c83.627-10.667 148.907-81.493 148.907-168.533s-65.28-157.867-148.907-168.533z", + "M705.28 590.080c37.973 35.413 62.72 79.787 62.72 135.253v128h85.333v-128c0-61.867-67.84-107.093-148.053-135.253z" + ], + "attrs": [], + "tags": [ + "user-add" + ], + "defaultCode": 59922, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 839, + "id": 270, + "name": "user-add", + "prevSize": 32, + "code": 59922 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 268 + }, + { + "icon": { + "paths": [ + "M512 544c69.547 0 130.987 16.64 180.907 38.4 46.080 20.48 75.093 66.56 75.093 116.48v69.12h-512v-68.693c0-50.347 29.013-96.427 75.093-116.48 49.92-22.187 111.36-38.827 180.907-38.827zM170.667 554.667c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM218.88 601.6c-15.787-2.56-31.573-4.267-48.213-4.267-42.24 0-82.347 8.96-118.613 24.747-31.573 13.653-52.053 44.373-52.053 78.933v66.987h192v-68.693c0-35.413 9.813-68.693 26.88-97.707zM853.333 554.667c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM1024 701.013c0-34.56-20.48-65.28-52.053-78.933-36.267-15.787-76.373-24.747-118.613-24.747-16.64 0-32.427 1.707-48.213 4.267 17.067 29.013 26.88 62.293 26.88 97.707v68.693h192v-66.987zM512 256c70.827 0 128 57.173 128 128s-57.173 128-128 128c-70.827 0-128-57.173-128-128s57.173-128 128-128z" + ], + "attrs": [], + "tags": [ + "user-group" + ], + "defaultCode": 59923, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 840, + "id": 271, + "name": "user-group", + "prevSize": 32, + "code": 59923 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 269 + }, + { + "icon": { + "paths": [ + "M682.667 469.333c70.827 0 127.573-57.173 127.573-128s-56.747-128-127.573-128c-70.827 0-128 57.173-128 128s57.173 128 128 128zM341.333 469.333c70.827 0 127.573-57.173 127.573-128s-56.747-128-127.573-128c-70.827 0-128 57.173-128 128s57.173 128 128 128zM341.333 554.667c-99.413 0-298.667 49.92-298.667 149.333v106.667h597.333v-106.667c0-99.413-199.253-149.333-298.667-149.333zM682.667 554.667c-12.373 0-26.453 0.853-41.387 2.133 49.493 35.84 84.053 84.053 84.053 147.2v106.667h256v-106.667c0-99.413-199.253-149.333-298.667-149.333z" + ], + "attrs": [], + "tags": [ + "user-multiple" + ], + "defaultCode": 59924, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 841, + "id": 272, + "name": "user-multiple", + "prevSize": 32, + "code": 59924 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 270 + }, + { + "icon": { + "paths": [ + "M903.872 375.011c-1.097 2.004-2.692 3.687-4.638 4.884s-4.169 1.867-6.455 1.942l-90.88 1.707c-1.673 0.005-3.332-0.333-4.877-0.993-1.54-0.66-2.927-1.629-4.083-2.847l-35.413-33.707-9.387 40.96c-0.589 2.372-1.818 4.538-3.546 6.266s-3.895 2.955-6.268 3.547l-84.907 15.445c78.080 12.8 223.573 61.866 224 253.44 0 2.406-0.683 4.766-1.967 6.805-1.284 2.035-3.115 3.674-5.286 4.715-2.172 1.054-4.595 1.493-7.002 1.267-2.411-0.226-4.71-1.105-6.652-2.547l-135.253-104.96c-1.89-1.779-3.349-3.968-4.267-6.4l-23.040-69.547-31.147 45.653c-1.011 1.506-2.334 2.782-3.883 3.738-1.545 0.956-3.273 1.574-5.077 1.809-1.847 0.158-3.708-0.060-5.47-0.649-1.762-0.585-3.383-1.527-4.77-2.765l-79.787-71.253-5.252 1.323c10.53 11.767 38.767 59.341 38.767 231.155 0 46.263-1.024 83.52-2.624 113.493 100.8 17.199 173.291 64.951 173.291 121.173h-512c0-64.482 95.363-117.828 219.418-126.703 8.802-33.489 15.249-70.724 15.249-107.964v-214.050l-4.757 1.199-110.505 77.653c-2.347 1.647-5.178 2.458-8.041 2.304-2.863-0.158-5.59-1.267-7.746-3.157l-28.16-25.173-5.12 80.213c-0.126 1.843-0.649 3.635-1.533 5.252-0.884 1.621-2.107 3.034-3.587 4.134l-90.88 68.693c-1.832 1.31-3.956 2.146-6.188 2.445s-4.501 0.047-6.612-0.738c-2.035-0.926-3.805-2.351-5.147-4.139-1.342-1.792-2.213-3.887-2.533-6.101-11.52-91.307 25.173-263.253 229.545-272.64l-97.278-40.107c-2.738-1.060-5.012-3.056-6.419-5.634s-1.853-5.57-1.261-8.446l11.093-58.453-65.707 42.24c-1.332 0.985-2.854 1.683-4.47 2.050s-3.29 0.396-4.916 0.083l-132.267-26.88c-2.464-0.386-4.742-1.544-6.506-3.308s-2.921-4.042-3.307-6.506c-0.591-2.422-0.462-4.964 0.372-7.314s2.336-4.405 4.321-5.913c180.907-133.12 302.078-38.827 375.891 65.707-28.587-102.4-13.227-218.88 129.28-239.36 2.159-0.197 4.331 0.127 6.332 0.944 2.005 0.817 3.785 2.103 5.188 3.75 1.472 1.574 2.453 3.539 2.833 5.658 0.375 2.12 0.137 4.303-0.7 6.288-5.547 17.493-14.933 44.8-26.027 75.52l-44.8 7.68 35.413 19.627c-14.874 43.799-33.574 86.202-55.893 126.72 91.733-114.773 254.293-141.227 342.613-12.373 1.34 1.905 2.116 4.148 2.236 6.473s-0.418 4.636-1.553 6.669z" + ], + "attrs": [], + "tags": [ + "vacation" + ], + "defaultCode": 59926, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 842, + "id": 273, + "name": "vacation", + "prevSize": 32, + "code": 59926 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 271 + }, + { + "icon": { + "paths": [ + "M512 42.667l-384 170.667v256c0 236.8 163.84 458.24 384 512 220.16-53.76 384-275.2 384-512v-256l-384-170.667zM426.667 725.333l-170.667-170.667 60.16-60.16 110.507 110.080 281.173-281.173 60.16 60.587-341.333 341.333z" + ], + "attrs": [], + "tags": [ + "verified" + ], + "defaultCode": 59927, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 843, + "id": 274, + "name": "verified", + "prevSize": 32, + "code": 59927 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 272 + }, + { + "icon": { + "paths": [ + "M640 341.333v341.333h-426.667v-341.333h426.667zM682.667 256h-512c-23.467 0-42.667 19.2-42.667 42.667v426.667c0 23.467 19.2 42.667 42.667 42.667h512c23.467 0 42.667-19.2 42.667-42.667v-149.333l170.667 170.667v-469.333l-170.667 170.667v-149.333c0-23.467-19.2-42.667-42.667-42.667z" + ], + "attrs": [], + "tags": [ + "video" + ], + "defaultCode": 59714, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 844, + "id": 275, + "name": "video", + "prevSize": 32, + "code": 59764 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 273 + }, + { + "icon": { + "paths": [ + "M597.333 234.667c46.933 0 85.333-38.4 85.333-85.333s-38.4-85.333-85.333-85.333c-46.933 0-85.333 38.4-85.333 85.333s38.4 85.333 85.333 85.333zM439.467 379.733l-119.467 601.6h89.6l76.8-341.333 89.6 85.333v256h85.333v-320l-89.6-85.333 25.6-128c55.467 64 140.8 106.667 234.667 106.667v-85.333c-81.067 0-149.333-42.667-183.467-102.4l-42.667-68.267c-23.893-37.973-71.68-53.333-113.067-35.84l-215.467 91.307v200.533h85.333v-145.067l76.8-29.867z" + ], + "width": 1067, + "attrs": [], + "tags": [ + "walk" + ], + "defaultCode": 59715, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 845, + "id": 276, + "name": "walk", + "prevSize": 32, + "code": 59782 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 274 + }, + { + "icon": { + "paths": [ + "M42.667 384l85.333 85.334c212.053-212.054 555.947-212.054 768 0l85.333-85.334c-258.987-258.987-679.253-258.987-938.667 0zM384 725.333l128 128 128-128c-70.4-70.827-185.173-70.827-256 0zM213.333 554.667l85.333 85.333c117.76-117.76 308.907-117.76 426.667 0l85.333-85.333c-164.693-164.694-432.213-164.694-597.333 0z" + ], + "attrs": [], + "tags": [ + "wifi" + ], + "defaultCode": 59928, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 846, + "id": 277, + "name": "wifi", + "prevSize": 32, + "code": 59928 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 275 + }, + { + "icon": { + "paths": [ + "M213.333 128c-47.36 0-85.333 37.973-85.333 85.333v597.333c0 22.63 8.99 44.335 24.994 60.339s37.708 24.994 60.34 24.994h597.333c22.63 0 44.335-8.99 60.339-24.994s24.994-37.709 24.994-60.339v-298.667h-85.333v298.667h-597.333v-597.333h298.667v-85.333h-298.667zM758.613 170.667c-7.68 0.065-15.027 3.127-20.48 8.533l-52.053 51.627 106.667 106.667 52.053-51.627c11.093-11.093 11.093-29.867 0-40.533l-66.133-66.133c-5.547-5.547-12.8-8.533-20.053-8.533zM655.787 261.12l-314.453 314.88v106.667h106.667l314.453-314.88-106.667-106.667z" + ], + "attrs": [], + "tags": [ + "write" + ], + "defaultCode": 59820, + "grid": 16 + }, + "attrs": [], + "properties": { + "order": 847, + "id": 278, + "name": "write", + "prevSize": 32, + "code": 59820 + }, + "setIdx": 0, + "setId": 0, + "iconIdx": 276 + } + ], + "height": 1024, + "metadata": { + "name": "zds" + }, + "preferences": { + "showGlyphs": true, + "showCodes": true, + "showQuickUse": true, + "showQuickUse2": true, + "showSVGs": true, + "fontPref": { + "prefix": "icon-", + "metadata": { + "fontFamily": "zds", + "majorVersion": 1, + "minorVersion": 0 + }, + "metrics": { + "emSize": 1024, + "baseline": 6.25, + "whitespace": 50 + }, + "embed": false, + "noie8": true, + "ie7": false, + "flutter": true, + "autoHost": true, + "includeMetadata": true, + "showMetrics": false, + "showMetadata": false, + "showVersion": false, + "showSelector": false + }, + "imagePref": { + "prefix": "icon-", + "png": true, + "useClassSelector": true, + "color": 0, + "bgColor": 16777215, + "name": "icomoon", + "classSelector": ".icon" + }, + "historySize": 50, + "gridSize": 16, + "quickUsageToken": { + "UntitledProject": "N2VkZjRkYTEyZDMxNzNkMzE1YWJkYjIwZDBjN2U3ZDQjMSMxNjkwNDQ4NDc4IyMjNWM2ZjkwMGZiODE5" + }, + "showGrid": true + } +} \ No newline at end of file diff --git a/lib/assets/fonts/zds.ttf b/lib/assets/fonts/zds.ttf new file mode 100644 index 0000000..30cdc48 Binary files /dev/null and b/lib/assets/fonts/zds.ttf differ diff --git a/lib/assets/images/calendar.svg b/lib/assets/images/calendar.svg new file mode 100644 index 0000000..55a5382 --- /dev/null +++ b/lib/assets/images/calendar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/chat.svg b/lib/assets/images/chat.svg new file mode 100644 index 0000000..dec1552 --- /dev/null +++ b/lib/assets/images/chat.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/cloud_fail.svg b/lib/assets/images/cloud_fail.svg new file mode 100644 index 0000000..3479bfc --- /dev/null +++ b/lib/assets/images/cloud_fail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/completed_tasks.svg b/lib/assets/images/completed_tasks.svg new file mode 100644 index 0000000..25894a2 --- /dev/null +++ b/lib/assets/images/completed_tasks.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/empty_box.svg b/lib/assets/images/empty_box.svg new file mode 100644 index 0000000..6af7519 --- /dev/null +++ b/lib/assets/images/empty_box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/load_fail.svg b/lib/assets/images/load_fail.svg new file mode 100644 index 0000000..5c36de0 --- /dev/null +++ b/lib/assets/images/load_fail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/map.svg b/lib/assets/images/map.svg new file mode 100644 index 0000000..f962e0e --- /dev/null +++ b/lib/assets/images/map.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/assets/images/notes.svg b/lib/assets/images/notes.svg new file mode 100644 index 0000000..7d0952a --- /dev/null +++ b/lib/assets/images/notes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/notifications.svg b/lib/assets/images/notifications.svg new file mode 100644 index 0000000..4d189ff --- /dev/null +++ b/lib/assets/images/notifications.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/punch.svg b/lib/assets/images/punch.svg new file mode 100644 index 0000000..2ee2538 --- /dev/null +++ b/lib/assets/images/punch.svg @@ -0,0 +1,7 @@ + + + diff --git a/lib/assets/images/sad_zebra.svg b/lib/assets/images/sad_zebra.svg new file mode 100644 index 0000000..40965a2 --- /dev/null +++ b/lib/assets/images/sad_zebra.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/search.svg b/lib/assets/images/search.svg new file mode 100644 index 0000000..1f8dc05 --- /dev/null +++ b/lib/assets/images/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/server_fail.svg b/lib/assets/images/server_fail.svg new file mode 100644 index 0000000..fd99522 --- /dev/null +++ b/lib/assets/images/server_fail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/images/sleeping_zebra.svg b/lib/assets/images/sleeping_zebra.svg new file mode 100644 index 0000000..4aba1ed --- /dev/null +++ b/lib/assets/images/sleeping_zebra.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/strings/ar.json b/lib/assets/strings/ar.json new file mode 100644 index 0000000..a589307 --- /dev/null +++ b/lib/assets/strings/ar.json @@ -0,0 +1,140 @@ +{ + "ACTION": "عمل", + "ADD_URL": "إضافة رابط", + "ALL": "الكل", + "ATTACHED_FILE": "ملف مرفق", + "ATTACH_MAXSIZE_VALIDATE": "يجب أن يكون حجم الملف أقل من {0}", + "AUDIO_FILE_OR_URL_MSG": "يرجى إما اختيار ملف صوتي أو إدخال عنوان URL لملف صوتي!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "الرجاء إدخال إما ملف صوتي أو عنوان URL صوتي ، وليس كلاهما!", + "Blur": "طمس", + "Blur Radius": "نصف القطر الضبابي", + "Brush": "فرشاة", + "CAMERA": "الة تصوير", + "CANCEL": "إلغاء", + "CHANGE_CASE": "تغيير الحالة", + "CHANGE_IMAGE": "تغيير الصورة", + "CHAR_LEFT": "الأحرف المتبقية {0}", + "CHOOSE_AUDIO": "اختر الصوت", + "CHOOSE_COLOR": "اختيار اللون", + "CHOOSE_FILE": "اختر ملف", + "CHOOSE_FILE_URL_MSG": "يرجى إما اختيار ملف أو إدخال عنوان URL للملف!", + "CHOOSE_IMAGE": "اختر صورة", + "CHOOSE_IMG_URL_MSG": "يرجى إما اختيار صورة أو إدخال عنوان URL للصورة!", + "CHOOSE_VIDEO": "اختر الفيديو", + "CLEAR": "واضح", + "CLOSE": "أغلق", + "COMPLETE_EDITING": "التحرير الكامل", + "COURIER": "ساعي", + "CROP": "ا & قتصاص", + "CUSTOM": "العادة", + "Color Opacity": "عتامة اللون", + "Crop": "ا & قتصاص", + "DEFAULT": "افتراضي", + "DELETE": "حذف", + "DELETE_ATTACHMENT": "حذف المرفق", + "DONE": "منجز", + "EDIT": "تصحيح", + "EDITOR_CIRCLE": "دائرة", + "EDITOR_CODE": "شفرة", + "EDITOR_DISC": "قرص", + "EDITOR_HEADER_1": "العنوان 1", + "EDITOR_HEADER_2": "العنوان 2", + "EDITOR_HEADER_3": "العنوان 3", + "EDITOR_HEADER_4": "العنوان 4", + "EDITOR_HEADER_5": "العنوان 5", + "EDITOR_HEADER_6": "العنوان 6", + "EDITOR_NORMAL": "طبيعي", + "EDITOR_PT": "نقطة", + "EDITOR_PX": "بكسل", + "EDITOR_QUOTE": "يقتبس", + "EDITOR_SQUARE": "مربع", + "EDITOR_TEXT": "نص", + "ENTER": "يدخل", + "ESCAPE": "يهرب", + "Emoji": "رمز تعبيري", + "FILE": "ملف", + "FILE_COMPRESSING_ER": "حدث خطأ أثناء ضغط الملف", + "FILE_DUPLICATE_ER": "الملف مرفق بالفعل. الرجاء اختيار ملف مختلف", + "FILE_OR_URL_MSG": "الرجاء إدخال ملف أو عنوان URL للملف ، وليس كلاهما!", + "FILE_PIXEL_SIZE_ER": "يجب أن يكون حجم بكسل الملف أقل من {0}", + "FILE_PROCESSING_ER": "حدث خطأ أثناء معالجة ملف", + "FILE_SIZE_CALCULATING_ER": "حدث خطأ أثناء حساب حجم الملف", + "FILE_UNSUPPORTED": "نوع الملف هذا غير مسموح به", + "FLIP": "يواجه", + "FROM": "من عند", + "Filter": "منقي", + "Flip": "يواجه", + "Freeform": "شكل حر", + "GALLERY": "صالة عرض", + "GIF": "GIF", + "HELP": "مساعدة", + "HIDE": "إخفاء", + "IMAGE_OR_URL_MSG": "الرجاء إدخال صورة أو عنوان URL للصورة ، وليس كلاهما!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "منتقي الصور. تم تحديد الصورة", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "منتقي الصور. لم يتم اختيار أي صورة", + "IMG_URL_MSG": "الرجاء إدخال عنوان URL للصورة!", + "INSERT_AUDIO": "أدخل الصوت", + "INSERT_FILE": "إدراج ملف", + "INSERT_IMAGE": "إدراج صورة", + "INSERT_LINK": "أدخل ارتباط", + "INSERT_PARAGRAPH": "أدخل فقرة", + "INSERT_TABLE": "إدراج جدول", + "INSERT_VIDEO": "أدخل الفيديو", + "Insert Your Message": "أدخل رسالتك", + "KEY_COMBINATION": "مجموعة المفاتيح", + "LINK": "وصلة", + "LINK_NAME": "اسم الرابط", + "LINK_URL": "URL رابط", + "LOWERCASE": "أحرف صغيرة", + "LOWER_ALPHA": "ألفا السفلى", + "LOWER_ROMAN": "الرومان السفلي", + "MAX_ATTACH_MSG": "تم الوصول إلى الحد الأقصى للمرفقات", + "MONTH": "شهر", + "NO_RESULTS": "لا نتائج", + "NUMBERED": "مرقمة", + "OK": "نعم", + "OPEN_IN_NEW_WINDOW": "افتح في نافذة جديدة", + "PICK_GIF": "اختر صورة متحركة", + "PICK_IMAGE": "اختر صورة", + "REDO_MSG": "أعد تنفيذ الأمر الأخير", + "REMOVE": "إزالة", + "RESET": "إعادة تعيين", + "REST_COLOR_MSG": "إعادة التعيين إلى اللون الافتراضي", + "ROTATE_LEFT": "استدر يسارا", + "ROTATE_RIGHT": "استدارة لليمين", + "Reset": "إعادة تعيين", + "Rotate left": "استدر يسارا", + "Rotate right": "استدارة لليمين", + "SANS_SERIF": "بلا الرقيق", + "SAVE": "حفظ", + "SEARCH_GIF": "ابحث في جميع ملفات GIF", + "SELECT_FROM_FILES": "اختر من الملفات", + "SELECT_STL_MSG": "حدد نمط القائمة", + "SENTENCE_CASE": "حالة الجملة", + "SET_COLOR": "اضبط اللون", + "SHOW": "تبين", + "SWIPE_TO_REVEAL_SEMANTIC": "اسحب لليسار للكشف عن الإجراءات", + "Select Emoji": "حدد Emoji", + "Slider Color": "لون المنزلق", + "Slider White Black Color": "المنزلق أبيض اللون الأسود", + "Square": "مربع", + "TAB": "فاتورة غير مدفوعة", + "TEXT_TO_DISPLAY": "النص المراد عرضه", + "TIMES_NEW_ROMAN": "تايمز نيو رومان", + "TITLE_CASE": "حالة العنوان", + "TO": "إلى", + "Text": "نص", + "UNDO_MSG": "تراجع عن آخر أمر", + "UNKNOWN": "مجهول", + "UNTAB": "Untab", + "UPPERCASE": "الأحرف الكبيرة", + "UPPER_ALPHA": "ألفا العلوي", + "UPPER_ROMAN": "ابر رومان", + "URL": "URL", + "URL_DUPLICATE_ER": "تم إرفاق URL بالفعل. الرجاء إدخال عنوان URL مختلف", + "URL_INVALID_ER": "الرجاء إدخال URL صالح", + "URL_MSG": "الرجاء إدخال URL!", + "VIDEO": "فيديو", + "VIEW": "رأي", + "WEEK": "أسبوع" +} \ No newline at end of file diff --git a/lib/assets/strings/bg.json b/lib/assets/strings/bg.json new file mode 100644 index 0000000..fe3e699 --- /dev/null +++ b/lib/assets/strings/bg.json @@ -0,0 +1,140 @@ +{ + "ACTION": "действие", + "ADD_URL": "Добавете URL адрес", + "ALL": "всичко", + "ATTACHED_FILE": "Прикачен файл", + "ATTACH_MAXSIZE_VALIDATE": "Размерът на файла трябва да е по-малък от {0}", + "AUDIO_FILE_OR_URL_MSG": "Моля, изберете аудио файл или въведете URL адрес на аудио файл!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Моля, въведете аудио файл или URL адрес на аудио, не и двете!", + "Blur": "Размазване", + "Blur Radius": "Радиус на замъгляване", + "Brush": "Четка", + "CAMERA": "Камера", + "CANCEL": "Отказ", + "CHANGE_CASE": "Смяна на регистър", + "CHANGE_IMAGE": "Промяна на изображението", + "CHAR_LEFT": "Оставащи знаци {0}", + "CHOOSE_AUDIO": "Изберете аудио", + "CHOOSE_COLOR": "Изберете цвят", + "CHOOSE_FILE": "Изберете файл", + "CHOOSE_FILE_URL_MSG": "Моля, изберете файл или въведете URL адрес на файл!", + "CHOOSE_IMAGE": "Изберете изображение", + "CHOOSE_IMG_URL_MSG": "Моля, изберете изображение или въведете URL адрес на изображение!", + "CHOOSE_VIDEO": "Изберете видео", + "CLEAR": "Ясно", + "CLOSE": "Близо", + "COMPLETE_EDITING": "Пълно редактиране", + "COURIER": "Куриер", + "CROP": "реколта", + "CUSTOM": "Персонализиран", + "Color Opacity": "Непрозрачност на цвета", + "Crop": "реколта", + "DEFAULT": "По подразбиране", + "DELETE": "Изтрий", + "DELETE_ATTACHMENT": "Изтрийте прикачения файл", + "DONE": "Свършен", + "EDIT": "редактиране", + "EDITOR_CIRCLE": "кръг", + "EDITOR_CODE": "код", + "EDITOR_DISC": "Диск", + "EDITOR_HEADER_1": "Заглавие 1", + "EDITOR_HEADER_2": "Заглавие 2", + "EDITOR_HEADER_3": "Заглавие 3", + "EDITOR_HEADER_4": "Заглавие 4", + "EDITOR_HEADER_5": "Заглавие 5", + "EDITOR_HEADER_6": "Заглавие 6", + "EDITOR_NORMAL": "нормално", + "EDITOR_PT": "pt", + "EDITOR_PX": "пиксела", + "EDITOR_QUOTE": "цитат", + "EDITOR_SQUARE": "Квадрат", + "EDITOR_TEXT": "Текст", + "ENTER": "ENTER", + "ESCAPE": "бягство", + "Emoji": "емотикони", + "FILE": "досие", + "FILE_COMPRESSING_ER": "Възникна грешка при компресиране на файл", + "FILE_DUPLICATE_ER": "Файлът вече е прикачен. Моля, изберете друг файл", + "FILE_OR_URL_MSG": "Моля, въведете или файл, или URL адрес на файл, не и двете!", + "FILE_PIXEL_SIZE_ER": "Размерът на файла в пикселите трябва да е по-малък от {0}", + "FILE_PROCESSING_ER": "Възникна грешка при обработката на файл", + "FILE_SIZE_CALCULATING_ER": "Възникна грешка при изчисляването на размера на файла", + "FILE_UNSUPPORTED": "Този тип файл не е разрешен", + "FLIP": "Обърни се", + "FROM": "от", + "Filter": "филтър", + "Flip": "Обърни се", + "Freeform": "Свободна форма", + "GALLERY": "галерия", + "GIF": "Gif", + "HELP": "Помогне", + "HIDE": "Крия", + "IMAGE_OR_URL_MSG": "Моля, въведете изображение или URL адрес на изображение, не и двете!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Инструмент за избор на изображения. Избрано изображение", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Инструмент за избор на изображения. Няма избрано изображение", + "IMG_URL_MSG": "Моля, въведете URL адрес на изображение!", + "INSERT_AUDIO": "Вмъкване на аудио", + "INSERT_FILE": "Вмъкване на файл", + "INSERT_IMAGE": "Вмъкване на изображение", + "INSERT_LINK": "Вмъкване на връзка", + "INSERT_PARAGRAPH": "Вмъкване на абзац", + "INSERT_TABLE": "Вмъкване на таблица", + "INSERT_VIDEO": "Вмъкване на видео", + "Insert Your Message": "Въведете вашето съобщение", + "KEY_COMBINATION": "Ключова комбинация", + "LINK": "Връзка", + "LINK_NAME": "Име на връзката", + "LINK_URL": "URL адрес на връзката", + "LOWERCASE": "малка буква", + "LOWER_ALPHA": "Долна Алфа", + "LOWER_ROMAN": "Долен римски", + "MAX_ATTACH_MSG": "Максималният лимит за прикачени файлове е достигнат", + "MONTH": "месец", + "NO_RESULTS": "Няма резултати", + "NUMBERED": "Номерирани", + "OK": "Добре", + "OPEN_IN_NEW_WINDOW": "Отвори в нов прозорец", + "PICK_GIF": "Изберете Gif", + "PICK_IMAGE": "Изберете изображение", + "REDO_MSG": "Повторете последната команда", + "REMOVE": "Премахване", + "RESET": "Нулиране", + "REST_COLOR_MSG": "Нулирайте цвета по подразбиране", + "ROTATE_LEFT": "Завърти наляво", + "ROTATE_RIGHT": "Завъртете надясно", + "Reset": "Нулиране", + "Rotate left": "Завърти наляво", + "Rotate right": "Завъртете надясно", + "SANS_SERIF": "Sans Serif", + "SAVE": "Запази", + "SEARCH_GIF": "Търсете във всички GIF файлове", + "SELECT_FROM_FILES": "Изберете от файлове", + "SELECT_STL_MSG": "Изберете стил на списък", + "SENTENCE_CASE": "Падеж на изречението", + "SET_COLOR": "Задайте цвят", + "SHOW": "шоу", + "SWIPE_TO_REVEAL_SEMANTIC": "Плъзнете наляво, за да разкриете действия", + "Select Emoji": "Изберете Emoji", + "Slider Color": "Цвят на плъзгача", + "Slider White Black Color": "Плъзгач Бял Черен Цвят", + "Square": "Квадрат", + "TAB": "РАЗДЕЛ", + "TEXT_TO_DISPLAY": "Текст за показване", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Регистър на заглавието", + "TO": "Да се", + "Text": "Текст", + "UNDO_MSG": "Отмяна на последната команда", + "UNKNOWN": "Неизвестен", + "UNTAB": "Премахване на таб", + "UPPERCASE": "ГЛАВНА БУКВА", + "UPPER_ALPHA": "Горна Алфа", + "UPPER_ROMAN": "Горноримски", + "URL": "URL", + "URL_DUPLICATE_ER": "URL вече е прикачен. Моля, въведете различен URL адрес", + "URL_INVALID_ER": "Моля, въведете валиден URL", + "URL_MSG": "Моля, въведете URL!", + "VIDEO": "Видео", + "VIEW": "изглед", + "WEEK": "седмица" +} \ No newline at end of file diff --git a/lib/assets/strings/bs.json b/lib/assets/strings/bs.json new file mode 100644 index 0000000..8e62d8e --- /dev/null +++ b/lib/assets/strings/bs.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Akcija", + "ADD_URL": "Dodajte URL", + "ALL": "Sve", + "ATTACHED_FILE": "Priloženi fajl", + "ATTACH_MAXSIZE_VALIDATE": "Veličina fajla mora biti manja od {0}", + "AUDIO_FILE_OR_URL_MSG": "Molimo ili odaberite audio fajl ili unesite URL audio fajla!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Unesite audio fajl ili audio URL, a ne oboje!", + "Blur": "Zamućenje", + "Blur Radius": "Radijus zamućenja", + "Brush": "Četka", + "CAMERA": "Kamera", + "CANCEL": "Otkaži", + "CHANGE_CASE": "Promijenite slučaj", + "CHANGE_IMAGE": "Promijeni sliku", + "CHAR_LEFT": "Likovi lijevo {0}", + "CHOOSE_AUDIO": "Odaberite audio", + "CHOOSE_COLOR": "Odaberite boju", + "CHOOSE_FILE": "Odabrati datoteku", + "CHOOSE_FILE_URL_MSG": "Molimo ili odaberite datoteku ili unesite URL datoteke!", + "CHOOSE_IMAGE": "Odaberite sliku", + "CHOOSE_IMG_URL_MSG": "Molimo ili odaberite sliku ili unesite URL slike!", + "CHOOSE_VIDEO": "Odaberite video", + "CLEAR": "Jasno", + "CLOSE": "Zatvori", + "COMPLETE_EDITING": "Kompletno uređivanje", + "COURIER": "Kurir", + "CROP": "Rezati", + "CUSTOM": "Custom", + "Color Opacity": "Prozirnost boje", + "Crop": "Rezati", + "DEFAULT": "Podrazumevano", + "DELETE": "Izbriši", + "DELETE_ATTACHMENT": "Izbrišite prilog", + "DONE": "Gotovo", + "EDIT": "Uredi", + "EDITOR_CIRCLE": "Krug", + "EDITOR_CODE": "kod", + "EDITOR_DISC": "Disc", + "EDITOR_HEADER_1": "Zaglavlje 1", + "EDITOR_HEADER_2": "Zaglavlje 2", + "EDITOR_HEADER_3": "Zaglavlje 3", + "EDITOR_HEADER_4": "Zaglavlje 4", + "EDITOR_HEADER_5": "Zaglavlje 5", + "EDITOR_HEADER_6": "Zaglavlje 6", + "EDITOR_NORMAL": "normalno", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citat", + "EDITOR_SQUARE": "Square", + "EDITOR_TEXT": "Tekst", + "ENTER": "ENTER", + "ESCAPE": "Bijeg", + "Emoji": "Emoji", + "FILE": "Datoteka", + "FILE_COMPRESSING_ER": "Došlo je do greške prilikom komprimiranja datoteke", + "FILE_DUPLICATE_ER": "Fajl je već priložen. Molimo odaberite drugu datoteku", + "FILE_OR_URL_MSG": "Unesite datoteku ili URL datoteke, a ne oboje!", + "FILE_PIXEL_SIZE_ER": "Veličina piksela fajla bi trebala biti manja od {0}", + "FILE_PROCESSING_ER": "Došlo je do greške prilikom obrade datoteke", + "FILE_SIZE_CALCULATING_ER": "Došlo je do greške prilikom izračunavanja veličine datoteke", + "FILE_UNSUPPORTED": "Ova vrsta datoteke nije dozvoljena", + "FLIP": "Flip", + "FROM": "Od", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freeform", + "GALLERY": "Galerija", + "GIF": "Gif", + "HELP": "Pomoć", + "HIDE": "Sakrij", + "IMAGE_OR_URL_MSG": "Unesite sliku ili URL slike, a ne oboje!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Birač slika. Slika je odabrana", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Birač slika. Nijedna slika nije odabrana", + "IMG_URL_MSG": "Unesite URL slike!", + "INSERT_AUDIO": "Insert Audio", + "INSERT_FILE": "Umetni datoteku", + "INSERT_IMAGE": "Umetni sliku", + "INSERT_LINK": "Insert Link", + "INSERT_PARAGRAPH": "Umetni paragraf", + "INSERT_TABLE": "Umetni tabelu", + "INSERT_VIDEO": "Umetni video", + "Insert Your Message": "Ubacite svoju poruku", + "KEY_COMBINATION": "Kombinacija tastera", + "LINK": "Veza", + "LINK_NAME": "Ime veze", + "LINK_URL": "URL veze", + "LOWERCASE": "mala slova", + "LOWER_ALPHA": "Lower Alpha", + "LOWER_ROMAN": "Lower Roman", + "MAX_ATTACH_MSG": "Maksimalno ograničenje priloga je dostignuto", + "MONTH": "Mesec", + "NO_RESULTS": "Nema rezultata", + "NUMBERED": "Numerisano", + "OK": "uredu", + "OPEN_IN_NEW_WINDOW": "Otvori u novom prozoru", + "PICK_GIF": "Odaberite Gif", + "PICK_IMAGE": "Izaberite sliku", + "REDO_MSG": "Ponovite zadnju komandu", + "REMOVE": "Ukloni", + "RESET": "Resetovati", + "REST_COLOR_MSG": "Vratite na zadanu boju", + "ROTATE_LEFT": "Rotirajte ulijevo", + "ROTATE_RIGHT": "Rotate Right", + "Reset": "Resetovati", + "Rotate left": "Rotirajte lijevo", + "Rotate right": "Rotirajte desno", + "SANS_SERIF": "Sans Serif", + "SAVE": "Spremi", + "SEARCH_GIF": "Pretražite sve GIF-ove", + "SELECT_FROM_FILES": "Odaberite iz datoteka", + "SELECT_STL_MSG": "Odaberite stil liste", + "SENTENCE_CASE": "Slučaj kazne", + "SET_COLOR": "Postavite boju", + "SHOW": "Pokaži", + "SWIPE_TO_REVEAL_SEMANTIC": "Prevucite ulijevo za otkrivanje radnji", + "Select Emoji": "Odaberite Emoji", + "Slider Color": "Boja klizača", + "Slider White Black Color": "Klizač Bijela Crna Boja", + "Square": "Square", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Tekst za prikaz", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Title Case", + "TO": "Za", + "Text": "Tekst", + "UNDO_MSG": "Poništite posljednju naredbu", + "UNKNOWN": "Nepoznato", + "UNTAB": "Untab", + "UPPERCASE": "VELIKA SLOVA", + "UPPER_ALPHA": "Upper Alpha", + "UPPER_ROMAN": "Upper Roman", + "URL": "URL", + "URL_DUPLICATE_ER": "URL je već priložen. Unesite drugi URL", + "URL_INVALID_ER": "Unesite važeći URL", + "URL_MSG": "Molimo unesite URL!", + "VIDEO": "Video", + "VIEW": "Pogled", + "WEEK": "Sedmicu" +} \ No newline at end of file diff --git a/lib/assets/strings/cs.json b/lib/assets/strings/cs.json new file mode 100644 index 0000000..5ed3f4b --- /dev/null +++ b/lib/assets/strings/cs.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Akce", + "ADD_URL": "Přidat adresu URL", + "ALL": "Vše", + "ATTACHED_FILE": "Přiložený soubor", + "ATTACH_MAXSIZE_VALIDATE": "Velikost souboru musí být menší než {0}", + "AUDIO_FILE_OR_URL_MSG": "Vyberte prosím zvukový soubor nebo zadejte adresu URL zvukového souboru!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Zadejte buď zvukový soubor, nebo adresu URL zvuku, nikoli obojí!", + "Blur": "Rozmazat", + "Blur Radius": "Poloměr rozostření", + "Brush": "Štětec", + "CAMERA": "Fotoaparát", + "CANCEL": "Storno", + "CHANGE_CASE": "Změnit případ", + "CHANGE_IMAGE": "Změnit obrázek", + "CHAR_LEFT": "Zbývající znaky {0}", + "CHOOSE_AUDIO": "Vyberte zvuk", + "CHOOSE_COLOR": "Vyberte barvu", + "CHOOSE_FILE": "Vyberte soubor", + "CHOOSE_FILE_URL_MSG": "Vyberte prosím soubor nebo zadejte URL souboru!", + "CHOOSE_IMAGE": "Vyberte obrázek", + "CHOOSE_IMG_URL_MSG": "Vyberte prosím obrázek nebo zadejte adresu URL obrázku!", + "CHOOSE_VIDEO": "Vyberte video", + "CLEAR": "Vymazat", + "CLOSE": "Zavřít", + "COMPLETE_EDITING": "Kompletní úprava", + "COURIER": "Kurýr", + "CROP": "Oříznout", + "CUSTOM": "Vlastní", + "Color Opacity": "Neprůhlednost barev", + "Crop": "Oříznout", + "DEFAULT": "Výchozí nastavení", + "DELETE": "Odstranit", + "DELETE_ATTACHMENT": "Smazat přílohu", + "DONE": "Hotovo", + "EDIT": "Upravit", + "EDITOR_CIRCLE": "Kruh", + "EDITOR_CODE": "kód", + "EDITOR_DISC": "Disk", + "EDITOR_HEADER_1": "Záhlaví 1", + "EDITOR_HEADER_2": "Záhlaví 2", + "EDITOR_HEADER_3": "Záhlaví 3", + "EDITOR_HEADER_4": "Záhlaví 4", + "EDITOR_HEADER_5": "Záhlaví 5", + "EDITOR_HEADER_6": "Záhlaví 6", + "EDITOR_NORMAL": "normální", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citát", + "EDITOR_SQUARE": "Náměstí", + "EDITOR_TEXT": "Text", + "ENTER": "ENTER", + "ESCAPE": "Uniknout", + "Emoji": "Emotikony", + "FILE": "Soubor", + "FILE_COMPRESSING_ER": "Při komprimaci souboru došlo k chybě", + "FILE_DUPLICATE_ER": "Soubor je již připojen. Vyberte prosím jiný soubor", + "FILE_OR_URL_MSG": "Zadejte buď soubor, nebo URL souboru, ne obojí!", + "FILE_PIXEL_SIZE_ER": "Velikost souboru v pixelech by měla být menší než {0}", + "FILE_PROCESSING_ER": "Při zpracování souboru došlo k chybě", + "FILE_SIZE_CALCULATING_ER": "Při výpočtu velikosti souboru došlo k chybě", + "FILE_UNSUPPORTED": "Tento typ souboru není povolen", + "FLIP": "Převrátit", + "FROM": "Od", + "Filter": "Filtr", + "Flip": "Převrátit", + "Freeform": "Volná forma", + "GALLERY": "Galerie", + "GIF": "Gif", + "HELP": "Nápověda", + "HIDE": "Skrýt", + "IMAGE_OR_URL_MSG": "Zadejte buď obrázek, nebo adresu URL obrázku, ne obojí!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Výběr obrázku. Obrázek vybrán", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Výběr obrázku. Není vybrán žádný obrázek", + "IMG_URL_MSG": "Zadejte adresu URL obrázku!", + "INSERT_AUDIO": "Vložit zvuk", + "INSERT_FILE": "Vložit soubor", + "INSERT_IMAGE": "Vložit obrázek", + "INSERT_LINK": "Vložit odkaz", + "INSERT_PARAGRAPH": "Vložit odstavec", + "INSERT_TABLE": "Vložit tabulku", + "INSERT_VIDEO": "Vložit video", + "Insert Your Message": "Vložte Vaši zprávu", + "KEY_COMBINATION": "Kombinace kláves", + "LINK": "Odkaz", + "LINK_NAME": "Název odkazu", + "LINK_URL": "Adresa URL odkazu", + "LOWERCASE": "malá písmena", + "LOWER_ALPHA": "Nižší alfa", + "LOWER_ROMAN": "Dolní římská", + "MAX_ATTACH_MSG": "Bylo dosaženo maximálního limitu příloh", + "MONTH": "Měsíc", + "NO_RESULTS": "Žádné výsledky", + "NUMBERED": "Číslovaný", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Otevři v novém okně", + "PICK_GIF": "Vyberte Gif", + "PICK_IMAGE": "Vyberte obrázek", + "REDO_MSG": "Opakujte poslední příkaz", + "REMOVE": "Odebrat", + "RESET": "Resetovat", + "REST_COLOR_MSG": "Obnovit výchozí barvu", + "ROTATE_LEFT": "Otočit doleva", + "ROTATE_RIGHT": "Otočit doprava", + "Reset": "Resetovat", + "Rotate left": "Otočit doleva", + "Rotate right": "Otočit doprava", + "SANS_SERIF": "Sans Serif", + "SAVE": "Uložit", + "SEARCH_GIF": "Prohledejte všechny GIFy", + "SELECT_FROM_FILES": "Vyberte ze souborů", + "SELECT_STL_MSG": "Vyberte styl seznamu", + "SENTENCE_CASE": "Případ věty", + "SET_COLOR": "Nastavit barvu", + "SHOW": "Zobrazit", + "SWIPE_TO_REVEAL_SEMANTIC": "Přejetím doleva zobrazíte akce", + "Select Emoji": "Vyberte Emoji", + "Slider Color": "Barva posuvníku", + "Slider White Black Color": "Posuvník Bílá Černá Barva", + "Square": "Náměstí", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text k zobrazení", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Název případ", + "TO": "Komu", + "Text": "Text", + "UNDO_MSG": "Vraťte zpět poslední příkaz", + "UNKNOWN": "Neznámý", + "UNTAB": "Untab", + "UPPERCASE": "VELKÁ PÍSMENA", + "UPPER_ALPHA": "Horní Alfa", + "UPPER_ROMAN": "horní římský", + "URL": "Adresa URL", + "URL_DUPLICATE_ER": "Adresa URL je již připojena. Zadejte prosím jinou adresu URL", + "URL_INVALID_ER": "Zadejte prosím platnou adresu URL", + "URL_MSG": "Zadejte adresu URL!", + "VIDEO": "Video", + "VIEW": "Zobrazit", + "WEEK": "Týden" +} \ No newline at end of file diff --git a/lib/assets/strings/da.json b/lib/assets/strings/da.json new file mode 100644 index 0000000..ddc4d53 --- /dev/null +++ b/lib/assets/strings/da.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Handling", + "ADD_URL": "Tilføj URL", + "ALL": "Alle", + "ATTACHED_FILE": "Attached file", + "ATTACH_MAXSIZE_VALIDATE": "Filstørrelsen skal være mindre end {0}", + "AUDIO_FILE_OR_URL_MSG": "Vælg venligst enten en lydfil eller indtast en lydfil URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Indtast enten en lydfil eller en lyd-URL, ikke begge!", + "Blur": "Blur", + "Blur Radius": "Blur Radius", + "Brush": "Brush", + "CAMERA": "Kamera", + "CANCEL": "Annuller", + "CHANGE_CASE": "Skift sag", + "CHANGE_IMAGE": "Change Image", + "CHAR_LEFT": "Tegn tilbage {0}", + "CHOOSE_AUDIO": "Vælg lyd", + "CHOOSE_COLOR": "Vælg en farve", + "CHOOSE_FILE": "Vælg fil", + "CHOOSE_FILE_URL_MSG": "Vælg venligst enten en fil eller indtast en fil-URL!", + "CHOOSE_IMAGE": "Vælg billede", + "CHOOSE_IMG_URL_MSG": "Vælg venligst enten et billede eller indtast en billed-URL!", + "CHOOSE_VIDEO": "Vælg video", + "CLEAR": "Ryd", + "CLOSE": "Luk", + "COMPLETE_EDITING": "Complete editing", + "COURIER": "kurer", + "CROP": "Beskær", + "CUSTOM": "Brugerdefineret", + "Color Opacity": "Color Opacity", + "Crop": "Beskær", + "DEFAULT": "Standard", + "DELETE": "Slet", + "DELETE_ATTACHMENT": "Delete attachment", + "DONE": "Udført", + "EDIT": "Rediger", + "EDITOR_CIRCLE": "Cirkel", + "EDITOR_CODE": "kode", + "EDITOR_DISC": "disk", + "EDITOR_HEADER_1": "Header 1", + "EDITOR_HEADER_2": "Header 2", + "EDITOR_HEADER_3": "Header 3", + "EDITOR_HEADER_4": "Header 4", + "EDITOR_HEADER_5": "Header 5", + "EDITOR_HEADER_6": "Header 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "quote", + "EDITOR_SQUARE": "Square", + "EDITOR_TEXT": "Tekst", + "ENTER": "GÅ IND", + "ESCAPE": "Flugt", + "Emoji": "Emoji", + "FILE": "Fil", + "FILE_COMPRESSING_ER": "Der opstod en fejl under komprimering af en fil", + "FILE_DUPLICATE_ER": "Filen er allerede vedhæftet. Vælg venligst en anden fil", + "FILE_OR_URL_MSG": "Indtast enten en fil eller en fil-URL, ikke begge!", + "FILE_PIXEL_SIZE_ER": "Filpixelstørrelse skal være mindre end {0}", + "FILE_PROCESSING_ER": "Der opstod en fejl under behandling af en fil", + "FILE_SIZE_CALCULATING_ER": "Der opstod en fejl under beregning af en filstørrelse", + "FILE_UNSUPPORTED": "This file type is not allowed", + "FLIP": "Flip", + "FROM": "Fra", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freeform", + "GALLERY": "Galleri", + "GIF": "Gif", + "HELP": "Hjælp", + "HIDE": "Skjul", + "IMAGE_OR_URL_MSG": "Indtast enten et billede eller en billed-URL, ikke begge!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Image Picker. Image selected", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Image Picker. No image selected", + "IMG_URL_MSG": "Indtast venligst en billed-URL!", + "INSERT_AUDIO": "Indsæt lyd", + "INSERT_FILE": "Insert File", + "INSERT_IMAGE": "Indsæt billede", + "INSERT_LINK": "Indsæt link", + "INSERT_PARAGRAPH": "Indsæt afsnit", + "INSERT_TABLE": "Indsæt tabel", + "INSERT_VIDEO": "Indsæt video", + "Insert Your Message": "Insert Your Message", + "KEY_COMBINATION": "Nøglekombination", + "LINK": "Link", + "LINK_NAME": "Linknavn", + "LINK_URL": "Link-URL", + "LOWERCASE": "små bogstaver", + "LOWER_ALPHA": "Nedre alfa", + "LOWER_ROMAN": "Nedre romersk", + "MAX_ATTACH_MSG": "Maks. grænse for vedhæftede filer nået", + "MONTH": "Måned", + "NO_RESULTS": "Ingen resultater", + "NUMBERED": "Nummereret", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Åbn i nyt vindue", + "PICK_GIF": "Vælg en gif", + "PICK_IMAGE": "Pick Image", + "REDO_MSG": "Gentag den sidste kommando", + "REMOVE": "Fjern", + "RESET": "Nulstil", + "REST_COLOR_MSG": "Nulstil til standardfarve", + "ROTATE_LEFT": "Rotate Left", + "ROTATE_RIGHT": "Rotate Right", + "Reset": "Nulstil", + "Rotate left": "Rotate left", + "Rotate right": "Rotate right", + "SANS_SERIF": "Sans serif", + "SAVE": "Gem", + "SEARCH_GIF": "Søg i alle GIF'er", + "SELECT_FROM_FILES": "Vælg fra filer", + "SELECT_STL_MSG": "Vælg listestil", + "SENTENCE_CASE": "Sætningssag", + "SET_COLOR": "Indstil farve", + "SHOW": "Vis", + "SWIPE_TO_REVEAL_SEMANTIC": "Swipe left to reveal actions", + "Select Emoji": "Select Emoji", + "Slider Color": "Slider Color", + "Slider White Black Color": "Slider White Black Color", + "Square": "Square", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Tekst, der skal vises", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titel Case", + "TO": "Til", + "Text": "Tekst", + "UNDO_MSG": "Fortryd den sidste kommando", + "UNKNOWN": "Unknown", + "UNTAB": "Fjern faneblad", + "UPPERCASE": "STORE BOGSTAVER", + "UPPER_ALPHA": "Øvre alfa", + "UPPER_ROMAN": "Øvre romersk", + "URL": "URL", + "URL_DUPLICATE_ER": "URL er allerede vedhæftet. Indtast venligst en anden URL", + "URL_INVALID_ER": "Indtast venligst en gyldig URL", + "URL_MSG": "Indtast venligst en URL!", + "VIDEO": "Video", + "VIEW": "Vis", + "WEEK": "Uge" +} \ No newline at end of file diff --git a/lib/assets/strings/de.json b/lib/assets/strings/de.json new file mode 100644 index 0000000..e7ce7da --- /dev/null +++ b/lib/assets/strings/de.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Aktion", + "ADD_URL": "URL hinzufügen", + "ALL": "Alle", + "ATTACHED_FILE": "Angehängte Datei", + "ATTACH_MAXSIZE_VALIDATE": "Die Dateigröße muss kleiner als {0} sein.", + "AUDIO_FILE_OR_URL_MSG": "Bitte wählen Sie entweder eine Audiodatei aus oder geben Sie eine Audiodatei-URL ein!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Bitte geben Sie entweder eine Audiodatei oder eine Audio-URL ein, nicht beides!", + "Blur": "Verwischen", + "Blur Radius": "Unschärferadius", + "Brush": "Bürste", + "CAMERA": "Kamera", + "CANCEL": "Abbrechen", + "CHANGE_CASE": "Fall ändern", + "CHANGE_IMAGE": "Bild ändern", + "CHAR_LEFT": "Verbleibende Zeichen {0}", + "CHOOSE_AUDIO": "Wählen Sie Audio", + "CHOOSE_COLOR": "Wähle eine Farbe", + "CHOOSE_FILE": "Datei wählen", + "CHOOSE_FILE_URL_MSG": "Bitte wählen Sie entweder eine Datei aus oder geben Sie eine Datei-URL ein!", + "CHOOSE_IMAGE": "Bild auswählen", + "CHOOSE_IMG_URL_MSG": "Bitte wählen Sie entweder ein Bild aus oder geben Sie eine Bild-URL ein!", + "CHOOSE_VIDEO": "Video auswählen", + "CLEAR": "Leeren", + "CLOSE": "Schließen", + "COMPLETE_EDITING": "Vollständige Bearbeitung", + "COURIER": "Kurier", + "CROP": "Zuschneiden", + "CUSTOM": "Benutzerdefiniert", + "Color Opacity": "Farbopazität", + "Crop": "Zuschneiden", + "DEFAULT": "Standard", + "DELETE": "Löschen", + "DELETE_ATTACHMENT": "Anhang löschen", + "DONE": "Fertig", + "EDIT": "Bearbeiten", + "EDITOR_CIRCLE": "Kreis", + "EDITOR_CODE": "Code", + "EDITOR_DISC": "Rabatt", + "EDITOR_HEADER_1": "Kopfzeile 1", + "EDITOR_HEADER_2": "Kopfzeile 2", + "EDITOR_HEADER_3": "Kopfzeile 3", + "EDITOR_HEADER_4": "Kopfzeile 4", + "EDITOR_HEADER_5": "Kopfzeile 5", + "EDITOR_HEADER_6": "Kopfzeile 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "Punkt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "zitieren", + "EDITOR_SQUARE": "Quadrat", + "EDITOR_TEXT": "Text", + "ENTER": "EINGEBEN", + "ESCAPE": "Fliehen", + "Emoji": "Emoji", + "FILE": "Datei", + "FILE_COMPRESSING_ER": "Beim Komprimieren einer Datei ist ein Fehler aufgetreten", + "FILE_DUPLICATE_ER": "Datei ist bereits angehängt. Bitte wählen Sie eine andere Datei", + "FILE_OR_URL_MSG": "Bitte geben Sie entweder eine Datei oder eine Datei-URL ein, nicht beides!", + "FILE_PIXEL_SIZE_ER": "Die Dateipixelgröße sollte kleiner als {0} sein.", + "FILE_PROCESSING_ER": "Beim Verarbeiten einer Datei ist ein Fehler aufgetreten", + "FILE_SIZE_CALCULATING_ER": "Beim Berechnen einer Dateigröße ist ein Fehler aufgetreten", + "FILE_UNSUPPORTED": "Dieser Dateityp ist nicht zulässig", + "FLIP": "Flip", + "FROM": "Von", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freiform", + "GALLERY": "Galerie", + "GIF": "Gif", + "HELP": "Hilfe", + "HIDE": "Ausblenden", + "IMAGE_OR_URL_MSG": "Bitte geben Sie entweder ein Bild oder eine Bild-URL ein, nicht beides!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Bildauswahl. Bild ausgewählt", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Bildauswahl. Kein Bild ausgewählt", + "IMG_URL_MSG": "Bitte geben Sie eine Bild-URL ein!", + "INSERT_AUDIO": "Audio einfügen", + "INSERT_FILE": "Datei einfügen", + "INSERT_IMAGE": "Bild einfügen", + "INSERT_LINK": "Link einfügen", + "INSERT_PARAGRAPH": "Absatz einfügen", + "INSERT_TABLE": "Tabelle einfügen", + "INSERT_VIDEO": "Video einfügen", + "Insert Your Message": "Fügen Sie Ihre Nachricht ein", + "KEY_COMBINATION": "Tastenkombination", + "LINK": "Verknüpfung", + "LINK_NAME": "Linkname", + "LINK_URL": "Link-URL", + "LOWERCASE": "Kleinbuchstaben", + "LOWER_ALPHA": "Unteres Alpha", + "LOWER_ROMAN": "Niederrömisch", + "MAX_ATTACH_MSG": "Maximales Limit für Anhänge erreicht", + "MONTH": "Monat", + "NO_RESULTS": "Keine Ergebnisse", + "NUMBERED": "Nummeriert", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "In einem neuen Fenster öffnen", + "PICK_GIF": "Wählen Sie ein GIF", + "PICK_IMAGE": "Bild auswählen", + "REDO_MSG": "Wiederholen Sie den letzten Befehl", + "REMOVE": "Entfernen", + "RESET": "Zurücksetzen", + "REST_COLOR_MSG": "Auf Standardfarbe zurücksetzen", + "ROTATE_LEFT": "Nach links drehen", + "ROTATE_RIGHT": "Nach rechts drehen", + "Reset": "Zurücksetzen", + "Rotate left": "Nach links drehen", + "Rotate right": "Drehe nach rechts", + "SANS_SERIF": "Serifenlos", + "SAVE": "Speichern", + "SEARCH_GIF": "Durchsuchen Sie alle GIFs", + "SELECT_FROM_FILES": "Wählen Sie aus Dateien aus", + "SELECT_STL_MSG": "Listenstil auswählen", + "SENTENCE_CASE": "Urteilsfall", + "SET_COLOR": "Farbe einstellen", + "SHOW": "Anzeigen", + "SWIPE_TO_REVEAL_SEMANTIC": "Wischen Sie nach links, um Aktionen anzuzeigen", + "Select Emoji": "Wählen Sie Emoticons aus", + "Slider Color": "Farbe des Schiebereglers", + "Slider White Black Color": "Schieberegler Farbe Weiß Schwarz", + "Square": "Quadrat", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text, der angezeigt werden soll", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titelfall", + "TO": "An", + "Text": "Text", + "UNDO_MSG": "Machen Sie den letzten Befehl rückgängig", + "UNKNOWN": "Unbekannt", + "UNTAB": "Enttab", + "UPPERCASE": "GROSSBUCHSTABEN", + "UPPER_ALPHA": "Oberes Alpha", + "UPPER_ROMAN": "Oberrömisch", + "URL": "URL", + "URL_DUPLICATE_ER": "Die URL ist bereits angehängt. Bitte geben Sie eine andere URL ein", + "URL_INVALID_ER": "Bitte geben Sie eine gültige URL ein", + "URL_MSG": "Bitte geben Sie eine URL ein!", + "VIDEO": "Video", + "VIEW": "Anzeigen", + "WEEK": "Woche" +} \ No newline at end of file diff --git a/lib/assets/strings/de_AT.json b/lib/assets/strings/de_AT.json new file mode 100644 index 0000000..e7ce7da --- /dev/null +++ b/lib/assets/strings/de_AT.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Aktion", + "ADD_URL": "URL hinzufügen", + "ALL": "Alle", + "ATTACHED_FILE": "Angehängte Datei", + "ATTACH_MAXSIZE_VALIDATE": "Die Dateigröße muss kleiner als {0} sein.", + "AUDIO_FILE_OR_URL_MSG": "Bitte wählen Sie entweder eine Audiodatei aus oder geben Sie eine Audiodatei-URL ein!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Bitte geben Sie entweder eine Audiodatei oder eine Audio-URL ein, nicht beides!", + "Blur": "Verwischen", + "Blur Radius": "Unschärferadius", + "Brush": "Bürste", + "CAMERA": "Kamera", + "CANCEL": "Abbrechen", + "CHANGE_CASE": "Fall ändern", + "CHANGE_IMAGE": "Bild ändern", + "CHAR_LEFT": "Verbleibende Zeichen {0}", + "CHOOSE_AUDIO": "Wählen Sie Audio", + "CHOOSE_COLOR": "Wähle eine Farbe", + "CHOOSE_FILE": "Datei wählen", + "CHOOSE_FILE_URL_MSG": "Bitte wählen Sie entweder eine Datei aus oder geben Sie eine Datei-URL ein!", + "CHOOSE_IMAGE": "Bild auswählen", + "CHOOSE_IMG_URL_MSG": "Bitte wählen Sie entweder ein Bild aus oder geben Sie eine Bild-URL ein!", + "CHOOSE_VIDEO": "Video auswählen", + "CLEAR": "Leeren", + "CLOSE": "Schließen", + "COMPLETE_EDITING": "Vollständige Bearbeitung", + "COURIER": "Kurier", + "CROP": "Zuschneiden", + "CUSTOM": "Benutzerdefiniert", + "Color Opacity": "Farbopazität", + "Crop": "Zuschneiden", + "DEFAULT": "Standard", + "DELETE": "Löschen", + "DELETE_ATTACHMENT": "Anhang löschen", + "DONE": "Fertig", + "EDIT": "Bearbeiten", + "EDITOR_CIRCLE": "Kreis", + "EDITOR_CODE": "Code", + "EDITOR_DISC": "Rabatt", + "EDITOR_HEADER_1": "Kopfzeile 1", + "EDITOR_HEADER_2": "Kopfzeile 2", + "EDITOR_HEADER_3": "Kopfzeile 3", + "EDITOR_HEADER_4": "Kopfzeile 4", + "EDITOR_HEADER_5": "Kopfzeile 5", + "EDITOR_HEADER_6": "Kopfzeile 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "Punkt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "zitieren", + "EDITOR_SQUARE": "Quadrat", + "EDITOR_TEXT": "Text", + "ENTER": "EINGEBEN", + "ESCAPE": "Fliehen", + "Emoji": "Emoji", + "FILE": "Datei", + "FILE_COMPRESSING_ER": "Beim Komprimieren einer Datei ist ein Fehler aufgetreten", + "FILE_DUPLICATE_ER": "Datei ist bereits angehängt. Bitte wählen Sie eine andere Datei", + "FILE_OR_URL_MSG": "Bitte geben Sie entweder eine Datei oder eine Datei-URL ein, nicht beides!", + "FILE_PIXEL_SIZE_ER": "Die Dateipixelgröße sollte kleiner als {0} sein.", + "FILE_PROCESSING_ER": "Beim Verarbeiten einer Datei ist ein Fehler aufgetreten", + "FILE_SIZE_CALCULATING_ER": "Beim Berechnen einer Dateigröße ist ein Fehler aufgetreten", + "FILE_UNSUPPORTED": "Dieser Dateityp ist nicht zulässig", + "FLIP": "Flip", + "FROM": "Von", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freiform", + "GALLERY": "Galerie", + "GIF": "Gif", + "HELP": "Hilfe", + "HIDE": "Ausblenden", + "IMAGE_OR_URL_MSG": "Bitte geben Sie entweder ein Bild oder eine Bild-URL ein, nicht beides!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Bildauswahl. Bild ausgewählt", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Bildauswahl. Kein Bild ausgewählt", + "IMG_URL_MSG": "Bitte geben Sie eine Bild-URL ein!", + "INSERT_AUDIO": "Audio einfügen", + "INSERT_FILE": "Datei einfügen", + "INSERT_IMAGE": "Bild einfügen", + "INSERT_LINK": "Link einfügen", + "INSERT_PARAGRAPH": "Absatz einfügen", + "INSERT_TABLE": "Tabelle einfügen", + "INSERT_VIDEO": "Video einfügen", + "Insert Your Message": "Fügen Sie Ihre Nachricht ein", + "KEY_COMBINATION": "Tastenkombination", + "LINK": "Verknüpfung", + "LINK_NAME": "Linkname", + "LINK_URL": "Link-URL", + "LOWERCASE": "Kleinbuchstaben", + "LOWER_ALPHA": "Unteres Alpha", + "LOWER_ROMAN": "Niederrömisch", + "MAX_ATTACH_MSG": "Maximales Limit für Anhänge erreicht", + "MONTH": "Monat", + "NO_RESULTS": "Keine Ergebnisse", + "NUMBERED": "Nummeriert", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "In einem neuen Fenster öffnen", + "PICK_GIF": "Wählen Sie ein GIF", + "PICK_IMAGE": "Bild auswählen", + "REDO_MSG": "Wiederholen Sie den letzten Befehl", + "REMOVE": "Entfernen", + "RESET": "Zurücksetzen", + "REST_COLOR_MSG": "Auf Standardfarbe zurücksetzen", + "ROTATE_LEFT": "Nach links drehen", + "ROTATE_RIGHT": "Nach rechts drehen", + "Reset": "Zurücksetzen", + "Rotate left": "Nach links drehen", + "Rotate right": "Drehe nach rechts", + "SANS_SERIF": "Serifenlos", + "SAVE": "Speichern", + "SEARCH_GIF": "Durchsuchen Sie alle GIFs", + "SELECT_FROM_FILES": "Wählen Sie aus Dateien aus", + "SELECT_STL_MSG": "Listenstil auswählen", + "SENTENCE_CASE": "Urteilsfall", + "SET_COLOR": "Farbe einstellen", + "SHOW": "Anzeigen", + "SWIPE_TO_REVEAL_SEMANTIC": "Wischen Sie nach links, um Aktionen anzuzeigen", + "Select Emoji": "Wählen Sie Emoticons aus", + "Slider Color": "Farbe des Schiebereglers", + "Slider White Black Color": "Schieberegler Farbe Weiß Schwarz", + "Square": "Quadrat", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text, der angezeigt werden soll", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titelfall", + "TO": "An", + "Text": "Text", + "UNDO_MSG": "Machen Sie den letzten Befehl rückgängig", + "UNKNOWN": "Unbekannt", + "UNTAB": "Enttab", + "UPPERCASE": "GROSSBUCHSTABEN", + "UPPER_ALPHA": "Oberes Alpha", + "UPPER_ROMAN": "Oberrömisch", + "URL": "URL", + "URL_DUPLICATE_ER": "Die URL ist bereits angehängt. Bitte geben Sie eine andere URL ein", + "URL_INVALID_ER": "Bitte geben Sie eine gültige URL ein", + "URL_MSG": "Bitte geben Sie eine URL ein!", + "VIDEO": "Video", + "VIEW": "Anzeigen", + "WEEK": "Woche" +} \ No newline at end of file diff --git a/lib/assets/strings/de_BE.json b/lib/assets/strings/de_BE.json new file mode 100644 index 0000000..98a4206 --- /dev/null +++ b/lib/assets/strings/de_BE.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Aktion", + "ADD_URL": "URL hinzufügen", + "ALL": "Alle", + "ATTACHED_FILE": "Angehängte Datei", + "ATTACH_MAXSIZE_VALIDATE": "Die Dateigröße muss kleiner als {0} sein.", + "AUDIO_FILE_OR_URL_MSG": "Bitte wählen Sie entweder eine Audiodatei aus oder geben Sie eine Audiodatei-URL ein!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Bitte geben Sie entweder eine Audiodatei oder eine Audio-URL ein, nicht beides!", + "Blur": "Verwischen", + "Blur Radius": "Unschärferadius", + "Brush": "Bürste", + "CAMERA": "Kamera", + "CANCEL": "Abbrechen", + "CHANGE_CASE": "Fall ändern", + "CHANGE_IMAGE": "Bild ändern", + "CHAR_LEFT": "Verbleibende Zeichen {0}", + "CHOOSE_AUDIO": "Wählen Sie Audio", + "CHOOSE_COLOR": "Wähle eine Farbe", + "CHOOSE_FILE": "Datei wählen", + "CHOOSE_FILE_URL_MSG": "Bitte wählen Sie entweder eine Datei aus oder geben Sie eine Datei-URL ein!", + "CHOOSE_IMAGE": "Bild auswählen", + "CHOOSE_IMG_URL_MSG": "Bitte wählen Sie entweder ein Bild aus oder geben Sie eine Bild-URL ein!", + "CHOOSE_VIDEO": "Video auswählen", + "CLEAR": "Klar", + "CLOSE": "Schließen", + "COMPLETE_EDITING": "Vollständige Bearbeitung", + "COURIER": "Kurier", + "CROP": "Ernte", + "CUSTOM": "Cusuts", + "Color Opacity": "Farbopazität", + "Crop": "Ernte", + "DEFAULT": "Standard", + "DELETE": "Löschen", + "DELETE_ATTACHMENT": "Anhang löschen", + "DONE": "Erledigen", + "EDIT": "Bearbeiten", + "EDITOR_CIRCLE": "Kreis", + "EDITOR_CODE": "Code", + "EDITOR_DISC": "Rabatt", + "EDITOR_HEADER_1": "Kopfzeile 1", + "EDITOR_HEADER_2": "Kopfzeile 2", + "EDITOR_HEADER_3": "Kopfzeile 3", + "EDITOR_HEADER_4": "Kopfzeile 4", + "EDITOR_HEADER_5": "Kopfzeile 5", + "EDITOR_HEADER_6": "Kopfzeile 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "Punkt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "zitieren", + "EDITOR_SQUARE": "Quadrat", + "EDITOR_TEXT": "Text", + "ENTER": "EINGEBEN", + "ESCAPE": "Fliehen", + "Emoji": "Emoji", + "FILE": "Datei", + "FILE_COMPRESSING_ER": "Beim Komprimieren einer Datei ist ein Fehler aufgetreten", + "FILE_DUPLICATE_ER": "Datei ist bereits angehängt. Bitte wählen Sie eine andere Datei", + "FILE_OR_URL_MSG": "Bitte geben Sie entweder eine Datei oder eine Datei-URL ein, nicht beides!", + "FILE_PIXEL_SIZE_ER": "Die Dateipixelgröße sollte kleiner als {0} sein.", + "FILE_PROCESSING_ER": "Beim Verarbeiten einer Datei ist ein Fehler aufgetreten", + "FILE_SIZE_CALCULATING_ER": "Beim Berechnen einer Dateigröße ist ein Fehler aufgetreten", + "FILE_UNSUPPORTED": "Dieser Dateityp ist nicht zulässig", + "FLIP": "Flip", + "FROM": "Von", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freiform", + "GALLERY": "Galerie", + "GIF": "Gif", + "HELP": "Hilfe", + "HIDE": "verbergen", + "IMAGE_OR_URL_MSG": "Bitte geben Sie entweder ein Bild oder eine Bild-URL ein, nicht beides!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Bildauswahl. Bild ausgewählt", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Bildauswahl. Kein Bild ausgewählt", + "IMG_URL_MSG": "Bitte geben Sie eine Bild-URL ein!", + "INSERT_AUDIO": "Audio einfügen", + "INSERT_FILE": "Datei einfügen", + "INSERT_IMAGE": "Bild einfügen", + "INSERT_LINK": "Link einfügen", + "INSERT_PARAGRAPH": "Absatz einfügen", + "INSERT_TABLE": "Tabelle einfügen", + "INSERT_VIDEO": "Video einfügen", + "Insert Your Message": "Fügen Sie Ihre Nachricht ein", + "KEY_COMBINATION": "Tastenkombination", + "LINK": "Verknüpfung", + "LINK_NAME": "Linkname", + "LINK_URL": "URL verknüpfen", + "LOWERCASE": "Kleinbuchstaben", + "LOWER_ALPHA": "Unteres Alpha", + "LOWER_ROMAN": "Niederrömisch", + "MAX_ATTACH_MSG": "Maximales Limit für Anhänge erreicht", + "MONTH": "Monat", + "NO_RESULTS": "Keine Ergebnisse", + "NUMBERED": "Nummeriert", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "In einem neuen Fenster öffnen", + "PICK_GIF": "Wählen Sie ein GIF", + "PICK_IMAGE": "Bild auswählen", + "REDO_MSG": "Wiederholen Sie den letzten Befehl", + "REMOVE": "Entfernen", + "RESET": "Zurücksetzen", + "REST_COLOR_MSG": "Auf Standardfarbe zurücksetzen", + "ROTATE_LEFT": "Nach links drehen", + "ROTATE_RIGHT": "Nach rechts drehen", + "Reset": "Zurücksetzen", + "Rotate left": "Nach links drehen", + "Rotate right": "Drehe nach rechts", + "SANS_SERIF": "Serifenlos", + "SAVE": "Speichern", + "SEARCH_GIF": "Durchsuchen Sie alle GIFs", + "SELECT_FROM_FILES": "Wählen Sie aus Dateien aus", + "SELECT_STL_MSG": "Listenstil auswählen", + "SENTENCE_CASE": "Satzfall", + "SET_COLOR": "Farbe einstellen", + "SHOW": "Anzeigen", + "SWIPE_TO_REVEAL_SEMANTIC": "Wischen Sie nach links, um Aktionen anzuzeigen", + "Select Emoji": "Wählen Sie Emoticons aus", + "Slider Color": "Farbe des Schiebereglers", + "Slider White Black Color": "Schieberegler Farbe Weiß Schwarz", + "Square": "Quadrat", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text, der angezeigt werden soll", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titelfall", + "TO": "Bis", + "Text": "Text", + "UNDO_MSG": "Machen Sie den letzten Befehl rückgängig", + "UNKNOWN": "Unbekannt", + "UNTAB": "Enttab", + "UPPERCASE": "GROSSBUCHSTABEN", + "UPPER_ALPHA": "Oberes Alpha", + "UPPER_ROMAN": "Oberrömisch", + "URL": "URL", + "URL_DUPLICATE_ER": "Die URL ist bereits angehängt. Bitte geben Sie eine andere URL ein", + "URL_INVALID_ER": "Bitte geben Sie eine gültige URL ein", + "URL_MSG": "Bitte geben Sie eine URL ein!", + "VIDEO": "Video", + "VIEW": "Anzeigen", + "WEEK": "Woche" +} \ No newline at end of file diff --git a/lib/assets/strings/de_CH.json b/lib/assets/strings/de_CH.json new file mode 100644 index 0000000..43a5541 --- /dev/null +++ b/lib/assets/strings/de_CH.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Aktion", + "ADD_URL": "URL hinzufügen", + "ALL": "Alle", + "ATTACHED_FILE": "Angehängte Datei", + "ATTACH_MAXSIZE_VALIDATE": "Die Dateigröße muss kleiner als {0} sein.", + "AUDIO_FILE_OR_URL_MSG": "Bitte wählen Sie entweder eine Audiodatei aus oder geben Sie eine Audiodatei-URL ein!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Bitte geben Sie entweder eine Audiodatei oder eine Audio-URL ein, nicht beides!", + "Blur": "Verwischen", + "Blur Radius": "Unschärferadius", + "Brush": "Bürste", + "CAMERA": "Kamera", + "CANCEL": "Abbrechen", + "CHANGE_CASE": "Fall ändern", + "CHANGE_IMAGE": "Bild ändern", + "CHAR_LEFT": "Verbleibende Zeichen {0}", + "CHOOSE_AUDIO": "Wählen Sie Audio", + "CHOOSE_COLOR": "Wähle eine Farbe", + "CHOOSE_FILE": "Datei wählen", + "CHOOSE_FILE_URL_MSG": "Bitte wählen Sie entweder eine Datei aus oder geben Sie eine Datei-URL ein!", + "CHOOSE_IMAGE": "Bild auswählen", + "CHOOSE_IMG_URL_MSG": "Bitte wählen Sie entweder ein Bild aus oder geben Sie eine Bild-URL ein!", + "CHOOSE_VIDEO": "Video auswählen", + "CLEAR": "Klar", + "CLOSE": "Schließen", + "COMPLETE_EDITING": "Vollständige Bearbeitung", + "COURIER": "Kurier", + "CROP": "Ernte", + "CUSTOM": "Cusuts", + "Color Opacity": "Farbopazität", + "Crop": "Ernte", + "DEFAULT": "Standard", + "DELETE": "Löschen", + "DELETE_ATTACHMENT": "Anhang löschen", + "DONE": "Erledigen", + "EDIT": "Bearbeiten", + "EDITOR_CIRCLE": "Kreis", + "EDITOR_CODE": "Code", + "EDITOR_DISC": "Rabatt", + "EDITOR_HEADER_1": "Kopfzeile 1", + "EDITOR_HEADER_2": "Kopfzeile 2", + "EDITOR_HEADER_3": "Kopfzeile 3", + "EDITOR_HEADER_4": "Kopfzeile 4", + "EDITOR_HEADER_5": "Kopfzeile 5", + "EDITOR_HEADER_6": "Kopfzeile 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "Punkt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "zitieren", + "EDITOR_SQUARE": "Quadrat", + "EDITOR_TEXT": "Text", + "ENTER": "EINGEBEN", + "ESCAPE": "Fliehen", + "Emoji": "Emoji", + "FILE": "Datei", + "FILE_COMPRESSING_ER": "Beim Komprimieren einer Datei ist ein Fehler aufgetreten", + "FILE_DUPLICATE_ER": "Datei ist bereits angehängt. Bitte wählen Sie eine andere Datei", + "FILE_OR_URL_MSG": "Bitte geben Sie entweder eine Datei oder eine Datei-URL ein, nicht beides!", + "FILE_PIXEL_SIZE_ER": "Die Dateipixelgröße sollte kleiner als {0} sein.", + "FILE_PROCESSING_ER": "Beim Verarbeiten einer Datei ist ein Fehler aufgetreten", + "FILE_SIZE_CALCULATING_ER": "Beim Berechnen einer Dateigröße ist ein Fehler aufgetreten", + "FILE_UNSUPPORTED": "Dieser Dateityp ist nicht zulässig", + "FLIP": "Flip", + "FROM": "Von", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freiform", + "GALLERY": "Galerie", + "GIF": "Gif", + "HELP": "Hilfe", + "HIDE": "verbergen", + "IMAGE_OR_URL_MSG": "Bitte geben Sie entweder ein Bild oder eine Bild-URL ein, nicht beides!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Bildauswahl. Bild ausgewählt", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Bildauswahl. Kein Bild ausgewählt", + "IMG_URL_MSG": "Bitte geben Sie eine Bild-URL ein!", + "INSERT_AUDIO": "Audio einfügen", + "INSERT_FILE": "Datei einfügen", + "INSERT_IMAGE": "Bild einfügen", + "INSERT_LINK": "Link einfügen", + "INSERT_PARAGRAPH": "Absatz einfügen", + "INSERT_TABLE": "Tabelle einfügen", + "INSERT_VIDEO": "Video einfügen", + "Insert Your Message": "Fügen Sie Ihre Nachricht ein", + "KEY_COMBINATION": "Tastenkombination", + "LINK": "Verknüpfung", + "LINK_NAME": "Linkname", + "LINK_URL": "URL verknüpfen", + "LOWERCASE": "Kleinbuchstaben", + "LOWER_ALPHA": "Unteres Alpha", + "LOWER_ROMAN": "Niederrömisch", + "MAX_ATTACH_MSG": "Maximales Limit für Anhänge erreicht", + "MONTH": "Monat", + "NO_RESULTS": "Keine Ergebnisse", + "NUMBERED": "Nummeriert", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "In einem neuen Fenster öffnen", + "PICK_GIF": "Wählen Sie ein GIF", + "PICK_IMAGE": "Bild auswählen", + "REDO_MSG": "Wiederholen Sie den letzten Befehl", + "REMOVE": "Entfernen", + "RESET": "Zurücksetzen", + "REST_COLOR_MSG": "Auf Standardfarbe zurücksetzen", + "ROTATE_LEFT": "Nach links drehen", + "ROTATE_RIGHT": "Nach rechts drehen", + "Reset": "Zurücksetzen", + "Rotate left": "Nach links drehen", + "Rotate right": "Drehe nach rechts", + "SANS_SERIF": "Serifenlos", + "SAVE": "Speichern", + "SEARCH_GIF": "Durchsuchen Sie alle GIFs", + "SELECT_FROM_FILES": "Wählen Sie aus Dateien aus", + "SELECT_STL_MSG": "Listenstil auswählen", + "SENTENCE_CASE": "Urteilsfall", + "SET_COLOR": "Farbe einstellen", + "SHOW": "Anzeigen", + "SWIPE_TO_REVEAL_SEMANTIC": "Wischen Sie nach links, um Aktionen anzuzeigen", + "Select Emoji": "Wählen Sie Emoticons aus", + "Slider Color": "Farbe des Schiebereglers", + "Slider White Black Color": "Schieberegler Farbe Weiß Schwarz", + "Square": "Quadrat", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text, der angezeigt werden soll", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titelfall", + "TO": "Bis", + "Text": "Text", + "UNDO_MSG": "Machen Sie den letzten Befehl rückgängig", + "UNKNOWN": "Unbekannt", + "UNTAB": "Enttab", + "UPPERCASE": "GROSSBUCHSTABEN", + "UPPER_ALPHA": "Oberes Alpha", + "UPPER_ROMAN": "Oberrömisch", + "URL": "URL", + "URL_DUPLICATE_ER": "Die URL ist bereits angehängt. Bitte geben Sie eine andere URL ein", + "URL_INVALID_ER": "Bitte geben Sie eine gültige URL ein", + "URL_MSG": "Bitte geben Sie eine URL ein!", + "VIDEO": "Video", + "VIEW": "Anzeigen", + "WEEK": "Woche" +} \ No newline at end of file diff --git a/lib/assets/strings/el.json b/lib/assets/strings/el.json new file mode 100644 index 0000000..96e9a4b --- /dev/null +++ b/lib/assets/strings/el.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Δράση", + "ADD_URL": "Προσθήκη διεύθυνσης URL", + "ALL": "Ολα", + "ATTACHED_FILE": "Συνημμένο αρχείο", + "ATTACH_MAXSIZE_VALIDATE": "Το μέγεθος του αρχείου πρέπει να είναι μικρότερο από {0}", + "AUDIO_FILE_OR_URL_MSG": "Επιλέξτε ένα αρχείο ήχου ή εισαγάγετε μια διεύθυνση URL αρχείου ήχου!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Εισαγάγετε είτε ένα αρχείο ήχου είτε μια διεύθυνση URL ήχου, όχι και τα δύο!", + "Blur": "Θολούρα", + "Blur Radius": "Ακτίνα θολώματος", + "Brush": "Βούρτσα", + "CAMERA": "ΦΩΤΟΓΡΑΦΙΚΗ ΜΗΧΑΝΗ", + "CANCEL": "Ματαίωση", + "CHANGE_CASE": "Αλλαγή υπόθεσης", + "CHANGE_IMAGE": "Αλλαγή εικόνας", + "CHAR_LEFT": "Χαρακτήρες που απομένουν {0}", + "CHOOSE_AUDIO": "Επιλέξτε ήχο", + "CHOOSE_COLOR": "Επιλέξτε ένα χρώμα", + "CHOOSE_FILE": "Επιλέξτε το αρχείο", + "CHOOSE_FILE_URL_MSG": "Επιλέξτε ένα αρχείο ή εισαγάγετε μια διεύθυνση URL αρχείου!", + "CHOOSE_IMAGE": "Επιλέξτε εικόνα", + "CHOOSE_IMG_URL_MSG": "Επιλέξτε μια εικόνα ή εισαγάγετε μια διεύθυνση URL εικόνας!", + "CHOOSE_VIDEO": "Επιλέξτε βίντεο", + "CLEAR": "Σαφή", + "CLOSE": "Κοντά", + "COMPLETE_EDITING": "Ολοκληρωμένη επεξεργασία", + "COURIER": "Μεταφορέας", + "CROP": "Καλλιέργεια", + "CUSTOM": "Εθιμο", + "Color Opacity": "Αδιαφάνεια χρώματος", + "Crop": "Καλλιέργεια", + "DEFAULT": "Προκαθορισμένο", + "DELETE": "Διαγράφω", + "DELETE_ATTACHMENT": "Διαγραφή συνημμένου", + "DONE": "Εγινε", + "EDIT": "Επεξεργασία", + "EDITOR_CIRCLE": "Κύκλος", + "EDITOR_CODE": "κώδικας", + "EDITOR_DISC": "Δίσκος", + "EDITOR_HEADER_1": "Κεφαλίδα 1", + "EDITOR_HEADER_2": "Κεφαλίδα 2", + "EDITOR_HEADER_3": "Κεφαλίδα 3", + "EDITOR_HEADER_4": "Κεφαλίδα 4", + "EDITOR_HEADER_5": "Κεφαλίδα 5", + "EDITOR_HEADER_6": "Κεφαλίδα 6", + "EDITOR_NORMAL": "κανονικός", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "παραθέτω, αναφορά", + "EDITOR_SQUARE": "τετράγωνο", + "EDITOR_TEXT": "Κείμενο", + "ENTER": "ΕΙΣΑΓΩ", + "ESCAPE": "Διαφυγή", + "Emoji": "Emoji", + "FILE": "Αρχείο", + "FILE_COMPRESSING_ER": "Παρουσιάστηκε σφάλμα κατά τη συμπίεση ενός αρχείου", + "FILE_DUPLICATE_ER": "Το αρχείο είναι ήδη συνημμένο. Επιλέξτε διαφορετικό αρχείο", + "FILE_OR_URL_MSG": "Εισαγάγετε είτε ένα αρχείο είτε μια διεύθυνση URL αρχείου, όχι και τα δύο!", + "FILE_PIXEL_SIZE_ER": "Το μέγεθος pixel αρχείου πρέπει να είναι μικρότερο από {0}", + "FILE_PROCESSING_ER": "Παρουσιάστηκε σφάλμα κατά την επεξεργασία ενός αρχείου", + "FILE_SIZE_CALCULATING_ER": "Παρουσιάστηκε σφάλμα κατά τον υπολογισμό ενός μεγέθους αρχείου", + "FILE_UNSUPPORTED": "Αυτός ο τύπος αρχείου δεν επιτρέπεται", + "FLIP": "Αναρρίπτω", + "FROM": "Από", + "Filter": "Φίλτρο", + "Flip": "Αναρρίπτω", + "Freeform": "Ελεύθερη μορφή", + "GALLERY": "Εκθεσιακός χώρος", + "GIF": "Gif", + "HELP": "Βοήθεια", + "HIDE": "Κρύβω", + "IMAGE_OR_URL_MSG": "Εισαγάγετε είτε μια εικόνα είτε μια διεύθυνση URL εικόνας, όχι και τα δύο!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Επιλογέας εικόνας. Επιλέχθηκε η εικόνα", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Επιλογέας εικόνας. Δεν έχει επιλεγεί εικόνα", + "IMG_URL_MSG": "Εισαγάγετε μια διεύθυνση URL εικόνας!", + "INSERT_AUDIO": "Εισαγωγή ήχου", + "INSERT_FILE": "Εισαγωγή αρχείου", + "INSERT_IMAGE": "Εισαγωγή εικόνας", + "INSERT_LINK": "Εισαγωγή συνδέσμου", + "INSERT_PARAGRAPH": "Εισαγωγή παραγράφου", + "INSERT_TABLE": "Εισαγωγή πίνακα", + "INSERT_VIDEO": "Εισαγωγή βίντεο", + "Insert Your Message": "Εισαγάγετε το μήνυμά σας", + "KEY_COMBINATION": "Συνδυασμός κλειδιών", + "LINK": "Σύνδεσμος", + "LINK_NAME": "Όνομα συνδέσμου", + "LINK_URL": "Διεύθυνση URL σύνδεσης", + "LOWERCASE": "πεζά", + "LOWER_ALPHA": "Κάτω Άλφα", + "LOWER_ROMAN": "Κάτω Ρωμαϊκή", + "MAX_ATTACH_MSG": "Συμπληρώθηκε το μέγιστο όριο συνημμένου", + "MONTH": "Μήνας", + "NO_RESULTS": "Χωρίς αποτέλεσμα", + "NUMBERED": "Αριθμημένο", + "OK": "Εντάξει", + "OPEN_IN_NEW_WINDOW": "Ανοιξε σε νέο παράθυρο", + "PICK_GIF": "Επιλέξτε ένα Gif", + "PICK_IMAGE": "Επιλέξτε εικόνα", + "REDO_MSG": "Επαναλάβετε την τελευταία εντολή", + "REMOVE": "Αφαιρώ", + "RESET": "Επαναφορά", + "REST_COLOR_MSG": "Επαναφορά στο προεπιλεγμένο χρώμα", + "ROTATE_LEFT": "Περιστροφή αριστερά", + "ROTATE_RIGHT": "Περιστροφή δεξιά", + "Reset": "Επαναφορά", + "Rotate left": "Περιστροφή αριστερά", + "Rotate right": "Περιστροφή δεξιά", + "SANS_SERIF": "Σανς σέριφ", + "SAVE": "Αποθηκεύσετε", + "SEARCH_GIF": "Αναζήτηση όλων των GIF", + "SELECT_FROM_FILES": "Επιλέξτε από αρχεία", + "SELECT_STL_MSG": "Επιλέξτε στυλ λίστας", + "SENTENCE_CASE": "Περίπτωση ποινής", + "SET_COLOR": "Σετ χρώματος", + "SHOW": "προβολή", + "SWIPE_TO_REVEAL_SEMANTIC": "Σύρετε προς τα αριστερά για να αποκαλύψετε ενέργειες", + "Select Emoji": "Επιλέξτε Emoji", + "Slider Color": "Χρώμα ρυθμιστικού", + "Slider White Black Color": "Slider Λευκό Μαύρο Χρώμα", + "Square": "τετράγωνο", + "TAB": "ΑΥΤΙ", + "TEXT_TO_DISPLAY": "Κείμενο προς εμφάνιση", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Υπόθεση τίτλου", + "TO": "Προς το", + "Text": "Κείμενο", + "UNDO_MSG": "Αναίρεση της τελευταίας εντολής", + "UNKNOWN": "Αγνωστος", + "UNTAB": "Untab", + "UPPERCASE": "ΚΕΦΑΛΑΙΟ", + "UPPER_ALPHA": "Άνω Άλφα", + "UPPER_ROMAN": "Άνω Ρωμαίος", + "URL": "URL", + "URL_DUPLICATE_ER": "Η διεύθυνση URL είναι ήδη συνδεδεμένη. Εισαγάγετε διαφορετική διεύθυνση URL", + "URL_INVALID_ER": "Εισαγάγετε έγκυρη διεύθυνση URL", + "URL_MSG": "Εισαγάγετε μια διεύθυνση URL!", + "VIDEO": "βίντεο", + "VIEW": "Θέα", + "WEEK": "Εβδομάδα" +} \ No newline at end of file diff --git a/lib/assets/strings/en.json b/lib/assets/strings/en.json new file mode 100644 index 0000000..f8c6cad --- /dev/null +++ b/lib/assets/strings/en.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Action", + "ADD_URL": "Add URL", + "ALL": "All", + "ATTACHED_FILE": "Attached file", + "ATTACH_MAXSIZE_VALIDATE": "File size must be less than {0}", + "AUDIO_FILE_OR_URL_MSG": "Please either choose an audio file or enter an audio file URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Please input either an audio file or an audio URL, not both!", + "Blur": "Blur", + "Blur Radius": "Blur Radius", + "Brush": "Brush", + "CAMERA": "Camera", + "CANCEL": "Cancel", + "CHANGE_CASE": "Change case", + "CHANGE_IMAGE": "Change Image", + "CHAR_LEFT": "Characters Left {0}", + "CHOOSE_AUDIO": "Choose audio", + "CHOOSE_COLOR": "Choose a Color", + "CHOOSE_FILE": "Choose file", + "CHOOSE_FILE_URL_MSG": "Please either choose a file or enter a file URL!", + "CHOOSE_IMAGE": "Choose image", + "CHOOSE_IMG_URL_MSG": "Please either choose an image or enter an image URL!", + "CHOOSE_VIDEO": "Choose video", + "CLEAR": "Clear", + "CLOSE": "Close", + "COMPLETE_EDITING": "Complete editing", + "COURIER": "Courier", + "CROP": "Crop", + "CUSTOM": "Custom", + "Color Opacity": "Color Opacity", + "Crop": "Crop", + "DEFAULT": "Default", + "DELETE": "Delete", + "DELETE_ATTACHMENT": "Delete attachment", + "DONE": "Done", + "EDIT": "Edit", + "EDITOR_CIRCLE": "Circle", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Disc", + "EDITOR_HEADER_1": "Header 1", + "EDITOR_HEADER_2": "Header 2", + "EDITOR_HEADER_3": "Header 3", + "EDITOR_HEADER_4": "Header 4", + "EDITOR_HEADER_5": "Header 5", + "EDITOR_HEADER_6": "Header 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "quote", + "EDITOR_SQUARE": "Square", + "EDITOR_TEXT": "Text", + "ENTER": "ENTER", + "ESCAPE": "Escape", + "Emoji": "Emoji", + "FILE": "File", + "FILE_COMPRESSING_ER": "An error occurred while compressing a file", + "FILE_DUPLICATE_ER": "File is already attached. Please choose different file", + "FILE_OR_URL_MSG": "Please input either a file or a file URL, not both!", + "FILE_PIXEL_SIZE_ER": "File pixel size should be less than {0}", + "FILE_PROCESSING_ER": "An error occurred while processing a file", + "FILE_SIZE_CALCULATING_ER": "An error occurred while calculating a file size", + "FILE_UNSUPPORTED": "This file type is not allowed", + "FLIP": "Flip", + "FROM": "From", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freeform", + "GALLERY": "Gallery", + "GIF": "Gif", + "HELP": "Help", + "HIDE": "Hide", + "IMAGE_OR_URL_MSG": "Please input either an image or an image URL, not both!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Image Picker. Image selected", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Image Picker. No image selected", + "IMG_URL_MSG": "Please enter an image URL!", + "INSERT_AUDIO": "Insert Audio", + "INSERT_FILE": "Insert File", + "INSERT_IMAGE": "Insert Image", + "INSERT_LINK": "Insert Link", + "INSERT_PARAGRAPH": "Insert Paragraph", + "INSERT_TABLE": "Insert Table", + "INSERT_VIDEO": "Insert Video", + "Insert Your Message": "Insert Your Message", + "KEY_COMBINATION": "Key Combination", + "LINK": "Link", + "LINK_NAME": "Link Name", + "LINK_URL": "Link URL", + "LOWERCASE": "lowercase", + "LOWER_ALPHA": "Lower Alpha", + "LOWER_ROMAN": "Lower Roman", + "MAX_ATTACH_MSG": "Attachment max limit reached", + "MONTH": "Month", + "NO_RESULTS": "No results", + "NUMBERED": "Numbered", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Open in new window", + "PICK_GIF": "Pick a Gif", + "PICK_IMAGE": "Pick Image", + "REDO_MSG": "Redo the last command", + "REMOVE": "Remove", + "RESET": "Reset", + "REST_COLOR_MSG": "Reset to default color", + "ROTATE_LEFT": "Rotate Left", + "ROTATE_RIGHT": "Rotate Right", + "Reset": "Reset", + "Rotate left": "Rotate left", + "Rotate right": "Rotate right", + "SANS_SERIF": "Sans Serif", + "SAVE": "Save", + "SEARCH_GIF": "Search all the GIFs", + "SELECT_FROM_FILES": "Select from files", + "SELECT_STL_MSG": "Select list style", + "SENTENCE_CASE": "Sentence case", + "SET_COLOR": "Set color", + "SHOW": "Show", + "SWIPE_TO_REVEAL_SEMANTIC": "Swipe left to reveal actions", + "Select Emoji": "Select Emoji", + "Slider Color": "Slider Color", + "Slider White Black Color": "Slider White Black Color", + "Square": "Square", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text to display", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Title Case", + "TO": "To", + "Text": "Text", + "UNDO_MSG": "Undo the last command", + "UNKNOWN": "Unknown", + "UNTAB": "Untab", + "UPPERCASE": "UPPERCASE", + "UPPER_ALPHA": "Upper Alpha", + "UPPER_ROMAN": "Upper Roman", + "URL": "URL", + "URL_DUPLICATE_ER": "URL is already attached. Please enter different URL", + "URL_INVALID_ER": "Please enter valid URL", + "URL_MSG": "Please enter a URL!", + "VIDEO": "Video", + "VIEW": "View", + "WEEK": "Week" +} \ No newline at end of file diff --git a/lib/assets/strings/en_CA.json b/lib/assets/strings/en_CA.json new file mode 100644 index 0000000..f8c6cad --- /dev/null +++ b/lib/assets/strings/en_CA.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Action", + "ADD_URL": "Add URL", + "ALL": "All", + "ATTACHED_FILE": "Attached file", + "ATTACH_MAXSIZE_VALIDATE": "File size must be less than {0}", + "AUDIO_FILE_OR_URL_MSG": "Please either choose an audio file or enter an audio file URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Please input either an audio file or an audio URL, not both!", + "Blur": "Blur", + "Blur Radius": "Blur Radius", + "Brush": "Brush", + "CAMERA": "Camera", + "CANCEL": "Cancel", + "CHANGE_CASE": "Change case", + "CHANGE_IMAGE": "Change Image", + "CHAR_LEFT": "Characters Left {0}", + "CHOOSE_AUDIO": "Choose audio", + "CHOOSE_COLOR": "Choose a Color", + "CHOOSE_FILE": "Choose file", + "CHOOSE_FILE_URL_MSG": "Please either choose a file or enter a file URL!", + "CHOOSE_IMAGE": "Choose image", + "CHOOSE_IMG_URL_MSG": "Please either choose an image or enter an image URL!", + "CHOOSE_VIDEO": "Choose video", + "CLEAR": "Clear", + "CLOSE": "Close", + "COMPLETE_EDITING": "Complete editing", + "COURIER": "Courier", + "CROP": "Crop", + "CUSTOM": "Custom", + "Color Opacity": "Color Opacity", + "Crop": "Crop", + "DEFAULT": "Default", + "DELETE": "Delete", + "DELETE_ATTACHMENT": "Delete attachment", + "DONE": "Done", + "EDIT": "Edit", + "EDITOR_CIRCLE": "Circle", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Disc", + "EDITOR_HEADER_1": "Header 1", + "EDITOR_HEADER_2": "Header 2", + "EDITOR_HEADER_3": "Header 3", + "EDITOR_HEADER_4": "Header 4", + "EDITOR_HEADER_5": "Header 5", + "EDITOR_HEADER_6": "Header 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "quote", + "EDITOR_SQUARE": "Square", + "EDITOR_TEXT": "Text", + "ENTER": "ENTER", + "ESCAPE": "Escape", + "Emoji": "Emoji", + "FILE": "File", + "FILE_COMPRESSING_ER": "An error occurred while compressing a file", + "FILE_DUPLICATE_ER": "File is already attached. Please choose different file", + "FILE_OR_URL_MSG": "Please input either a file or a file URL, not both!", + "FILE_PIXEL_SIZE_ER": "File pixel size should be less than {0}", + "FILE_PROCESSING_ER": "An error occurred while processing a file", + "FILE_SIZE_CALCULATING_ER": "An error occurred while calculating a file size", + "FILE_UNSUPPORTED": "This file type is not allowed", + "FLIP": "Flip", + "FROM": "From", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freeform", + "GALLERY": "Gallery", + "GIF": "Gif", + "HELP": "Help", + "HIDE": "Hide", + "IMAGE_OR_URL_MSG": "Please input either an image or an image URL, not both!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Image Picker. Image selected", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Image Picker. No image selected", + "IMG_URL_MSG": "Please enter an image URL!", + "INSERT_AUDIO": "Insert Audio", + "INSERT_FILE": "Insert File", + "INSERT_IMAGE": "Insert Image", + "INSERT_LINK": "Insert Link", + "INSERT_PARAGRAPH": "Insert Paragraph", + "INSERT_TABLE": "Insert Table", + "INSERT_VIDEO": "Insert Video", + "Insert Your Message": "Insert Your Message", + "KEY_COMBINATION": "Key Combination", + "LINK": "Link", + "LINK_NAME": "Link Name", + "LINK_URL": "Link URL", + "LOWERCASE": "lowercase", + "LOWER_ALPHA": "Lower Alpha", + "LOWER_ROMAN": "Lower Roman", + "MAX_ATTACH_MSG": "Attachment max limit reached", + "MONTH": "Month", + "NO_RESULTS": "No results", + "NUMBERED": "Numbered", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Open in new window", + "PICK_GIF": "Pick a Gif", + "PICK_IMAGE": "Pick Image", + "REDO_MSG": "Redo the last command", + "REMOVE": "Remove", + "RESET": "Reset", + "REST_COLOR_MSG": "Reset to default color", + "ROTATE_LEFT": "Rotate Left", + "ROTATE_RIGHT": "Rotate Right", + "Reset": "Reset", + "Rotate left": "Rotate left", + "Rotate right": "Rotate right", + "SANS_SERIF": "Sans Serif", + "SAVE": "Save", + "SEARCH_GIF": "Search all the GIFs", + "SELECT_FROM_FILES": "Select from files", + "SELECT_STL_MSG": "Select list style", + "SENTENCE_CASE": "Sentence case", + "SET_COLOR": "Set color", + "SHOW": "Show", + "SWIPE_TO_REVEAL_SEMANTIC": "Swipe left to reveal actions", + "Select Emoji": "Select Emoji", + "Slider Color": "Slider Color", + "Slider White Black Color": "Slider White Black Color", + "Square": "Square", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text to display", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Title Case", + "TO": "To", + "Text": "Text", + "UNDO_MSG": "Undo the last command", + "UNKNOWN": "Unknown", + "UNTAB": "Untab", + "UPPERCASE": "UPPERCASE", + "UPPER_ALPHA": "Upper Alpha", + "UPPER_ROMAN": "Upper Roman", + "URL": "URL", + "URL_DUPLICATE_ER": "URL is already attached. Please enter different URL", + "URL_INVALID_ER": "Please enter valid URL", + "URL_MSG": "Please enter a URL!", + "VIDEO": "Video", + "VIEW": "View", + "WEEK": "Week" +} \ No newline at end of file diff --git a/lib/assets/strings/en_GB.json b/lib/assets/strings/en_GB.json new file mode 100644 index 0000000..0f01046 --- /dev/null +++ b/lib/assets/strings/en_GB.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Action", + "ADD_URL": "Add URL", + "ALL": "All", + "ATTACHED_FILE": "Attached file", + "ATTACH_MAXSIZE_VALIDATE": "File size must be less than {0}", + "AUDIO_FILE_OR_URL_MSG": "Please either choose an audio file or enter an audio file URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Please input either an audio file or an audio URL, not both!", + "Blur": "Blur", + "Blur Radius": "Blur Radius", + "Brush": "Brush", + "CAMERA": "Camera", + "CANCEL": "Cancel", + "CHANGE_CASE": "Change case", + "CHANGE_IMAGE": "Change Image", + "CHAR_LEFT": "Characters Left {0}", + "CHOOSE_AUDIO": "Choose audio", + "CHOOSE_COLOR": "Choose a Colour", + "CHOOSE_FILE": "Choose file", + "CHOOSE_FILE_URL_MSG": "Please either choose a file or enter a file URL!", + "CHOOSE_IMAGE": "Choose image", + "CHOOSE_IMG_URL_MSG": "Please either choose an image or enter an image URL!", + "CHOOSE_VIDEO": "Choose video", + "CLEAR": "Clear", + "CLOSE": "Close", + "COMPLETE_EDITING": "Complete editing", + "COURIER": "Courier", + "CROP": "Crop", + "CUSTOM": "Custom", + "Color Opacity": "Colour Opacity", + "Crop": "Crop", + "DEFAULT": "Default", + "DELETE": "Delete", + "DELETE_ATTACHMENT": "Delete attachment", + "DONE": "Done", + "EDIT": "Edit", + "EDITOR_CIRCLE": "Circle", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Disc", + "EDITOR_HEADER_1": "Header 1", + "EDITOR_HEADER_2": "Header 2", + "EDITOR_HEADER_3": "Header 3", + "EDITOR_HEADER_4": "Header 4", + "EDITOR_HEADER_5": "Header 5", + "EDITOR_HEADER_6": "Header 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "quote", + "EDITOR_SQUARE": "Square", + "EDITOR_TEXT": "Text", + "ENTER": "ENTER", + "ESCAPE": "Escape", + "Emoji": "Elmo", + "FILE": "File", + "FILE_COMPRESSING_ER": "An error occurred while compressing a file", + "FILE_DUPLICATE_ER": "File is already attached. Please choose different file", + "FILE_OR_URL_MSG": "Please input either a file or a file URL, not both!", + "FILE_PIXEL_SIZE_ER": "File pixel size should be less than {0}", + "FILE_PROCESSING_ER": "An error occurred while processing a file", + "FILE_SIZE_CALCULATING_ER": "An error occurred while calculating a file size", + "FILE_UNSUPPORTED": "This file type is not allowed", + "FLIP": "Flip", + "FROM": "From", + "Filter": "Filter", + "Flip": "Flip", + "Freeform": "Freeform", + "GALLERY": "Gallery", + "GIF": "Gif", + "HELP": "Help", + "HIDE": "Hide", + "IMAGE_OR_URL_MSG": "Please input either an image or an image URL, not both!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Image Picker. Image selected", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Image Picker. No image selected", + "IMG_URL_MSG": "Please enter an image URL!", + "INSERT_AUDIO": "Insert Audio", + "INSERT_FILE": "Insert File", + "INSERT_IMAGE": "Insert Image", + "INSERT_LINK": "Insert Link", + "INSERT_PARAGRAPH": "Insert Paragraph", + "INSERT_TABLE": "Insert Table", + "INSERT_VIDEO": "Insert Video", + "Insert Your Message": "Insert Your Message", + "KEY_COMBINATION": "Key Combination", + "LINK": "Link", + "LINK_NAME": "Link Name", + "LINK_URL": "Link URL", + "LOWERCASE": "lowercase", + "LOWER_ALPHA": "Lower Alpha", + "LOWER_ROMAN": "Lower Roman", + "MAX_ATTACH_MSG": "Attachment max limit reached", + "MONTH": "Month", + "NO_RESULTS": "No results", + "NUMBERED": "Numbered", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Open in new window", + "PICK_GIF": "Pick a Gif", + "PICK_IMAGE": "Pick Image", + "REDO_MSG": "Redo the last command", + "REMOVE": "Remove", + "RESET": "Reset", + "REST_COLOR_MSG": "Reset to default colour", + "ROTATE_LEFT": "Rotate Left", + "ROTATE_RIGHT": "Rotate Right", + "Reset": "Reset", + "Rotate left": "Rotate left", + "Rotate right": "Rotate right", + "SANS_SERIF": "Sans Serif", + "SAVE": "Save", + "SEARCH_GIF": "Search all the GIFs", + "SELECT_FROM_FILES": "Select from files", + "SELECT_STL_MSG": "Select list style", + "SENTENCE_CASE": "Sentence case", + "SET_COLOR": "Set colour", + "SHOW": "Show", + "SWIPE_TO_REVEAL_SEMANTIC": "Swipe left to reveal actions", + "Select Emoji": "Select Elmo", + "Slider Color": "Slider Colour", + "Slider White Black Color": "Slider White Black Colour", + "Square": "Square", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text to display", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Title Case", + "TO": "To", + "Text": "Text", + "UNDO_MSG": "Undo the last command", + "UNKNOWN": "Unknown", + "UNTAB": "Un tab", + "UPPERCASE": "UPPERCASE", + "UPPER_ALPHA": "Upper Alpha", + "UPPER_ROMAN": "Upper Roman", + "URL": "URL", + "URL_DUPLICATE_ER": "URL is already attached. Please enter different URL", + "URL_INVALID_ER": "Please enter valid URL", + "URL_MSG": "Please enter a URL!", + "VIDEO": "Video", + "VIEW": "View", + "WEEK": "Week" +} \ No newline at end of file diff --git a/lib/assets/strings/es.json b/lib/assets/strings/es.json new file mode 100644 index 0000000..98e83fc --- /dev/null +++ b/lib/assets/strings/es.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Acción", + "ADD_URL": "Agregar URL", + "ALL": "Todos", + "ATTACHED_FILE": "Archivo adjunto", + "ATTACH_MAXSIZE_VALIDATE": "El tamaño del archivo debe ser inferior a {0}", + "AUDIO_FILE_OR_URL_MSG": "¡Elija un archivo de audio o ingrese una URL de archivo de audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ingrese un archivo de audio o una URL de audio, ¡no ambos!", + "Blur": "Difuminar", + "Blur Radius": "Radio de desenfoque", + "Brush": "Cepillar", + "CAMERA": "Cámara", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Cambiar caso", + "CHANGE_IMAGE": "Cambiar imagen", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "elegir sonido", + "CHOOSE_COLOR": "Elige un color", + "CHOOSE_FILE": "Elija el archivo", + "CHOOSE_FILE_URL_MSG": "¡Elija un archivo o ingrese una URL de archivo!", + "CHOOSE_IMAGE": "elegir imagen", + "CHOOSE_IMG_URL_MSG": "¡Elija una imagen o ingrese una URL de imagen!", + "CHOOSE_VIDEO": "elegir vídeo", + "CLEAR": "Claro", + "CLOSE": "Cerrar", + "COMPLETE_EDITING": "Edición completa", + "COURIER": "mensajero", + "CROP": "Cosecha", + "CUSTOM": "Personalizado", + "Color Opacity": "Opacidad de color", + "Crop": "Cosecha", + "DEFAULT": "Defecto", + "DELETE": "Borrar", + "DELETE_ATTACHMENT": "Eliminar archivo adjunto", + "DONE": "Terminado", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Circulo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Desct", + "EDITOR_HEADER_1": "Encabezado 1", + "EDITOR_HEADER_2": "Encabezado 2", + "EDITOR_HEADER_3": "Encabezado 3", + "EDITOR_HEADER_4": "Encabezado 4", + "EDITOR_HEADER_5": "Encabezado 5", + "EDITOR_HEADER_6": "Encabezado 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "punto", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "cita", + "EDITOR_SQUARE": "Cuadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "INGRESAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Archivo", + "FILE_COMPRESSING_ER": "Ocurrió un error al comprimir un archivo", + "FILE_DUPLICATE_ER": "El archivo ya está adjunto. Por favor, elija un archivo diferente", + "FILE_OR_URL_MSG": "Ingrese un archivo o una URL de archivo, ¡no ambos!", + "FILE_PIXEL_SIZE_ER": "El tamaño de píxel del archivo debe ser inferior a {0}", + "FILE_PROCESSING_ER": "Ocurrió un error al procesar un archivo", + "FILE_SIZE_CALCULATING_ER": "Se produjo un error al calcular el tamaño de un archivo", + "FILE_UNSUPPORTED": "Este tipo de archivo no está permitido", + "FLIP": "Voltear", + "FROM": "De", + "Filter": "Filtro", + "Flip": "Voltear", + "Freeform": "Forma libre", + "GALLERY": "Galería", + "GIF": "Gif", + "HELP": "Ayuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Ingrese una imagen o una URL de imagen, ¡no ambas!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selector de imágenes. Imagen seleccionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selector de imágenes. Ninguna imagen seleccionada", + "IMG_URL_MSG": "¡Por favor ingrese una URL de imagen!", + "INSERT_AUDIO": "Insertar audio", + "INSERT_FILE": "Insertar archivo", + "INSERT_IMAGE": "Insertar imagen", + "INSERT_LINK": "Insertar el link", + "INSERT_PARAGRAPH": "Insertar párrafo", + "INSERT_TABLE": "Insertar tabla", + "INSERT_VIDEO": "Insertar vídeo", + "Insert Your Message": "Inserta tu mensaje", + "KEY_COMBINATION": "combinación de teclas", + "LINK": "Enlace", + "LINK_NAME": "Nombre del enlace", + "LINK_URL": "URL del enlace", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "alfa inferior", + "LOWER_ROMAN": "bajo romano", + "MAX_ATTACH_MSG": "Límite máximo de archivos adjuntos alcanzado", + "MONTH": "Mes", + "NO_RESULTS": "No hay resultados", + "NUMBERED": "Numerado", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Abrir en Nueva ventana", + "PICK_GIF": "elige un gif", + "PICK_IMAGE": "Elegir imagen", + "REDO_MSG": "Rehacer el último comando", + "REMOVE": "Eliminar", + "RESET": "restaurar", + "REST_COLOR_MSG": "Restablecer al color predeterminado", + "ROTATE_LEFT": "Girar a la izquierda", + "ROTATE_RIGHT": "Gira a la derecha", + "Reset": "restaurar", + "Rotate left": "Girar a la izquierda", + "Rotate right": "Gira a la derecha", + "SANS_SERIF": "sans serif", + "SAVE": "Guardar", + "SEARCH_GIF": "Buscar todos los GIF", + "SELECT_FROM_FILES": "Seleccionar de archivos", + "SELECT_STL_MSG": "Seleccionar estilo de lista", + "SENTENCE_CASE": "Caso de sentencia", + "SET_COLOR": "Establecer color", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Desliza hacia la izquierda para revelar acciones", + "Select Emoji": "Seleccionar emoji", + "Slider Color": "Color del control deslizante", + "Slider White Black Color": "Control deslizante Blanco Negro Color", + "Square": "Cuadrado", + "TAB": "PESTAÑA", + "TEXT_TO_DISPLAY": "Texto para mostrar", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titulo del caso", + "TO": "A", + "Text": "Texto", + "UNDO_MSG": "Deshacer el último comando", + "UNKNOWN": "Desconocido", + "UNTAB": "desabrochar", + "UPPERCASE": "MAYÚSCULAS", + "UPPER_ALPHA": "alfa superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "La URL ya está adjunta. Ingrese una URL diferente", + "URL_INVALID_ER": "Introduce una URL válida", + "URL_MSG": "¡Por favor ingrese una URL!", + "VIDEO": "Vídeo", + "VIEW": "Vista", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/es_CL.json b/lib/assets/strings/es_CL.json new file mode 100644 index 0000000..df70f11 --- /dev/null +++ b/lib/assets/strings/es_CL.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Acción", + "ADD_URL": "Agregar URL", + "ALL": "Todos", + "ATTACHED_FILE": "Archivo adjunto", + "ATTACH_MAXSIZE_VALIDATE": "El tamaño del archivo debe ser inferior a {0}", + "AUDIO_FILE_OR_URL_MSG": "¡Elija un archivo de audio o ingrese una URL de archivo de audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ingrese un archivo de audio o una URL de audio, ¡no ambos!", + "Blur": "Difuminar", + "Blur Radius": "Radio de desenfoque", + "Brush": "Cepillar", + "CAMERA": "Cámara", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Cambiar caso", + "CHANGE_IMAGE": "Cambiar imagen", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "elegir sonido", + "CHOOSE_COLOR": "Elige un color", + "CHOOSE_FILE": "Elija el archivo", + "CHOOSE_FILE_URL_MSG": "¡Elija un archivo o ingrese una URL de archivo!", + "CHOOSE_IMAGE": "elegir imagen", + "CHOOSE_IMG_URL_MSG": "¡Elija una imagen o ingrese una URL de imagen!", + "CHOOSE_VIDEO": "elegir vídeo", + "CLEAR": "Borrar", + "CLOSE": "Cerrar", + "COMPLETE_EDITING": "Edición completa", + "COURIER": "mensajero", + "CROP": "Recortar", + "CUSTOM": "Personalizar", + "Color Opacity": "Opacidad de color", + "Crop": "Recortar", + "DEFAULT": "Predeterminado", + "DELETE": "Eliminar", + "DELETE_ATTACHMENT": "Eliminar archivo adjunto", + "DONE": "Listo", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Círculo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Desct", + "EDITOR_HEADER_1": "Encabezado 1", + "EDITOR_HEADER_2": "Encabezado 2", + "EDITOR_HEADER_3": "Encabezado 3", + "EDITOR_HEADER_4": "Encabezado 4", + "EDITOR_HEADER_5": "Encabezado 5", + "EDITOR_HEADER_6": "Encabezado 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "punto", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "cita", + "EDITOR_SQUARE": "Cuadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "INGRESAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Archivo", + "FILE_COMPRESSING_ER": "Ocurrió un error al comprimir un archivo", + "FILE_DUPLICATE_ER": "El archivo ya está adjunto. Por favor, elija un archivo diferente", + "FILE_OR_URL_MSG": "Ingrese un archivo o una URL de archivo, ¡no ambos!", + "FILE_PIXEL_SIZE_ER": "El tamaño de píxel del archivo debe ser inferior a {0}", + "FILE_PROCESSING_ER": "Ocurrió un error al procesar un archivo", + "FILE_SIZE_CALCULATING_ER": "Ocurrió un error al calcular el tamaño de un archivo", + "FILE_UNSUPPORTED": "Este tipo de archivo no está permitido", + "FLIP": "Voltear", + "FROM": "Desde", + "Filter": "Filtro", + "Flip": "Voltear", + "Freeform": "Forma libre", + "GALLERY": "Galería", + "GIF": "Gif", + "HELP": "Ayuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Ingrese una imagen o una URL de imagen, ¡no ambas!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selector de imágenes. Imagen seleccionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selector de imágenes. Ninguna imagen seleccionada", + "IMG_URL_MSG": "¡Por favor ingrese una URL de imagen!", + "INSERT_AUDIO": "Insertar audio", + "INSERT_FILE": "Insertar archivo", + "INSERT_IMAGE": "Insertar imagen", + "INSERT_LINK": "Insertar el link", + "INSERT_PARAGRAPH": "Insertar párrafo", + "INSERT_TABLE": "Insertar tabla", + "INSERT_VIDEO": "Insertar vídeo", + "Insert Your Message": "Inserta tu mensaje", + "KEY_COMBINATION": "combinación de teclas", + "LINK": "Enlace", + "LINK_NAME": "Nombre del enlace", + "LINK_URL": "URL del vínculo", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "alfa inferior", + "LOWER_ROMAN": "bajo romano", + "MAX_ATTACH_MSG": "Límite máximo de archivos adjuntos alcanzado", + "MONTH": "Mes", + "NO_RESULTS": "No hay resultados", + "NUMBERED": "Numerado", + "OK": "Aceptar", + "OPEN_IN_NEW_WINDOW": "Abrir en Nueva ventana", + "PICK_GIF": "elige un gif", + "PICK_IMAGE": "Elegir imagen", + "REDO_MSG": "Rehacer el último comando", + "REMOVE": "Eliminar", + "RESET": "Restablecer", + "REST_COLOR_MSG": "Restablecer al color predeterminado", + "ROTATE_LEFT": "Girar a la izquierda", + "ROTATE_RIGHT": "Gira a la derecha", + "Reset": "Restablecer", + "Rotate left": "Girar a la izquierda", + "Rotate right": "Gira a la derecha", + "SANS_SERIF": "sans serif", + "SAVE": "Guardar", + "SEARCH_GIF": "Buscar todos los GIF", + "SELECT_FROM_FILES": "Seleccionar de archivos", + "SELECT_STL_MSG": "Seleccionar estilo de lista", + "SENTENCE_CASE": "Caso de sentencia", + "SET_COLOR": "Establecer color", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Desliza hacia la izquierda para revelar acciones", + "Select Emoji": "Seleccionar emoji", + "Slider Color": "Color del control deslizante", + "Slider White Black Color": "Control deslizante Blanco Negro Color", + "Square": "Cuadrado", + "TAB": "PESTAÑA", + "TEXT_TO_DISPLAY": "Texto para mostrar", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titulo del caso", + "TO": "Hasta", + "Text": "Texto", + "UNDO_MSG": "Deshacer el último comando", + "UNKNOWN": "Desconocido", + "UNTAB": "desabrochar", + "UPPERCASE": "MAYÚSCULAS", + "UPPER_ALPHA": "alfa superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "La URL ya está adjunta. Ingrese una URL diferente", + "URL_INVALID_ER": "Introduce una URL válida", + "URL_MSG": "¡Por favor ingrese una URL!", + "VIDEO": "Video", + "VIEW": "Ver", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/es_EC.json b/lib/assets/strings/es_EC.json new file mode 100644 index 0000000..cce73b5 --- /dev/null +++ b/lib/assets/strings/es_EC.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Acción", + "ADD_URL": "Agregar URL", + "ALL": "Todos", + "ATTACHED_FILE": "Archivo adjunto", + "ATTACH_MAXSIZE_VALIDATE": "El tamaño del archivo debe ser inferior a {0}", + "AUDIO_FILE_OR_URL_MSG": "¡Elija un archivo de audio o ingrese una URL de archivo de audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ingrese un archivo de audio o una URL de audio, ¡no ambos!", + "Blur": "Difuminar", + "Blur Radius": "Radio de desenfoque", + "Brush": "Cepillar", + "CAMERA": "Cámara", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Cambiar caso", + "CHANGE_IMAGE": "Cambiar imagen", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "elegir sonido", + "CHOOSE_COLOR": "Elige un color", + "CHOOSE_FILE": "Elija el archivo", + "CHOOSE_FILE_URL_MSG": "¡Elija un archivo o ingrese una URL de archivo!", + "CHOOSE_IMAGE": "elegir imagen", + "CHOOSE_IMG_URL_MSG": "¡Elija una imagen o ingrese una URL de imagen!", + "CHOOSE_VIDEO": "elegir vídeo", + "CLEAR": "Borrar", + "CLOSE": "Cerrar", + "COMPLETE_EDITING": "Edición completa", + "COURIER": "mensajero", + "CROP": "Recortar", + "CUSTOM": "Personalizar", + "Color Opacity": "Opacidad de color", + "Crop": "Recortar", + "DEFAULT": "Predeterminado", + "DELETE": "Eliminar", + "DELETE_ATTACHMENT": "Eliminar archivo adjunto", + "DONE": "Listo", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Círculo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Desct", + "EDITOR_HEADER_1": "Encabezado 1", + "EDITOR_HEADER_2": "Encabezado 2", + "EDITOR_HEADER_3": "Encabezado 3", + "EDITOR_HEADER_4": "Encabezado 4", + "EDITOR_HEADER_5": "Encabezado 5", + "EDITOR_HEADER_6": "Encabezado 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "punto", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "cita", + "EDITOR_SQUARE": "Cuadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "INGRESAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Archivo", + "FILE_COMPRESSING_ER": "Ocurrió un error al comprimir un archivo", + "FILE_DUPLICATE_ER": "El archivo ya está adjunto. Por favor, elija un archivo diferente", + "FILE_OR_URL_MSG": "Ingrese un archivo o una URL de archivo, ¡no ambos!", + "FILE_PIXEL_SIZE_ER": "El tamaño de píxel del archivo debe ser inferior a {0}", + "FILE_PROCESSING_ER": "Ocurrió un error al procesar un archivo", + "FILE_SIZE_CALCULATING_ER": "Se produjo un error al calcular el tamaño de un archivo", + "FILE_UNSUPPORTED": "Este tipo de archivo no está permitido", + "FLIP": "Voltear", + "FROM": "Desde", + "Filter": "Filtro", + "Flip": "Voltear", + "Freeform": "Forma libre", + "GALLERY": "Galería", + "GIF": "Gif", + "HELP": "Ayuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Ingrese una imagen o una URL de imagen, ¡no ambas!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selector de imágenes. Imagen seleccionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selector de imágenes. Ninguna imagen seleccionada", + "IMG_URL_MSG": "¡Por favor ingrese una URL de imagen!", + "INSERT_AUDIO": "Insertar audio", + "INSERT_FILE": "Insertar archivo", + "INSERT_IMAGE": "Insertar imagen", + "INSERT_LINK": "Insertar el link", + "INSERT_PARAGRAPH": "Insertar párrafo", + "INSERT_TABLE": "Insertar tabla", + "INSERT_VIDEO": "Insertar vídeo", + "Insert Your Message": "Inserta tu mensaje", + "KEY_COMBINATION": "combinación de teclas", + "LINK": "Enlace", + "LINK_NAME": "Nombre del enlace", + "LINK_URL": "URL del vínculo", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "alfa inferior", + "LOWER_ROMAN": "bajo romano", + "MAX_ATTACH_MSG": "Límite máximo de archivos adjuntos alcanzado", + "MONTH": "Mes", + "NO_RESULTS": "No hay resultados", + "NUMBERED": "Numerado", + "OK": "Aceptar", + "OPEN_IN_NEW_WINDOW": "Abrir en Nueva ventana", + "PICK_GIF": "elige un gif", + "PICK_IMAGE": "Elegir imagen", + "REDO_MSG": "Rehacer el último comando", + "REMOVE": "Eliminar", + "RESET": "Restablecer", + "REST_COLOR_MSG": "Restablecer al color predeterminado", + "ROTATE_LEFT": "Girar a la izquierda", + "ROTATE_RIGHT": "Gira a la derecha", + "Reset": "Restablecer", + "Rotate left": "Girar a la izquierda", + "Rotate right": "Gira a la derecha", + "SANS_SERIF": "sans serif", + "SAVE": "Guardar", + "SEARCH_GIF": "Buscar todos los GIF", + "SELECT_FROM_FILES": "Seleccionar de archivos", + "SELECT_STL_MSG": "Seleccionar estilo de lista", + "SENTENCE_CASE": "Caso de sentencia", + "SET_COLOR": "Establecer color", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Desliza hacia la izquierda para revelar acciones", + "Select Emoji": "Seleccionar emoji", + "Slider Color": "Color del control deslizante", + "Slider White Black Color": "Control deslizante Blanco Negro Color", + "Square": "Cuadrado", + "TAB": "PESTAÑA", + "TEXT_TO_DISPLAY": "Texto para mostrar", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titulo del caso", + "TO": "Hasta", + "Text": "Texto", + "UNDO_MSG": "Deshacer el último comando", + "UNKNOWN": "Desconocido", + "UNTAB": "desabrochar", + "UPPERCASE": "MAYÚSCULAS", + "UPPER_ALPHA": "alfa superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "La URL ya está adjunta. Ingrese una URL diferente", + "URL_INVALID_ER": "Introduce una URL válida", + "URL_MSG": "¡Por favor ingrese una URL!", + "VIDEO": "Video", + "VIEW": "Ver", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/es_ES.json b/lib/assets/strings/es_ES.json new file mode 100644 index 0000000..397d55c --- /dev/null +++ b/lib/assets/strings/es_ES.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Acción", + "ADD_URL": "Añadir URL", + "ALL": "Todo", + "ATTACHED_FILE": "Archivo adjunto", + "ATTACH_MAXSIZE_VALIDATE": "El tamaño del archivo debe ser inferior a {0}", + "AUDIO_FILE_OR_URL_MSG": "¡Elija un archivo de audio o ingrese una URL de archivo de audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ingrese un archivo de audio o una URL de audio, ¡no ambos!", + "Blur": "Difuminar", + "Blur Radius": "Radio de desenfoque", + "Brush": "Cepillar", + "CAMERA": "Cámara", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Cambiar caso", + "CHANGE_IMAGE": "Cambiar imagen", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "elegir sonido", + "CHOOSE_COLOR": "Elige un color", + "CHOOSE_FILE": "Elija el archivo", + "CHOOSE_FILE_URL_MSG": "¡Elija un archivo o ingrese una URL de archivo!", + "CHOOSE_IMAGE": "elegir imagen", + "CHOOSE_IMG_URL_MSG": "¡Elija una imagen o ingrese una URL de imagen!", + "CHOOSE_VIDEO": "elegir vídeo", + "CLEAR": "Borrar", + "CLOSE": "Cerrar", + "COMPLETE_EDITING": "Edición completa", + "COURIER": "mensajero", + "CROP": "Recortar", + "CUSTOM": "Personalizado", + "Color Opacity": "Opacidad de color", + "Crop": "Recortar", + "DEFAULT": "Valor predeterminado", + "DELETE": "Eliminar", + "DELETE_ATTACHMENT": "Eliminar archivo adjunto", + "DONE": "Hecho", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Círculo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Desct", + "EDITOR_HEADER_1": "Encabezado 1", + "EDITOR_HEADER_2": "Encabezado 2", + "EDITOR_HEADER_3": "Encabezado 3", + "EDITOR_HEADER_4": "Encabezado 4", + "EDITOR_HEADER_5": "Encabezado 5", + "EDITOR_HEADER_6": "Encabezado 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "punto", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "cita", + "EDITOR_SQUARE": "Cuadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "INGRESAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Archivo", + "FILE_COMPRESSING_ER": "Ocurrió un error al comprimir un archivo", + "FILE_DUPLICATE_ER": "El archivo ya está adjunto. Por favor, elija un archivo diferente", + "FILE_OR_URL_MSG": "Ingrese un archivo o una URL de archivo, ¡no ambos!", + "FILE_PIXEL_SIZE_ER": "El tamaño de píxel del archivo debe ser inferior a {0}", + "FILE_PROCESSING_ER": "Ocurrió un error al procesar un archivo", + "FILE_SIZE_CALCULATING_ER": "Se produjo un error al calcular el tamaño de un archivo", + "FILE_UNSUPPORTED": "Este tipo de archivo no está permitido", + "FLIP": "Dar la vuelta", + "FROM": "De", + "Filter": "Filtro", + "Flip": "Dar la vuelta", + "Freeform": "Forma libre", + "GALLERY": "Galería", + "GIF": "Gif", + "HELP": "Ayuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Ingrese una imagen o una URL de imagen, ¡no ambas!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selector de imágenes. Imagen seleccionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selector de imágenes. Ninguna imagen seleccionada", + "IMG_URL_MSG": "¡Por favor ingrese una URL de imagen!", + "INSERT_AUDIO": "Insertar audio", + "INSERT_FILE": "Insertar archivo", + "INSERT_IMAGE": "Insertar imagen", + "INSERT_LINK": "Insertar el link", + "INSERT_PARAGRAPH": "Insertar párrafo", + "INSERT_TABLE": "Insertar tabla", + "INSERT_VIDEO": "Insertar vídeo", + "Insert Your Message": "Inserta tu mensaje", + "KEY_COMBINATION": "combinación de teclas", + "LINK": "Enlace", + "LINK_NAME": "Nombre del enlace", + "LINK_URL": "URL de enlace", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "alfa inferior", + "LOWER_ROMAN": "bajo romano", + "MAX_ATTACH_MSG": "Límite máximo de archivos adjuntos alcanzado", + "MONTH": "Mes", + "NO_RESULTS": "No hay resultados", + "NUMBERED": "Numerado", + "OK": "Aceptar", + "OPEN_IN_NEW_WINDOW": "Abrir en Nueva ventana", + "PICK_GIF": "elige un gif", + "PICK_IMAGE": "Elegir imagen", + "REDO_MSG": "Rehacer el último comando", + "REMOVE": "Eliminar", + "RESET": "Restablecer", + "REST_COLOR_MSG": "Restablecer al color predeterminado", + "ROTATE_LEFT": "Girar a hacia la izquierda", + "ROTATE_RIGHT": "Girar hacia la derecha", + "Reset": "Restablecer", + "Rotate left": "Girar a la izquierda", + "Rotate right": "Gira a la derecha", + "SANS_SERIF": "sans serif", + "SAVE": "Guardar", + "SEARCH_GIF": "Busca todos los GIF", + "SELECT_FROM_FILES": "Seleccionar de archivos", + "SELECT_STL_MSG": "Seleccionar estilo de lista", + "SENTENCE_CASE": "Caso de sentencia", + "SET_COLOR": "Establecer color", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Desliza hacia la izquierda para revelar acciones", + "Select Emoji": "Seleccionar emoji", + "Slider Color": "Color del control deslizante", + "Slider White Black Color": "Control deslizante Blanco Negro Color", + "Square": "Cuadrado", + "TAB": "PESTAÑA", + "TEXT_TO_DISPLAY": "Texto para mostrar", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titulo del caso", + "TO": "Para", + "Text": "Texto", + "UNDO_MSG": "Deshacer el último comando", + "UNKNOWN": "Desconocido", + "UNTAB": "desabrochar", + "UPPERCASE": "MAYÚSCULAS", + "UPPER_ALPHA": "alfa superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "La URL ya está adjunta. Ingrese una URL diferente", + "URL_INVALID_ER": "Introduce una URL válida", + "URL_MSG": "¡Por favor ingrese una URL!", + "VIDEO": "Vídeo", + "VIEW": "Ver", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/es_PE.json b/lib/assets/strings/es_PE.json new file mode 100644 index 0000000..cce73b5 --- /dev/null +++ b/lib/assets/strings/es_PE.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Acción", + "ADD_URL": "Agregar URL", + "ALL": "Todos", + "ATTACHED_FILE": "Archivo adjunto", + "ATTACH_MAXSIZE_VALIDATE": "El tamaño del archivo debe ser inferior a {0}", + "AUDIO_FILE_OR_URL_MSG": "¡Elija un archivo de audio o ingrese una URL de archivo de audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ingrese un archivo de audio o una URL de audio, ¡no ambos!", + "Blur": "Difuminar", + "Blur Radius": "Radio de desenfoque", + "Brush": "Cepillar", + "CAMERA": "Cámara", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Cambiar caso", + "CHANGE_IMAGE": "Cambiar imagen", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "elegir sonido", + "CHOOSE_COLOR": "Elige un color", + "CHOOSE_FILE": "Elija el archivo", + "CHOOSE_FILE_URL_MSG": "¡Elija un archivo o ingrese una URL de archivo!", + "CHOOSE_IMAGE": "elegir imagen", + "CHOOSE_IMG_URL_MSG": "¡Elija una imagen o ingrese una URL de imagen!", + "CHOOSE_VIDEO": "elegir vídeo", + "CLEAR": "Borrar", + "CLOSE": "Cerrar", + "COMPLETE_EDITING": "Edición completa", + "COURIER": "mensajero", + "CROP": "Recortar", + "CUSTOM": "Personalizar", + "Color Opacity": "Opacidad de color", + "Crop": "Recortar", + "DEFAULT": "Predeterminado", + "DELETE": "Eliminar", + "DELETE_ATTACHMENT": "Eliminar archivo adjunto", + "DONE": "Listo", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Círculo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Desct", + "EDITOR_HEADER_1": "Encabezado 1", + "EDITOR_HEADER_2": "Encabezado 2", + "EDITOR_HEADER_3": "Encabezado 3", + "EDITOR_HEADER_4": "Encabezado 4", + "EDITOR_HEADER_5": "Encabezado 5", + "EDITOR_HEADER_6": "Encabezado 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "punto", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "cita", + "EDITOR_SQUARE": "Cuadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "INGRESAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Archivo", + "FILE_COMPRESSING_ER": "Ocurrió un error al comprimir un archivo", + "FILE_DUPLICATE_ER": "El archivo ya está adjunto. Por favor, elija un archivo diferente", + "FILE_OR_URL_MSG": "Ingrese un archivo o una URL de archivo, ¡no ambos!", + "FILE_PIXEL_SIZE_ER": "El tamaño de píxel del archivo debe ser inferior a {0}", + "FILE_PROCESSING_ER": "Ocurrió un error al procesar un archivo", + "FILE_SIZE_CALCULATING_ER": "Se produjo un error al calcular el tamaño de un archivo", + "FILE_UNSUPPORTED": "Este tipo de archivo no está permitido", + "FLIP": "Voltear", + "FROM": "Desde", + "Filter": "Filtro", + "Flip": "Voltear", + "Freeform": "Forma libre", + "GALLERY": "Galería", + "GIF": "Gif", + "HELP": "Ayuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Ingrese una imagen o una URL de imagen, ¡no ambas!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selector de imágenes. Imagen seleccionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selector de imágenes. Ninguna imagen seleccionada", + "IMG_URL_MSG": "¡Por favor ingrese una URL de imagen!", + "INSERT_AUDIO": "Insertar audio", + "INSERT_FILE": "Insertar archivo", + "INSERT_IMAGE": "Insertar imagen", + "INSERT_LINK": "Insertar el link", + "INSERT_PARAGRAPH": "Insertar párrafo", + "INSERT_TABLE": "Insertar tabla", + "INSERT_VIDEO": "Insertar vídeo", + "Insert Your Message": "Inserta tu mensaje", + "KEY_COMBINATION": "combinación de teclas", + "LINK": "Enlace", + "LINK_NAME": "Nombre del enlace", + "LINK_URL": "URL del vínculo", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "alfa inferior", + "LOWER_ROMAN": "bajo romano", + "MAX_ATTACH_MSG": "Límite máximo de archivos adjuntos alcanzado", + "MONTH": "Mes", + "NO_RESULTS": "No hay resultados", + "NUMBERED": "Numerado", + "OK": "Aceptar", + "OPEN_IN_NEW_WINDOW": "Abrir en Nueva ventana", + "PICK_GIF": "elige un gif", + "PICK_IMAGE": "Elegir imagen", + "REDO_MSG": "Rehacer el último comando", + "REMOVE": "Eliminar", + "RESET": "Restablecer", + "REST_COLOR_MSG": "Restablecer al color predeterminado", + "ROTATE_LEFT": "Girar a la izquierda", + "ROTATE_RIGHT": "Gira a la derecha", + "Reset": "Restablecer", + "Rotate left": "Girar a la izquierda", + "Rotate right": "Gira a la derecha", + "SANS_SERIF": "sans serif", + "SAVE": "Guardar", + "SEARCH_GIF": "Buscar todos los GIF", + "SELECT_FROM_FILES": "Seleccionar de archivos", + "SELECT_STL_MSG": "Seleccionar estilo de lista", + "SENTENCE_CASE": "Caso de sentencia", + "SET_COLOR": "Establecer color", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Desliza hacia la izquierda para revelar acciones", + "Select Emoji": "Seleccionar emoji", + "Slider Color": "Color del control deslizante", + "Slider White Black Color": "Control deslizante Blanco Negro Color", + "Square": "Cuadrado", + "TAB": "PESTAÑA", + "TEXT_TO_DISPLAY": "Texto para mostrar", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titulo del caso", + "TO": "Hasta", + "Text": "Texto", + "UNDO_MSG": "Deshacer el último comando", + "UNKNOWN": "Desconocido", + "UNTAB": "desabrochar", + "UPPERCASE": "MAYÚSCULAS", + "UPPER_ALPHA": "alfa superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "La URL ya está adjunta. Ingrese una URL diferente", + "URL_INVALID_ER": "Introduce una URL válida", + "URL_MSG": "¡Por favor ingrese una URL!", + "VIDEO": "Video", + "VIEW": "Ver", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/es_PR.json b/lib/assets/strings/es_PR.json new file mode 100644 index 0000000..cce73b5 --- /dev/null +++ b/lib/assets/strings/es_PR.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Acción", + "ADD_URL": "Agregar URL", + "ALL": "Todos", + "ATTACHED_FILE": "Archivo adjunto", + "ATTACH_MAXSIZE_VALIDATE": "El tamaño del archivo debe ser inferior a {0}", + "AUDIO_FILE_OR_URL_MSG": "¡Elija un archivo de audio o ingrese una URL de archivo de audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ingrese un archivo de audio o una URL de audio, ¡no ambos!", + "Blur": "Difuminar", + "Blur Radius": "Radio de desenfoque", + "Brush": "Cepillar", + "CAMERA": "Cámara", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Cambiar caso", + "CHANGE_IMAGE": "Cambiar imagen", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "elegir sonido", + "CHOOSE_COLOR": "Elige un color", + "CHOOSE_FILE": "Elija el archivo", + "CHOOSE_FILE_URL_MSG": "¡Elija un archivo o ingrese una URL de archivo!", + "CHOOSE_IMAGE": "elegir imagen", + "CHOOSE_IMG_URL_MSG": "¡Elija una imagen o ingrese una URL de imagen!", + "CHOOSE_VIDEO": "elegir vídeo", + "CLEAR": "Borrar", + "CLOSE": "Cerrar", + "COMPLETE_EDITING": "Edición completa", + "COURIER": "mensajero", + "CROP": "Recortar", + "CUSTOM": "Personalizar", + "Color Opacity": "Opacidad de color", + "Crop": "Recortar", + "DEFAULT": "Predeterminado", + "DELETE": "Eliminar", + "DELETE_ATTACHMENT": "Eliminar archivo adjunto", + "DONE": "Listo", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Círculo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Desct", + "EDITOR_HEADER_1": "Encabezado 1", + "EDITOR_HEADER_2": "Encabezado 2", + "EDITOR_HEADER_3": "Encabezado 3", + "EDITOR_HEADER_4": "Encabezado 4", + "EDITOR_HEADER_5": "Encabezado 5", + "EDITOR_HEADER_6": "Encabezado 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "punto", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "cita", + "EDITOR_SQUARE": "Cuadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "INGRESAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Archivo", + "FILE_COMPRESSING_ER": "Ocurrió un error al comprimir un archivo", + "FILE_DUPLICATE_ER": "El archivo ya está adjunto. Por favor, elija un archivo diferente", + "FILE_OR_URL_MSG": "Ingrese un archivo o una URL de archivo, ¡no ambos!", + "FILE_PIXEL_SIZE_ER": "El tamaño de píxel del archivo debe ser inferior a {0}", + "FILE_PROCESSING_ER": "Ocurrió un error al procesar un archivo", + "FILE_SIZE_CALCULATING_ER": "Se produjo un error al calcular el tamaño de un archivo", + "FILE_UNSUPPORTED": "Este tipo de archivo no está permitido", + "FLIP": "Voltear", + "FROM": "Desde", + "Filter": "Filtro", + "Flip": "Voltear", + "Freeform": "Forma libre", + "GALLERY": "Galería", + "GIF": "Gif", + "HELP": "Ayuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Ingrese una imagen o una URL de imagen, ¡no ambas!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selector de imágenes. Imagen seleccionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selector de imágenes. Ninguna imagen seleccionada", + "IMG_URL_MSG": "¡Por favor ingrese una URL de imagen!", + "INSERT_AUDIO": "Insertar audio", + "INSERT_FILE": "Insertar archivo", + "INSERT_IMAGE": "Insertar imagen", + "INSERT_LINK": "Insertar el link", + "INSERT_PARAGRAPH": "Insertar párrafo", + "INSERT_TABLE": "Insertar tabla", + "INSERT_VIDEO": "Insertar vídeo", + "Insert Your Message": "Inserta tu mensaje", + "KEY_COMBINATION": "combinación de teclas", + "LINK": "Enlace", + "LINK_NAME": "Nombre del enlace", + "LINK_URL": "URL del vínculo", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "alfa inferior", + "LOWER_ROMAN": "bajo romano", + "MAX_ATTACH_MSG": "Límite máximo de archivos adjuntos alcanzado", + "MONTH": "Mes", + "NO_RESULTS": "No hay resultados", + "NUMBERED": "Numerado", + "OK": "Aceptar", + "OPEN_IN_NEW_WINDOW": "Abrir en Nueva ventana", + "PICK_GIF": "elige un gif", + "PICK_IMAGE": "Elegir imagen", + "REDO_MSG": "Rehacer el último comando", + "REMOVE": "Eliminar", + "RESET": "Restablecer", + "REST_COLOR_MSG": "Restablecer al color predeterminado", + "ROTATE_LEFT": "Girar a la izquierda", + "ROTATE_RIGHT": "Gira a la derecha", + "Reset": "Restablecer", + "Rotate left": "Girar a la izquierda", + "Rotate right": "Gira a la derecha", + "SANS_SERIF": "sans serif", + "SAVE": "Guardar", + "SEARCH_GIF": "Buscar todos los GIF", + "SELECT_FROM_FILES": "Seleccionar de archivos", + "SELECT_STL_MSG": "Seleccionar estilo de lista", + "SENTENCE_CASE": "Caso de sentencia", + "SET_COLOR": "Establecer color", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Desliza hacia la izquierda para revelar acciones", + "Select Emoji": "Seleccionar emoji", + "Slider Color": "Color del control deslizante", + "Slider White Black Color": "Control deslizante Blanco Negro Color", + "Square": "Cuadrado", + "TAB": "PESTAÑA", + "TEXT_TO_DISPLAY": "Texto para mostrar", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titulo del caso", + "TO": "Hasta", + "Text": "Texto", + "UNDO_MSG": "Deshacer el último comando", + "UNKNOWN": "Desconocido", + "UNTAB": "desabrochar", + "UPPERCASE": "MAYÚSCULAS", + "UPPER_ALPHA": "alfa superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "La URL ya está adjunta. Ingrese una URL diferente", + "URL_INVALID_ER": "Introduce una URL válida", + "URL_MSG": "¡Por favor ingrese una URL!", + "VIDEO": "Video", + "VIEW": "Ver", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/es_UY.json b/lib/assets/strings/es_UY.json new file mode 100644 index 0000000..cce73b5 --- /dev/null +++ b/lib/assets/strings/es_UY.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Acción", + "ADD_URL": "Agregar URL", + "ALL": "Todos", + "ATTACHED_FILE": "Archivo adjunto", + "ATTACH_MAXSIZE_VALIDATE": "El tamaño del archivo debe ser inferior a {0}", + "AUDIO_FILE_OR_URL_MSG": "¡Elija un archivo de audio o ingrese una URL de archivo de audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ingrese un archivo de audio o una URL de audio, ¡no ambos!", + "Blur": "Difuminar", + "Blur Radius": "Radio de desenfoque", + "Brush": "Cepillar", + "CAMERA": "Cámara", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Cambiar caso", + "CHANGE_IMAGE": "Cambiar imagen", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "elegir sonido", + "CHOOSE_COLOR": "Elige un color", + "CHOOSE_FILE": "Elija el archivo", + "CHOOSE_FILE_URL_MSG": "¡Elija un archivo o ingrese una URL de archivo!", + "CHOOSE_IMAGE": "elegir imagen", + "CHOOSE_IMG_URL_MSG": "¡Elija una imagen o ingrese una URL de imagen!", + "CHOOSE_VIDEO": "elegir vídeo", + "CLEAR": "Borrar", + "CLOSE": "Cerrar", + "COMPLETE_EDITING": "Edición completa", + "COURIER": "mensajero", + "CROP": "Recortar", + "CUSTOM": "Personalizar", + "Color Opacity": "Opacidad de color", + "Crop": "Recortar", + "DEFAULT": "Predeterminado", + "DELETE": "Eliminar", + "DELETE_ATTACHMENT": "Eliminar archivo adjunto", + "DONE": "Listo", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Círculo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Desct", + "EDITOR_HEADER_1": "Encabezado 1", + "EDITOR_HEADER_2": "Encabezado 2", + "EDITOR_HEADER_3": "Encabezado 3", + "EDITOR_HEADER_4": "Encabezado 4", + "EDITOR_HEADER_5": "Encabezado 5", + "EDITOR_HEADER_6": "Encabezado 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "punto", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "cita", + "EDITOR_SQUARE": "Cuadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "INGRESAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Archivo", + "FILE_COMPRESSING_ER": "Ocurrió un error al comprimir un archivo", + "FILE_DUPLICATE_ER": "El archivo ya está adjunto. Por favor, elija un archivo diferente", + "FILE_OR_URL_MSG": "Ingrese un archivo o una URL de archivo, ¡no ambos!", + "FILE_PIXEL_SIZE_ER": "El tamaño de píxel del archivo debe ser inferior a {0}", + "FILE_PROCESSING_ER": "Ocurrió un error al procesar un archivo", + "FILE_SIZE_CALCULATING_ER": "Se produjo un error al calcular el tamaño de un archivo", + "FILE_UNSUPPORTED": "Este tipo de archivo no está permitido", + "FLIP": "Voltear", + "FROM": "Desde", + "Filter": "Filtro", + "Flip": "Voltear", + "Freeform": "Forma libre", + "GALLERY": "Galería", + "GIF": "Gif", + "HELP": "Ayuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Ingrese una imagen o una URL de imagen, ¡no ambas!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selector de imágenes. Imagen seleccionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selector de imágenes. Ninguna imagen seleccionada", + "IMG_URL_MSG": "¡Por favor ingrese una URL de imagen!", + "INSERT_AUDIO": "Insertar audio", + "INSERT_FILE": "Insertar archivo", + "INSERT_IMAGE": "Insertar imagen", + "INSERT_LINK": "Insertar el link", + "INSERT_PARAGRAPH": "Insertar párrafo", + "INSERT_TABLE": "Insertar tabla", + "INSERT_VIDEO": "Insertar vídeo", + "Insert Your Message": "Inserta tu mensaje", + "KEY_COMBINATION": "combinación de teclas", + "LINK": "Enlace", + "LINK_NAME": "Nombre del enlace", + "LINK_URL": "URL del vínculo", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "alfa inferior", + "LOWER_ROMAN": "bajo romano", + "MAX_ATTACH_MSG": "Límite máximo de archivos adjuntos alcanzado", + "MONTH": "Mes", + "NO_RESULTS": "No hay resultados", + "NUMBERED": "Numerado", + "OK": "Aceptar", + "OPEN_IN_NEW_WINDOW": "Abrir en Nueva ventana", + "PICK_GIF": "elige un gif", + "PICK_IMAGE": "Elegir imagen", + "REDO_MSG": "Rehacer el último comando", + "REMOVE": "Eliminar", + "RESET": "Restablecer", + "REST_COLOR_MSG": "Restablecer al color predeterminado", + "ROTATE_LEFT": "Girar a la izquierda", + "ROTATE_RIGHT": "Gira a la derecha", + "Reset": "Restablecer", + "Rotate left": "Girar a la izquierda", + "Rotate right": "Gira a la derecha", + "SANS_SERIF": "sans serif", + "SAVE": "Guardar", + "SEARCH_GIF": "Buscar todos los GIF", + "SELECT_FROM_FILES": "Seleccionar de archivos", + "SELECT_STL_MSG": "Seleccionar estilo de lista", + "SENTENCE_CASE": "Caso de sentencia", + "SET_COLOR": "Establecer color", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Desliza hacia la izquierda para revelar acciones", + "Select Emoji": "Seleccionar emoji", + "Slider Color": "Color del control deslizante", + "Slider White Black Color": "Control deslizante Blanco Negro Color", + "Square": "Cuadrado", + "TAB": "PESTAÑA", + "TEXT_TO_DISPLAY": "Texto para mostrar", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titulo del caso", + "TO": "Hasta", + "Text": "Texto", + "UNDO_MSG": "Deshacer el último comando", + "UNKNOWN": "Desconocido", + "UNTAB": "desabrochar", + "UPPERCASE": "MAYÚSCULAS", + "UPPER_ALPHA": "alfa superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "La URL ya está adjunta. Ingrese una URL diferente", + "URL_INVALID_ER": "Introduce una URL válida", + "URL_MSG": "¡Por favor ingrese una URL!", + "VIDEO": "Video", + "VIEW": "Ver", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/fi.json b/lib/assets/strings/fi.json new file mode 100644 index 0000000..4672d16 --- /dev/null +++ b/lib/assets/strings/fi.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Toiminto", + "ADD_URL": "Lisää URL", + "ALL": "Kaikki", + "ATTACHED_FILE": "Liitetiedosto", + "ATTACH_MAXSIZE_VALIDATE": "Tiedoston koon on oltava pienempi kuin {0}", + "AUDIO_FILE_OR_URL_MSG": "Ole hyvä ja valitse äänitiedosto tai anna äänitiedoston URL-osoite!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Syötä joko äänitiedosto tai äänen URL-osoite, älä molempia!", + "Blur": "Hämärtää", + "Blur Radius": "Sumennuksen säde", + "Brush": "Harjata", + "CAMERA": "Kamera", + "CANCEL": "Peruuta", + "CHANGE_CASE": "Vaihda kirjainkokoa", + "CHANGE_IMAGE": "Vaihda kuva", + "CHAR_LEFT": "Merkkejä jäljellä {0}", + "CHOOSE_AUDIO": "Valitse ääni", + "CHOOSE_COLOR": "Valitse väri", + "CHOOSE_FILE": "Valitse tiedosto", + "CHOOSE_FILE_URL_MSG": "Ole hyvä ja valitse tiedosto tai anna tiedoston URL-osoite!", + "CHOOSE_IMAGE": "Valitse kuva", + "CHOOSE_IMG_URL_MSG": "Ole hyvä ja valitse kuva tai anna kuvan URL-osoite!", + "CHOOSE_VIDEO": "Valitse video", + "CLEAR": "Tyhjennä", + "CLOSE": "Sulje", + "COMPLETE_EDITING": "Täydellinen muokkaus", + "COURIER": "Kuriiri", + "CROP": "Rajaa", + "CUSTOM": "Mukautettu", + "Color Opacity": "Värin opasiteetti", + "Crop": "Rajaa", + "DEFAULT": "Oletus", + "DELETE": "Poista", + "DELETE_ATTACHMENT": "Poista liite", + "DONE": "Valmis", + "EDIT": "Muokkaa", + "EDITOR_CIRCLE": "Ympyrä", + "EDITOR_CODE": "koodi", + "EDITOR_DISC": "Disc", + "EDITOR_HEADER_1": "Otsikko 1", + "EDITOR_HEADER_2": "Otsikko 2", + "EDITOR_HEADER_3": "Otsikko 3", + "EDITOR_HEADER_4": "Otsikko 4", + "EDITOR_HEADER_5": "Otsikko 5", + "EDITOR_HEADER_6": "Otsikko 6", + "EDITOR_NORMAL": "normaali", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "lainata", + "EDITOR_SQUARE": "Neliö", + "EDITOR_TEXT": "Teksti", + "ENTER": "TULLA SISÄÄN", + "ESCAPE": "Paeta", + "Emoji": "Emoji", + "FILE": "Tiedosto", + "FILE_COMPRESSING_ER": "Tiedostoa pakattaessa tapahtui virhe", + "FILE_DUPLICATE_ER": "Tiedosto on jo liitetty. Valitse toinen tiedosto", + "FILE_OR_URL_MSG": "Syötä joko tiedosto tai tiedoston URL-osoite, älä molempia!", + "FILE_PIXEL_SIZE_ER": "Tiedoston pikselin koon tulee olla pienempi kuin {0}", + "FILE_PROCESSING_ER": "Tiedostoa käsiteltäessä tapahtui virhe", + "FILE_SIZE_CALCULATING_ER": "Virhe laskettaessa tiedostokokoa", + "FILE_UNSUPPORTED": "Tämä tiedostotyyppi ei ole sallittu", + "FLIP": "Käännä", + "FROM": "Lähettäjä", + "Filter": "Suodatin", + "Flip": "Käännä", + "Freeform": "Vapaa muoto", + "GALLERY": "Galleria", + "GIF": "Gif", + "HELP": "Ohje", + "HIDE": "Piilota", + "IMAGE_OR_URL_MSG": "Syötä joko kuva tai kuvan URL-osoite, älä molempia!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Kuvanvalitsin. Kuva valittu", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Kuvanvalitsin. Ei valittua kuvaa", + "IMG_URL_MSG": "Anna kuvan URL-osoite!", + "INSERT_AUDIO": "Lisää ääni", + "INSERT_FILE": "Lisää tiedosto", + "INSERT_IMAGE": "Lisää kuva", + "INSERT_LINK": "Lisää linkki", + "INSERT_PARAGRAPH": "Lisää kappale", + "INSERT_TABLE": "Lisää taulukko", + "INSERT_VIDEO": "Lisää video", + "Insert Your Message": "Lisää viestisi", + "KEY_COMBINATION": "Näppäinyhdistelmä", + "LINK": "Linkki", + "LINK_NAME": "Linkin nimi", + "LINK_URL": "Linkin URL-osoite", + "LOWERCASE": "pienet kirjaimet", + "LOWER_ALPHA": "Alempi Alfa", + "LOWER_ROMAN": "Alempi roomalainen", + "MAX_ATTACH_MSG": "Liitteen enimmäisraja saavutettu", + "MONTH": "Kuukausi", + "NO_RESULTS": "Ei tuloksia", + "NUMBERED": "Numeroitu", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Avaa uudessa ikkunassa", + "PICK_GIF": "Valitse Gif", + "PICK_IMAGE": "Valitse kuva", + "REDO_MSG": "Toista viimeinen komento", + "REMOVE": "Poista", + "RESET": "Nollaa", + "REST_COLOR_MSG": "Palauta oletusväri", + "ROTATE_LEFT": "Kierrä vasemmalle", + "ROTATE_RIGHT": "Kierrä oikealle", + "Reset": "Nollaa", + "Rotate left": "Käänny vasemmalle", + "Rotate right": "Kierrä oikealle", + "SANS_SERIF": "Sans Serif", + "SAVE": "Tallenna", + "SEARCH_GIF": "Etsi kaikki GIF-tiedostot", + "SELECT_FROM_FILES": "Valitse tiedostoista", + "SELECT_STL_MSG": "Valitse luettelon tyyli", + "SENTENCE_CASE": "Lausetapaus", + "SET_COLOR": "Aseta väri", + "SHOW": "Näytä", + "SWIPE_TO_REVEAL_SEMANTIC": "Pyyhkäise vasemmalle nähdäksesi toiminnot", + "Select Emoji": "Valitse Emoji", + "Slider Color": "Liukusäätimen väri", + "Slider White Black Color": "Liukusäädin Valkoinen Musta Väri", + "Square": "Neliö", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Näytettävä teksti", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Otsikkotapaus", + "TO": "-", + "Text": "Teksti", + "UNDO_MSG": "Kumoa viimeinen komento", + "UNKNOWN": "Tuntematon", + "UNTAB": "Poista välilehti", + "UPPERCASE": "SUURI KIRJAIT", + "UPPER_ALPHA": "Ylä alfa", + "UPPER_ROMAN": "Ylä-Rooma", + "URL": "URL", + "URL_DUPLICATE_ER": "URL on jo liitetty. Anna eri URL-osoite", + "URL_INVALID_ER": "Anna kelvollinen URL-osoite", + "URL_MSG": "Anna URL-osoite!", + "VIDEO": "Video", + "VIEW": "Näytä", + "WEEK": "Viikko" +} \ No newline at end of file diff --git a/lib/assets/strings/fr.json b/lib/assets/strings/fr.json new file mode 100644 index 0000000..2f2b4ca --- /dev/null +++ b/lib/assets/strings/fr.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Action", + "ADD_URL": "Ajouter une URL", + "ALL": "Tout", + "ATTACHED_FILE": "Pièce jointe", + "ATTACH_MAXSIZE_VALIDATE": "La taille du fichier doit être inférieure à {0}", + "AUDIO_FILE_OR_URL_MSG": "Veuillez choisir un fichier audio ou entrer une URL de fichier audio !", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Veuillez saisir soit un fichier audio, soit une URL audio, pas les deux !", + "Blur": "Se brouiller", + "Blur Radius": "Rayon de flou", + "Brush": "Brosse", + "CAMERA": "Appareil photo", + "CANCEL": "Annuler", + "CHANGE_CASE": "Changer de cas", + "CHANGE_IMAGE": "Changer l'image", + "CHAR_LEFT": "Caractères restants {0}", + "CHOOSE_AUDIO": "Choisissez le son", + "CHOOSE_COLOR": "Choisir une couleur", + "CHOOSE_FILE": "Choisir le fichier", + "CHOOSE_FILE_URL_MSG": "Veuillez choisir un fichier ou entrer une URL de fichier !", + "CHOOSE_IMAGE": "Choisissez l'image", + "CHOOSE_IMG_URL_MSG": "Veuillez choisir une image ou entrer une URL d'image !", + "CHOOSE_VIDEO": "Choisissez la vidéo", + "CLEAR": "Effacer", + "CLOSE": "Fermer", + "COMPLETE_EDITING": "Édition complète", + "COURIER": "Courrier", + "CROP": "Rogner", + "CUSTOM": "Personnalisé", + "Color Opacity": "Opacité des couleurs", + "Crop": "Rogner", + "DEFAULT": "Par défaut", + "DELETE": "Supprimer", + "DELETE_ATTACHMENT": "Supprimer la pièce jointe", + "DONE": "Terminé", + "EDIT": "Modifier", + "EDITOR_CIRCLE": "Cercle", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Disque", + "EDITOR_HEADER_1": "En-tête 1", + "EDITOR_HEADER_2": "En-tête 2", + "EDITOR_HEADER_3": "En-tête 3", + "EDITOR_HEADER_4": "En-tête 4", + "EDITOR_HEADER_5": "En-tête 5", + "EDITOR_HEADER_6": "En-tête 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citation", + "EDITOR_SQUARE": "Carré", + "EDITOR_TEXT": "Texte", + "ENTER": "ENTRER", + "ESCAPE": "S'échapper", + "Emoji": "Emoji", + "FILE": "Fichier", + "FILE_COMPRESSING_ER": "Une erreur s'est produite lors de la compression d'un fichier", + "FILE_DUPLICATE_ER": "Le fichier est déjà joint. Veuillez choisir un autre fichier", + "FILE_OR_URL_MSG": "Veuillez saisir soit un fichier, soit une URL de fichier, pas les deux !", + "FILE_PIXEL_SIZE_ER": "La taille en pixels du fichier doit être inférieure à {0}", + "FILE_PROCESSING_ER": "Une erreur s'est produite lors du traitement d'un fichier", + "FILE_SIZE_CALCULATING_ER": "Une erreur s'est produite lors du calcul de la taille d'un fichier", + "FILE_UNSUPPORTED": "Ce type de fichier n'est pas autorisé", + "FLIP": "Basculer", + "FROM": "De", + "Filter": "Filtre", + "Flip": "Basculer", + "Freeform": "Forme libre", + "GALLERY": "Galerie", + "GIF": "GIF", + "HELP": "Aide", + "HIDE": "Masquer", + "IMAGE_OR_URL_MSG": "Veuillez entrer une image ou une URL d'image, pas les deux !", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Sélecteur d'images. Image sélectionnée", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Sélecteur d'images. Aucune image sélectionnée", + "IMG_URL_MSG": "Veuillez entrer une URL d'image !", + "INSERT_AUDIO": "Insérer du son", + "INSERT_FILE": "Insérer un fichier", + "INSERT_IMAGE": "Insérer une image", + "INSERT_LINK": "Insérer un lien", + "INSERT_PARAGRAPH": "Insérer un paragraphe", + "INSERT_TABLE": "Insérer un tableau", + "INSERT_VIDEO": "Insérer une vidéo", + "Insert Your Message": "Insérez votre message", + "KEY_COMBINATION": "Combinaison de touches", + "LINK": "Lien", + "LINK_NAME": "Nom du lien", + "LINK_URL": "URL du lien", + "LOWERCASE": "minuscule", + "LOWER_ALPHA": "Alpha inférieur", + "LOWER_ROMAN": "Bas-romain", + "MAX_ATTACH_MSG": "Limite maximale de pièces jointes atteinte", + "MONTH": "Mois", + "NO_RESULTS": "Aucun résultat", + "NUMBERED": "Numéroté", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Ouvrir dans une nouvelle fenêtre", + "PICK_GIF": "Choisissez un GIF", + "PICK_IMAGE": "Choisir une image", + "REDO_MSG": "Refaire la dernière commande", + "REMOVE": "Supprimer", + "RESET": "Réinitialiser", + "REST_COLOR_MSG": "Rétablir la couleur par défaut", + "ROTATE_LEFT": "Rotation vers la gauche", + "ROTATE_RIGHT": "Rotation vers la droite", + "Reset": "Réinitialiser", + "Rotate left": "Tourne à gauche", + "Rotate right": "Tourner à droite", + "SANS_SERIF": "Sans empattement", + "SAVE": "Enregistrer", + "SEARCH_GIF": "Rechercher tous les GIF", + "SELECT_FROM_FILES": "Sélectionnez parmi les fichiers", + "SELECT_STL_MSG": "Sélectionner le style de liste", + "SENTENCE_CASE": "Cas de peine", + "SET_COLOR": "Définir la couleur", + "SHOW": "Afficher", + "SWIPE_TO_REVEAL_SEMANTIC": "Balayez vers la gauche pour révéler les actions", + "Select Emoji": "Sélectionnez l'emoji", + "Slider Color": "Couleur du curseur", + "Slider White Black Color": "Curseur Blanc Noir Couleur", + "Square": "Carré", + "TAB": "LANGUETTE", + "TEXT_TO_DISPLAY": "Texte à afficher", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Casse du titre", + "TO": "À", + "Text": "Texte", + "UNDO_MSG": "Annuler la dernière commande", + "UNKNOWN": "Inconnue", + "UNTAB": "Désonglet", + "UPPERCASE": "MAJUSCULE", + "UPPER_ALPHA": "Alpha supérieur", + "UPPER_ROMAN": "Romain supérieur", + "URL": "URL", + "URL_DUPLICATE_ER": "L'URL est déjà jointe. Veuillez entrer une URL différente", + "URL_INVALID_ER": "Veuillez entrer une URL valide", + "URL_MSG": "Veuillez saisir une URL !", + "VIDEO": "Vidéo", + "VIEW": "Afficher", + "WEEK": "Semaine" +} \ No newline at end of file diff --git a/lib/assets/strings/fr_BE.json b/lib/assets/strings/fr_BE.json new file mode 100644 index 0000000..fbdda32 --- /dev/null +++ b/lib/assets/strings/fr_BE.json @@ -0,0 +1,140 @@ +{ + "ACTION": "action", + "ADD_URL": "Ajouter l'URL", + "ALL": "Tout", + "ATTACHED_FILE": "Pièce jointe", + "ATTACH_MAXSIZE_VALIDATE": "La taille du fichier doit être inférieure à {0}", + "AUDIO_FILE_OR_URL_MSG": "Veuillez choisir un fichier audio ou entrer une URL de fichier audio !", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Veuillez saisir soit un fichier audio, soit une URL audio, pas les deux !", + "Blur": "Se brouiller", + "Blur Radius": "Rayon de flou", + "Brush": "Brosse", + "CAMERA": "Caméra", + "CANCEL": "Annuler", + "CHANGE_CASE": "Changer de cas", + "CHANGE_IMAGE": "Changer l'image", + "CHAR_LEFT": "Caractères restants {0}", + "CHOOSE_AUDIO": "Choisissez le son", + "CHOOSE_COLOR": "Choisir une couleur", + "CHOOSE_FILE": "Choisir le fichier", + "CHOOSE_FILE_URL_MSG": "Veuillez choisir un fichier ou entrer une URL de fichier !", + "CHOOSE_IMAGE": "Choisissez l'image", + "CHOOSE_IMG_URL_MSG": "Veuillez choisir une image ou entrer une URL d'image !", + "CHOOSE_VIDEO": "Choisissez la vidéo", + "CLEAR": "Dégager", + "CLOSE": "Fermer", + "COMPLETE_EDITING": "Édition complète", + "COURIER": "Courrier", + "CROP": "Surgir", + "CUSTOM": "Douane", + "Color Opacity": "Opacité des couleurs", + "Crop": "Surgir", + "DEFAULT": "Défaut", + "DELETE": "Effacer", + "DELETE_ATTACHMENT": "Supprimer la pièce jointe", + "DONE": "Terminé", + "EDIT": "modifier", + "EDITOR_CIRCLE": "Cercle", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Disque", + "EDITOR_HEADER_1": "En-tête 1", + "EDITOR_HEADER_2": "En-tête 2", + "EDITOR_HEADER_3": "En-tête 3", + "EDITOR_HEADER_4": "En-tête 4", + "EDITOR_HEADER_5": "En-tête 5", + "EDITOR_HEADER_6": "En-tête 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citation", + "EDITOR_SQUARE": "Carré", + "EDITOR_TEXT": "Texte", + "ENTER": "ENTRER", + "ESCAPE": "S'échapper", + "Emoji": "Emoji", + "FILE": "Fichier", + "FILE_COMPRESSING_ER": "Une erreur s'est produite lors de la compression d'un fichier", + "FILE_DUPLICATE_ER": "Le fichier est déjà joint. Veuillez choisir un autre fichier", + "FILE_OR_URL_MSG": "Veuillez saisir soit un fichier, soit une URL de fichier, pas les deux !", + "FILE_PIXEL_SIZE_ER": "La taille en pixels du fichier doit être inférieure à {0}", + "FILE_PROCESSING_ER": "Une erreur s'est produite lors du traitement d'un fichier", + "FILE_SIZE_CALCULATING_ER": "Une erreur s'est produite lors du calcul de la taille d'un fichier", + "FILE_UNSUPPORTED": "Ce type de fichier n'est pas autorisé", + "FLIP": "Retourner", + "FROM": "De", + "Filter": "Filtre", + "Flip": "Retourner", + "Freeform": "Forme libre", + "GALLERY": "Galerie", + "GIF": "GIF", + "HELP": "Aidez-moi", + "HIDE": "Cacher", + "IMAGE_OR_URL_MSG": "Veuillez entrer une image ou une URL d'image, pas les deux !", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Sélecteur d'images. Image sélectionnée", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Sélecteur d'images. Aucune image sélectionnée", + "IMG_URL_MSG": "Veuillez entrer une URL d'image !", + "INSERT_AUDIO": "Insérer du son", + "INSERT_FILE": "Insérer un fichier", + "INSERT_IMAGE": "Insérer une image", + "INSERT_LINK": "Insérer un lien", + "INSERT_PARAGRAPH": "Insérer un paragraphe", + "INSERT_TABLE": "Insérer un tableau", + "INSERT_VIDEO": "Insérer une vidéo", + "Insert Your Message": "Insérez votre message", + "KEY_COMBINATION": "Combinaison de touches", + "LINK": "Lien", + "LINK_NAME": "Nom du lien", + "LINK_URL": "URL du lien", + "LOWERCASE": "minuscule", + "LOWER_ALPHA": "Alpha inférieur", + "LOWER_ROMAN": "Bas-romain", + "MAX_ATTACH_MSG": "Limite maximale de pièces jointes atteinte", + "MONTH": "Mois", + "NO_RESULTS": "Aucun résultat", + "NUMBERED": "Numéroté", + "OK": "d'accord", + "OPEN_IN_NEW_WINDOW": "Ouvrir dans une nouvelle fenêtre", + "PICK_GIF": "Choisissez un GIF", + "PICK_IMAGE": "Choisir une image", + "REDO_MSG": "Refaire la dernière commande", + "REMOVE": "Retirer", + "RESET": "Réinitialiser", + "REST_COLOR_MSG": "Rétablir la couleur par défaut", + "ROTATE_LEFT": "Tourne à gauche", + "ROTATE_RIGHT": "Tourner à droite", + "Reset": "Réinitialiser", + "Rotate left": "Tourne à gauche", + "Rotate right": "Tourner à droite", + "SANS_SERIF": "Sans empattement", + "SAVE": "sauvegarder", + "SEARCH_GIF": "Rechercher tous les GIF", + "SELECT_FROM_FILES": "Sélectionnez parmi les fichiers", + "SELECT_STL_MSG": "Sélectionner le style de liste", + "SENTENCE_CASE": "Cas de peine", + "SET_COLOR": "Définir la couleur", + "SHOW": "Spectacle", + "SWIPE_TO_REVEAL_SEMANTIC": "Balayez vers la gauche pour révéler les actions", + "Select Emoji": "Sélectionnez l'emoji", + "Slider Color": "Couleur du curseur", + "Slider White Black Color": "Curseur Blanc Noir Couleur", + "Square": "Carré", + "TAB": "LANGUETTE", + "TEXT_TO_DISPLAY": "Texte à afficher", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Casse du titre", + "TO": "À", + "Text": "Texte", + "UNDO_MSG": "Annuler la dernière commande", + "UNKNOWN": "Inconnue", + "UNTAB": "Désonglet", + "UPPERCASE": "MAJUSCULE", + "UPPER_ALPHA": "Alpha supérieur", + "UPPER_ROMAN": "Romain supérieur", + "URL": "URL", + "URL_DUPLICATE_ER": "L'URL est déjà jointe. Veuillez entrer une URL différente", + "URL_INVALID_ER": "Veuillez entrer une URL valide", + "URL_MSG": "Veuillez saisir une URL !", + "VIDEO": "Vidéo", + "VIEW": "Vue", + "WEEK": "La semaine" +} \ No newline at end of file diff --git a/lib/assets/strings/fr_CA.json b/lib/assets/strings/fr_CA.json new file mode 100644 index 0000000..028a35a --- /dev/null +++ b/lib/assets/strings/fr_CA.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Action", + "ADD_URL": "Ajouter une URL", + "ALL": "Tous", + "ATTACHED_FILE": "Pièce jointe", + "ATTACH_MAXSIZE_VALIDATE": "La taille du fichier doit être inférieure à {0}", + "AUDIO_FILE_OR_URL_MSG": "Veuillez choisir un fichier audio ou entrer une URL de fichier audio !", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Veuillez saisir soit un fichier audio, soit une URL audio, pas les deux !", + "Blur": "Se brouiller", + "Blur Radius": "Rayon de flou", + "Brush": "Brosse", + "CAMERA": "Appareil photo", + "CANCEL": "Annuler", + "CHANGE_CASE": "Changer de cas", + "CHANGE_IMAGE": "Changer l'image", + "CHAR_LEFT": "Caractères restants {0}", + "CHOOSE_AUDIO": "Choisissez le son", + "CHOOSE_COLOR": "Choisir une couleur", + "CHOOSE_FILE": "Choisir le fichier", + "CHOOSE_FILE_URL_MSG": "Veuillez choisir un fichier ou entrer une URL de fichier !", + "CHOOSE_IMAGE": "Choisissez l'image", + "CHOOSE_IMG_URL_MSG": "Veuillez choisir une image ou entrer une URL d'image !", + "CHOOSE_VIDEO": "Choisissez la vidéo", + "CLEAR": "Effacer", + "CLOSE": "Fermer", + "COMPLETE_EDITING": "Édition complète", + "COURIER": "Courrier", + "CROP": "Rogner", + "CUSTOM": "Personnalisé", + "Color Opacity": "Opacité des couleurs", + "Crop": "Rogner", + "DEFAULT": "Par défaut", + "DELETE": "Supprimer", + "DELETE_ATTACHMENT": "Supprimer la pièce jointe", + "DONE": "Terminé", + "EDIT": "Modifier", + "EDITOR_CIRCLE": "Encercler", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Disque", + "EDITOR_HEADER_1": "En-tête 1", + "EDITOR_HEADER_2": "En-tête 2", + "EDITOR_HEADER_3": "En-tête 3", + "EDITOR_HEADER_4": "En-tête 4", + "EDITOR_HEADER_5": "En-tête 5", + "EDITOR_HEADER_6": "En-tête 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citation", + "EDITOR_SQUARE": "Carré", + "EDITOR_TEXT": "Texte", + "ENTER": "ENTRER", + "ESCAPE": "S'échapper", + "Emoji": "Émoticône", + "FILE": "Fichier", + "FILE_COMPRESSING_ER": "Une erreur s'est produite lors de la compression d'un fichier", + "FILE_DUPLICATE_ER": "Le fichier est déjà joint. Veuillez choisir un autre fichier", + "FILE_OR_URL_MSG": "Veuillez saisir soit un fichier, soit une URL de fichier, pas les deux !", + "FILE_PIXEL_SIZE_ER": "La taille en pixels du fichier doit être inférieure à {0}", + "FILE_PROCESSING_ER": "Une erreur s'est produite lors du traitement d'un fichier", + "FILE_SIZE_CALCULATING_ER": "Une erreur s'est produite lors du calcul de la taille d'un fichier", + "FILE_UNSUPPORTED": "Ce type de fichier n'est pas autorisé", + "FLIP": "Retourner", + "FROM": "De", + "Filter": "Filtre", + "Flip": "Retourner", + "Freeform": "Forme libre", + "GALLERY": "Galerie", + "GIF": "GIF", + "HELP": "Aide", + "HIDE": "Masquer", + "IMAGE_OR_URL_MSG": "Veuillez entrer une image ou une URL d'image, pas les deux !", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Sélecteur d'images. Image sélectionnée", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Sélecteur d'images. Aucune image sélectionnée", + "IMG_URL_MSG": "Veuillez entrer une URL d'image !", + "INSERT_AUDIO": "Insérer du son", + "INSERT_FILE": "Insérer un fichier", + "INSERT_IMAGE": "Insérer une image", + "INSERT_LINK": "Insérer un lien", + "INSERT_PARAGRAPH": "Insérer un paragraphe", + "INSERT_TABLE": "Insérer un tableau", + "INSERT_VIDEO": "Insérer une vidéo", + "Insert Your Message": "Insérez votre message", + "KEY_COMBINATION": "Combinaison de touches", + "LINK": "Lien", + "LINK_NAME": "Nom du lien", + "LINK_URL": "URL du lien", + "LOWERCASE": "minuscule", + "LOWER_ALPHA": "Alpha inférieur", + "LOWER_ROMAN": "Bas-romain", + "MAX_ATTACH_MSG": "Limite maximale de pièces jointes atteinte", + "MONTH": "Mois", + "NO_RESULTS": "Aucun résultat", + "NUMBERED": "Numéroté", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Ouvrir dans une nouvelle fenêtre", + "PICK_GIF": "Choisissez un GIF", + "PICK_IMAGE": "Choisir une image", + "REDO_MSG": "Refaire la dernière commande", + "REMOVE": "Retirer", + "RESET": "Réinitialiser", + "REST_COLOR_MSG": "Rétablir la couleur par défaut", + "ROTATE_LEFT": "Rotation à gauche", + "ROTATE_RIGHT": "Rotation à droite", + "Reset": "Réinitialiser", + "Rotate left": "Tourne à gauche", + "Rotate right": "Tourner à droite", + "SANS_SERIF": "Sans empattement", + "SAVE": "Enregistrer", + "SEARCH_GIF": "Rechercher tous les GIF", + "SELECT_FROM_FILES": "Sélectionnez parmi les fichiers", + "SELECT_STL_MSG": "Sélectionner le style de liste", + "SENTENCE_CASE": "Cas de peine", + "SET_COLOR": "Définir la couleur", + "SHOW": "Afficher", + "SWIPE_TO_REVEAL_SEMANTIC": "Balayez vers la gauche pour révéler les actions", + "Select Emoji": "Sélectionnez l'emoji", + "Slider Color": "Couleur du curseur", + "Slider White Black Color": "Curseur Blanc Noir Couleur", + "Square": "Carré", + "TAB": "LANGUETTE", + "TEXT_TO_DISPLAY": "Texte à afficher", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Casse du titre", + "TO": "À", + "Text": "Texte", + "UNDO_MSG": "Annuler la dernière commande", + "UNKNOWN": "Inconnue", + "UNTAB": "Désonglet", + "UPPERCASE": "MAJUSCULE", + "UPPER_ALPHA": "Alpha supérieur", + "UPPER_ROMAN": "Romain supérieur", + "URL": "URL", + "URL_DUPLICATE_ER": "L'URL est déjà jointe. Veuillez entrer une URL différente", + "URL_INVALID_ER": "Veuillez entrer une URL valide", + "URL_MSG": "Veuillez saisir une URL !", + "VIDEO": "Vidéo", + "VIEW": "Voir", + "WEEK": "Semaine" +} \ No newline at end of file diff --git a/lib/assets/strings/fr_CH.json b/lib/assets/strings/fr_CH.json new file mode 100644 index 0000000..fbdda32 --- /dev/null +++ b/lib/assets/strings/fr_CH.json @@ -0,0 +1,140 @@ +{ + "ACTION": "action", + "ADD_URL": "Ajouter l'URL", + "ALL": "Tout", + "ATTACHED_FILE": "Pièce jointe", + "ATTACH_MAXSIZE_VALIDATE": "La taille du fichier doit être inférieure à {0}", + "AUDIO_FILE_OR_URL_MSG": "Veuillez choisir un fichier audio ou entrer une URL de fichier audio !", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Veuillez saisir soit un fichier audio, soit une URL audio, pas les deux !", + "Blur": "Se brouiller", + "Blur Radius": "Rayon de flou", + "Brush": "Brosse", + "CAMERA": "Caméra", + "CANCEL": "Annuler", + "CHANGE_CASE": "Changer de cas", + "CHANGE_IMAGE": "Changer l'image", + "CHAR_LEFT": "Caractères restants {0}", + "CHOOSE_AUDIO": "Choisissez le son", + "CHOOSE_COLOR": "Choisir une couleur", + "CHOOSE_FILE": "Choisir le fichier", + "CHOOSE_FILE_URL_MSG": "Veuillez choisir un fichier ou entrer une URL de fichier !", + "CHOOSE_IMAGE": "Choisissez l'image", + "CHOOSE_IMG_URL_MSG": "Veuillez choisir une image ou entrer une URL d'image !", + "CHOOSE_VIDEO": "Choisissez la vidéo", + "CLEAR": "Dégager", + "CLOSE": "Fermer", + "COMPLETE_EDITING": "Édition complète", + "COURIER": "Courrier", + "CROP": "Surgir", + "CUSTOM": "Douane", + "Color Opacity": "Opacité des couleurs", + "Crop": "Surgir", + "DEFAULT": "Défaut", + "DELETE": "Effacer", + "DELETE_ATTACHMENT": "Supprimer la pièce jointe", + "DONE": "Terminé", + "EDIT": "modifier", + "EDITOR_CIRCLE": "Cercle", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Disque", + "EDITOR_HEADER_1": "En-tête 1", + "EDITOR_HEADER_2": "En-tête 2", + "EDITOR_HEADER_3": "En-tête 3", + "EDITOR_HEADER_4": "En-tête 4", + "EDITOR_HEADER_5": "En-tête 5", + "EDITOR_HEADER_6": "En-tête 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citation", + "EDITOR_SQUARE": "Carré", + "EDITOR_TEXT": "Texte", + "ENTER": "ENTRER", + "ESCAPE": "S'échapper", + "Emoji": "Emoji", + "FILE": "Fichier", + "FILE_COMPRESSING_ER": "Une erreur s'est produite lors de la compression d'un fichier", + "FILE_DUPLICATE_ER": "Le fichier est déjà joint. Veuillez choisir un autre fichier", + "FILE_OR_URL_MSG": "Veuillez saisir soit un fichier, soit une URL de fichier, pas les deux !", + "FILE_PIXEL_SIZE_ER": "La taille en pixels du fichier doit être inférieure à {0}", + "FILE_PROCESSING_ER": "Une erreur s'est produite lors du traitement d'un fichier", + "FILE_SIZE_CALCULATING_ER": "Une erreur s'est produite lors du calcul de la taille d'un fichier", + "FILE_UNSUPPORTED": "Ce type de fichier n'est pas autorisé", + "FLIP": "Retourner", + "FROM": "De", + "Filter": "Filtre", + "Flip": "Retourner", + "Freeform": "Forme libre", + "GALLERY": "Galerie", + "GIF": "GIF", + "HELP": "Aidez-moi", + "HIDE": "Cacher", + "IMAGE_OR_URL_MSG": "Veuillez entrer une image ou une URL d'image, pas les deux !", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Sélecteur d'images. Image sélectionnée", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Sélecteur d'images. Aucune image sélectionnée", + "IMG_URL_MSG": "Veuillez entrer une URL d'image !", + "INSERT_AUDIO": "Insérer du son", + "INSERT_FILE": "Insérer un fichier", + "INSERT_IMAGE": "Insérer une image", + "INSERT_LINK": "Insérer un lien", + "INSERT_PARAGRAPH": "Insérer un paragraphe", + "INSERT_TABLE": "Insérer un tableau", + "INSERT_VIDEO": "Insérer une vidéo", + "Insert Your Message": "Insérez votre message", + "KEY_COMBINATION": "Combinaison de touches", + "LINK": "Lien", + "LINK_NAME": "Nom du lien", + "LINK_URL": "URL du lien", + "LOWERCASE": "minuscule", + "LOWER_ALPHA": "Alpha inférieur", + "LOWER_ROMAN": "Bas-romain", + "MAX_ATTACH_MSG": "Limite maximale de pièces jointes atteinte", + "MONTH": "Mois", + "NO_RESULTS": "Aucun résultat", + "NUMBERED": "Numéroté", + "OK": "d'accord", + "OPEN_IN_NEW_WINDOW": "Ouvrir dans une nouvelle fenêtre", + "PICK_GIF": "Choisissez un GIF", + "PICK_IMAGE": "Choisir une image", + "REDO_MSG": "Refaire la dernière commande", + "REMOVE": "Retirer", + "RESET": "Réinitialiser", + "REST_COLOR_MSG": "Rétablir la couleur par défaut", + "ROTATE_LEFT": "Tourne à gauche", + "ROTATE_RIGHT": "Tourner à droite", + "Reset": "Réinitialiser", + "Rotate left": "Tourne à gauche", + "Rotate right": "Tourner à droite", + "SANS_SERIF": "Sans empattement", + "SAVE": "sauvegarder", + "SEARCH_GIF": "Rechercher tous les GIF", + "SELECT_FROM_FILES": "Sélectionnez parmi les fichiers", + "SELECT_STL_MSG": "Sélectionner le style de liste", + "SENTENCE_CASE": "Cas de peine", + "SET_COLOR": "Définir la couleur", + "SHOW": "Spectacle", + "SWIPE_TO_REVEAL_SEMANTIC": "Balayez vers la gauche pour révéler les actions", + "Select Emoji": "Sélectionnez l'emoji", + "Slider Color": "Couleur du curseur", + "Slider White Black Color": "Curseur Blanc Noir Couleur", + "Square": "Carré", + "TAB": "LANGUETTE", + "TEXT_TO_DISPLAY": "Texte à afficher", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Casse du titre", + "TO": "À", + "Text": "Texte", + "UNDO_MSG": "Annuler la dernière commande", + "UNKNOWN": "Inconnue", + "UNTAB": "Désonglet", + "UPPERCASE": "MAJUSCULE", + "UPPER_ALPHA": "Alpha supérieur", + "UPPER_ROMAN": "Romain supérieur", + "URL": "URL", + "URL_DUPLICATE_ER": "L'URL est déjà jointe. Veuillez entrer une URL différente", + "URL_INVALID_ER": "Veuillez entrer une URL valide", + "URL_MSG": "Veuillez saisir une URL !", + "VIDEO": "Vidéo", + "VIEW": "Vue", + "WEEK": "La semaine" +} \ No newline at end of file diff --git a/lib/assets/strings/hr.json b/lib/assets/strings/hr.json new file mode 100644 index 0000000..aed68ce --- /dev/null +++ b/lib/assets/strings/hr.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Radnja", + "ADD_URL": "Dodajte URL", + "ALL": "svi", + "ATTACHED_FILE": "Priloženi dokument", + "ATTACH_MAXSIZE_VALIDATE": "Veličina datoteke mora biti manja od {0}", + "AUDIO_FILE_OR_URL_MSG": "Odaberite audio datoteku ili unesite URL audio datoteke!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Unesite audio datoteku ili audio URL, ne oboje!", + "Blur": "Zamutiti", + "Blur Radius": "Radijus zamućenja", + "Brush": "Četka", + "CAMERA": "Fotoaparat", + "CANCEL": "Otkazati", + "CHANGE_CASE": "Promjena velikih i malih slova", + "CHANGE_IMAGE": "Promijeni sliku", + "CHAR_LEFT": "Preostalo znakova {0}", + "CHOOSE_AUDIO": "Odaberite audio", + "CHOOSE_COLOR": "Odaberite boju", + "CHOOSE_FILE": "Odaberite datoteku", + "CHOOSE_FILE_URL_MSG": "Odaberite datoteku ili unesite URL datoteke!", + "CHOOSE_IMAGE": "Odaberite sliku", + "CHOOSE_IMG_URL_MSG": "Odaberite sliku ili unesite URL slike!", + "CHOOSE_VIDEO": "Odaberite video", + "CLEAR": "Čisto", + "CLOSE": "Zatvoriti", + "COMPLETE_EDITING": "Kompletno uređivanje", + "COURIER": "Kurir", + "CROP": "Usjev", + "CUSTOM": "Prilagođen", + "Color Opacity": "Neprozirnost boje", + "Crop": "Usjev", + "DEFAULT": "Zadano", + "DELETE": "Izbrisati", + "DELETE_ATTACHMENT": "Izbriši privitak", + "DONE": "Gotovo", + "EDIT": "Uredi", + "EDITOR_CIRCLE": "Krug", + "EDITOR_CODE": "kodirati", + "EDITOR_DISC": "Disk", + "EDITOR_HEADER_1": "Zaglavlje 1", + "EDITOR_HEADER_2": "Zaglavlje 2", + "EDITOR_HEADER_3": "Zaglavlje 3", + "EDITOR_HEADER_4": "Zaglavlje 4", + "EDITOR_HEADER_5": "Zaglavlje 5", + "EDITOR_HEADER_6": "Zaglavlje 6", + "EDITOR_NORMAL": "normalan", + "EDITOR_PT": "točka", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citat", + "EDITOR_SQUARE": "Kvadrat", + "EDITOR_TEXT": "Tekst", + "ENTER": "UNESI", + "ESCAPE": "Pobjeći", + "Emoji": "emoji", + "FILE": "Datoteka", + "FILE_COMPRESSING_ER": "Došlo je do pogreške prilikom sažimanja datoteke", + "FILE_DUPLICATE_ER": "Datoteka je već priložena. Odaberite drugu datoteku", + "FILE_OR_URL_MSG": "Unesite ili datoteku ili URL datoteke, ne oboje!", + "FILE_PIXEL_SIZE_ER": "Veličina datoteke u pikselu treba biti manja od {0}", + "FILE_PROCESSING_ER": "Došlo je do pogreške prilikom obrade datoteke", + "FILE_SIZE_CALCULATING_ER": "Došlo je do pogreške prilikom izračuna veličine datoteke", + "FILE_UNSUPPORTED": "Ova vrsta datoteke nije dopuštena", + "FLIP": "Preokrenite", + "FROM": "Iz", + "Filter": "filtar", + "Flip": "Preokrenite", + "Freeform": "Slobodan oblik", + "GALLERY": "Galerija", + "GIF": "Gif", + "HELP": "Pomozite", + "HIDE": "Sakriti", + "IMAGE_OR_URL_MSG": "Unesite sliku ili URL slike, ne oboje!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Birač slika. Slika je odabrana", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Birač slika. Nijedna slika nije odabrana", + "IMG_URL_MSG": "Unesite URL slike!", + "INSERT_AUDIO": "Umetni zvuk", + "INSERT_FILE": "Umetni datoteku", + "INSERT_IMAGE": "Umetni sliku", + "INSERT_LINK": "Umetni vezu", + "INSERT_PARAGRAPH": "Umetni odlomak", + "INSERT_TABLE": "Umetni tablicu", + "INSERT_VIDEO": "Umetni video", + "Insert Your Message": "Unesite svoju poruku", + "KEY_COMBINATION": "Kombinacija tipki", + "LINK": "Veza", + "LINK_NAME": "Ime veze", + "LINK_URL": "URL veze", + "LOWERCASE": "mala slova", + "LOWER_ALPHA": "Niža Alfa", + "LOWER_ROMAN": "donjorimski", + "MAX_ATTACH_MSG": "Dosegnuto je maksimalno ograničenje privitaka", + "MONTH": "Mjesec", + "NO_RESULTS": "Nema rezultata", + "NUMBERED": "Numerirano", + "OK": "u redu", + "OPEN_IN_NEW_WINDOW": "Otvori u novom prozoru", + "PICK_GIF": "Odaberite Gif", + "PICK_IMAGE": "Odaberite sliku", + "REDO_MSG": "Ponovi zadnju naredbu", + "REMOVE": "Ukloniti", + "RESET": "Reset", + "REST_COLOR_MSG": "Vrati na zadanu boju", + "ROTATE_LEFT": "Rotirajte ulijevo", + "ROTATE_RIGHT": "Rotirajte udesno", + "Reset": "Reset", + "Rotate left": "Zakreni lijevo", + "Rotate right": "Zakreni desno", + "SANS_SERIF": "Sans Serif", + "SAVE": "Uštedjeti", + "SEARCH_GIF": "Pretražite sve GIF-ove", + "SELECT_FROM_FILES": "Odaberite iz datoteka", + "SELECT_STL_MSG": "Odaberite stil popisa", + "SENTENCE_CASE": "Padež rečenice", + "SET_COLOR": "Postavite boju", + "SHOW": "Pokazati", + "SWIPE_TO_REVEAL_SEMANTIC": "Prijeđite prstom ulijevo da biste otkrili radnje", + "Select Emoji": "Odaberite Emoji", + "Slider Color": "Boja klizača", + "Slider White Black Color": "Klizač Bijela Crna Boja", + "Square": "Kvadrat", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Tekst za prikaz", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Velika i mala slova", + "TO": "Do", + "Text": "Tekst", + "UNDO_MSG": "Poništavanje zadnje naredbe", + "UNKNOWN": "nepoznato", + "UNTAB": "Ukloni tab", + "UPPERCASE": "VELIKA SLOVA", + "UPPER_ALPHA": "Gornja Alfa", + "UPPER_ROMAN": "gornjorimski", + "URL": "URL", + "URL_DUPLICATE_ER": "URL je već priložen. Unesite drugi URL", + "URL_INVALID_ER": "Unesite važeći URL", + "URL_MSG": "Unesite URL!", + "VIDEO": "Video", + "VIEW": "Pogled", + "WEEK": "Tjedan" +} \ No newline at end of file diff --git a/lib/assets/strings/hu.json b/lib/assets/strings/hu.json new file mode 100644 index 0000000..2308e64 --- /dev/null +++ b/lib/assets/strings/hu.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Művelet", + "ADD_URL": "URL-cím hozzáadása", + "ALL": "Mind", + "ATTACHED_FILE": "Csatolt fájl", + "ATTACH_MAXSIZE_VALIDATE": "A fájl méretének kisebbnek kell lennie, mint {0}", + "AUDIO_FILE_OR_URL_MSG": "Kérjük, válasszon hangfájlt, vagy adjon meg egy hangfájl URL-címét!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Kérjük, adjon meg egy hangfájlt vagy egy audio URL-t, ne mindkettőt!", + "Blur": "Elhomályosít", + "Blur Radius": "Elmosási sugár", + "Brush": "Kefe", + "CAMERA": "Kamera", + "CANCEL": "Mégse", + "CHANGE_CASE": "Váltson kisbetűt", + "CHANGE_IMAGE": "Kép módosítása", + "CHAR_LEFT": "Karakter maradt {0}", + "CHOOSE_AUDIO": "Válasszon hangot", + "CHOOSE_COLOR": "Válasszon egy színt", + "CHOOSE_FILE": "Válassz fájlt", + "CHOOSE_FILE_URL_MSG": "Kérjük, válasszon egy fájlt, vagy adjon meg egy fájl URL-címét!", + "CHOOSE_IMAGE": "Válasszon képet", + "CHOOSE_IMG_URL_MSG": "Kérjük, válasszon képet, vagy adja meg a kép URL-jét!", + "CHOOSE_VIDEO": "Válassz videót", + "CLEAR": "Törlés", + "CLOSE": "Bezárás", + "COMPLETE_EDITING": "Teljes szerkesztés", + "COURIER": "futár", + "CROP": "Körülvágás", + "CUSTOM": "Egyéni", + "Color Opacity": "Szín átlátszatlansága", + "Crop": "Körülvágás", + "DEFAULT": "Alapértelmezett", + "DELETE": "Törlés", + "DELETE_ATTACHMENT": "Melléklet törlése", + "DONE": "Kész", + "EDIT": "Szerkesztés", + "EDITOR_CIRCLE": "Kör", + "EDITOR_CODE": "kód", + "EDITOR_DISC": "Lemez", + "EDITOR_HEADER_1": "1. fejléc", + "EDITOR_HEADER_2": "2. fejléc", + "EDITOR_HEADER_3": "3. fejléc", + "EDITOR_HEADER_4": "4. fejléc", + "EDITOR_HEADER_5": "5. fejléc", + "EDITOR_HEADER_6": "6. fejléc", + "EDITOR_NORMAL": "Normál", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "idézet", + "EDITOR_SQUARE": "Négyzet", + "EDITOR_TEXT": "Szöveg", + "ENTER": "BELÉP", + "ESCAPE": "Menekülni", + "Emoji": "Emoji", + "FILE": "Fájl", + "FILE_COMPRESSING_ER": "Hiba történt egy fájl tömörítése közben", + "FILE_DUPLICATE_ER": "A fájl már csatolva van. Kérjük, válasszon másik fájlt", + "FILE_OR_URL_MSG": "Kérjük, adjon meg egy fájlt vagy egy fájl URL-címét, ne mindkettőt!", + "FILE_PIXEL_SIZE_ER": "A fájl pixelméretének kisebbnek kell lennie, mint {0}", + "FILE_PROCESSING_ER": "Hiba történt egy fájl feldolgozása közben", + "FILE_SIZE_CALCULATING_ER": "Hiba történt a fájlméret kiszámítása közben", + "FILE_UNSUPPORTED": "Ez a fájltípus nem engedélyezett", + "FLIP": "Tükrözés", + "FROM": "Feladó", + "Filter": "Szűrő", + "Flip": "Tükrözés", + "Freeform": "Szabad forma", + "GALLERY": "Galéria", + "GIF": "Gif", + "HELP": "Súgó", + "HIDE": "Elrejtés", + "IMAGE_OR_URL_MSG": "Kérjük, adjon meg egy képet vagy egy kép URL-jét, ne mindkettőt!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Képválasztó. Kép kiválasztva", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Képválasztó. Nincs kép kiválasztva", + "IMG_URL_MSG": "Kérjük, adja meg a kép URL-jét!", + "INSERT_AUDIO": "Hang beszúrása", + "INSERT_FILE": "Fájl beszúrása", + "INSERT_IMAGE": "Kép beszúrása", + "INSERT_LINK": "Hivatkozás beszúrása", + "INSERT_PARAGRAPH": "Bekezdés beszúrása", + "INSERT_TABLE": "Táblázat beszúrása", + "INSERT_VIDEO": "Videó beszúrása", + "Insert Your Message": "Helyezze be az üzenetet", + "KEY_COMBINATION": "Kulcskombináció", + "LINK": "Link", + "LINK_NAME": "Hivatkozás neve", + "LINK_URL": "Hivatkozás URL-címe", + "LOWERCASE": "kisbetűvel", + "LOWER_ALPHA": "Alsó alfa", + "LOWER_ROMAN": "Alsó római", + "MAX_ATTACH_MSG": "Elérte a melléklet maximális korlátját", + "MONTH": "Hónap", + "NO_RESULTS": "Nincs eredmény", + "NUMBERED": "Számozott", + "OK": "Rendben", + "OPEN_IN_NEW_WINDOW": "Megnyitás új ablakban", + "PICK_GIF": "Válassz egy GIF-et", + "PICK_IMAGE": "Válassza ki a képet", + "REDO_MSG": "Ismételje meg az utolsó parancsot", + "REMOVE": "Eltávolítás", + "RESET": "Visszaállítás", + "REST_COLOR_MSG": "Visszaállítás az alapértelmezett színre", + "ROTATE_LEFT": "Forgatás balra", + "ROTATE_RIGHT": "Forgatás jobbra", + "Reset": "Visszaállítás", + "Rotate left": "Forgasd balra", + "Rotate right": "Forgasd jobbra", + "SANS_SERIF": "Sans Serif", + "SAVE": "Mentés", + "SEARCH_GIF": "Keresés az összes GIF között", + "SELECT_FROM_FILES": "Válasszon a fájlok közül", + "SELECT_STL_MSG": "Válassza ki a lista stílusát", + "SENTENCE_CASE": "Mondat eset", + "SET_COLOR": "Állítsa be a színt", + "SHOW": "Megjelenítés", + "SWIPE_TO_REVEAL_SEMANTIC": "Csúsztassa balra a műveletek megjelenítéséhez", + "Select Emoji": "Válassza az Emoji lehetőséget", + "Slider Color": "Csúszka színe", + "Slider White Black Color": "Csúszka Fehér Fekete Szín", + "Square": "Négyzet", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Megjelenítendő szöveg", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Cím Case", + "TO": "Címzett", + "Text": "Szöveg", + "UNDO_MSG": "Az utolsó parancs visszavonása", + "UNKNOWN": "Ismeretlen", + "UNTAB": "Untab", + "UPPERCASE": "NAGYBETŰS", + "UPPER_ALPHA": "Felső Alfa", + "UPPER_ROMAN": "felső római", + "URL": "URL-cím", + "URL_DUPLICATE_ER": "Az URL már csatolva van. Kérjük, adjon meg másik URL-t", + "URL_INVALID_ER": "Kérjük, érvényes URL-t adjon meg", + "URL_MSG": "Kérjük, adjon meg egy URL-t!", + "VIDEO": "Videó", + "VIEW": "Megtekintés", + "WEEK": "Hét" +} \ No newline at end of file diff --git a/lib/assets/strings/in.json b/lib/assets/strings/in.json new file mode 100644 index 0000000..12bbecb --- /dev/null +++ b/lib/assets/strings/in.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Tindakan", + "ADD_URL": "Tambahkan URL", + "ALL": "Semua", + "ATTACHED_FILE": "File terlampir", + "ATTACH_MAXSIZE_VALIDATE": "Ukuran berkas harus kurang dari {0}", + "AUDIO_FILE_OR_URL_MSG": "Harap pilih file audio atau masukkan URL file audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Harap masukkan file audio atau URL audio, jangan keduanya!", + "Blur": "Mengaburkan", + "Blur Radius": "Radius kabur", + "Brush": "Sikat", + "CAMERA": "Kamera", + "CANCEL": "Membatalkan", + "CHANGE_CASE": "Ganti kasus", + "CHANGE_IMAGE": "Ganti gambar", + "CHAR_LEFT": "Karakter Tersisa {0}", + "CHOOSE_AUDIO": "Pilih audio", + "CHOOSE_COLOR": "Pilih Warna", + "CHOOSE_FILE": "Pilih File", + "CHOOSE_FILE_URL_MSG": "Harap pilih file atau masukkan URL file!", + "CHOOSE_IMAGE": "Pilih gambar", + "CHOOSE_IMG_URL_MSG": "Harap pilih gambar atau masukkan URL gambar!", + "CHOOSE_VIDEO": "Pilih video", + "CLEAR": "Jernih", + "CLOSE": "Dekat", + "COMPLETE_EDITING": "Selesai mengedit", + "COURIER": "Kurir", + "CROP": "Tanaman", + "CUSTOM": "Adat", + "Color Opacity": "Opasitas Warna", + "Crop": "Tanaman", + "DEFAULT": "Default", + "DELETE": "Menghapus", + "DELETE_ATTACHMENT": "Hapus lampiran", + "DONE": "Selesai", + "EDIT": "Edit", + "EDITOR_CIRCLE": "Lingkaran", + "EDITOR_CODE": "kode", + "EDITOR_DISC": "Cakram", + "EDITOR_HEADER_1": "Tajuk 1", + "EDITOR_HEADER_2": "Tajuk 2", + "EDITOR_HEADER_3": "Tajuk 3", + "EDITOR_HEADER_4": "Tajuk 4", + "EDITOR_HEADER_5": "Tajuk 5", + "EDITOR_HEADER_6": "Tajuk 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "mengutip", + "EDITOR_SQUARE": "Persegi", + "EDITOR_TEXT": "Teks", + "ENTER": "MEMASUKI", + "ESCAPE": "Melarikan diri", + "Emoji": "Emoji", + "FILE": "Mengajukan", + "FILE_COMPRESSING_ER": "Terjadi kesalahan saat mengompresi file", + "FILE_DUPLICATE_ER": "File sudah terlampir. Silakan pilih file yang berbeda", + "FILE_OR_URL_MSG": "Harap masukkan file atau URL file, jangan keduanya!", + "FILE_PIXEL_SIZE_ER": "Ukuran piksel berkas harus kurang dari {0}", + "FILE_PROCESSING_ER": "Terjadi kesalahan saat memproses file", + "FILE_SIZE_CALCULATING_ER": "Terjadi kesalahan saat menghitung ukuran file", + "FILE_UNSUPPORTED": "Jenis file ini tidak diperbolehkan", + "FLIP": "Balik", + "FROM": "Dari", + "Filter": "Saring", + "Flip": "Balik", + "Freeform": "Bebas dari", + "GALLERY": "Galeri", + "GIF": "GIF", + "HELP": "Membantu", + "HIDE": "Menyembunyikan", + "IMAGE_OR_URL_MSG": "Harap masukkan gambar atau URL gambar, jangan keduanya!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Pemilih Gambar. Gambar dipilih", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Pemilih Gambar. Tidak ada gambar yang dipilih", + "IMG_URL_MSG": "Masukkan URL gambar!", + "INSERT_AUDIO": "Masukkan Audio", + "INSERT_FILE": "Sisipkan File", + "INSERT_IMAGE": "Sisipkan Gambar", + "INSERT_LINK": "Sisipkan Tautan", + "INSERT_PARAGRAPH": "Sisipkan Paragraf", + "INSERT_TABLE": "Sisipkan Tabel", + "INSERT_VIDEO": "Sisipkan Video", + "Insert Your Message": "Masukkan Pesan Anda", + "KEY_COMBINATION": "Kombinasi Kunci", + "LINK": "Tautan", + "LINK_NAME": "Nama Tautan", + "LINK_URL": "URL tautan", + "LOWERCASE": "huruf kecil", + "LOWER_ALPHA": "Alfa rendah", + "LOWER_ROMAN": "Romawi Bawah", + "MAX_ATTACH_MSG": "Batas maksimum lampiran tercapai", + "MONTH": "Bulan", + "NO_RESULTS": "Tidak ada hasil", + "NUMBERED": "Bernomor", + "OK": "oke", + "OPEN_IN_NEW_WINDOW": "Buka di jendela baru", + "PICK_GIF": "Pilih GIF", + "PICK_IMAGE": "Pilih Gambar", + "REDO_MSG": "Ulangi perintah terakhir", + "REMOVE": "Menghapus", + "RESET": "Setel ulang", + "REST_COLOR_MSG": "Setel ulang ke warna default", + "ROTATE_LEFT": "Putar Kiri", + "ROTATE_RIGHT": "Putar ke kanan", + "Reset": "Setel ulang", + "Rotate left": "Putar ke kiri", + "Rotate right": "Putar ke kanan", + "SANS_SERIF": "Sans Serif", + "SAVE": "Menyimpan", + "SEARCH_GIF": "Cari semua GIF", + "SELECT_FROM_FILES": "Pilih dari file", + "SELECT_STL_MSG": "Pilih gaya daftar", + "SENTENCE_CASE": "Kasus kalimat", + "SET_COLOR": "Atur warna", + "SHOW": "Menunjukkan", + "SWIPE_TO_REVEAL_SEMANTIC": "Geser ke kiri untuk mengungkapkan tindakan", + "Select Emoji": "Pilih Emoji", + "Slider Color": "Warna Slider", + "Slider White Black Color": "Slider Warna Putih Hitam", + "Square": "Persegi", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Teks untuk ditampilkan", + "TIMES_NEW_ROMAN": "zaman Romawi Baru", + "TITLE_CASE": "Kasus Judul", + "TO": "Untuk", + "Text": "Teks", + "UNDO_MSG": "Batalkan perintah terakhir", + "UNKNOWN": "Tidak dikenal", + "UNTAB": "Buka tab", + "UPPERCASE": "HURUF BESAR", + "UPPER_ALPHA": "Alfa Atas", + "UPPER_ROMAN": "Romawi Atas", + "URL": "URL", + "URL_DUPLICATE_ER": "URL sudah terpasang. Harap masukkan URL yang berbeda", + "URL_INVALID_ER": "Harap masukkan URL yang valid", + "URL_MSG": "Masukkan URL!", + "VIDEO": "Video", + "VIEW": "Melihat", + "WEEK": "Minggu" +} \ No newline at end of file diff --git a/lib/assets/strings/it.json b/lib/assets/strings/it.json new file mode 100644 index 0000000..98872af --- /dev/null +++ b/lib/assets/strings/it.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Azione", + "ADD_URL": "Aggiungi URL", + "ALL": "Tutti", + "ATTACHED_FILE": "File allegato", + "ATTACH_MAXSIZE_VALIDATE": "La dimensione del file deve essere inferiore a {0}", + "AUDIO_FILE_OR_URL_MSG": "Scegli un file audio o inserisci l'URL di un file audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Inserisci un file audio o un URL audio, non entrambi!", + "Blur": "Sfocatura", + "Blur Radius": "Raggio di sfocatura", + "Brush": "Spazzola", + "CAMERA": "telecamera", + "CANCEL": "Annulla", + "CHANGE_CASE": "Cambia caso", + "CHANGE_IMAGE": "Cambia immagine", + "CHAR_LEFT": "Caratteri rimasti {0}", + "CHOOSE_AUDIO": "Scegli l'audio", + "CHOOSE_COLOR": "Scegli un colore", + "CHOOSE_FILE": "Scegli il file", + "CHOOSE_FILE_URL_MSG": "Scegli un file o inserisci l'URL di un file!", + "CHOOSE_IMAGE": "Scegli l'immagine", + "CHOOSE_IMG_URL_MSG": "Scegli un'immagine o inserisci l'URL di un'immagine!", + "CHOOSE_VIDEO": "Scegli video", + "CLEAR": "Chiaro", + "CLOSE": "Vicino", + "COMPLETE_EDITING": "Modifica completa", + "COURIER": "Corriere", + "CROP": "Ritaglia", + "CUSTOM": "costume", + "Color Opacity": "Opacità del colore", + "Crop": "Ritaglia", + "DEFAULT": "Predefinito", + "DELETE": "Elimina", + "DELETE_ATTACHMENT": "Elimina allegato", + "DONE": "Fatto", + "EDIT": "modificare", + "EDITOR_CIRCLE": "Cerchio", + "EDITOR_CODE": "codice", + "EDITOR_DISC": "Disco", + "EDITOR_HEADER_1": "Intestazione 1", + "EDITOR_HEADER_2": "Intestazione 2", + "EDITOR_HEADER_3": "Intestazione 3", + "EDITOR_HEADER_4": "Intestazione 4", + "EDITOR_HEADER_5": "Intestazione 5", + "EDITOR_HEADER_6": "Intestazione 6", + "EDITOR_NORMAL": "normale", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citazione", + "EDITOR_SQUARE": "Piazza", + "EDITOR_TEXT": "Testo", + "ENTER": "ACCEDERE", + "ESCAPE": "Fuga", + "Emoji": "emoji", + "FILE": "File", + "FILE_COMPRESSING_ER": "Si è verificato un errore durante la compressione di un file", + "FILE_DUPLICATE_ER": "Il file è già allegato. Si prega di scegliere un file diverso", + "FILE_OR_URL_MSG": "Inserisci un file o un URL di file, non entrambi!", + "FILE_PIXEL_SIZE_ER": "La dimensione in pixel del file deve essere inferiore a {0}", + "FILE_PROCESSING_ER": "Si è verificato un errore durante l'elaborazione di un file", + "FILE_SIZE_CALCULATING_ER": "Si è verificato un errore durante il calcolo delle dimensioni di un file", + "FILE_UNSUPPORTED": "Questo tipo di file non è consentito", + "FLIP": "Flip", + "FROM": "A partire dal", + "Filter": "Filtro", + "Flip": "Flip", + "Freeform": "Forma libera", + "GALLERY": "Galleria", + "GIF": "Gif", + "HELP": "Aiuto", + "HIDE": "Nascondere", + "IMAGE_OR_URL_MSG": "Inserisci un'immagine o un URL immagine, non entrambi!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selettore di immagini. Immagine selezionata", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selettore di immagini. Nessuna immagine selezionata", + "IMG_URL_MSG": "Inserisci l'URL di un'immagine!", + "INSERT_AUDIO": "Inserisci Audio", + "INSERT_FILE": "Inserisci file", + "INSERT_IMAGE": "Inserisci immagine", + "INSERT_LINK": "Inserisci collegamento", + "INSERT_PARAGRAPH": "Inserisci paragrafo", + "INSERT_TABLE": "Inserisci tabella", + "INSERT_VIDEO": "Inserisci Video", + "Insert Your Message": "Inserisci il tuo messaggio", + "KEY_COMBINATION": "Combinazione di tasti", + "LINK": "Collegamento", + "LINK_NAME": "Nome collegamento", + "LINK_URL": "URL del link", + "LOWERCASE": "minuscolo", + "LOWER_ALPHA": "Alfa inferiore", + "LOWER_ROMAN": "Romano inferiore", + "MAX_ATTACH_MSG": "Limite massimo di allegati raggiunto", + "MONTH": "Mese", + "NO_RESULTS": "Nessun risultato", + "NUMBERED": "Numerato", + "OK": "ok", + "OPEN_IN_NEW_WINDOW": "Apri in una nuova finestra", + "PICK_GIF": "Scegli una Gif", + "PICK_IMAGE": "Scegli immagine", + "REDO_MSG": "Ripeti l'ultimo comando", + "REMOVE": "Rimuovere", + "RESET": "Ripristina", + "REST_COLOR_MSG": "Ripristina il colore predefinito", + "ROTATE_LEFT": "Gira a sinistra", + "ROTATE_RIGHT": "Ruota a destra", + "Reset": "Ripristina", + "Rotate left": "Gira a sinistra", + "Rotate right": "Ruota a destra", + "SANS_SERIF": "Sans Serif", + "SAVE": "Salvare", + "SEARCH_GIF": "Cerca tutte le GIF", + "SELECT_FROM_FILES": "Seleziona dai file", + "SELECT_STL_MSG": "Seleziona lo stile dell'elenco", + "SENTENCE_CASE": "Caso di condanna", + "SET_COLOR": "Imposta il colore", + "SHOW": "Mostrare", + "SWIPE_TO_REVEAL_SEMANTIC": "Scorri verso sinistra per visualizzare le azioni", + "Select Emoji": "Seleziona Emoticon", + "Slider Color": "Colore cursore", + "Slider White Black Color": "Cursore Colore Bianco Nero", + "Square": "Piazza", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Testo da visualizzare", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Caso titolo", + "TO": "A", + "Text": "Testo", + "UNDO_MSG": "Annulla l'ultimo comando", + "UNKNOWN": "Sconosciuto", + "UNTAB": "Togli scheda", + "UPPERCASE": "MAIUSCOLO", + "UPPER_ALPHA": "Alfa superiore", + "UPPER_ROMAN": "Romano superiore", + "URL": "URL", + "URL_DUPLICATE_ER": "L'URL è già allegato. Inserisci un URL diverso", + "URL_INVALID_ER": "Inserisci un URL valido", + "URL_MSG": "Inserisci un URL!", + "VIDEO": "video", + "VIEW": "vista", + "WEEK": "Settimana" +} \ No newline at end of file diff --git a/lib/assets/strings/ja.json b/lib/assets/strings/ja.json new file mode 100644 index 0000000..4bb4b29 --- /dev/null +++ b/lib/assets/strings/ja.json @@ -0,0 +1,140 @@ +{ + "ACTION": "アクション", + "ADD_URL": "URLを追加", + "ALL": "すべて", + "ATTACHED_FILE": "添付ファイル", + "ATTACH_MAXSIZE_VALIDATE": "ファイルサイズは {0} 未満である必要があります", + "AUDIO_FILE_OR_URL_MSG": "音声ファイルを選択するか、音声ファイルの URL を入力してください。", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "音声ファイルまたは音声 URL の両方ではなく、どちらかを入力してください。", + "Blur": "ぼかし", + "Blur Radius": "ぼかし半径", + "Brush": "みがきます", + "CAMERA": "カメラ", + "CANCEL": "キャンセル", + "CHANGE_CASE": "大文字と小文字を変更する", + "CHANGE_IMAGE": "画像を変更する", + "CHAR_LEFT": "残り文字数 {0}", + "CHOOSE_AUDIO": "オーディオを選択", + "CHOOSE_COLOR": "色を選択してください", + "CHOOSE_FILE": "ファイルを選ぶ", + "CHOOSE_FILE_URL_MSG": "ファイルを選択するか、ファイルの URL を入力してください。", + "CHOOSE_IMAGE": "画像を選択してください", + "CHOOSE_IMG_URL_MSG": "画像を選択するか、画像の URL を入力してください。", + "CHOOSE_VIDEO": "ビデオを選択してください", + "CLEAR": "クリア", + "CLOSE": "閉じる", + "COMPLETE_EDITING": "完全な編集", + "COURIER": "宅配便", + "CROP": "切り抜き", + "CUSTOM": "カスタム", + "Color Opacity": "色の不透明度", + "Crop": "切り抜き", + "DEFAULT": "デフォルト", + "DELETE": "削除する", + "DELETE_ATTACHMENT": "添付ファイルを削除", + "DONE": "完了", + "EDIT": "編集する", + "EDITOR_CIRCLE": "サークル", + "EDITOR_CODE": "コード", + "EDITOR_DISC": "ディスク", + "EDITOR_HEADER_1": "ヘッダー 1", + "EDITOR_HEADER_2": "ヘッダー 2", + "EDITOR_HEADER_3": "ヘッダー 3", + "EDITOR_HEADER_4": "ヘッダー 4", + "EDITOR_HEADER_5": "ヘッダー 5", + "EDITOR_HEADER_6": "ヘッダー 6", + "EDITOR_NORMAL": "普通", + "EDITOR_PT": "ポイント", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "引用", + "EDITOR_SQUARE": "四角", + "EDITOR_TEXT": "テキスト", + "ENTER": "入力", + "ESCAPE": "エスケープ", + "Emoji": "絵文字", + "FILE": "ファイル", + "FILE_COMPRESSING_ER": "ファイルの圧縮中にエラーが発生しました", + "FILE_DUPLICATE_ER": "ファイルはすでに添付されています。別のファイルを選択してください", + "FILE_OR_URL_MSG": "ファイルまたはファイル URL の両方ではなく、どちらかを入力してください。", + "FILE_PIXEL_SIZE_ER": "ファイルのピクセル サイズは {0} 未満である必要があります", + "FILE_PROCESSING_ER": "ファイルの処理中にエラーが発生しました", + "FILE_SIZE_CALCULATING_ER": "ファイルサイズの計算中にエラーが発生しました", + "FILE_UNSUPPORTED": "このファイルタイプは許可されていません", + "FLIP": "フリップ", + "FROM": "から", + "Filter": "フィルタ", + "Flip": "フリップ", + "Freeform": "フリーフォーム", + "GALLERY": "ギャラリー", + "GIF": "GIF", + "HELP": "助けて", + "HIDE": "隠す", + "IMAGE_OR_URL_MSG": "画像または画像の URL の両方ではなく、どちらか一方を入力してください。", + "IMAGE_PICKER_SEMANTIC_SELECTED": "画像ピッカー。選択した画像", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "画像ピッカー。画像が選択されていません", + "IMG_URL_MSG": "画像の URL を入力してください。", + "INSERT_AUDIO": "オーディオの挿入", + "INSERT_FILE": "ファイルを挿入", + "INSERT_IMAGE": "画像の挿入", + "INSERT_LINK": "リンクを挿入", + "INSERT_PARAGRAPH": "段落の挿入", + "INSERT_TABLE": "テーブルの挿入", + "INSERT_VIDEO": "ビデオの挿入", + "Insert Your Message": "メッセージを挿入", + "KEY_COMBINATION": "キーの組み合わせ", + "LINK": "リンク", + "LINK_NAME": "リンク名", + "LINK_URL": "リンクURL", + "LOWERCASE": "小文字", + "LOWER_ALPHA": "下位アルファ", + "LOWER_ROMAN": "ローワーローマン", + "MAX_ATTACH_MSG": "アタッチメントの上限に達しました", + "MONTH": "月", + "NO_RESULTS": "結果がありません", + "NUMBERED": "番号付き", + "OK": "わかった", + "OPEN_IN_NEW_WINDOW": "新しいウィンドウで開きます", + "PICK_GIF": "GIF を選択してください", + "PICK_IMAGE": "画像を選択", + "REDO_MSG": "最後のコマンドをやり直す", + "REMOVE": "削除する", + "RESET": "リセット", + "REST_COLOR_MSG": "デフォルトの色にリセット", + "ROTATE_LEFT": "左に回転", + "ROTATE_RIGHT": "右に回る", + "Reset": "リセット", + "Rotate left": "左に回転", + "Rotate right": "右に回る", + "SANS_SERIF": "サンセリフ", + "SAVE": "保存する", + "SEARCH_GIF": "すべてのGIFを検索", + "SELECT_FROM_FILES": "ファイルから選択", + "SELECT_STL_MSG": "リストのスタイルを選択", + "SENTENCE_CASE": "文例", + "SET_COLOR": "色を設定する", + "SHOW": "見せる", + "SWIPE_TO_REVEAL_SEMANTIC": "左にスワイプしてアクションを表示します", + "Select Emoji": "絵文字を選択", + "Slider Color": "スライダーの色", + "Slider White Black Color": "スライダー ホワイト ブラック カラー", + "Square": "四角", + "TAB": "タブ", + "TEXT_TO_DISPLAY": "表示するテキスト", + "TIMES_NEW_ROMAN": "タイムズ ニュー ローマン", + "TITLE_CASE": "タイトルケース", + "TO": "に", + "Text": "テキスト", + "UNDO_MSG": "最後のコマンドを元に戻します", + "UNKNOWN": "わからない", + "UNTAB": "タブを解除", + "UPPERCASE": "大文字", + "UPPER_ALPHA": "アッパーアルファ", + "UPPER_ROMAN": "アッパーローマ", + "URL": "URL", + "URL_DUPLICATE_ER": "URLはすでに添付されています。別の URL を入力してください", + "URL_INVALID_ER": "有効な URL を入力してください", + "URL_MSG": "URLを入力してください!", + "VIDEO": "ビデオ", + "VIEW": "見る", + "WEEK": "週間" +} \ No newline at end of file diff --git a/lib/assets/strings/ka.json b/lib/assets/strings/ka.json new file mode 100644 index 0000000..3dfed25 --- /dev/null +++ b/lib/assets/strings/ka.json @@ -0,0 +1,140 @@ +{ + "ACTION": "მოქმედება", + "ADD_URL": "დაამატეთ URL", + "ALL": "ყველა", + "ATTACHED_FILE": "Მიმაგრებული ფაილი", + "ATTACH_MAXSIZE_VALIDATE": "ფაილის ზომა უნდა იყოს {0}-ზე ნაკლები", + "AUDIO_FILE_OR_URL_MSG": "გთხოვთ ან აირჩიოთ აუდიო ფაილი ან შეიყვანოთ აუდიო ფაილის URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "გთხოვთ შეიყვანოთ აუდიო ფაილი ან აუდიო URL და არა ორივე!", + "Blur": "დაბინდვა", + "Blur Radius": "დაბინდვის რადიუსი", + "Brush": "ფუნჯი", + "CAMERA": "კამერა", + "CANCEL": "გაუქმება", + "CHANGE_CASE": "საქმის შეცვლა", + "CHANGE_IMAGE": "სურათის შეცვლა", + "CHAR_LEFT": "დარჩა სიმბოლოები {0}", + "CHOOSE_AUDIO": "აირჩიეთ აუდიო", + "CHOOSE_COLOR": "აირჩიეთ ფერი", + "CHOOSE_FILE": "Აირჩიეთ ფაილი", + "CHOOSE_FILE_URL_MSG": "გთხოვთ ან აირჩიოთ ფაილი ან შეიყვანოთ ფაილის URL!", + "CHOOSE_IMAGE": "აირჩიეთ სურათი", + "CHOOSE_IMG_URL_MSG": "გთხოვთ, აირჩიოთ სურათი ან შეიყვანოთ სურათის URL!", + "CHOOSE_VIDEO": "აირჩიეთ ვიდეო", + "CLEAR": "ნათელია", + "CLOSE": "დახურვა", + "COMPLETE_EDITING": "სრული რედაქტირება", + "COURIER": "კურიერი", + "CROP": "მოსავალი", + "CUSTOM": "საბაჟო", + "Color Opacity": "ფერის გამჭვირვალობა", + "Crop": "მოსავალი", + "DEFAULT": "ნაგულისხმევი", + "DELETE": "წაშლა", + "DELETE_ATTACHMENT": "დანართის წაშლა", + "DONE": "შესრულებულია", + "EDIT": "რედაქტირება", + "EDITOR_CIRCLE": "წრე", + "EDITOR_CODE": "კოდი", + "EDITOR_DISC": "დისკი", + "EDITOR_HEADER_1": "სათაური 1", + "EDITOR_HEADER_2": "სათაური 2", + "EDITOR_HEADER_3": "სათაური 3", + "EDITOR_HEADER_4": "სათაური 4", + "EDITOR_HEADER_5": "სათაური 5", + "EDITOR_HEADER_6": "სათაური 6", + "EDITOR_NORMAL": "ნორმალური", + "EDITOR_PT": "პტ", + "EDITOR_PX": "გვ", + "EDITOR_QUOTE": "ციტატა", + "EDITOR_SQUARE": "მოედანი", + "EDITOR_TEXT": "ტექსტი", + "ENTER": "ENTER", + "ESCAPE": "გაქცევა", + "Emoji": "ემოჯი", + "FILE": "ფაილი", + "FILE_COMPRESSING_ER": "ფაილის შეკუმშვისას მოხდა შეცდომა", + "FILE_DUPLICATE_ER": "ფაილი უკვე დართულია. გთხოვთ, აირჩიოთ სხვა ფაილი", + "FILE_OR_URL_MSG": "გთხოვთ, შეიყვანოთ ფაილი ან ფაილის URL და არა ორივე!", + "FILE_PIXEL_SIZE_ER": "ფაილის პიქსელის ზომა უნდა იყოს {0}-ზე ნაკლები", + "FILE_PROCESSING_ER": "ფაილის დამუშავებისას მოხდა შეცდომა", + "FILE_SIZE_CALCULATING_ER": "ფაილის ზომის გამოთვლისას მოხდა შეცდომა", + "FILE_UNSUPPORTED": "ფაილის ეს ტიპი დაუშვებელია", + "FLIP": "გადაატრიალეთ", + "FROM": "აქედან", + "Filter": "ფილტრი", + "Flip": "გადაატრიალეთ", + "Freeform": "Თავისუფალი ფორმა", + "GALLERY": "გალერეა", + "GIF": "გიფ", + "HELP": "დახმარება", + "HIDE": "დამალვა", + "IMAGE_OR_URL_MSG": "გთხოვთ შეიყვანოთ სურათი ან სურათის URL და არა ორივე!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "გამოსახულების ამომრჩევი. სურათი არჩეულია", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "გამოსახულების ამომრჩევი. სურათი არ არის არჩეული", + "IMG_URL_MSG": "გთხოვთ შეიყვანოთ სურათის URL!", + "INSERT_AUDIO": "აუდიოს ჩასმა", + "INSERT_FILE": "ჩადეთ ფაილი", + "INSERT_IMAGE": "სურათის ჩასმა", + "INSERT_LINK": "ლინკის ჩასმა", + "INSERT_PARAGRAPH": "აბზაცის ჩასმა", + "INSERT_TABLE": "ცხრილის ჩასმა", + "INSERT_VIDEO": "ვიდეოს ჩასმა", + "Insert Your Message": "ჩადეთ თქვენი შეტყობინება", + "KEY_COMBINATION": "გასაღების კომბინაცია", + "LINK": "Ბმული", + "LINK_NAME": "ბმულის სახელი", + "LINK_URL": "ბმულის URL", + "LOWERCASE": "პატარა ასო", + "LOWER_ALPHA": "ქვედა ალფა", + "LOWER_ROMAN": "ქვედა რომაული", + "MAX_ATTACH_MSG": "მიღწეულია დანართის მაქსიმალური ლიმიტი", + "MONTH": "თვე", + "NO_RESULTS": "არანაირი შედეგი", + "NUMBERED": "დანომრილი", + "OK": "კარგი", + "OPEN_IN_NEW_WINDOW": "გახსენით ახალ ფანჯარაში", + "PICK_GIF": "აირჩიე გიფი", + "PICK_IMAGE": "აირჩიეთ სურათი", + "REDO_MSG": "გაიმეორეთ ბოლო ბრძანება", + "REMOVE": "ამოიღეთ", + "RESET": "გადატვირთვა", + "REST_COLOR_MSG": "გადატვირთეთ ნაგულისხმევ ფერზე", + "ROTATE_LEFT": "შეტრიალეთ მარცხნივ", + "ROTATE_RIGHT": "როტაცია მარჯვნივ", + "Reset": "გადატვირთვა", + "Rotate left": "შეტრიალეთ მარცხნივ", + "Rotate right": "როტაცია მარჯვნივ", + "SANS_SERIF": "Sans Serif", + "SAVE": "Შენახვა", + "SEARCH_GIF": "მოძებნეთ ყველა GIF", + "SELECT_FROM_FILES": "აირჩიეთ ფაილებიდან", + "SELECT_STL_MSG": "აირჩიეთ სიის სტილი", + "SENTENCE_CASE": "სასჯელის შემთხვევა", + "SET_COLOR": "დააყენეთ ფერი", + "SHOW": "შოუ", + "SWIPE_TO_REVEAL_SEMANTIC": "გადაფურცლეთ მარცხნივ მოქმედებების გამოსავლენად", + "Select Emoji": "აირჩიეთ Emoji", + "Slider Color": "სლაიდერის ფერი", + "Slider White Black Color": "სლაიდერი თეთრი შავი ფერი", + "Square": "მოედანი", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "ტექსტი საჩვენებლად", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "სათაურის საქმე", + "TO": "რომ", + "Text": "ტექსტი", + "UNDO_MSG": "გააუქმეთ ბოლო ბრძანება", + "UNKNOWN": "უცნობი", + "UNTAB": "უნტაბი", + "UPPERCASE": "ზედა", + "UPPER_ALPHA": "ზედა ალფა", + "UPPER_ROMAN": "ზემო რომაული", + "URL": "URL", + "URL_DUPLICATE_ER": "URL უკვე დართულია. გთხოვთ, შეიყვანოთ სხვადასხვა URL", + "URL_INVALID_ER": "გთხოვთ, შეიყვანოთ სწორი URL", + "URL_MSG": "გთხოვთ შეიყვანოთ URL!", + "VIDEO": "ვიდეო", + "VIEW": "ხედი", + "WEEK": "კვირა" +} \ No newline at end of file diff --git a/lib/assets/strings/ko.json b/lib/assets/strings/ko.json new file mode 100644 index 0000000..aaab22a --- /dev/null +++ b/lib/assets/strings/ko.json @@ -0,0 +1,140 @@ +{ + "ACTION": "동작", + "ADD_URL": "URL 추가", + "ALL": "모든", + "ATTACHED_FILE": "첨부 파일", + "ATTACH_MAXSIZE_VALIDATE": "파일 크기는 {0}보다 작아야 합니다.", + "AUDIO_FILE_OR_URL_MSG": "오디오 파일을 선택하거나 오디오 파일 URL을 입력하세요!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "오디오 파일 또는 오디오 URL 중 하나만 입력하세요. 둘 다 입력하면 안 됩니다!", + "Blur": "흐림", + "Blur Radius": "흐림 반경", + "Brush": "브러시", + "CAMERA": "카메라", + "CANCEL": "취소", + "CHANGE_CASE": "대소문자 변경", + "CHANGE_IMAGE": "이미지 변경", + "CHAR_LEFT": "문자 왼쪽 {0}", + "CHOOSE_AUDIO": "오디오 선택", + "CHOOSE_COLOR": "색상 선택", + "CHOOSE_FILE": "파일을 선택", + "CHOOSE_FILE_URL_MSG": "파일을 선택하거나 파일 URL을 입력하세요!", + "CHOOSE_IMAGE": "이미지 선택", + "CHOOSE_IMG_URL_MSG": "이미지를 선택하거나 이미지 URL을 입력하세요!", + "CHOOSE_VIDEO": "비디오 선택", + "CLEAR": "분명한", + "CLOSE": "닫기", + "COMPLETE_EDITING": "편집 완료", + "COURIER": "택배원", + "CROP": "수확고", + "CUSTOM": "관습", + "Color Opacity": "색상 불투명도", + "Crop": "수확고", + "DEFAULT": "태만", + "DELETE": "지우다", + "DELETE_ATTACHMENT": "첨부파일 삭제", + "DONE": "끝난", + "EDIT": "편집하다", + "EDITOR_CIRCLE": "원", + "EDITOR_CODE": "암호", + "EDITOR_DISC": "디스크", + "EDITOR_HEADER_1": "헤더 1", + "EDITOR_HEADER_2": "헤더 2", + "EDITOR_HEADER_3": "헤더 3", + "EDITOR_HEADER_4": "헤더 4", + "EDITOR_HEADER_5": "헤더 5", + "EDITOR_HEADER_6": "헤더 6", + "EDITOR_NORMAL": "정상", + "EDITOR_PT": "태평양 표준시", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "인용하다", + "EDITOR_SQUARE": "정사각형", + "EDITOR_TEXT": "본문", + "ENTER": "입력하다", + "ESCAPE": "탈출하다", + "Emoji": "이모티콘", + "FILE": "파일", + "FILE_COMPRESSING_ER": "파일을 압축하는 동안 오류가 발생했습니다.", + "FILE_DUPLICATE_ER": "파일이 이미 첨부되어 있습니다. 다른 파일을 선택하십시오", + "FILE_OR_URL_MSG": "파일 또는 파일 URL 중 하나만 입력하세요. 둘 다 입력하면 안 됩니다.", + "FILE_PIXEL_SIZE_ER": "파일 픽셀 크기는 {0}보다 작아야 합니다.", + "FILE_PROCESSING_ER": "파일을 처리하는 동안 오류가 발생했습니다.", + "FILE_SIZE_CALCULATING_ER": "파일 크기를 계산하는 동안 오류가 발생했습니다.", + "FILE_UNSUPPORTED": "이 파일 형식은 허용되지 않습니다.", + "FLIP": "튀기다", + "FROM": "에서", + "Filter": "필터", + "Flip": "튀기다", + "Freeform": "자유 형식", + "GALLERY": "갱도", + "GIF": "GIF", + "HELP": "도움", + "HIDE": "숨는 장소", + "IMAGE_OR_URL_MSG": "이미지 또는 이미지 URL 중 하나만 입력하세요. 둘 다 입력하면 안 됩니다.", + "IMAGE_PICKER_SEMANTIC_SELECTED": "이미지 선택기. 선택한 이미지", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "이미지 선택기. 선택한 이미지가 없습니다.", + "IMG_URL_MSG": "이미지 URL을 입력하세요!", + "INSERT_AUDIO": "오디오 삽입", + "INSERT_FILE": "파일 삽입", + "INSERT_IMAGE": "이미지 삽입", + "INSERT_LINK": "링크 삽입", + "INSERT_PARAGRAPH": "단락 삽입", + "INSERT_TABLE": "표 삽입", + "INSERT_VIDEO": "비디오 삽입", + "Insert Your Message": "메시지 삽입", + "KEY_COMBINATION": "키 조합", + "LINK": "링크", + "LINK_NAME": "링크 이름", + "LINK_URL": "링크 URL", + "LOWERCASE": "소문자", + "LOWER_ALPHA": "낮은 알파", + "LOWER_ROMAN": "낮은 로마", + "MAX_ATTACH_MSG": "첨부 파일 최대 한도 도달", + "MONTH": "달", + "NO_RESULTS": "결과 없음", + "NUMBERED": "번호가 매겨진", + "OK": "좋아요", + "OPEN_IN_NEW_WINDOW": "새 창에서 열기", + "PICK_GIF": "GIF 선택", + "PICK_IMAGE": "이미지 선택", + "REDO_MSG": "마지막 명령 다시 실행", + "REMOVE": "풀다", + "RESET": "다시 놓기", + "REST_COLOR_MSG": "기본 색상으로 재설정", + "ROTATE_LEFT": "왼쪽으로 회전", + "ROTATE_RIGHT": "오른쪽으로 회전", + "Reset": "다시 놓기", + "Rotate left": "왼쪽으로 회전", + "Rotate right": "오른쪽으로 회전", + "SANS_SERIF": "산세 리프", + "SAVE": "구하다", + "SEARCH_GIF": "모든 GIF 검색", + "SELECT_FROM_FILES": "파일에서 선택", + "SELECT_STL_MSG": "목록 스타일 선택", + "SENTENCE_CASE": "문장 케이스", + "SET_COLOR": "색상 설정", + "SHOW": "보여 주다", + "SWIPE_TO_REVEAL_SEMANTIC": "왼쪽으로 스와이프하여 작업 표시", + "Select Emoji": "이모티콘 선택", + "Slider Color": "슬라이더 색상", + "Slider White Black Color": "슬라이더 화이트 블랙 색상", + "Square": "정사각형", + "TAB": "탭", + "TEXT_TO_DISPLAY": "표시할 텍스트", + "TIMES_NEW_ROMAN": "타임즈 뉴 로만", + "TITLE_CASE": "제목 케이스", + "TO": "에", + "Text": "본문", + "UNDO_MSG": "마지막 명령 취소", + "UNKNOWN": "알려지지 않은", + "UNTAB": "Untab", + "UPPERCASE": "대문자", + "UPPER_ALPHA": "어퍼 알파", + "UPPER_ROMAN": "어퍼 로마", + "URL": "URL", + "URL_DUPLICATE_ER": "URL이 이미 첨부되어 있습니다. 다른 URL을 입력하세요.", + "URL_INVALID_ER": "유효한 URL을 입력하세요.", + "URL_MSG": "URL을 입력하세요!", + "VIDEO": "비디오", + "VIEW": "전망", + "WEEK": "주" +} \ No newline at end of file diff --git a/lib/assets/strings/lt.json b/lib/assets/strings/lt.json new file mode 100644 index 0000000..3a9d921 --- /dev/null +++ b/lib/assets/strings/lt.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Veiksmas", + "ADD_URL": "Pridėti URL", + "ALL": "Visi", + "ATTACHED_FILE": "Pridedamas failas", + "ATTACH_MAXSIZE_VALIDATE": "Failo dydis turi būti mažesnis nei {0}", + "AUDIO_FILE_OR_URL_MSG": "Pasirinkite garso failą arba įveskite garso failo URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Įveskite garso failą arba garso URL, o ne abu!", + "Blur": "Suliejimas", + "Blur Radius": "Suliejimo spindulys", + "Brush": "Šepetys", + "CAMERA": "Fotoaparatas", + "CANCEL": "Atšaukti", + "CHANGE_CASE": "Keisti didžiąją raidę", + "CHANGE_IMAGE": "Keisti vaizdą", + "CHAR_LEFT": "Liko simboliai {0}", + "CHOOSE_AUDIO": "Pasirinkite garsą", + "CHOOSE_COLOR": "Pasirinkite spalvą", + "CHOOSE_FILE": "Pasirinkti failą", + "CHOOSE_FILE_URL_MSG": "Pasirinkite failą arba įveskite failo URL!", + "CHOOSE_IMAGE": "Pasirinkite vaizdą", + "CHOOSE_IMG_URL_MSG": "Pasirinkite vaizdą arba įveskite vaizdo URL!", + "CHOOSE_VIDEO": "Pasirinkite vaizdo įrašą", + "CLEAR": "Išvalyti", + "CLOSE": "Uždaryti", + "COMPLETE_EDITING": "Užbaigti redagavimą", + "COURIER": "Kurjeris", + "CROP": "Apkarpyti", + "CUSTOM": "Pasirinktinis", + "Color Opacity": "Spalvos neskaidrumas", + "Crop": "Apkarpyti", + "DEFAULT": "Numatytasis", + "DELETE": "Ištrinti", + "DELETE_ATTACHMENT": "Ištrinti priedą", + "DONE": "Atlikta", + "EDIT": "Redaguoti", + "EDITOR_CIRCLE": "Apskritimas", + "EDITOR_CODE": "kodas", + "EDITOR_DISC": "Diskas", + "EDITOR_HEADER_1": "1 antraštė", + "EDITOR_HEADER_2": "2 antraštė", + "EDITOR_HEADER_3": "3 antraštė", + "EDITOR_HEADER_4": "4 antraštė", + "EDITOR_HEADER_5": "5 antraštė", + "EDITOR_HEADER_6": "6 antraštė", + "EDITOR_NORMAL": "normalus", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citata", + "EDITOR_SQUARE": "Kvadratas", + "EDITOR_TEXT": "Tekstas", + "ENTER": "ENTER", + "ESCAPE": "Pabegti", + "Emoji": "Jaustukas", + "FILE": "Failas", + "FILE_COMPRESSING_ER": "Suglaudinant failą įvyko klaida", + "FILE_DUPLICATE_ER": "Failas jau pridėtas. Pasirinkite kitą failą", + "FILE_OR_URL_MSG": "Įveskite failą arba failo URL, o ne abu!", + "FILE_PIXEL_SIZE_ER": "Failo pikselio dydis turi būti mažesnis nei {0}", + "FILE_PROCESSING_ER": "Apdorojant failą įvyko klaida", + "FILE_SIZE_CALCULATING_ER": "Skaičiuojant failo dydį įvyko klaida", + "FILE_UNSUPPORTED": "Šis failo tipas neleidžiamas", + "FLIP": "Apversti", + "FROM": "Nuo", + "Filter": "Filtras", + "Flip": "Apversti", + "Freeform": "Laisva forma", + "GALLERY": "Galerija", + "GIF": "Gif", + "HELP": "Pagalba", + "HIDE": "Slėpti", + "IMAGE_OR_URL_MSG": "Įveskite vaizdą arba vaizdo URL, o ne abu!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Vaizdo rinkiklis. Pasirinktas vaizdas", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Vaizdo rinkiklis. Nepasirinktas joks vaizdas", + "IMG_URL_MSG": "Įveskite vaizdo URL!", + "INSERT_AUDIO": "Įdėkite garso įrašą", + "INSERT_FILE": "Įterpti failą", + "INSERT_IMAGE": "Įterpti paveikslėlį", + "INSERT_LINK": "Įterpti nuorodą", + "INSERT_PARAGRAPH": "Įterpti pastraipą", + "INSERT_TABLE": "Įterpti lentelę", + "INSERT_VIDEO": "Įterpti vaizdo įrašą", + "Insert Your Message": "Įdėkite savo pranešimą", + "KEY_COMBINATION": "Raktų derinys", + "LINK": "Nuoroda", + "LINK_NAME": "Sąsajos pavadinimas", + "LINK_URL": "Saito URL", + "LOWERCASE": "mažosios raidės", + "LOWER_ALPHA": "Žemutinė Alfa", + "LOWER_ROMAN": "Žemutinė romėniška", + "MAX_ATTACH_MSG": "Pasiektas maksimalus priedo limitas", + "MONTH": "Mėnuo", + "NO_RESULTS": "Jokių rezultatų", + "NUMBERED": "Sunumeruoti", + "OK": "Gerai", + "OPEN_IN_NEW_WINDOW": "Atidaryti naujame lange", + "PICK_GIF": "Pasirinkite Gif", + "PICK_IMAGE": "Pasirinkite vaizdą", + "REDO_MSG": "Pakartokite paskutinę komandą", + "REMOVE": "Šalinti", + "RESET": "Nustatyti iš naujo", + "REST_COLOR_MSG": "Iš naujo nustatyti numatytąją spalvą", + "ROTATE_LEFT": "Pasukti į kairę", + "ROTATE_RIGHT": "Sukti į dešinę", + "Reset": "Nustatyti iš naujo", + "Rotate left": "Pasukti į kairę", + "Rotate right": "Pasukite dešinėn", + "SANS_SERIF": "Sans Serif", + "SAVE": "Išsaugoti", + "SEARCH_GIF": "Ieškokite visų GIF", + "SELECT_FROM_FILES": "Pasirinkite iš failų", + "SELECT_STL_MSG": "Pasirinkite sąrašo stilių", + "SENTENCE_CASE": "Sakinio byla", + "SET_COLOR": "Nustatyti spalvą", + "SHOW": "Rodyti", + "SWIPE_TO_REVEAL_SEMANTIC": "Braukite kairėn, kad atskleistumėte veiksmus", + "Select Emoji": "Pasirinkite jaustukai", + "Slider Color": "Slankiklio spalva", + "Slider White Black Color": "Slankiklis Balta Juoda Spalva", + "Square": "Kvadratas", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Rodomas tekstas", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Pavadinimo byla", + "TO": "Iki", + "Text": "Tekstas", + "UNDO_MSG": "Atšaukti paskutinę komandą", + "UNKNOWN": "Nežinoma", + "UNTAB": "Untab", + "UPPERCASE": "DIDŽIOSIOS raidės", + "UPPER_ALPHA": "Viršutinė Alfa", + "UPPER_ROMAN": "Aukštutinė romėniška", + "URL": "URL", + "URL_DUPLICATE_ER": "URL jau pridėtas. Įveskite kitą URL", + "URL_INVALID_ER": "Įveskite tinkamą URL", + "URL_MSG": "Įveskite URL!", + "VIDEO": "Vaizdo įrašas", + "VIEW": "Peržiūrėti", + "WEEK": "Savaitė" +} \ No newline at end of file diff --git a/lib/assets/strings/lv.json b/lib/assets/strings/lv.json new file mode 100644 index 0000000..dfb9ec4 --- /dev/null +++ b/lib/assets/strings/lv.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Darbība", + "ADD_URL": "Pievienot URL adresi", + "ALL": "Visi", + "ATTACHED_FILE": "Pievienotais fails", + "ATTACH_MAXSIZE_VALIDATE": "Faila lielumam ir jābūt mazākam par {0}", + "AUDIO_FILE_OR_URL_MSG": "Lūdzu, izvēlieties audio failu vai ievadiet audio faila URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Lūdzu, ievadiet audio failu vai audio URL, nevis abus!", + "Blur": "Aizmiglot", + "Blur Radius": "Aizmiglošanas rādiuss", + "Brush": "Ota", + "CAMERA": "Kamera", + "CANCEL": "Atcelt", + "CHANGE_CASE": "Mainīt reģistru", + "CHANGE_IMAGE": "Mainīt attēlu", + "CHAR_LEFT": "Atlicis rakstzīmes {0}", + "CHOOSE_AUDIO": "Izvēlieties audio", + "CHOOSE_COLOR": "Izvēlieties krāsu", + "CHOOSE_FILE": "Izvēlēties failu", + "CHOOSE_FILE_URL_MSG": "Lūdzu, izvēlieties failu vai ievadiet faila URL!", + "CHOOSE_IMAGE": "Izvēlieties attēlu", + "CHOOSE_IMG_URL_MSG": "Lūdzu, izvēlieties attēlu vai ievadiet attēla URL!", + "CHOOSE_VIDEO": "Izvēlieties video", + "CLEAR": "Attīrīt", + "CLOSE": "Aizvērt", + "COMPLETE_EDITING": "Pabeigt rediģēšanu", + "COURIER": "Kurjers", + "CROP": "Kadrēt", + "CUSTOM": "Individualizēts", + "Color Opacity": "Krāsu necaurredzamība", + "Crop": "Kadrēt", + "DEFAULT": "Noklusējums", + "DELETE": "Dzēst", + "DELETE_ATTACHMENT": "Dzēst pielikumu", + "DONE": "Izdarīts", + "EDIT": "Rediģēt", + "EDITOR_CIRCLE": "Aplis", + "EDITOR_CODE": "kodu", + "EDITOR_DISC": "Disks", + "EDITOR_HEADER_1": "1. galvene", + "EDITOR_HEADER_2": "2. galvene", + "EDITOR_HEADER_3": "3. galvene", + "EDITOR_HEADER_4": "4. galvene", + "EDITOR_HEADER_5": "5. galvene", + "EDITOR_HEADER_6": "6. galvene", + "EDITOR_NORMAL": "normāli", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citāts", + "EDITOR_SQUARE": "Kvadrāts", + "EDITOR_TEXT": "Teksts", + "ENTER": "ENTER", + "ESCAPE": "Bēgt", + "Emoji": "Emocijzīme", + "FILE": "Datne", + "FILE_COMPRESSING_ER": "Saspiežot failu, radās kļūda", + "FILE_DUPLICATE_ER": "Fails jau ir pievienots. Lūdzu, izvēlieties citu failu", + "FILE_OR_URL_MSG": "Lūdzu, ievadiet vai nu failu, vai faila URL, nevis abus!", + "FILE_PIXEL_SIZE_ER": "Faila pikseļa lielumam ir jābūt mazākam par {0}", + "FILE_PROCESSING_ER": "Apstrādājot failu, radās kļūda", + "FILE_SIZE_CALCULATING_ER": "Aprēķinot faila lielumu, radās kļūda", + "FILE_UNSUPPORTED": "Šis faila veids nav atļauts", + "FLIP": "Apgriezt", + "FROM": "No", + "Filter": "Filtrs", + "Flip": "Apgriezt", + "Freeform": "Brīvā formā", + "GALLERY": "Galerija", + "GIF": "Gif", + "HELP": "Palīdzība", + "HIDE": "Paslēpt", + "IMAGE_OR_URL_MSG": "Lūdzu, ievadiet attēlu vai attēla URL, nevis abus!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Attēlu atlasītājs. Attēls atlasīts", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Attēlu atlasītājs. Nav atlasīts neviens attēls", + "IMG_URL_MSG": "Lūdzu, ievadiet attēla URL!", + "INSERT_AUDIO": "Ievietojiet audio", + "INSERT_FILE": "Ievietot failu", + "INSERT_IMAGE": "Ievietot attēlu", + "INSERT_LINK": "Ievietot saiti", + "INSERT_PARAGRAPH": "Ievietot rindkopu", + "INSERT_TABLE": "Ievietojiet tabulu", + "INSERT_VIDEO": "Ievietot video", + "Insert Your Message": "Ievietojiet savu ziņojumu", + "KEY_COMBINATION": "Taustiņu kombinācija", + "LINK": "Saite", + "LINK_NAME": "Saites nosaukums", + "LINK_URL": "Saites URL", + "LOWERCASE": "mazie burti", + "LOWER_ALPHA": "Apakšējā Alfa", + "LOWER_ROMAN": "Lejas romiešu", + "MAX_ATTACH_MSG": "Sasniegts pielikuma maksimālais ierobežojums", + "MONTH": "Mēnesis", + "NO_RESULTS": "Nav rezultātu", + "NUMBERED": "Numurēts", + "OK": "Labi", + "OPEN_IN_NEW_WINDOW": "Atvērt jaunā logā", + "PICK_GIF": "Izvēlieties Gif", + "PICK_IMAGE": "Izvēlieties attēlu", + "REDO_MSG": "Atkārtojiet pēdējo komandu", + "REMOVE": "Noņemt", + "RESET": "Atiestatīt", + "REST_COLOR_MSG": "Atiestatīt uz noklusējuma krāsu", + "ROTATE_LEFT": "Pagriezt pa kreisi", + "ROTATE_RIGHT": "Pagriezt pa labi", + "Reset": "Atiestatīt", + "Rotate left": "Pagriezt pa kreisi", + "Rotate right": "Pagriezt pa labi", + "SANS_SERIF": "Sans serif", + "SAVE": "Saglabāt", + "SEARCH_GIF": "Meklēt visus GIF", + "SELECT_FROM_FILES": "Izvēlieties no failiem", + "SELECT_STL_MSG": "Izvēlieties saraksta stilu", + "SENTENCE_CASE": "Teikuma gadījums", + "SET_COLOR": "Iestatīt krāsu", + "SHOW": "Rādīt", + "SWIPE_TO_REVEAL_SEMANTIC": "Velciet pa kreisi, lai atklātu darbības", + "Select Emoji": "Atlasiet Emoji", + "Slider Color": "Slīdņa krāsa", + "Slider White Black Color": "Slīdnis Balts Melns Krāsa", + "Square": "Kvadrāts", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Parādāmais teksts", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Virsraksta lieta", + "TO": "Līdz", + "Text": "Teksts", + "UNDO_MSG": "Atsaukt pēdējo komandu", + "UNKNOWN": "Nezināms", + "UNTAB": "Untab", + "UPPERCASE": "LIELIE BURTI", + "UPPER_ALPHA": "Augšējā Alfa", + "UPPER_ROMAN": "Augšromietis", + "URL": "URL", + "URL_DUPLICATE_ER": "URL jau ir pievienots. Lūdzu, ievadiet citu URL", + "URL_INVALID_ER": "Lūdzu, ievadiet derīgu URL", + "URL_MSG": "Lūdzu, ievadiet URL!", + "VIDEO": "Video", + "VIEW": "Skatīt", + "WEEK": "Nedēļa" +} \ No newline at end of file diff --git a/lib/assets/strings/mk.json b/lib/assets/strings/mk.json new file mode 100644 index 0000000..a044e1f --- /dev/null +++ b/lib/assets/strings/mk.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Акција", + "ADD_URL": "Додадете URL", + "ALL": "Сите", + "ATTACHED_FILE": "Приложениот документ", + "ATTACH_MAXSIZE_VALIDATE": "Големината на датотеката мора да биде помала од {0}", + "AUDIO_FILE_OR_URL_MSG": "Ве молиме или изберете аудио датотека или внесете URL на аудио датотека!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ве молиме внесете аудио датотека или аудио URL, а не и двете!", + "Blur": "Заматено", + "Blur Radius": "Радиус на заматување", + "Brush": "Четка", + "CAMERA": "Камера", + "CANCEL": "Откажи", + "CHANGE_CASE": "Смени случај", + "CHANGE_IMAGE": "Променете ја сликата", + "CHAR_LEFT": "Оставени знаци {0}", + "CHOOSE_AUDIO": "Изберете аудио", + "CHOOSE_COLOR": "Изберете Боја", + "CHOOSE_FILE": "Изберете датотека", + "CHOOSE_FILE_URL_MSG": "Ве молиме или изберете датотека или внесете URL на датотека!", + "CHOOSE_IMAGE": "Изберете слика", + "CHOOSE_IMG_URL_MSG": "Ве молиме изберете слика или внесете URL на сликата!", + "CHOOSE_VIDEO": "Изберете видео", + "CLEAR": "Јасно", + "CLOSE": "Затвори", + "COMPLETE_EDITING": "Целосно уредување", + "COURIER": "Курир", + "CROP": "Исечете", + "CUSTOM": "Прилагодено", + "Color Opacity": "Непроѕирност на бојата", + "Crop": "Исечете", + "DEFAULT": "Стандардно", + "DELETE": "Избриши", + "DELETE_ATTACHMENT": "Избришете го прилогот", + "DONE": "Направено", + "EDIT": "Уредување", + "EDITOR_CIRCLE": "Заокружете", + "EDITOR_CODE": "код", + "EDITOR_DISC": "Диск", + "EDITOR_HEADER_1": "Заглавие 1", + "EDITOR_HEADER_2": "Заглавие 2", + "EDITOR_HEADER_3": "Заглавие 3", + "EDITOR_HEADER_4": "Заглавие 4", + "EDITOR_HEADER_5": "Заглавие 5", + "EDITOR_HEADER_6": "Заглавие 6", + "EDITOR_NORMAL": "нормално", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "цитат", + "EDITOR_SQUARE": "Плоштад", + "EDITOR_TEXT": "Текст", + "ENTER": "ENTER", + "ESCAPE": "Бегство", + "Emoji": "Емоџи", + "FILE": "Датотека", + "FILE_COMPRESSING_ER": "Се појави грешка при компресирање на датотека", + "FILE_DUPLICATE_ER": "Датотеката е веќе прикачена. Ве молиме изберете друга датотека", + "FILE_OR_URL_MSG": "Ве молиме внесете или датотека или URL на датотека, а не и двете!", + "FILE_PIXEL_SIZE_ER": "Големината на пикселот на датотеката треба да биде помала од {0}", + "FILE_PROCESSING_ER": "Се појави грешка при обработката на датотеката", + "FILE_SIZE_CALCULATING_ER": "Се појави грешка при пресметувањето на големината на датотеката", + "FILE_UNSUPPORTED": "Овој тип на датотека не е дозволен", + "FLIP": "Преврти", + "FROM": "Од", + "Filter": "Филтер", + "Flip": "Преврти", + "Freeform": "Слободна форма", + "GALLERY": "Галерија", + "GIF": "Гиф", + "HELP": "Помош", + "HIDE": "Крие", + "IMAGE_OR_URL_MSG": "Ве молиме внесете или слика или URL на сликата, а не и двете!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Избирач на слики. Сликата е избрана", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Избирач на слики. Нема избрана слика", + "IMG_URL_MSG": "Ве молиме внесете URL на сликата!", + "INSERT_AUDIO": "Вметнете аудио", + "INSERT_FILE": "Вметнете датотека", + "INSERT_IMAGE": "Вметнете слика", + "INSERT_LINK": "Вметнете врска", + "INSERT_PARAGRAPH": "Вметни став", + "INSERT_TABLE": "Вметнете табела", + "INSERT_VIDEO": "Вметнете видео", + "Insert Your Message": "Вметнете ја вашата порака", + "KEY_COMBINATION": "Комбинација на клучеви", + "LINK": "Врска", + "LINK_NAME": "Име на врската", + "LINK_URL": "Врска URL", + "LOWERCASE": "мали букви", + "LOWER_ALPHA": "Долна Алфа", + "LOWER_ROMAN": "долен римски", + "MAX_ATTACH_MSG": "Достигнато е максималното ограничување на прилогот", + "MONTH": "Месец", + "NO_RESULTS": "Нема резултати", + "NUMBERED": "Нумерирани", + "OK": "добро", + "OPEN_IN_NEW_WINDOW": "Отвори во нов прозорец", + "PICK_GIF": "Изберете гиф", + "PICK_IMAGE": "Изберете слика", + "REDO_MSG": "Повторете ја последната команда", + "REMOVE": "Отстрани", + "RESET": "Ресетирање", + "REST_COLOR_MSG": "Ресетирање на стандардната боја", + "ROTATE_LEFT": "Ротирајте лево", + "ROTATE_RIGHT": "Ротирај десно", + "Reset": "Ресетирање", + "Rotate left": "Ротирајте лево", + "Rotate right": "Ротирај десно", + "SANS_SERIF": "Санс Сериф", + "SAVE": "Зачувај", + "SEARCH_GIF": "Пребарајте ги сите GIF-датотеки", + "SELECT_FROM_FILES": "Изберете од датотеките", + "SELECT_STL_MSG": "Изберете стил на листа", + "SENTENCE_CASE": "Случај на реченица", + "SET_COLOR": "Поставете боја", + "SHOW": "Прикажи", + "SWIPE_TO_REVEAL_SEMANTIC": "Повлечете лево за да откриете дејства", + "Select Emoji": "Изберете Емоџи", + "Slider Color": "Боја на лизгачот", + "Slider White Black Color": "Лизгач Бела Црна боја", + "Square": "Плоштад", + "TAB": "ТАБ", + "TEXT_TO_DISPLAY": "Текст за прикажување", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Случај за наслов", + "TO": "До", + "Text": "Текст", + "UNDO_MSG": "Вратете ја последната команда", + "UNKNOWN": "Непознато", + "UNTAB": "Untab", + "UPPERCASE": "ГОЛЕМИ букви", + "UPPER_ALPHA": "Горна Алфа", + "UPPER_ROMAN": "Горноримски", + "URL": "URL", + "URL_DUPLICATE_ER": "URL-то е веќе прикачено. Ве молиме внесете различна URL адреса", + "URL_INVALID_ER": "Внесете важечка URL-адреса", + "URL_MSG": "Ве молиме внесете URL!", + "VIDEO": "Видео", + "VIEW": "Прикажи", + "WEEK": "Недела" +} \ No newline at end of file diff --git a/lib/assets/strings/nb.json b/lib/assets/strings/nb.json new file mode 100644 index 0000000..98a056c --- /dev/null +++ b/lib/assets/strings/nb.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Handling", + "ADD_URL": "Legg til URL", + "ALL": "Alle", + "ATTACHED_FILE": "Vedlagt fil", + "ATTACH_MAXSIZE_VALIDATE": "Filstørrelsen må være mindre enn {0}", + "AUDIO_FILE_OR_URL_MSG": "Velg enten en lydfil eller skriv inn en lydfil-URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Vennligst skriv inn enten en lydfil eller en lyd-URL, ikke begge!", + "Blur": "Uklarhet", + "Blur Radius": "Uskarp radius", + "Brush": "Børste", + "CAMERA": "Kamera", + "CANCEL": "Avbryt", + "CHANGE_CASE": "Endre store og små bokstaver", + "CHANGE_IMAGE": "Endre bilde", + "CHAR_LEFT": "Tegn igjen {0}", + "CHOOSE_AUDIO": "Velg lyd", + "CHOOSE_COLOR": "Velg en farge", + "CHOOSE_FILE": "Velg Fil", + "CHOOSE_FILE_URL_MSG": "Velg enten en fil eller skriv inn en fil-URL!", + "CHOOSE_IMAGE": "Velg bilde", + "CHOOSE_IMG_URL_MSG": "Velg enten et bilde eller skriv inn en bilde-URL!", + "CHOOSE_VIDEO": "Velg video", + "CLEAR": "Tøm", + "CLOSE": "Lukk", + "COMPLETE_EDITING": "Fullfør redigering", + "COURIER": "kurer", + "CROP": "Beskjær", + "CUSTOM": "Egendefinert", + "Color Opacity": "Fargeopasitet", + "Crop": "Beskjær", + "DEFAULT": "Standard", + "DELETE": "Slett", + "DELETE_ATTACHMENT": "Slett vedlegg", + "DONE": "Ferdig", + "EDIT": "Rediger", + "EDITOR_CIRCLE": "Sirkel", + "EDITOR_CODE": "kode", + "EDITOR_DISC": "Plate", + "EDITOR_HEADER_1": "Overskrift 1", + "EDITOR_HEADER_2": "Overskrift 2", + "EDITOR_HEADER_3": "Overskrift 3", + "EDITOR_HEADER_4": "Overskrift 4", + "EDITOR_HEADER_5": "Overskrift 5", + "EDITOR_HEADER_6": "Overskrift 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "sitat", + "EDITOR_SQUARE": "Torget", + "EDITOR_TEXT": "Tekst", + "ENTER": "TAST INN", + "ESCAPE": "Flukt", + "Emoji": "Emoji", + "FILE": "Fil", + "FILE_COMPRESSING_ER": "Det oppstod en feil under komprimering av en fil", + "FILE_DUPLICATE_ER": "Filen er allerede vedlagt. Velg en annen fil", + "FILE_OR_URL_MSG": "Vennligst skriv inn enten en fil eller en fil-URL, ikke begge!", + "FILE_PIXEL_SIZE_ER": "Filpikselstørrelsen skal være mindre enn {0}", + "FILE_PROCESSING_ER": "Det oppstod en feil under behandling av en fil", + "FILE_SIZE_CALCULATING_ER": "Det oppstod en feil under beregning av filstørrelse", + "FILE_UNSUPPORTED": "Denne filtypen er ikke tillatt", + "FLIP": "Vend", + "FROM": "Fra", + "Filter": "Filtrer", + "Flip": "Vend", + "Freeform": "Fri form", + "GALLERY": "Galleri", + "GIF": "Gif", + "HELP": "Hjelp", + "HIDE": "Skjul", + "IMAGE_OR_URL_MSG": "Vennligst skriv inn enten et bilde eller en bilde-URL, ikke begge!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Bildevelger. Bilde er valgt", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Bildevelger. Ingen bilder er valgt", + "IMG_URL_MSG": "Vennligst skriv inn en bilde-URL!", + "INSERT_AUDIO": "Sett inn lyd", + "INSERT_FILE": "Sett inn fil", + "INSERT_IMAGE": "Sett inn bilde", + "INSERT_LINK": "Sett inn lenke", + "INSERT_PARAGRAPH": "Sett inn avsnitt", + "INSERT_TABLE": "Sett inn tabell", + "INSERT_VIDEO": "Sett inn video", + "Insert Your Message": "Sett inn meldingen din", + "KEY_COMBINATION": "Nøkkelkombinasjon", + "LINK": "Link", + "LINK_NAME": "Navn på lenke", + "LINK_URL": "URL-adresse for kobling", + "LOWERCASE": "små bokstaver", + "LOWER_ALPHA": "Nedre alfa", + "LOWER_ROMAN": "Nedre romersk", + "MAX_ATTACH_MSG": "Maksgrensen for vedlegg nådd", + "MONTH": "Måned", + "NO_RESULTS": "Ingen resultater", + "NUMBERED": "Nummerert", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Åpne i nytt vindu", + "PICK_GIF": "Velg en gif", + "PICK_IMAGE": "Velg bilde", + "REDO_MSG": "Gjenta den siste kommandoen", + "REMOVE": "Fjern", + "RESET": "Tilbakestill", + "REST_COLOR_MSG": "Tilbakestill til standardfarge", + "ROTATE_LEFT": "Roter mot venstre", + "ROTATE_RIGHT": "Roter mot høyre", + "Reset": "Tilbakestill", + "Rotate left": "Rotér mot venstre", + "Rotate right": "Roter til høyre", + "SANS_SERIF": "Sans serif", + "SAVE": "Lagre", + "SEARCH_GIF": "Søk i alle GIF-ene", + "SELECT_FROM_FILES": "Velg fra filer", + "SELECT_STL_MSG": "Velg listestil", + "SENTENCE_CASE": "Setningssak", + "SET_COLOR": "Sett farge", + "SHOW": "Vis", + "SWIPE_TO_REVEAL_SEMANTIC": "Sveip til venstre for å avsløre handlinger", + "Select Emoji": "Velg Emoji", + "Slider Color": "Slider Farge", + "Slider White Black Color": "Slider Hvit Svart Farge", + "Square": "Torget", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Tekst som skal vises", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Tittelsak", + "TO": "Til", + "Text": "Tekst", + "UNDO_MSG": "Angre den siste kommandoen", + "UNKNOWN": "Ukjent", + "UNTAB": "Fjern fanen", + "UPPERCASE": "STOR BOKSTAV", + "UPPER_ALPHA": "Øvre alfa", + "UPPER_ROMAN": "Øvre romersk", + "URL": "URL-adresse", + "URL_DUPLICATE_ER": "URL er allerede vedlagt. Vennligst skriv inn en annen URL", + "URL_INVALID_ER": "Vennligst skriv inn gyldig URL", + "URL_MSG": "Vennligst skriv inn en URL!", + "VIDEO": "Video", + "VIEW": "Vis", + "WEEK": "Uke" +} \ No newline at end of file diff --git a/lib/assets/strings/nl.json b/lib/assets/strings/nl.json new file mode 100644 index 0000000..9782ed9 --- /dev/null +++ b/lib/assets/strings/nl.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Actie", + "ADD_URL": "URL toevoegen", + "ALL": "Alle", + "ATTACHED_FILE": "Bijgevoegd bestand", + "ATTACH_MAXSIZE_VALIDATE": "Bestandsgrootte moet kleiner zijn dan {0}", + "AUDIO_FILE_OR_URL_MSG": "Kies een audiobestand of voer de URL van een audiobestand in!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Voer een audiobestand of een audio-URL in, niet beide!", + "Blur": "Vervagen", + "Blur Radius": "Straal vervagen", + "Brush": "Borstel", + "CAMERA": "Camera", + "CANCEL": "Annuleren", + "CHANGE_CASE": "Wissel van zaak", + "CHANGE_IMAGE": "Afbeelding wijzigen", + "CHAR_LEFT": "Karakters links {0}", + "CHOOSE_AUDIO": "Kies audio", + "CHOOSE_COLOR": "Kies een kleur", + "CHOOSE_FILE": "Kies bestand", + "CHOOSE_FILE_URL_MSG": "Kies een bestand of voer een bestands-URL in!", + "CHOOSE_IMAGE": "Kies afbeelding", + "CHOOSE_IMG_URL_MSG": "Kies een afbeelding of voer een afbeeldings-URL in!", + "CHOOSE_VIDEO": "Kies video", + "CLEAR": "Wissen", + "CLOSE": "Sluiten", + "COMPLETE_EDITING": "Volledige bewerking", + "COURIER": "Koerier", + "CROP": "Bijsnijden", + "CUSTOM": "Aangepast", + "Color Opacity": "Kleurdekking", + "Crop": "Bijsnijden", + "DEFAULT": "Standard", + "DELETE": "Verwijderen", + "DELETE_ATTACHMENT": "Bijlage verwijderen", + "DONE": "Gereed", + "EDIT": "Bewerken", + "EDITOR_CIRCLE": "Cirkel", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Schijf", + "EDITOR_HEADER_1": "Kop 1", + "EDITOR_HEADER_2": "Kop 2", + "EDITOR_HEADER_3": "Kop 3", + "EDITOR_HEADER_4": "Kop 4", + "EDITOR_HEADER_5": "Kop 5", + "EDITOR_HEADER_6": "Kop 6", + "EDITOR_NORMAL": "normaal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citaat", + "EDITOR_SQUARE": "Vierkant", + "EDITOR_TEXT": "Tekst", + "ENTER": "BINNENKOMEN", + "ESCAPE": "Ontsnappen", + "Emoji": "Emoji", + "FILE": "Bestand", + "FILE_COMPRESSING_ER": "Er is een fout opgetreden tijdens het comprimeren van een bestand", + "FILE_DUPLICATE_ER": "Bestand is al bijgevoegd. Kies een ander bestand", + "FILE_OR_URL_MSG": "Voer een bestand of een bestands-URL in, niet beide!", + "FILE_PIXEL_SIZE_ER": "Bestandspixelgrootte moet kleiner zijn dan {0}", + "FILE_PROCESSING_ER": "Er is een fout opgetreden tijdens het verwerken van een bestand", + "FILE_SIZE_CALCULATING_ER": "Er is een fout opgetreden bij het berekenen van een bestandsgrootte", + "FILE_UNSUPPORTED": "Dit bestandstype is niet toegestaan", + "FLIP": "Spiegelen", + "FROM": "Van", + "Filter": "Filter", + "Flip": "Spiegelen", + "Freeform": "Vrije vorm", + "GALLERY": "Galerij", + "GIF": "Gif", + "HELP": "Help", + "HIDE": "Verberg", + "IMAGE_OR_URL_MSG": "Voer een afbeelding of een afbeeldings-URL in, niet beide!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Afbeeldingskiezer. Afbeelding geselecteerd", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Afbeeldingskiezer. Geen afbeelding geselecteerd", + "IMG_URL_MSG": "Voer een afbeeldings-URL in!", + "INSERT_AUDIO": "Audio invoegen", + "INSERT_FILE": "Bestand invoegen", + "INSERT_IMAGE": "Voeg afbeelding in", + "INSERT_LINK": "Koppeling invoegen", + "INSERT_PARAGRAPH": "Alinea invoegen", + "INSERT_TABLE": "Tabel invoegen", + "INSERT_VIDEO": "Video invoegen", + "Insert Your Message": "Voeg uw bericht in", + "KEY_COMBINATION": "Sleutelcombinatie", + "LINK": "Koppeling", + "LINK_NAME": "Naam koppeling", + "LINK_URL": "Koppelings-URL", + "LOWERCASE": "kleine letters", + "LOWER_ALPHA": "Lagere alfa", + "LOWER_ROMAN": "Neder-Romeins", + "MAX_ATTACH_MSG": "Maximale limiet voor bijlagen bereikt", + "MONTH": "Maand", + "NO_RESULTS": "Geen resultaten", + "NUMBERED": "Genummerd", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Openen in een nieuw venster", + "PICK_GIF": "Kies een gif", + "PICK_IMAGE": "Kies afbeelding", + "REDO_MSG": "Voer de laatste opdracht opnieuw uit", + "REMOVE": "Verwijderen", + "RESET": "Opnieuw instellen", + "REST_COLOR_MSG": "Resetten naar standaardkleur", + "ROTATE_LEFT": "Linksom draaien", + "ROTATE_RIGHT": "Rechtsom draaien", + "Reset": "Opnieuw instellen", + "Rotate left": "Draai naar links", + "Rotate right": "Draai naar rechts", + "SANS_SERIF": "Sans Serif", + "SAVE": "Opslaan", + "SEARCH_GIF": "Doorzoek alle GIF's", + "SELECT_FROM_FILES": "Selecteer uit bestanden", + "SELECT_STL_MSG": "Selecteer lijststijl", + "SENTENCE_CASE": "Zin zaak", + "SET_COLOR": "Kleur instellen", + "SHOW": "Weergeven", + "SWIPE_TO_REVEAL_SEMANTIC": "Veeg naar links om acties weer te geven", + "Select Emoji": "Selecteer Emoji", + "Slider Color": "Schuifregelaar kleur", + "Slider White Black Color": "Schuifregelaar Wit Zwart Kleur", + "Square": "Vierkant", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Tekst om weer te geven", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Hoofdzaak", + "TO": "Tot", + "Text": "Tekst", + "UNDO_MSG": "Maak de laatste opdracht ongedaan", + "UNKNOWN": "Onbekend", + "UNTAB": "Onttab", + "UPPERCASE": "HOOFDLETTERS", + "UPPER_ALPHA": "Bovenste alfa", + "UPPER_ROMAN": "Opper-Romeins", + "URL": "URL", + "URL_DUPLICATE_ER": "URL is al bijgevoegd. Voer een andere URL in", + "URL_INVALID_ER": "Voer een geldige URL in", + "URL_MSG": "Voer een URL in!", + "VIDEO": "Video", + "VIEW": "Weergeven", + "WEEK": "Week" +} \ No newline at end of file diff --git a/lib/assets/strings/pl.json b/lib/assets/strings/pl.json new file mode 100644 index 0000000..94908ba --- /dev/null +++ b/lib/assets/strings/pl.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Działanie", + "ADD_URL": "Dodaj URL", + "ALL": "Wszystkie", + "ATTACHED_FILE": "Załączony plik", + "ATTACH_MAXSIZE_VALIDATE": "Rozmiar pliku musi być mniejszy niż {0}", + "AUDIO_FILE_OR_URL_MSG": "Wybierz plik audio lub wprowadź adres URL pliku audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Wprowadź plik audio lub adres URL audio, a nie jedno i drugie!", + "Blur": "Plama", + "Blur Radius": "Promień rozmycia", + "Brush": "Szczotka", + "CAMERA": "Aparat", + "CANCEL": "Anuluj", + "CHANGE_CASE": "Zmień przypadek", + "CHANGE_IMAGE": "Zmień obraz", + "CHAR_LEFT": "Pozostało znaków {0}", + "CHOOSE_AUDIO": "Wybierz dźwięk", + "CHOOSE_COLOR": "Wybierz kolor", + "CHOOSE_FILE": "Wybierz plik", + "CHOOSE_FILE_URL_MSG": "Wybierz plik lub wprowadź adres URL pliku!", + "CHOOSE_IMAGE": "Wybierz obraz", + "CHOOSE_IMG_URL_MSG": "Wybierz obraz lub wprowadź adres URL obrazu!", + "CHOOSE_VIDEO": "Wybierz wideo", + "CLEAR": "Usuń", + "CLOSE": "Zamknij", + "COMPLETE_EDITING": "Pełna edycja", + "COURIER": "Kurier", + "CROP": "Przytnij", + "CUSTOM": "Niestandardowe", + "Color Opacity": "Nieprzezroczystość koloru", + "Crop": "Przytnij", + "DEFAULT": "Domyślny", + "DELETE": "Usuń", + "DELETE_ATTACHMENT": "Usuń załącznik", + "DONE": "Gotowe", + "EDIT": "Edytuj", + "EDITOR_CIRCLE": "Okrąg", + "EDITOR_CODE": "kod", + "EDITOR_DISC": "Dysk", + "EDITOR_HEADER_1": "Nagłówek 1", + "EDITOR_HEADER_2": "Nagłówek 2", + "EDITOR_HEADER_3": "Nagłówek 3", + "EDITOR_HEADER_4": "Nagłówek 4", + "EDITOR_HEADER_5": "Nagłówek 5", + "EDITOR_HEADER_6": "Nagłówek 6", + "EDITOR_NORMAL": "normalna", + "EDITOR_PT": "pkt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "cytat", + "EDITOR_SQUARE": "Kwadrat", + "EDITOR_TEXT": "Tekst", + "ENTER": "WCHODZIĆ", + "ESCAPE": "Ucieczka", + "Emoji": "Emoji", + "FILE": "Plik", + "FILE_COMPRESSING_ER": "Wystąpił błąd podczas kompresowania pliku", + "FILE_DUPLICATE_ER": "Plik jest już dołączony. Wybierz inny plik", + "FILE_OR_URL_MSG": "Wprowadź plik lub adres URL pliku, a nie jedno i drugie!", + "FILE_PIXEL_SIZE_ER": "Rozmiar pliku w pikselach powinien być mniejszy niż {0}", + "FILE_PROCESSING_ER": "Wystąpił błąd podczas przetwarzania pliku", + "FILE_SIZE_CALCULATING_ER": "Wystąpił błąd podczas obliczania rozmiaru pliku", + "FILE_UNSUPPORTED": "Ten typ pliku jest niedozwolony", + "FLIP": "Trzepnięcie", + "FROM": "Od", + "Filter": "Filtr", + "Flip": "Trzepnięcie", + "Freeform": "Dowolna forma", + "GALLERY": "Galeria", + "GIF": "Gif", + "HELP": "Pomoc", + "HIDE": "Ukryj", + "IMAGE_OR_URL_MSG": "Wprowadź obraz lub adres URL obrazu, a nie jedno i drugie!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selektor obrazów. Wybrano obraz", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selektor obrazów. Nie wybrano obrazu", + "IMG_URL_MSG": "Wprowadź adres URL obrazu!", + "INSERT_AUDIO": "Wstaw dźwięk", + "INSERT_FILE": "Wstaw plik", + "INSERT_IMAGE": "Umieść obraz", + "INSERT_LINK": "Wstaw łącze", + "INSERT_PARAGRAPH": "Wstaw akapit", + "INSERT_TABLE": "Wypełnij tabelę", + "INSERT_VIDEO": "Wstaw wideo", + "Insert Your Message": "Wstaw swoją wiadomość", + "KEY_COMBINATION": "Kombinacja klawiszy", + "LINK": "Połączyć", + "LINK_NAME": "Nazwa linku", + "LINK_URL": "Link URL", + "LOWERCASE": "małe litery", + "LOWER_ALPHA": "Dolna Alfa", + "LOWER_ROMAN": "dolny rzymski", + "MAX_ATTACH_MSG": "Osiągnięto maksymalny limit załączników", + "MONTH": "Miesiąc", + "NO_RESULTS": "Brak wyników", + "NUMBERED": "Numerowane", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Otworzyć w nowym oknie", + "PICK_GIF": "Wybierz GIF-a", + "PICK_IMAGE": "Wybierz obraz", + "REDO_MSG": "Powtórz ostatnie polecenie", + "REMOVE": "Usuń", + "RESET": "Resetuj", + "REST_COLOR_MSG": "Zresetuj do domyślnego koloru", + "ROTATE_LEFT": "Obrót w lewo", + "ROTATE_RIGHT": "Obróć w prawo", + "Reset": "Resetuj", + "Rotate left": "Obrót w lewo", + "Rotate right": "Obróć w prawo", + "SANS_SERIF": "Bezszeryfowe", + "SAVE": "Zapisz", + "SEARCH_GIF": "Przeszukaj wszystkie GIF-y", + "SELECT_FROM_FILES": "Wybierz z plików", + "SELECT_STL_MSG": "Wybierz styl listy", + "SENTENCE_CASE": "Sprawa zdania", + "SET_COLOR": "Ustaw kolor", + "SHOW": "Pokaż", + "SWIPE_TO_REVEAL_SEMANTIC": "Przesuń w lewo, aby wyświetlić działania", + "Select Emoji": "Wybierz emotikony", + "Slider Color": "Kolor suwaka", + "Slider White Black Color": "Suwak biały czarny kolor", + "Square": "Kwadrat", + "TAB": "PATKA", + "TEXT_TO_DISPLAY": "Tekst do wyświetlenia", + "TIMES_NEW_ROMAN": "Czcionka Times New Roman", + "TITLE_CASE": "Tytuł sprawy", + "TO": "Do", + "Text": "Tekst", + "UNDO_MSG": "Cofnij ostatnie polecenie", + "UNKNOWN": "Nieznany", + "UNTAB": "Odblokuj", + "UPPERCASE": "DUŻE LITERY", + "UPPER_ALPHA": "Górna Alfa", + "UPPER_ROMAN": "Górny rzymski", + "URL": "URL", + "URL_DUPLICATE_ER": "Adres URL jest już dołączony. Wprowadź inny adres URL", + "URL_INVALID_ER": "Wprowadź prawidłowy adres URL", + "URL_MSG": "Wprowadź adres URL!", + "VIDEO": "Wideo", + "VIEW": "Widok", + "WEEK": "Tydzień" +} \ No newline at end of file diff --git a/lib/assets/strings/pt.json b/lib/assets/strings/pt.json new file mode 100644 index 0000000..a729037 --- /dev/null +++ b/lib/assets/strings/pt.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Ação", + "ADD_URL": "Adicionar URL", + "ALL": "Tudo", + "ATTACHED_FILE": "Arquivo anexo", + "ATTACH_MAXSIZE_VALIDATE": "O tamanho do arquivo deve ser menor que {0}", + "AUDIO_FILE_OR_URL_MSG": "Por favor, escolha um arquivo de áudio ou insira um URL de arquivo de áudio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Insira um arquivo de áudio ou um URL de áudio, não ambos!", + "Blur": "Borrão", + "Blur Radius": "Raio do desfoque", + "Brush": "Escovar", + "CAMERA": "Câmara", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Alterar maiúsculas e minúsculas", + "CHANGE_IMAGE": "Alterar imagem", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "Escolha o áudio", + "CHOOSE_COLOR": "Escolha uma cor", + "CHOOSE_FILE": "Escolher arquivo", + "CHOOSE_FILE_URL_MSG": "Por favor, escolha um arquivo ou digite um URL de arquivo!", + "CHOOSE_IMAGE": "Escolha a imagem", + "CHOOSE_IMG_URL_MSG": "Por favor, escolha uma imagem ou digite um URL de imagem!", + "CHOOSE_VIDEO": "Escolha o vídeo", + "CLEAR": "Limpar", + "CLOSE": "Fechar", + "COMPLETE_EDITING": "Edição completa", + "COURIER": "Correio", + "CROP": "Recortar", + "CUSTOM": "Personalizado", + "Color Opacity": "Opacidade de cor", + "Crop": "Recortar", + "DEFAULT": "Predefinição", + "DELETE": "Eliminar", + "DELETE_ATTACHMENT": "Excluir anexo", + "DONE": "Concluído", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Círculo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Disco", + "EDITOR_HEADER_1": "Cabeçalho 1", + "EDITOR_HEADER_2": "Cabeçalho 2", + "EDITOR_HEADER_3": "Cabeçalho 3", + "EDITOR_HEADER_4": "Cabeçalho 4", + "EDITOR_HEADER_5": "Cabeçalho 5", + "EDITOR_HEADER_6": "Cabeçalho 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citar", + "EDITOR_SQUARE": "Quadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "DIGITAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Ficheiro", + "FILE_COMPRESSING_ER": "Ocorreu um erro ao compactar um arquivo", + "FILE_DUPLICATE_ER": "O arquivo já está anexado. Escolha um arquivo diferente", + "FILE_OR_URL_MSG": "Insira um arquivo ou um URL de arquivo, não ambos!", + "FILE_PIXEL_SIZE_ER": "O tamanho do pixel do arquivo deve ser menor que {0}", + "FILE_PROCESSING_ER": "Ocorreu um erro ao processar um arquivo", + "FILE_SIZE_CALCULATING_ER": "Ocorreu um erro ao calcular o tamanho do arquivo", + "FILE_UNSUPPORTED": "Este tipo de arquivo não é permitido", + "FLIP": "Virar", + "FROM": "De", + "Filter": "Filtro", + "Flip": "Virar", + "Freeform": "Forma livre", + "GALLERY": "Galeria", + "GIF": "Gif", + "HELP": "Ajuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Insira uma imagem ou um URL de imagem, não ambos!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selecionador de imagens. Imagem selecionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selecionador de imagens. Nenhuma imagem selecionada", + "IMG_URL_MSG": "Insira um URL de imagem!", + "INSERT_AUDIO": "Inserir Áudio", + "INSERT_FILE": "Inserir arquivo", + "INSERT_IMAGE": "Inserir Imagem", + "INSERT_LINK": "Inserir link", + "INSERT_PARAGRAPH": "Inserir parágrafo", + "INSERT_TABLE": "Insira a tabela", + "INSERT_VIDEO": "Inserir vídeo", + "Insert Your Message": "Insira sua mensagem", + "KEY_COMBINATION": "combinação de teclas", + "LINK": "Link", + "LINK_NAME": "Nome da ligação", + "LINK_URL": "URL da ligação", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "Alfa inferior", + "LOWER_ROMAN": "baixo romano", + "MAX_ATTACH_MSG": "Limite máximo de anexos atingido", + "MONTH": "Mês", + "NO_RESULTS": "Sem resultados", + "NUMBERED": "Numerado", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Abrir em nova janela", + "PICK_GIF": "Escolha um GIF", + "PICK_IMAGE": "Escolher imagem", + "REDO_MSG": "Refazer o último comando", + "REMOVE": "Remover", + "RESET": "Reinicializar", + "REST_COLOR_MSG": "Redefinir para a cor padrão", + "ROTATE_LEFT": "Vire à esquerda", + "ROTATE_RIGHT": "Vire à direita", + "Reset": "Reinicializar", + "Rotate left": "Vire à esquerda", + "Rotate right": "Vire à direita", + "SANS_SERIF": "Sem serifa", + "SAVE": "Guardar", + "SEARCH_GIF": "Pesquise todos os GIFs", + "SELECT_FROM_FILES": "Selecione a partir de arquivos", + "SELECT_STL_MSG": "Selecione o estilo da lista", + "SENTENCE_CASE": "Caso de sentença", + "SET_COLOR": "Definir cor", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Deslize para a esquerda para revelar ações", + "Select Emoji": "Selecione Emoji", + "Slider Color": "Cor do controle deslizante", + "Slider White Black Color": "Controle deslizante Branco Preto Cor", + "Square": "Quadrado", + "TAB": "ABA", + "TEXT_TO_DISPLAY": "Texto a ser exibido", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Título", + "TO": "Até", + "Text": "Texto", + "UNDO_MSG": "Desfazer o último comando", + "UNKNOWN": "Desconhecido", + "UNTAB": "Untab", + "UPPERCASE": "MAIÚSCULAS", + "UPPER_ALPHA": "Alfa superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "A URL já está anexada. Insira um URL diferente", + "URL_INVALID_ER": "Insira um URL válido", + "URL_MSG": "Insira um URL!", + "VIDEO": "Vídeo", + "VIEW": "Ver", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/pt_BR.json b/lib/assets/strings/pt_BR.json new file mode 100644 index 0000000..0e3ad7c --- /dev/null +++ b/lib/assets/strings/pt_BR.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Ação", + "ADD_URL": "Adicionar URL", + "ALL": "Todos", + "ATTACHED_FILE": "Arquivo anexo", + "ATTACH_MAXSIZE_VALIDATE": "O tamanho do arquivo deve ser menor que {0}", + "AUDIO_FILE_OR_URL_MSG": "Por favor, escolha um arquivo de áudio ou insira um URL de arquivo de áudio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Insira um arquivo de áudio ou um URL de áudio, não ambos!", + "Blur": "Borrão", + "Blur Radius": "Raio do desfoque", + "Brush": "Escovar", + "CAMERA": "Câmera", + "CANCEL": "Cancelar", + "CHANGE_CASE": "Alterar maiúsculas e minúsculas", + "CHANGE_IMAGE": "Alterar imagem", + "CHAR_LEFT": "Caracteres restantes {0}", + "CHOOSE_AUDIO": "Escolha o áudio", + "CHOOSE_COLOR": "Escolha uma cor", + "CHOOSE_FILE": "Escolher arquivo", + "CHOOSE_FILE_URL_MSG": "Por favor, escolha um arquivo ou digite um URL de arquivo!", + "CHOOSE_IMAGE": "Escolha a imagem", + "CHOOSE_IMG_URL_MSG": "Por favor, escolha uma imagem ou digite um URL de imagem!", + "CHOOSE_VIDEO": "Escolha o vídeo", + "CLEAR": "Apagar", + "CLOSE": "Fechar", + "COMPLETE_EDITING": "Edição completa", + "COURIER": "Correio", + "CROP": "Cortar", + "CUSTOM": "Personalizado", + "Color Opacity": "Opacidade de cor", + "Crop": "Cortar", + "DEFAULT": "Padrão", + "DELETE": "Excluir", + "DELETE_ATTACHMENT": "Excluir anexo", + "DONE": "Concluído", + "EDIT": "Editar", + "EDITOR_CIRCLE": "Círculo", + "EDITOR_CODE": "código", + "EDITOR_DISC": "Disco", + "EDITOR_HEADER_1": "Cabeçalho 1", + "EDITOR_HEADER_2": "Cabeçalho 2", + "EDITOR_HEADER_3": "Cabeçalho 3", + "EDITOR_HEADER_4": "Cabeçalho 4", + "EDITOR_HEADER_5": "Cabeçalho 5", + "EDITOR_HEADER_6": "Cabeçalho 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citar", + "EDITOR_SQUARE": "Quadrado", + "EDITOR_TEXT": "Texto", + "ENTER": "DIGITAR", + "ESCAPE": "Escapar", + "Emoji": "Emoji", + "FILE": "Arquivo", + "FILE_COMPRESSING_ER": "Ocorreu um erro ao compactar um arquivo", + "FILE_DUPLICATE_ER": "O arquivo já está anexado. Escolha um arquivo diferente", + "FILE_OR_URL_MSG": "Insira um arquivo ou um URL de arquivo, não ambos!", + "FILE_PIXEL_SIZE_ER": "O tamanho do pixel do arquivo deve ser menor que {0}", + "FILE_PROCESSING_ER": "Ocorreu um erro ao processar um arquivo", + "FILE_SIZE_CALCULATING_ER": "Ocorreu um erro ao calcular um tamanho de arquivo", + "FILE_UNSUPPORTED": "Este tipo de arquivo não é permitido", + "FLIP": "Flip", + "FROM": "De", + "Filter": "Filtro", + "Flip": "Flip", + "Freeform": "Forma livre", + "GALLERY": "Galeria", + "GIF": "Gif", + "HELP": "Ajuda", + "HIDE": "Ocultar", + "IMAGE_OR_URL_MSG": "Insira uma imagem ou um URL de imagem, não ambos!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selecionador de imagens. Imagem selecionada", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selecionador de imagens. Nenhuma imagem selecionada", + "IMG_URL_MSG": "Insira um URL de imagem!", + "INSERT_AUDIO": "Inserir Áudio", + "INSERT_FILE": "Inserir arquivo", + "INSERT_IMAGE": "Inserir Imagem", + "INSERT_LINK": "Inserir link", + "INSERT_PARAGRAPH": "Inserir parágrafo", + "INSERT_TABLE": "Insira a tabela", + "INSERT_VIDEO": "Inserir vídeo", + "Insert Your Message": "Insira sua mensagem", + "KEY_COMBINATION": "combinação de teclas", + "LINK": "Link", + "LINK_NAME": "Nome do link", + "LINK_URL": "URL do link", + "LOWERCASE": "minúsculas", + "LOWER_ALPHA": "Alfa inferior", + "LOWER_ROMAN": "baixo romano", + "MAX_ATTACH_MSG": "Limite máximo de anexos atingido", + "MONTH": "Mês", + "NO_RESULTS": "Sem resultados", + "NUMBERED": "Numerado", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Abrir em nova janela", + "PICK_GIF": "Escolha um GIF", + "PICK_IMAGE": "Escolha a imagem", + "REDO_MSG": "Refazer o último comando", + "REMOVE": "Remover", + "RESET": "Redefinir", + "REST_COLOR_MSG": "Redefinir para a cor padrão", + "ROTATE_LEFT": "Girar para a esquerda", + "ROTATE_RIGHT": "Girar para a direita", + "Reset": "Redefinir", + "Rotate left": "Vire à esquerda", + "Rotate right": "Vire à direita", + "SANS_SERIF": "Sem serifa", + "SAVE": "Salvar", + "SEARCH_GIF": "Pesquise todos os GIFs", + "SELECT_FROM_FILES": "Selecione a partir de arquivos", + "SELECT_STL_MSG": "Selecione o estilo da lista", + "SENTENCE_CASE": "Caso de sentença", + "SET_COLOR": "Definir cor", + "SHOW": "Mostrar", + "SWIPE_TO_REVEAL_SEMANTIC": "Deslize para a esquerda para revelar ações", + "Select Emoji": "Selecione Emoji", + "Slider Color": "Cor do controle deslizante", + "Slider White Black Color": "Controle deslizante Branco Preto Cor", + "Square": "Quadrado", + "TAB": "ABA", + "TEXT_TO_DISPLAY": "Texto a ser exibido", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Título", + "TO": "Para", + "Text": "Texto", + "UNDO_MSG": "Desfazer o último comando", + "UNKNOWN": "Desconhecido", + "UNTAB": "Untab", + "UPPERCASE": "MAIÚSCULAS", + "UPPER_ALPHA": "Alfa Superior", + "UPPER_ROMAN": "romano superior", + "URL": "URL", + "URL_DUPLICATE_ER": "A URL já está anexada. Insira um URL diferente", + "URL_INVALID_ER": "Insira um URL válido", + "URL_MSG": "Insira um URL!", + "VIDEO": "Vídeo", + "VIEW": "Visualizar", + "WEEK": "Semana" +} \ No newline at end of file diff --git a/lib/assets/strings/ro.json b/lib/assets/strings/ro.json new file mode 100644 index 0000000..1def6b4 --- /dev/null +++ b/lib/assets/strings/ro.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Acțiune", + "ADD_URL": "Adaugă URL", + "ALL": "Toate", + "ATTACHED_FILE": "Fisier atasat", + "ATTACH_MAXSIZE_VALIDATE": "Dimensiunea fișierului trebuie să fie mai mică de {0}", + "AUDIO_FILE_OR_URL_MSG": "Vă rugăm fie să alegeți un fișier audio, fie să introduceți adresa URL a unui fișier audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Introduceți fie un fișier audio, fie o adresă URL audio, nu ambele!", + "Blur": "Estompa", + "Blur Radius": "Raza de estompare", + "Brush": "Perie", + "CAMERA": "aparat foto", + "CANCEL": "Anulare", + "CHANGE_CASE": "Schimbă cazul", + "CHANGE_IMAGE": "Schimbați imaginea", + "CHAR_LEFT": "Caractere rămase {0}", + "CHOOSE_AUDIO": "Alegeți audio", + "CHOOSE_COLOR": "Alegeți o culoare", + "CHOOSE_FILE": "Alege fișierul", + "CHOOSE_FILE_URL_MSG": "Vă rugăm fie să alegeți un fișier, fie să introduceți adresa URL a fișierului!", + "CHOOSE_IMAGE": "Alege imaginea", + "CHOOSE_IMG_URL_MSG": "Vă rugăm fie să alegeți o imagine, fie să introduceți adresa URL a unei imagini!", + "CHOOSE_VIDEO": "Alegeți videoclipul", + "CLEAR": "clar", + "CLOSE": "Închide", + "COMPLETE_EDITING": "Editare completă", + "COURIER": "Curier", + "CROP": "A decupa", + "CUSTOM": "Personalizat", + "Color Opacity": "Opacitatea culorii", + "Crop": "A decupa", + "DEFAULT": "Mod implicit", + "DELETE": "Șterge", + "DELETE_ATTACHMENT": "Șterge atașamentul", + "DONE": "Terminat", + "EDIT": "Editați | ×", + "EDITOR_CIRCLE": "Cerc", + "EDITOR_CODE": "cod", + "EDITOR_DISC": "Disc", + "EDITOR_HEADER_1": "Antetul 1", + "EDITOR_HEADER_2": "Antetul 2", + "EDITOR_HEADER_3": "Antetul 3", + "EDITOR_HEADER_4": "Antetul 4", + "EDITOR_HEADER_5": "Antetul 5", + "EDITOR_HEADER_6": "Antetul 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pct", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citat", + "EDITOR_SQUARE": "Pătrat", + "EDITOR_TEXT": "Text", + "ENTER": "INTRODUCE", + "ESCAPE": "Evadare", + "Emoji": "Emoji", + "FILE": "Fişier", + "FILE_COMPRESSING_ER": "A apărut o eroare la comprimarea unui fișier", + "FILE_DUPLICATE_ER": "Fișierul este deja atașat. Vă rugăm să alegeți un fișier diferit", + "FILE_OR_URL_MSG": "Introduceți fie un fișier, fie o adresă URL a fișierului, nu ambele!", + "FILE_PIXEL_SIZE_ER": "Dimensiunea pixelilor fișierului trebuie să fie mai mică de {0}", + "FILE_PROCESSING_ER": "A apărut o eroare la procesarea unui fișier", + "FILE_SIZE_CALCULATING_ER": "A apărut o eroare la calcularea dimensiunii unui fișier", + "FILE_UNSUPPORTED": "Acest tip de fișier nu este permis", + "FLIP": "Flip", + "FROM": "Din", + "Filter": "Filtru", + "Flip": "Flip", + "Freeform": "Liber de la", + "GALLERY": "Galerie", + "GIF": "Gif", + "HELP": "Ajutor", + "HIDE": "Ascunde", + "IMAGE_OR_URL_MSG": "Introduceți fie o imagine, fie o adresă URL a imaginii, nu ambele!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Selector de imagini. Imaginea selectată", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Selector de imagini. Nicio imagine selectată", + "IMG_URL_MSG": "Vă rugăm să introduceți adresa URL a unei imagini!", + "INSERT_AUDIO": "Inserați audio", + "INSERT_FILE": "Inserați fișier", + "INSERT_IMAGE": "Inserați imaginea", + "INSERT_LINK": "Inserați linkul", + "INSERT_PARAGRAPH": "Inserați Paragraf", + "INSERT_TABLE": "Inserați tabel", + "INSERT_VIDEO": "Inserați videoclipul", + "Insert Your Message": "Introduceți mesajul dvs", + "KEY_COMBINATION": "Combinație de taste", + "LINK": "Legătură", + "LINK_NAME": "Numele legăturii", + "LINK_URL": "URL-ul linkului", + "LOWERCASE": "litere mici", + "LOWER_ALPHA": "Alfa inferioară", + "LOWER_ROMAN": "Romanul de jos", + "MAX_ATTACH_MSG": "Limita maximă a atașamentului a fost atinsă", + "MONTH": "Lună", + "NO_RESULTS": "Fara rezultate", + "NUMBERED": "Numerotat", + "OK": "Bine", + "OPEN_IN_NEW_WINDOW": "Deschide într-o fereastră nouă", + "PICK_GIF": "Alegeți un Gif", + "PICK_IMAGE": "Alegeți imaginea", + "REDO_MSG": "Repet ultima comandă", + "REMOVE": "Elimina", + "RESET": "restabili", + "REST_COLOR_MSG": "Resetați la culoarea implicită", + "ROTATE_LEFT": "Roteste la stanga", + "ROTATE_RIGHT": "Invarte spre dreapta", + "Reset": "restabili", + "Rotate left": "Roteste la stanga", + "Rotate right": "Invarte spre dreapta", + "SANS_SERIF": "Sans Serif", + "SAVE": "Salvați", + "SEARCH_GIF": "Căutați toate GIF-urile", + "SELECT_FROM_FILES": "Selectați din fișiere", + "SELECT_STL_MSG": "Selectați stilul listei", + "SENTENCE_CASE": "Cazul sentinței", + "SET_COLOR": "Setați culoarea", + "SHOW": "Spectacol", + "SWIPE_TO_REVEAL_SEMANTIC": "Glisați spre stânga pentru a dezvălui acțiuni", + "Select Emoji": "Selectați Emoji", + "Slider Color": "Culoare cursor", + "Slider White Black Color": "Slider Alb Negru Culoare", + "Square": "Pătrat", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text de afișat", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Cazul de titlu", + "TO": "La", + "Text": "Text", + "UNDO_MSG": "Anulați ultima comandă", + "UNKNOWN": "Necunoscut", + "UNTAB": "Defilați", + "UPPERCASE": "MAJUSCULE", + "UPPER_ALPHA": "Alfa Superioară", + "UPPER_ROMAN": "Romanul de Sus", + "URL": "URL-", + "URL_DUPLICATE_ER": "Adresa URL este deja atașată. Vă rugăm să introduceți o adresă URL diferită", + "URL_INVALID_ER": "Vă rugăm să introduceți o adresă URL validă", + "URL_MSG": "Vă rugăm să introduceți o adresă URL!", + "VIDEO": "Video", + "VIEW": "Vedere", + "WEEK": "Săptămână" +} \ No newline at end of file diff --git a/lib/assets/strings/ru.json b/lib/assets/strings/ru.json new file mode 100644 index 0000000..f9369f9 --- /dev/null +++ b/lib/assets/strings/ru.json @@ -0,0 +1,140 @@ +{ + "ACTION": "действие", + "ADD_URL": "Добавить сайт", + "ALL": "Все", + "ATTACHED_FILE": "Прикрепленный файл", + "ATTACH_MAXSIZE_VALIDATE": "Размер файла должен быть меньше {0}", + "AUDIO_FILE_OR_URL_MSG": "Пожалуйста, выберите аудиофайл или введите URL-адрес аудиофайла!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Пожалуйста, введите либо аудиофайл, либо URL-адрес аудио, но не то и другое одновременно!", + "Blur": "Размытие", + "Blur Radius": "Радиус размытия", + "Brush": "Щетка", + "CAMERA": "камера", + "CANCEL": "отменить", + "CHANGE_CASE": "Изменить регистр", + "CHANGE_IMAGE": "Изменить изображение", + "CHAR_LEFT": "Осталось {0} символов", + "CHOOSE_AUDIO": "Выбрать аудио", + "CHOOSE_COLOR": "Выберите цвет", + "CHOOSE_FILE": "Выберите файл", + "CHOOSE_FILE_URL_MSG": "Пожалуйста, выберите файл или введите URL-адрес файла!", + "CHOOSE_IMAGE": "Выбрать изображение", + "CHOOSE_IMG_URL_MSG": "Пожалуйста, выберите изображение или введите URL-адрес изображения!", + "CHOOSE_VIDEO": "Выбрать видео", + "CLEAR": "Прозрачный", + "CLOSE": "близко", + "COMPLETE_EDITING": "Полное редактирование", + "COURIER": "Курьер", + "CROP": "урожай", + "CUSTOM": "изготовленный на заказ", + "Color Opacity": "Непрозрачность цвета", + "Crop": "урожай", + "DEFAULT": "По умолчанию", + "DELETE": "удалять", + "DELETE_ATTACHMENT": "Удалить вложение", + "DONE": "Готово", + "EDIT": "редактировать", + "EDITOR_CIRCLE": "Круг", + "EDITOR_CODE": "код", + "EDITOR_DISC": "Диск", + "EDITOR_HEADER_1": "Заголовок 1", + "EDITOR_HEADER_2": "Заголовок 2", + "EDITOR_HEADER_3": "Заголовок 3", + "EDITOR_HEADER_4": "Заголовок 4", + "EDITOR_HEADER_5": "Заголовок 5", + "EDITOR_HEADER_6": "Заголовок 6", + "EDITOR_NORMAL": "нормальный", + "EDITOR_PT": "пт", + "EDITOR_PX": "ПВ", + "EDITOR_QUOTE": "цитировать", + "EDITOR_SQUARE": "Квадрат", + "EDITOR_TEXT": "Текст", + "ENTER": "ВХОДИТЬ", + "ESCAPE": "Побег", + "Emoji": "Emoji", + "FILE": "файл", + "FILE_COMPRESSING_ER": "Произошла ошибка при сжатии файла", + "FILE_DUPLICATE_ER": "Файл уже прикреплен. Пожалуйста, выберите другой файл", + "FILE_OR_URL_MSG": "Пожалуйста, введите либо файл, либо URL-адрес файла, но не оба!", + "FILE_PIXEL_SIZE_ER": "Размер файла в пикселях должен быть меньше {0}.", + "FILE_PROCESSING_ER": "Произошла ошибка при обработке файла", + "FILE_SIZE_CALCULATING_ER": "Произошла ошибка при вычислении размера файла", + "FILE_UNSUPPORTED": "Этот тип файла не разрешен", + "FLIP": "Подбросить", + "FROM": "От", + "Filter": "Фильтр", + "Flip": "Подбросить", + "Freeform": "Свободная форма", + "GALLERY": "Галерея", + "GIF": "гифка", + "HELP": "Помогите", + "HIDE": "Спрятать", + "IMAGE_OR_URL_MSG": "Пожалуйста, введите изображение или URL-адрес изображения, но не оба!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Средство выбора изображений. Изображение выбрано", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Средство выбора изображений. Изображение не выбрано", + "IMG_URL_MSG": "Пожалуйста, введите URL изображения!", + "INSERT_AUDIO": "Вставить аудио", + "INSERT_FILE": "Вставить файл", + "INSERT_IMAGE": "Вставить изображение", + "INSERT_LINK": "Вставить ссылку", + "INSERT_PARAGRAPH": "Вставить абзац", + "INSERT_TABLE": "Вставить таблицу", + "INSERT_VIDEO": "Вставить видео", + "Insert Your Message": "Вставьте ваше сообщение", + "KEY_COMBINATION": "Комбинация клавиш", + "LINK": "Связь", + "LINK_NAME": "Имя ссылки", + "LINK_URL": "URL ссылки", + "LOWERCASE": "нижний регистр", + "LOWER_ALPHA": "Нижняя Альфа", + "LOWER_ROMAN": "Нижний римский", + "MAX_ATTACH_MSG": "Достигнуто максимальное количество прикрепленных файлов", + "MONTH": "Месяц", + "NO_RESULTS": "Без результатов", + "NUMBERED": "Пронумерованный", + "OK": "Ok", + "OPEN_IN_NEW_WINDOW": "Открыть в новом окне", + "PICK_GIF": "Выберите гифку", + "PICK_IMAGE": "Выбрать изображение", + "REDO_MSG": "Повторить последнюю команду", + "REMOVE": "Удалить", + "RESET": "Сброс", + "REST_COLOR_MSG": "Сбросить цвет по умолчанию", + "ROTATE_LEFT": "Повернуть налево", + "ROTATE_RIGHT": "Повернуть вправо", + "Reset": "Сброс", + "Rotate left": "Поверните влево", + "Rotate right": "Повернуть вправо", + "SANS_SERIF": "Без засечек", + "SAVE": "Сохранить", + "SEARCH_GIF": "Поиск по всем GIF", + "SELECT_FROM_FILES": "Выбрать из файлов", + "SELECT_STL_MSG": "Выберите стиль списка", + "SENTENCE_CASE": "Случай приговора", + "SET_COLOR": "Установить цвет", + "SHOW": "Шоу", + "SWIPE_TO_REVEAL_SEMANTIC": "Проведите влево, чтобы открыть действия", + "Select Emoji": "Выберите эмодзи", + "Slider Color": "Цвет слайдера", + "Slider White Black Color": "Слайдер Белый Черный Цвет", + "Square": "Квадрат", + "TAB": "Вкладка", + "TEXT_TO_DISPLAY": "Текст для отображения", + "TIMES_NEW_ROMAN": "Таймс Нью Роман", + "TITLE_CASE": "Название дела", + "TO": "к", + "Text": "Текст", + "UNDO_MSG": "Отменить последнюю команду", + "UNKNOWN": "Неизвестный", + "UNTAB": "Развернуть", + "UPPERCASE": "ВЕРХНИЙ РЕГИСТР", + "UPPER_ALPHA": "Верхняя Альфа", + "UPPER_ROMAN": "Верхний римский", + "URL": "URL", + "URL_DUPLICATE_ER": "URL уже прикреплен. Пожалуйста, введите другой URL", + "URL_INVALID_ER": "Пожалуйста, введите действительный URL", + "URL_MSG": "Пожалуйста, введите URL!", + "VIDEO": "видео", + "VIEW": "Посмотреть", + "WEEK": "Неделю" +} \ No newline at end of file diff --git a/lib/assets/strings/sk.json b/lib/assets/strings/sk.json new file mode 100644 index 0000000..4d17b54 --- /dev/null +++ b/lib/assets/strings/sk.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Akcia", + "ADD_URL": "Pridať adresu URL", + "ALL": "Všetko", + "ATTACHED_FILE": "Priložený súbor", + "ATTACH_MAXSIZE_VALIDATE": "Veľkosť súboru musí byť menšia ako {0}", + "AUDIO_FILE_OR_URL_MSG": "Vyberte buď zvukový súbor, alebo zadajte adresu URL zvukového súboru!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Zadajte buď zvukový súbor alebo adresu URL zvuku, nie oboje!", + "Blur": "Rozmazať", + "Blur Radius": "Polomer rozostrenia", + "Brush": "Kefa", + "CAMERA": "Fotoaparát", + "CANCEL": "Zrušiť", + "CHANGE_CASE": "Zmeniť prípad", + "CHANGE_IMAGE": "Zmeniť obrázok", + "CHAR_LEFT": "Počet zostávajúcich znakov: {0}", + "CHOOSE_AUDIO": "Vyberte zvuk", + "CHOOSE_COLOR": "Vyberte farbu", + "CHOOSE_FILE": "Vyberte súbor", + "CHOOSE_FILE_URL_MSG": "Vyberte súbor alebo zadajte adresu URL súboru!", + "CHOOSE_IMAGE": "Vyberte obrázok", + "CHOOSE_IMG_URL_MSG": "Vyberte obrázok alebo zadajte adresu URL obrázka!", + "CHOOSE_VIDEO": "Vyberte video", + "CLEAR": "Vymazať", + "CLOSE": "Zavrieť", + "COMPLETE_EDITING": "Kompletná úprava", + "COURIER": "Kuriér", + "CROP": "Orezať", + "CUSTOM": "Vlastné", + "Color Opacity": "Nepriehľadnosť farieb", + "Crop": "Orezať", + "DEFAULT": "Predvolené", + "DELETE": "Vymazať", + "DELETE_ATTACHMENT": "Odstrániť prílohu", + "DONE": "Hotovo", + "EDIT": "Upraviť", + "EDITOR_CIRCLE": "Kruh", + "EDITOR_CODE": "kód", + "EDITOR_DISC": "disk", + "EDITOR_HEADER_1": "Hlavička 1", + "EDITOR_HEADER_2": "Hlavička 2", + "EDITOR_HEADER_3": "Hlavička 3", + "EDITOR_HEADER_4": "Hlavička 4", + "EDITOR_HEADER_5": "Hlavička 5", + "EDITOR_HEADER_6": "Hlavička 6", + "EDITOR_NORMAL": "normálne", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "citovať", + "EDITOR_SQUARE": "Námestie", + "EDITOR_TEXT": "Text", + "ENTER": "ENTER", + "ESCAPE": "uniknúť", + "Emoji": "Emodži", + "FILE": "Súbor", + "FILE_COMPRESSING_ER": "Pri komprimácii súboru sa vyskytla chyba", + "FILE_DUPLICATE_ER": "Súbor je už pripojený. Vyberte iný súbor", + "FILE_OR_URL_MSG": "Zadajte buď súbor alebo URL súboru, nie oboje!", + "FILE_PIXEL_SIZE_ER": "Veľkosť súboru v pixeloch by mala byť menšia ako {0}", + "FILE_PROCESSING_ER": "Pri spracovaní súboru sa vyskytla chyba", + "FILE_SIZE_CALCULATING_ER": "Pri výpočte veľkosti súboru sa vyskytla chyba", + "FILE_UNSUPPORTED": "Tento typ súboru nie je povolený", + "FLIP": "Preklopiť", + "FROM": "Od", + "Filter": "Filter", + "Flip": "Preklopiť", + "Freeform": "Voľný tvar", + "GALLERY": "Galéria", + "GIF": "Gif", + "HELP": "Pomoc", + "HIDE": "Skryť", + "IMAGE_OR_URL_MSG": "Zadajte buď obrázok alebo adresu URL obrázka, nie oboje!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Nástroj na výber obrázkov. Obrázok je vybratý", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Nástroj na výber obrázkov. Nie je vybratý žiadny obrázok", + "IMG_URL_MSG": "Zadajte adresu URL obrázka!", + "INSERT_AUDIO": "Vložiť zvuk", + "INSERT_FILE": "Vložiť súbor", + "INSERT_IMAGE": "Vložiť obrázok", + "INSERT_LINK": "Vložiť odkaz", + "INSERT_PARAGRAPH": "Vložiť odsek", + "INSERT_TABLE": "Vložiť tabuľku", + "INSERT_VIDEO": "Vložiť video", + "Insert Your Message": "Vložte svoju správu", + "KEY_COMBINATION": "Kombinácia klávesov", + "LINK": "Odkaz", + "LINK_NAME": "Názov prepojenia", + "LINK_URL": "Prepojiť URL", + "LOWERCASE": "malými písmenami", + "LOWER_ALPHA": "Dolná alfa", + "LOWER_ROMAN": "Dolná rímska", + "MAX_ATTACH_MSG": "Bol dosiahnutý maximálny limit príloh", + "MONTH": "Mesiac", + "NO_RESULTS": "Žiadne výsledky", + "NUMBERED": "Číslované", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Otvoriť v novom okne", + "PICK_GIF": "Vyberte Gif", + "PICK_IMAGE": "Vyberte obrázok", + "REDO_MSG": "Zopakujte posledný príkaz", + "REMOVE": "Odobrať", + "RESET": "Resetovať", + "REST_COLOR_MSG": "Obnoviť predvolenú farbu", + "ROTATE_LEFT": "Otočiť vľavo", + "ROTATE_RIGHT": "Otočiť vpravo", + "Reset": "Resetovať", + "Rotate left": "Otočiť doľava", + "Rotate right": "Otočiť doprava", + "SANS_SERIF": "Sans Serif", + "SAVE": "Uložiť", + "SEARCH_GIF": "Vyhľadajte všetky GIFy", + "SELECT_FROM_FILES": "Vyberte zo súborov", + "SELECT_STL_MSG": "Vyberte štýl zoznamu", + "SENTENCE_CASE": "Prípad vety", + "SET_COLOR": "Nastaviť farbu", + "SHOW": "Zobraziť", + "SWIPE_TO_REVEAL_SEMANTIC": "Potiahnutím doľava zobrazíte akcie", + "Select Emoji": "Vyberte položku Emoji", + "Slider Color": "Farba posúvača", + "Slider White Black Color": "Posuvník Biela Čierna Farba", + "Square": "Námestie", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Text na zobrazenie", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Názov prípad", + "TO": "Do", + "Text": "Text", + "UNDO_MSG": "Zrušte posledný príkaz", + "UNKNOWN": "Neznámy", + "UNTAB": "Untab", + "UPPERCASE": "VEĽKÉ PÍSMENÁ", + "UPPER_ALPHA": "Horná alfa", + "UPPER_ROMAN": "horný rímsky", + "URL": "URL", + "URL_DUPLICATE_ER": "Adresa URL je už pripojená. Zadajte inú adresu URL", + "URL_INVALID_ER": "Zadajte platnú adresu URL", + "URL_MSG": "Zadajte adresu URL!", + "VIDEO": "Video", + "VIEW": "Zobraziť", + "WEEK": "Týždeň" +} \ No newline at end of file diff --git a/lib/assets/strings/sl.json b/lib/assets/strings/sl.json new file mode 100644 index 0000000..f5d0f09 --- /dev/null +++ b/lib/assets/strings/sl.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Dejanje", + "ADD_URL": "Dodajanje URL-ja", + "ALL": "Vse", + "ATTACHED_FILE": "Pripeta datoteka", + "ATTACH_MAXSIZE_VALIDATE": "Velikost datoteke mora biti manjša od {0}", + "AUDIO_FILE_OR_URL_MSG": "Izberite zvočno datoteko ali vnesite URL zvočne datoteke!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Vnesite zvočno datoteko ali zvočni URL, ne obojega!", + "Blur": "Zameglitev", + "Blur Radius": "Polmer zameglitve", + "Brush": "Čopič", + "CAMERA": "Fotoaparat", + "CANCEL": "Prekliči", + "CHANGE_CASE": "Spremeni velike in male črke", + "CHANGE_IMAGE": "Spremeni sliko", + "CHAR_LEFT": "Preostalo znakov {0}", + "CHOOSE_AUDIO": "Izberite zvok", + "CHOOSE_COLOR": "Izberite barvo", + "CHOOSE_FILE": "Izberite datoteko", + "CHOOSE_FILE_URL_MSG": "Izberite datoteko ali vnesite URL datoteke!", + "CHOOSE_IMAGE": "Izberite sliko", + "CHOOSE_IMG_URL_MSG": "Izberite sliko ali vnesite URL slike!", + "CHOOSE_VIDEO": "Izberite video", + "CLEAR": "Počisti", + "CLOSE": "Zapri", + "COMPLETE_EDITING": "Popolno urejanje", + "COURIER": "Kurir", + "CROP": "Obreži", + "CUSTOM": "Po meri", + "Color Opacity": "Barvna motnost", + "Crop": "Obreži", + "DEFAULT": "Privzeto", + "DELETE": "Izbriši", + "DELETE_ATTACHMENT": "Izbriši prilogo", + "DONE": "Končano", + "EDIT": "Uredi", + "EDITOR_CIRCLE": "Krog", + "EDITOR_CODE": "Koda", + "EDITOR_DISC": "Disk", + "EDITOR_HEADER_1": "Glava 1", + "EDITOR_HEADER_2": "Glava 2", + "EDITOR_HEADER_3": "Glava 3", + "EDITOR_HEADER_4": "Glava 4", + "EDITOR_HEADER_5": "Glava 5", + "EDITOR_HEADER_6": "Glava 6", + "EDITOR_NORMAL": "normalno", + "EDITOR_PT": "točka", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "kvota", + "EDITOR_SQUARE": "kvadrat", + "EDITOR_TEXT": "Besedilo", + "ENTER": "ENTER", + "ESCAPE": "Pobeg", + "Emoji": "Čustveni simboli", + "FILE": "Datoteka", + "FILE_COMPRESSING_ER": "Med stiskanjem datoteke je prišlo do napake", + "FILE_DUPLICATE_ER": "Datoteka je že priložena. Izberite drugo datoteko", + "FILE_OR_URL_MSG": "Vnesite datoteko ali URL datoteke, ne obojega!", + "FILE_PIXEL_SIZE_ER": "Velikost slikovnih pik datoteke mora biti manjša od {0}", + "FILE_PROCESSING_ER": "Med obdelavo datoteke je prišlo do napake", + "FILE_SIZE_CALCULATING_ER": "Pri izračunu velikosti datoteke je prišlo do napake", + "FILE_UNSUPPORTED": "Ta vrsta datoteke ni dovoljena", + "FLIP": "Obrni", + "FROM": "Od", + "Filter": "Filter", + "Flip": "Obrni", + "Freeform": "Prosta oblika", + "GALLERY": "Galerija", + "GIF": "Gif", + "HELP": "Pomoč", + "HIDE": "Skrij", + "IMAGE_OR_URL_MSG": "Vnesite sliko ali URL slike, ne obojega!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Izbirnik slik. Slika izbrana", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Izbirnik slik. Izbrana ni nobena slika", + "IMG_URL_MSG": "Vnesite URL slike!", + "INSERT_AUDIO": "Vstavi zvok", + "INSERT_FILE": "Vstavi datoteko", + "INSERT_IMAGE": "Vstavi sliko", + "INSERT_LINK": "Vstavi povezavo", + "INSERT_PARAGRAPH": "Vstavi odstavek", + "INSERT_TABLE": "Vstavi tabelo", + "INSERT_VIDEO": "Vstavi video", + "Insert Your Message": "Vstavite svoje sporočilo", + "KEY_COMBINATION": "Kombinacija tipk", + "LINK": "Povezava", + "LINK_NAME": "Ime povezave", + "LINK_URL": "Povezava URL", + "LOWERCASE": "male črke", + "LOWER_ALPHA": "Spodnja alfa", + "LOWER_ROMAN": "spodnjerimski", + "MAX_ATTACH_MSG": "Dosežena je največja omejitev prilog", + "MONTH": "Mesec", + "NO_RESULTS": "Brez rezultatov", + "NUMBERED": "Oštevilčeno", + "OK": "V redu", + "OPEN_IN_NEW_WINDOW": "Odpri v novem oknu", + "PICK_GIF": "Izberite gif", + "PICK_IMAGE": "Izberite sliko", + "REDO_MSG": "Ponovite zadnji ukaz", + "REMOVE": "Odstrani", + "RESET": "Ponastavi", + "REST_COLOR_MSG": "Ponastavi na privzeto barvo", + "ROTATE_LEFT": "Vrtenje v levo", + "ROTATE_RIGHT": "Zavrti desno", + "Reset": "Ponastavi", + "Rotate left": "Zasukaj levo", + "Rotate right": "Zavrtite desno", + "SANS_SERIF": "Sans Serif", + "SAVE": "Shrani", + "SEARCH_GIF": "Iščite po vseh GIF-ih", + "SELECT_FROM_FILES": "Izberite med datotekami", + "SELECT_STL_MSG": "Izberite slog seznama", + "SENTENCE_CASE": "Stavkovni primer", + "SET_COLOR": "Nastavite barvo", + "SHOW": "Prikaži:", + "SWIPE_TO_REVEAL_SEMANTIC": "Povlecite levo, da razkrijete dejanja", + "Select Emoji": "Izberite Emoji", + "Slider Color": "Barva drsnika", + "Slider White Black Color": "Drsnik bele črne barve", + "Square": "kvadrat", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Besedilo za prikaz", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Naslov z velikimi črkami", + "TO": "Za", + "Text": "Besedilo", + "UNDO_MSG": "Razveljavi zadnji ukaz", + "UNKNOWN": "neznano", + "UNTAB": "Odstrani tab", + "UPPERCASE": "VELIKE ČRKE", + "UPPER_ALPHA": "Zgornja alfa", + "UPPER_ROMAN": "Gornjerimski", + "URL": "URL", + "URL_DUPLICATE_ER": "URL je že priložen. Vnesite drug URL", + "URL_INVALID_ER": "Vnesite veljaven URL", + "URL_MSG": "Vnesite URL!", + "VIDEO": "Video", + "VIEW": "Ogled", + "WEEK": "Teden" +} \ No newline at end of file diff --git a/lib/assets/strings/sq.json b/lib/assets/strings/sq.json new file mode 100644 index 0000000..a8c1b53 --- /dev/null +++ b/lib/assets/strings/sq.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Veprimi", + "ADD_URL": "Shto URL", + "ALL": "Të gjitha", + "ATTACHED_FILE": "Dosja e bashkangjitur", + "ATTACH_MAXSIZE_VALIDATE": "Madhësia e skedarit duhet të jetë më e vogël se {0}", + "AUDIO_FILE_OR_URL_MSG": "Ju lutemi zgjidhni një skedar audio ose futni një URL të skedarit audio!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Ju lutemi futni ose një skedar audio ose një URL audio, jo të dyja!", + "Blur": "E turbullt", + "Blur Radius": "Rrezja e turbullimit", + "Brush": "Furçë", + "CAMERA": "Kamera", + "CANCEL": "Anulo", + "CHANGE_CASE": "Ndrysho rastin", + "CHANGE_IMAGE": "Ndrysho imazhin", + "CHAR_LEFT": "Karakteret e mbetura {0}", + "CHOOSE_AUDIO": "Zgjidhni audio", + "CHOOSE_COLOR": "Zgjidhni një ngjyrë", + "CHOOSE_FILE": "Zgjidhni skedarin", + "CHOOSE_FILE_URL_MSG": "Ju lutemi zgjidhni një skedar ose futni një URL të skedarit!", + "CHOOSE_IMAGE": "Zgjidhni imazhin", + "CHOOSE_IMG_URL_MSG": "Ju lutemi zgjidhni një imazh ose vendosni një URL të imazhit!", + "CHOOSE_VIDEO": "Zgjidhni videon", + "CLEAR": "Qartë", + "CLOSE": "Mbylle", + "COMPLETE_EDITING": "Redaktimi i plotë", + "COURIER": "Korrier", + "CROP": "Pritini", + "CUSTOM": "Me porosi", + "Color Opacity": "Opaciteti i ngjyrave", + "Crop": "Pritini", + "DEFAULT": "E paracaktuar", + "DELETE": "Fshije", + "DELETE_ATTACHMENT": "Fshi bashkëngjitjen", + "DONE": "U krye", + "EDIT": "Redakto", + "EDITOR_CIRCLE": "Rretho", + "EDITOR_CODE": "kodi", + "EDITOR_DISC": "Disku", + "EDITOR_HEADER_1": "Kreu 1", + "EDITOR_HEADER_2": "Kreu 2", + "EDITOR_HEADER_3": "Kreu 3", + "EDITOR_HEADER_4": "Kreu 4", + "EDITOR_HEADER_5": "Kreu 5", + "EDITOR_HEADER_6": "Kreu 6", + "EDITOR_NORMAL": "normale", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "kuotë", + "EDITOR_SQUARE": "Sheshi", + "EDITOR_TEXT": "Teksti", + "ENTER": "HYN", + "ESCAPE": "Ik", + "Emoji": "Emoji", + "FILE": "Skedari", + "FILE_COMPRESSING_ER": "Ndodhi një gabim gjatë kompresimit të një skedari", + "FILE_DUPLICATE_ER": "Skedari është bashkangjitur tashmë. Ju lutemi zgjidhni skedarë të ndryshëm", + "FILE_OR_URL_MSG": "Ju lutemi futni ose një skedar ose një URL të skedarit, jo të dyja!", + "FILE_PIXEL_SIZE_ER": "Madhësia e pikselit të skedarit duhet të jetë më e vogël se {0}", + "FILE_PROCESSING_ER": "Ndodhi një gabim gjatë përpunimit të një skedari", + "FILE_SIZE_CALCULATING_ER": "Ndodhi një gabim gjatë llogaritjes së madhësisë së skedarit", + "FILE_UNSUPPORTED": "Ky lloj skedari nuk lejohet", + "FLIP": "Rrokullisje", + "FROM": "Nga", + "Filter": "Filtro", + "Flip": "Rrokullisje", + "Freeform": "Formë të lirë", + "GALLERY": "Galeri", + "GIF": "Gif", + "HELP": "Ndihmë", + "HIDE": "Fshih", + "IMAGE_OR_URL_MSG": "Ju lutemi futni një imazh ose një URL të imazhit, jo të dyja!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Zgjedhësi i imazhit. Imazhi i zgjedhur", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Zgjedhësi i imazhit. Asnjë imazh i zgjedhur", + "IMG_URL_MSG": "Ju lutemi shkruani një URL të imazhit!", + "INSERT_AUDIO": "Fut audio", + "INSERT_FILE": "Fut skedarin", + "INSERT_IMAGE": "Fut imazhin", + "INSERT_LINK": "Fut lidhjen", + "INSERT_PARAGRAPH": "Fut Paragrafin", + "INSERT_TABLE": "Fut tabelën", + "INSERT_VIDEO": "Fut video", + "Insert Your Message": "Fut Mesazhin Tuaj", + "KEY_COMBINATION": "Kombinimi i çelësave", + "LINK": "Lidhje", + "LINK_NAME": "Emri i lidhjes", + "LINK_URL": "URL-ja e lidhjes", + "LOWERCASE": "shkronja të vogla", + "LOWER_ALPHA": "Alfa e poshtme", + "LOWER_ROMAN": "Romani i Poshtëm", + "MAX_ATTACH_MSG": "U arrit kufiri maksimal i bashkëngjitjes", + "MONTH": "Muaj", + "NO_RESULTS": "Nuk ka rezultate", + "NUMBERED": "Të numëruara", + "OK": "Ne rregull", + "OPEN_IN_NEW_WINDOW": "Hapni në dritare të re", + "PICK_GIF": "Zgjidh një Gif", + "PICK_IMAGE": "Zgjidh imazhin", + "REDO_MSG": "Ribëni komandën e fundit", + "REMOVE": "Hiq", + "RESET": "Rivendos", + "REST_COLOR_MSG": "Rivendos në ngjyrën e paracaktuar", + "ROTATE_LEFT": "Rrotullo majtas", + "ROTATE_RIGHT": "Rrotullo djathtas", + "Reset": "Rivendos", + "Rotate left": "Rrotulloje majtas", + "Rotate right": "Rrotullo djathtas", + "SANS_SERIF": "Sans Serif", + "SAVE": "Ruaj", + "SEARCH_GIF": "Kërkoni të gjitha GIF-të", + "SELECT_FROM_FILES": "Zgjidhni nga skedarët", + "SELECT_STL_MSG": "Zgjidhni stilin e listës", + "SENTENCE_CASE": "Rasti i fjalisë", + "SET_COLOR": "Vendosni ngjyrën", + "SHOW": "Shfaqje", + "SWIPE_TO_REVEAL_SEMANTIC": "Rrëshqitni majtas për të zbuluar veprimet", + "Select Emoji": "Zgjidhni Emoji", + "Slider Color": "Ngjyra e rrëshqitësit", + "Slider White Black Color": "Rrëshqitës Bardhë Ngjyra e zezë", + "Square": "Sheshi", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Teksti për t'u shfaqur", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Rasti i titullit", + "TO": "për të", + "Text": "Teksti", + "UNDO_MSG": "Zhbër komandën e fundit", + "UNKNOWN": "E panjohur", + "UNTAB": "Untab", + "UPPERCASE": "ME SHKRONJA KAPITALE", + "UPPER_ALPHA": "Alfa e sipërme", + "UPPER_ROMAN": "romake e sipërme", + "URL": "URL", + "URL_DUPLICATE_ER": "URL-ja është bashkangjitur tashmë. Ju lutemi shkruani URL të ndryshme", + "URL_INVALID_ER": "Ju lutemi shkruani URL të vlefshme", + "URL_MSG": "Ju lutemi shkruani një URL!", + "VIDEO": "Video", + "VIEW": "Pamje", + "WEEK": "javë" +} \ No newline at end of file diff --git a/lib/assets/strings/sr.json b/lib/assets/strings/sr.json new file mode 100644 index 0000000..26a0c5c --- /dev/null +++ b/lib/assets/strings/sr.json @@ -0,0 +1,140 @@ +{ + "ACTION": "поступак", + "ADD_URL": "Додајте УРЛ адресу", + "ALL": "Све", + "ATTACHED_FILE": "Приложена датотека", + "ATTACH_MAXSIZE_VALIDATE": "Величина датотеке мора бити мања од {0}", + "AUDIO_FILE_OR_URL_MSG": "Изаберите аудио датотеку или унесите УРЛ аудио датотеке!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Унесите аудио датотеку или аудио УРЛ, а не обоје!", + "Blur": "Блур", + "Blur Radius": "Радијус замућења", + "Brush": "Четка", + "CAMERA": "Камера", + "CANCEL": "Поништити, отказати", + "CHANGE_CASE": "Промените случај", + "CHANGE_IMAGE": "Цханге Имаге", + "CHAR_LEFT": "Ликови лево {0}", + "CHOOSE_AUDIO": "Изаберите аудио", + "CHOOSE_COLOR": "Изаберите боју", + "CHOOSE_FILE": "Одаберите датотеку", + "CHOOSE_FILE_URL_MSG": "Изаберите датотеку или унесите УРЛ датотеке!", + "CHOOSE_IMAGE": "Изаберите слику", + "CHOOSE_IMG_URL_MSG": "Изаберите слику или унесите УРЛ слике!", + "CHOOSE_VIDEO": "Изаберите видео", + "CLEAR": "Јасно", + "CLOSE": "Близу", + "COMPLETE_EDITING": "Комплетно уређивање", + "COURIER": "Курир", + "CROP": "Усев", + "CUSTOM": "Обичај", + "Color Opacity": "Прозирност боје", + "Crop": "Усев", + "DEFAULT": "Уобичајено", + "DELETE": "Избриши", + "DELETE_ATTACHMENT": "Избришите прилог", + "DONE": "Готово", + "EDIT": "Уредити", + "EDITOR_CIRCLE": "Круг", + "EDITOR_CODE": "код", + "EDITOR_DISC": "Дисц", + "EDITOR_HEADER_1": "Заглавље 1", + "EDITOR_HEADER_2": "Заглавље 2", + "EDITOR_HEADER_3": "Заглавље 3", + "EDITOR_HEADER_4": "Заглавље 4", + "EDITOR_HEADER_5": "Заглавље 5", + "EDITOR_HEADER_6": "Заглавље 6", + "EDITOR_NORMAL": "нормалан", + "EDITOR_PT": "пт", + "EDITOR_PX": "пк", + "EDITOR_QUOTE": "цитат", + "EDITOR_SQUARE": "Квадрат", + "EDITOR_TEXT": "Текст", + "ENTER": "ЕНТЕР", + "ESCAPE": "Есцапе", + "Emoji": "Емоји", + "FILE": "Филе", + "FILE_COMPRESSING_ER": "Дошло је до грешке при компримовању датотеке", + "FILE_DUPLICATE_ER": "Датотека је већ приложена. Молимо изаберите другу датотеку", + "FILE_OR_URL_MSG": "Унесите датотеку или УРЛ датотеке, а не обоје!", + "FILE_PIXEL_SIZE_ER": "Величина пиксела датотеке треба да буде мања од {0}", + "FILE_PROCESSING_ER": "Дошло је до грешке приликом обраде датотеке", + "FILE_SIZE_CALCULATING_ER": "Дошло је до грешке при израчунавању величине датотеке", + "FILE_UNSUPPORTED": "Овај тип датотеке није дозвољен", + "FLIP": "Флип", + "FROM": "Фром", + "Filter": "Филтер", + "Flip": "Флип", + "Freeform": "Слободној форми", + "GALLERY": "Галерија", + "GIF": "Гиф", + "HELP": "Помоћ", + "HIDE": "Сакрити", + "IMAGE_OR_URL_MSG": "Унесите слику или УРЛ слике, а не обоје!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Бирач слика. Слика је изабрана", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Бирач слика. Ниједна слика није изабрана", + "IMG_URL_MSG": "Унесите УРЛ слике!", + "INSERT_AUDIO": "Инсерт Аудио", + "INSERT_FILE": "Уметни датотеку", + "INSERT_IMAGE": "Уметни слику", + "INSERT_LINK": "Уметни везу", + "INSERT_PARAGRAPH": "Уметни параграф", + "INSERT_TABLE": "Уметните табелу", + "INSERT_VIDEO": "Уметни видео", + "Insert Your Message": "Убаците своју поруку", + "KEY_COMBINATION": "Комбинација тастера", + "LINK": "Линк", + "LINK_NAME": "Име везе", + "LINK_URL": "УРЛ адреса везе", + "LOWERCASE": "мала слова", + "LOWER_ALPHA": "Ловер Алпха", + "LOWER_ROMAN": "доњоримски", + "MAX_ATTACH_MSG": "Максимално ограничење прилога је достигнуто", + "MONTH": "Месец дана", + "NO_RESULTS": "Нема резултата", + "NUMBERED": "Нумерисано", + "OK": "У реду", + "OPEN_IN_NEW_WINDOW": "Отвори у новом прозору", + "PICK_GIF": "Изаберите Гиф", + "PICK_IMAGE": "Изаберите слику", + "REDO_MSG": "Поновите последњу команду", + "REMOVE": "Уклони", + "RESET": "Ресетовање", + "REST_COLOR_MSG": "Вратите на подразумевану боју", + "ROTATE_LEFT": "Ротирајте лево", + "ROTATE_RIGHT": "Ротирајте десно", + "Reset": "Ресетовање", + "Rotate left": "Ротирајте лево", + "Rotate right": "Ротирајте десно", + "SANS_SERIF": "Санс Сериф", + "SAVE": "сачувати", + "SEARCH_GIF": "Претражите све ГИФ-ове", + "SELECT_FROM_FILES": "Изаберите неку од датотека", + "SELECT_STL_MSG": "Изаберите стил листе", + "SENTENCE_CASE": "Случај реченице", + "SET_COLOR": "Подесите боју", + "SHOW": "Покажи", + "SWIPE_TO_REVEAL_SEMANTIC": "Превуците налево да бисте открили радње", + "Select Emoji": "Изаберите Емоџи", + "Slider Color": "Боја клизача", + "Slider White Black Color": "Клизач Бела Црна Боја", + "Square": "Квадрат", + "TAB": "ТАБ", + "TEXT_TO_DISPLAY": "Текст за приказ", + "TIMES_NEW_ROMAN": "Тимес Нев Роман", + "TITLE_CASE": "Титле Цасе", + "TO": "До", + "Text": "Текст", + "UNDO_MSG": "Поништите последњу команду", + "UNKNOWN": "Непознат", + "UNTAB": "Унтаб", + "UPPERCASE": "ВЕЛИКА СЛОВА", + "UPPER_ALPHA": "Уппер Алпха", + "UPPER_ROMAN": "горњоримски", + "URL": "УРЛ адреса", + "URL_DUPLICATE_ER": "УРЛ је већ приложен. Унесите другу УРЛ адресу", + "URL_INVALID_ER": "Унесите важећи УРЛ", + "URL_MSG": "Унесите УРЛ!", + "VIDEO": "Видео", + "VIEW": "Поглед", + "WEEK": "Недеља" +} \ No newline at end of file diff --git a/lib/assets/strings/sv.json b/lib/assets/strings/sv.json new file mode 100644 index 0000000..5744bad --- /dev/null +++ b/lib/assets/strings/sv.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Åtgärd", + "ADD_URL": "Lägg till URL", + "ALL": "Alla", + "ATTACHED_FILE": "Bifogad fil", + "ATTACH_MAXSIZE_VALIDATE": "Filstorleken måste vara mindre än {0}", + "AUDIO_FILE_OR_URL_MSG": "Vänligen välj antingen en ljudfil eller ange en ljudfils URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Vänligen ange antingen en ljudfil eller en ljud-URL, inte båda!", + "Blur": "Fläck", + "Blur Radius": "Oskärpa radie", + "Brush": "Borsta", + "CAMERA": "Kamera", + "CANCEL": "Avbryt", + "CHANGE_CASE": "Ändra skiftläge", + "CHANGE_IMAGE": "Ändra bild", + "CHAR_LEFT": "Tecken kvar {0}", + "CHOOSE_AUDIO": "Välj ljud", + "CHOOSE_COLOR": "Välj en färg", + "CHOOSE_FILE": "Välj FIL", + "CHOOSE_FILE_URL_MSG": "Välj antingen en fil eller ange en fil-URL!", + "CHOOSE_IMAGE": "Välj bild", + "CHOOSE_IMG_URL_MSG": "Välj antingen en bild eller ange en bildadress!", + "CHOOSE_VIDEO": "Välj video", + "CLEAR": "Rensa", + "CLOSE": "Stäng", + "COMPLETE_EDITING": "Komplett redigering", + "COURIER": "Kurir", + "CROP": "Beskär", + "CUSTOM": "Anpassad", + "Color Opacity": "Färgopacitet", + "Crop": "Beskär", + "DEFAULT": "Standard", + "DELETE": "Radera", + "DELETE_ATTACHMENT": "Ta bort bilaga", + "DONE": "Klart", + "EDIT": "Redigera", + "EDITOR_CIRCLE": "Cirkel", + "EDITOR_CODE": "koda", + "EDITOR_DISC": "Skiva", + "EDITOR_HEADER_1": "Rubrik 1", + "EDITOR_HEADER_2": "Rubrik 2", + "EDITOR_HEADER_3": "Rubrik 3", + "EDITOR_HEADER_4": "Rubrik 4", + "EDITOR_HEADER_5": "Rubrik 5", + "EDITOR_HEADER_6": "Rubrik 6", + "EDITOR_NORMAL": "vanligt", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "Citat", + "EDITOR_SQUARE": "Fyrkant", + "EDITOR_TEXT": "Text", + "ENTER": "STIGA PÅ", + "ESCAPE": "Fly", + "Emoji": "Emoji", + "FILE": "Fil", + "FILE_COMPRESSING_ER": "Ett fel uppstod vid komprimering av en fil", + "FILE_DUPLICATE_ER": "Filen är redan bifogad. Välj en annan fil", + "FILE_OR_URL_MSG": "Ange antingen en fil eller en fil-URL, inte båda!", + "FILE_PIXEL_SIZE_ER": "Filpixelstorlek bör vara mindre än {0}", + "FILE_PROCESSING_ER": "Ett fel uppstod när en fil bearbetades", + "FILE_SIZE_CALCULATING_ER": "Ett fel uppstod vid beräkning av en filstorlek", + "FILE_UNSUPPORTED": "Denna filtyp är inte tillåten", + "FLIP": "Vänd", + "FROM": "Från", + "Filter": "Filter", + "Flip": "Vänd", + "Freeform": "Fri form", + "GALLERY": "Galleri", + "GIF": "Gif", + "HELP": "Hjälp", + "HIDE": "Dölj", + "IMAGE_OR_URL_MSG": "Vänligen ange antingen en bild eller en bildadress, inte båda!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Bildväljare. Bild vald", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Bildväljare. Ingen bild har valts", + "IMG_URL_MSG": "Ange en bildadress!", + "INSERT_AUDIO": "Sätt in ljud", + "INSERT_FILE": "Mata in fil", + "INSERT_IMAGE": "Infoga bild", + "INSERT_LINK": "Infoga länk", + "INSERT_PARAGRAPH": "Infoga stycke", + "INSERT_TABLE": "Sätt in tabell", + "INSERT_VIDEO": "Infoga video", + "Insert Your Message": "Infoga ditt meddelande", + "KEY_COMBINATION": "Nyckelkombination", + "LINK": "Länk", + "LINK_NAME": "Länknamn", + "LINK_URL": "URL-adress till länk", + "LOWERCASE": "små bokstäver", + "LOWER_ALPHA": "Lägre alfa", + "LOWER_ROMAN": "Nedre romerska", + "MAX_ATTACH_MSG": "Maximal gräns för bilaga har uppnåtts", + "MONTH": "Månad", + "NO_RESULTS": "Inga resultat", + "NUMBERED": "Numrerad", + "OK": "OK", + "OPEN_IN_NEW_WINDOW": "Öppna i nytt fönster", + "PICK_GIF": "Välj en gif", + "PICK_IMAGE": "Välj bild", + "REDO_MSG": "Gör om det senaste kommandot", + "REMOVE": "Ta bort", + "RESET": "Återställ", + "REST_COLOR_MSG": "Återställ till standardfärg", + "ROTATE_LEFT": "Rotera åt vänster", + "ROTATE_RIGHT": "Rotera åt höger", + "Reset": "Återställ", + "Rotate left": "Rotera vänster", + "Rotate right": "Vrid höger", + "SANS_SERIF": "Sans serif", + "SAVE": "Spara", + "SEARCH_GIF": "Sök igenom alla GIF-filer", + "SELECT_FROM_FILES": "Välj från filer", + "SELECT_STL_MSG": "Välj liststil", + "SENTENCE_CASE": "Strafffall", + "SET_COLOR": "Ställ in färg", + "SHOW": "Visa", + "SWIPE_TO_REVEAL_SEMANTIC": "Svep åt vänster för att avslöja åtgärder", + "Select Emoji": "Välj Emoji", + "Slider Color": "Slider Färg", + "Slider White Black Color": "Slider Vit Svart Färg", + "Square": "Fyrkant", + "TAB": "FLIK", + "TEXT_TO_DISPLAY": "Text att visa", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Titel Case", + "TO": "Till", + "Text": "Text", + "UNDO_MSG": "Ångra det sista kommandot", + "UNKNOWN": "Okänd", + "UNTAB": "Ta bort flik", + "UPPERCASE": "VERSAL", + "UPPER_ALPHA": "Övre alfa", + "UPPER_ROMAN": "övre romerska", + "URL": "URL", + "URL_DUPLICATE_ER": "URL är redan bifogad. Ange en annan URL", + "URL_INVALID_ER": "Ange en giltig URL", + "URL_MSG": "Vänligen ange en URL!", + "VIDEO": "Video", + "VIEW": "Visa", + "WEEK": "Vecka" +} \ No newline at end of file diff --git a/lib/assets/strings/th.json b/lib/assets/strings/th.json new file mode 100644 index 0000000..4d1cf2c --- /dev/null +++ b/lib/assets/strings/th.json @@ -0,0 +1,140 @@ +{ + "ACTION": "การกระทำ", + "ADD_URL": "เพิ่ม URL", + "ALL": "ทั้งหมด", + "ATTACHED_FILE": "ไฟล์ที่แนบมา", + "ATTACH_MAXSIZE_VALIDATE": "ขนาดไฟล์ต้องน้อยกว่า {0}", + "AUDIO_FILE_OR_URL_MSG": "โปรดเลือกไฟล์เสียงหรือป้อน URL ไฟล์เสียง!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "โปรดป้อนไฟล์เสียงหรือ URL เสียง ไม่ใช่ทั้งสองอย่าง!", + "Blur": "เบลอ", + "Blur Radius": "รัศมีการเบลอ", + "Brush": "แปรง", + "CAMERA": "กล้อง", + "CANCEL": "ยกเลิก", + "CHANGE_CASE": "เปลี่ยนกรณี", + "CHANGE_IMAGE": "เปลี่ยนภาพ", + "CHAR_LEFT": "เหลืออักขระ {0}", + "CHOOSE_AUDIO": "เลือกเสียง", + "CHOOSE_COLOR": "เลือกสี", + "CHOOSE_FILE": "เลือกไฟล์", + "CHOOSE_FILE_URL_MSG": "โปรดเลือกไฟล์หรือป้อน URL ของไฟล์!", + "CHOOSE_IMAGE": "เลือกรูปภาพ", + "CHOOSE_IMG_URL_MSG": "โปรดเลือกรูปภาพหรือป้อน URL ของรูปภาพ!", + "CHOOSE_VIDEO": "เลือกวิดีโอ", + "CLEAR": "ชัดเจน", + "CLOSE": "ปิด", + "COMPLETE_EDITING": "แก้ไขให้เสร็จ", + "COURIER": "จัดส่ง", + "CROP": "พืชผล", + "CUSTOM": "กำหนดเอง", + "Color Opacity": "ความทึบของสี", + "Crop": "พืชผล", + "DEFAULT": "ค่าเริ่มต้น", + "DELETE": "ลบ", + "DELETE_ATTACHMENT": "ลบไฟล์แนบ", + "DONE": "เสร็จสิ้น", + "EDIT": "แก้ไข", + "EDITOR_CIRCLE": "วงกลม", + "EDITOR_CODE": "รหัส", + "EDITOR_DISC": "แผ่นดิสก์", + "EDITOR_HEADER_1": "ส่วนหัว 1", + "EDITOR_HEADER_2": "ส่วนหัว 2", + "EDITOR_HEADER_3": "ส่วนหัว 3", + "EDITOR_HEADER_4": "ส่วนหัว 4", + "EDITOR_HEADER_5": "ส่วนหัว 5", + "EDITOR_HEADER_6": "ส่วนหัว 6", + "EDITOR_NORMAL": "ปกติ", + "EDITOR_PT": "พ", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "อ้าง", + "EDITOR_SQUARE": "สี่เหลี่ยม", + "EDITOR_TEXT": "ข้อความ", + "ENTER": "เข้า", + "ESCAPE": "หนี", + "Emoji": "อิโมจิ", + "FILE": "ไฟล์", + "FILE_COMPRESSING_ER": "เกิดข้อผิดพลาดขณะบีบอัดไฟล์", + "FILE_DUPLICATE_ER": "แนบไฟล์เรียบร้อยแล้ว โปรดเลือกไฟล์อื่น", + "FILE_OR_URL_MSG": "โปรดป้อนไฟล์หรือ URL ไฟล์อย่างใดอย่างหนึ่ง ไม่ใช่ทั้งสองอย่าง!", + "FILE_PIXEL_SIZE_ER": "ขนาดพิกเซลของไฟล์ควรน้อยกว่า {0}", + "FILE_PROCESSING_ER": "เกิดข้อผิดพลาดขณะประมวลผลไฟล์", + "FILE_SIZE_CALCULATING_ER": "เกิดข้อผิดพลาดขณะคำนวณขนาดไฟล์", + "FILE_UNSUPPORTED": "ไม่อนุญาตไฟล์ประเภทนี้", + "FLIP": "พลิก", + "FROM": "จาก", + "Filter": "กรอง", + "Flip": "พลิก", + "Freeform": "รูปแบบอิสระ", + "GALLERY": "เฉลียง", + "GIF": "Gif", + "HELP": "ช่วยด้วย", + "HIDE": "ปิดบัง", + "IMAGE_OR_URL_MSG": "โปรดป้อนรูปภาพหรือ URL รูปภาพ ไม่ใช่ทั้งสองอย่าง!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "ตัวเลือกรูปภาพ ภาพที่เลือก", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "ตัวเลือกรูปภาพ ไม่ได้เลือกภาพ", + "IMG_URL_MSG": "กรุณาใส่ URL รูปภาพ!", + "INSERT_AUDIO": "แทรกเสียง", + "INSERT_FILE": "แทรกไฟล์", + "INSERT_IMAGE": "แทรกรูปภาพ", + "INSERT_LINK": "ใส่ลิงค์", + "INSERT_PARAGRAPH": "แทรกย่อหน้า", + "INSERT_TABLE": "แทรกตาราง", + "INSERT_VIDEO": "แทรกวิดีโอ", + "Insert Your Message": "ใส่ข้อความของคุณ", + "KEY_COMBINATION": "คีย์ผสม", + "LINK": "ลิงค์", + "LINK_NAME": "ชื่อลิงค์", + "LINK_URL": "ลิงค์ URL", + "LOWERCASE": "ตัวพิมพ์เล็ก", + "LOWER_ALPHA": "อัลฟ่าตอนล่าง", + "LOWER_ROMAN": "โรมันตอนล่าง", + "MAX_ATTACH_MSG": "ไฟล์แนบถึงขีดจำกัดสูงสุดแล้ว", + "MONTH": "เดือน", + "NO_RESULTS": "ไม่มีผลลัพธ์", + "NUMBERED": "หมายเลข", + "OK": "ตกลง", + "OPEN_IN_NEW_WINDOW": "เปิดหน้าต่างใหม่", + "PICK_GIF": "เลือก Gif", + "PICK_IMAGE": "เลือกรูปภาพ", + "REDO_MSG": "ทำซ้ำคำสั่งสุดท้าย", + "REMOVE": "เอาออก", + "RESET": "รีเซ็ต", + "REST_COLOR_MSG": "รีเซ็ตเป็นสีเริ่มต้น", + "ROTATE_LEFT": "หมุนซ้าย", + "ROTATE_RIGHT": "หมุนขวา", + "Reset": "รีเซ็ต", + "Rotate left": "หมุนซ้าย", + "Rotate right": "หมุนไปทางขวา", + "SANS_SERIF": "ซานเซอริฟ", + "SAVE": "บันทึก", + "SEARCH_GIF": "ค้นหา GIF ทั้งหมด", + "SELECT_FROM_FILES": "เลือกจากไฟล์", + "SELECT_STL_MSG": "เลือกรูปแบบรายการ", + "SENTENCE_CASE": "กรณีประโยค", + "SET_COLOR": "ตั้งค่าสี", + "SHOW": "แสดง", + "SWIPE_TO_REVEAL_SEMANTIC": "ปัดไปทางซ้ายเพื่อดูการกระทำ", + "Select Emoji": "เลือกอีโมจิ", + "Slider Color": "แถบเลื่อนสี", + "Slider White Black Color": "สไลเดอร์ สีขาว สีดำ", + "Square": "สี่เหลี่ยม", + "TAB": "แท็บ", + "TEXT_TO_DISPLAY": "ข้อความที่จะแสดง", + "TIMES_NEW_ROMAN": "ไทมส์นิวโรมัน", + "TITLE_CASE": "กรณีชื่อเรื่อง", + "TO": "ไปยัง", + "Text": "ข้อความ", + "UNDO_MSG": "เลิกทำคำสั่งสุดท้าย", + "UNKNOWN": "ไม่รู้จัก", + "UNTAB": "แท็บ", + "UPPERCASE": "ตัวพิมพ์ใหญ่", + "UPPER_ALPHA": "อัลฟ่าตอนบน", + "UPPER_ROMAN": "อัปเปอร์โรมัน", + "URL": "URL", + "URL_DUPLICATE_ER": "แนบ URL เรียบร้อยแล้ว โปรดป้อน URL อื่น", + "URL_INVALID_ER": "โปรดป้อน URL ที่ถูกต้อง", + "URL_MSG": "กรุณาใส่ URL!", + "VIDEO": "วีดีโอ", + "VIEW": "ดู", + "WEEK": "สัปดาห์" +} \ No newline at end of file diff --git a/lib/assets/strings/tl.json b/lib/assets/strings/tl.json new file mode 100644 index 0000000..c869c07 --- /dev/null +++ b/lib/assets/strings/tl.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Pagkilos", + "ADD_URL": "Magdagdag ng URL", + "ALL": "Lahat", + "ATTACHED_FILE": "Naka-attach na file", + "ATTACH_MAXSIZE_VALIDATE": "Ang laki ng file ay dapat na mas mababa sa {0}", + "AUDIO_FILE_OR_URL_MSG": "Mangyaring pumili ng audio file o maglagay ng URL ng audio file!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Mangyaring maglagay ng alinman sa isang audio file o isang audio URL, hindi pareho!", + "Blur": "Malabo", + "Blur Radius": "Palabuin ang Radius", + "Brush": "Magsipilyo", + "CAMERA": "Camera", + "CANCEL": "Pagkansela", + "CHANGE_CASE": "Palitan ng kaso", + "CHANGE_IMAGE": "Baguhin ang Larawan", + "CHAR_LEFT": "Mga Character na Naiwan {0}", + "CHOOSE_AUDIO": "Pumili ng audio", + "CHOOSE_COLOR": "Pumili ng Kulay", + "CHOOSE_FILE": "Pumili ng file", + "CHOOSE_FILE_URL_MSG": "Mangyaring pumili ng file o magpasok ng URL ng file!", + "CHOOSE_IMAGE": "Pumili ng larawan", + "CHOOSE_IMG_URL_MSG": "Mangyaring pumili ng larawan o maglagay ng URL ng larawan!", + "CHOOSE_VIDEO": "Pumili ng video", + "CLEAR": "Malinaw", + "CLOSE": "Isara", + "COMPLETE_EDITING": "Kumpletuhin ang pag-edit", + "COURIER": "Courier", + "CROP": "Pag-crop", + "CUSTOM": "Pasadyang", + "Color Opacity": "Opacity ng Kulay", + "Crop": "Pag-crop", + "DEFAULT": "Default", + "DELETE": "Tanggalin", + "DELETE_ATTACHMENT": "Tanggalin ang attachment", + "DONE": "Tapos na", + "EDIT": "I-edit", + "EDITOR_CIRCLE": "Bilog", + "EDITOR_CODE": "code", + "EDITOR_DISC": "Disc", + "EDITOR_HEADER_1": "Header 1", + "EDITOR_HEADER_2": "Header 2", + "EDITOR_HEADER_3": "Header 3", + "EDITOR_HEADER_4": "Header 4", + "EDITOR_HEADER_5": "Header 5", + "EDITOR_HEADER_6": "Header 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "pt", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "quote", + "EDITOR_SQUARE": "Square", + "EDITOR_TEXT": "Teksto", + "ENTER": "PUMASOK", + "ESCAPE": "tumakas", + "Emoji": "Emoji", + "FILE": "File", + "FILE_COMPRESSING_ER": "Nagkaroon ng error habang nagko-compress ng file", + "FILE_DUPLICATE_ER": "Naka-attach na ang file. Mangyaring pumili ng ibang file", + "FILE_OR_URL_MSG": "Mangyaring maglagay ng alinman sa isang file o isang URL ng file, hindi pareho!", + "FILE_PIXEL_SIZE_ER": "Ang laki ng pixel ng file ay dapat na mas mababa sa {0}", + "FILE_PROCESSING_ER": "Nagkaroon ng error habang pinoproseso ang isang file", + "FILE_SIZE_CALCULATING_ER": "May naganap na error habang kinakalkula ang laki ng file", + "FILE_UNSUPPORTED": "Ang ganitong uri ng file ay hindi pinapayagan", + "FLIP": "I-flip", + "FROM": "Mula sa", + "Filter": "Filter", + "Flip": "I-flip", + "Freeform": "Malayang anyo", + "GALLERY": "Gallery", + "GIF": "Gif", + "HELP": "Tulong", + "HIDE": "Tago", + "IMAGE_OR_URL_MSG": "Mangyaring maglagay ng alinman sa isang imahe o isang URL ng larawan, hindi pareho!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Tagapili ng Larawan. Pinili ang larawan", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Tagapili ng Larawan. Walang napiling larawan", + "IMG_URL_MSG": "Mangyaring magpasok ng URL ng larawan!", + "INSERT_AUDIO": "Ipasok ang Audio", + "INSERT_FILE": "Ipasok ang File", + "INSERT_IMAGE": "Ipasok ang Larawan", + "INSERT_LINK": "Ipasok ang Link", + "INSERT_PARAGRAPH": "Ipasok ang Talata", + "INSERT_TABLE": "Ipasok ang Talahanayan", + "INSERT_VIDEO": "Ipasok ang Video", + "Insert Your Message": "Ipasok ang Iyong Mensahe", + "KEY_COMBINATION": "Susing Kumbinasyon", + "LINK": "Link", + "LINK_NAME": "Pangalan ng Link", + "LINK_URL": "Link URL", + "LOWERCASE": "maliit na titik", + "LOWER_ALPHA": "Mababang Alpha", + "LOWER_ROMAN": "Lower Roman", + "MAX_ATTACH_MSG": "Naabot na ang max na limitasyon ng attachment", + "MONTH": "Buwan", + "NO_RESULTS": "Walang resulta", + "NUMBERED": "May bilang", + "OK": "OK lang", + "OPEN_IN_NEW_WINDOW": "Magbukas sa bagong bintana", + "PICK_GIF": "Pumili ng Gif", + "PICK_IMAGE": "Pumili ng Larawan", + "REDO_MSG": "Gawin muli ang huling utos", + "REMOVE": "Alisin", + "RESET": "I-reset", + "REST_COLOR_MSG": "I-reset sa default na kulay", + "ROTATE_LEFT": "I-rotate sa Kaliwa", + "ROTATE_RIGHT": "Iikot pa puntang kanan", + "Reset": "I-reset", + "Rotate left": "I-rotate pakaliwa", + "Rotate right": "Iikot pa puntang kanan", + "SANS_SERIF": "Sans Serif", + "SAVE": "I-save", + "SEARCH_GIF": "Hanapin ang lahat ng mga GIF", + "SELECT_FROM_FILES": "Pumili mula sa mga file", + "SELECT_STL_MSG": "Piliin ang istilo ng listahan", + "SENTENCE_CASE": "Kaso ng pangungusap", + "SET_COLOR": "Itakda ang kulay", + "SHOW": "Ipakita", + "SWIPE_TO_REVEAL_SEMANTIC": "Mag-swipe pakaliwa para ipakita ang mga aksyon", + "Select Emoji": "Piliin ang Emoji", + "Slider Color": "Kulay ng Slider", + "Slider White Black Color": "Slider Puting Itim na Kulay", + "Square": "Square", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Tekstong ipapakita", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Kaso ng Pamagat", + "TO": "Sa", + "Text": "Teksto", + "UNDO_MSG": "I-undo ang huling utos", + "UNKNOWN": "Hindi alam", + "UNTAB": "I-untab", + "UPPERCASE": "UPPERCASE", + "UPPER_ALPHA": "Itaas na Alpha", + "UPPER_ROMAN": "Upper Roman", + "URL": "URL", + "URL_DUPLICATE_ER": "Naka-attach na ang URL. Mangyaring maglagay ng ibang URL", + "URL_INVALID_ER": "Mangyaring magpasok ng wastong URL", + "URL_MSG": "Mangyaring magpasok ng isang URL!", + "VIDEO": "Video", + "VIEW": "Tingnan", + "WEEK": "Linggo" +} \ No newline at end of file diff --git a/lib/assets/strings/tr.json b/lib/assets/strings/tr.json new file mode 100644 index 0000000..bd4a630 --- /dev/null +++ b/lib/assets/strings/tr.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Aksiyon", + "ADD_URL": "URL ekle", + "ALL": "Herşey", + "ATTACHED_FILE": "Ekli dosya", + "ATTACH_MAXSIZE_VALIDATE": "Dosya boyutu {0} boyutundan küçük olmalıdır", + "AUDIO_FILE_OR_URL_MSG": "Lütfen bir ses dosyası seçin veya bir ses dosyası URL'si girin!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Lütfen bir ses dosyası veya bir ses URL'si girin, ikisini birden değil!", + "Blur": "Bulanıklık", + "Blur Radius": "Bulanıklık Yarıçapı", + "Brush": "Fırçalamak", + "CAMERA": "Kamera", + "CANCEL": "İptal etmek", + "CHANGE_CASE": "Davayı değiştir", + "CHANGE_IMAGE": "Resmi değiştir", + "CHAR_LEFT": "Kalan Karakterler {0}", + "CHOOSE_AUDIO": "ses seç", + "CHOOSE_COLOR": "Bir renk seç", + "CHOOSE_FILE": "Dosya seçin", + "CHOOSE_FILE_URL_MSG": "Lütfen bir dosya seçin veya bir dosya URL'si girin!", + "CHOOSE_IMAGE": "resim seç", + "CHOOSE_IMG_URL_MSG": "Lütfen bir resim seçin veya bir resim URL'si girin!", + "CHOOSE_VIDEO": "video seç", + "CLEAR": "Açık", + "CLOSE": "Kapat", + "COMPLETE_EDITING": "Düzenlemeyi tamamla", + "COURIER": "Kurye", + "CROP": "ekin", + "CUSTOM": "görenek", + "Color Opacity": "Renk Opaklığı", + "Crop": "ekin", + "DEFAULT": "Varsayılan", + "DELETE": "silmek", + "DELETE_ATTACHMENT": "Eki sil", + "DONE": "tamam", + "EDIT": "Düzenle", + "EDITOR_CIRCLE": "Daire", + "EDITOR_CODE": "kod", + "EDITOR_DISC": "Disk", + "EDITOR_HEADER_1": "Başlık 1", + "EDITOR_HEADER_2": "Başlık 2", + "EDITOR_HEADER_3": "Başlık 3", + "EDITOR_HEADER_4": "Başlık 4", + "EDITOR_HEADER_5": "Başlık 5", + "EDITOR_HEADER_6": "Başlık 6", + "EDITOR_NORMAL": "normal", + "EDITOR_PT": "nokta", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "alıntı", + "EDITOR_SQUARE": "Kare", + "EDITOR_TEXT": "Metin", + "ENTER": "GİRMEK", + "ESCAPE": "Kaçmak", + "Emoji": "Emoji", + "FILE": "Dosya", + "FILE_COMPRESSING_ER": "Dosya sıkıştırılırken bir hata oluştu", + "FILE_DUPLICATE_ER": "Dosya zaten eklenmiş. Lütfen farklı dosya seçin", + "FILE_OR_URL_MSG": "Lütfen bir dosya veya dosya URL'si girin, ikisini birden değil!", + "FILE_PIXEL_SIZE_ER": "Dosya piksel boyutu {0} değerinden küçük olmalıdır", + "FILE_PROCESSING_ER": "Dosya işlenirken bir hata oluştu", + "FILE_SIZE_CALCULATING_ER": "Dosya boyutu hesaplanırken bir hata oluştu", + "FILE_UNSUPPORTED": "Bu dosya türüne izin verilmiyor", + "FLIP": "çevir", + "FROM": "itibaren", + "Filter": "filtre", + "Flip": "çevir", + "Freeform": "Serbest çalışma", + "GALLERY": "galeri", + "GIF": "gif", + "HELP": "yardım et", + "HIDE": "Saklamak", + "IMAGE_OR_URL_MSG": "Lütfen bir resim veya resim URL'si girin, ikisini birden değil!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Görüntü Seçici. Resim seçildi", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Görüntü Seçici. Resim seçilmedi", + "IMG_URL_MSG": "Lütfen bir resim URL'si girin!", + "INSERT_AUDIO": "Ses Ekle", + "INSERT_FILE": "Dosya ekle", + "INSERT_IMAGE": "Resim ekle", + "INSERT_LINK": "Bağlantı Ekle", + "INSERT_PARAGRAPH": "Paragraf Ekle", + "INSERT_TABLE": "Tablo Ekle", + "INSERT_VIDEO": "Video Ekle", + "Insert Your Message": "Mesajınızı Ekleyin", + "KEY_COMBINATION": "Tuş Kombinasyonu", + "LINK": "Bağlantı", + "LINK_NAME": "Bağlantı Adı", + "LINK_URL": "bağlantı adresi", + "LOWERCASE": "küçük harf", + "LOWER_ALPHA": "Alt Alfa", + "LOWER_ROMAN": "Aşağı Roma", + "MAX_ATTACH_MSG": "Ek maksimum sınırına ulaşıldı", + "MONTH": "Ay", + "NO_RESULTS": "Sonuç yok", + "NUMBERED": "Sayılı", + "OK": "Tamam", + "OPEN_IN_NEW_WINDOW": "Yeni pencerede aç", + "PICK_GIF": "Bir Gif Seç", + "PICK_IMAGE": "Resim Seç", + "REDO_MSG": "Son komutu yeniden yap", + "REMOVE": "Kaldır", + "RESET": "Sıfırla", + "REST_COLOR_MSG": "Varsayılan renge sıfırla", + "ROTATE_LEFT": "Sola dön", + "ROTATE_RIGHT": "Sağa Döndür", + "Reset": "Sıfırla", + "Rotate left": "Sola dön", + "Rotate right": "sağa döndür", + "SANS_SERIF": "Sans Serif", + "SAVE": "Kayıt etmek", + "SEARCH_GIF": "Tüm GIF'leri ara", + "SELECT_FROM_FILES": "dosyalardan seç", + "SELECT_STL_MSG": "Liste stilini seçin", + "SENTENCE_CASE": "cümle durumu", + "SET_COLOR": "rengi ayarla", + "SHOW": "Göstermek", + "SWIPE_TO_REVEAL_SEMANTIC": "Eylemleri ortaya çıkarmak için sola kaydırın", + "Select Emoji": "Emoji'yi seçin", + "Slider Color": "Sürgü Rengi", + "Slider White Black Color": "Kaydırıcı Beyaz Siyah Renk", + "Square": "Kare", + "TAB": "SEKME", + "TEXT_TO_DISPLAY": "Görüntülenecek metin", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Başlık Örneği", + "TO": "için", + "Text": "Metin", + "UNDO_MSG": "Son komutu geri al", + "UNKNOWN": "Bilinmeyen", + "UNTAB": "Sekmeyi kaldır", + "UPPERCASE": "BÜYÜK HARF", + "UPPER_ALPHA": "Üst Alfa", + "UPPER_ROMAN": "Yukarı Roma", + "URL": "URL", + "URL_DUPLICATE_ER": "URL zaten eklenmiş. Lütfen farklı bir URL girin", + "URL_INVALID_ER": "Lütfen geçerli bir URL girin", + "URL_MSG": "Lütfen bir URL girin!", + "VIDEO": "Video", + "VIEW": "Görünüm", + "WEEK": "Hafta" +} \ No newline at end of file diff --git a/lib/assets/strings/uk.json b/lib/assets/strings/uk.json new file mode 100644 index 0000000..3266cc3 --- /dev/null +++ b/lib/assets/strings/uk.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Дія", + "ADD_URL": "Додати URL-адресу", + "ALL": "Усі", + "ATTACHED_FILE": "Вкладений файл", + "ATTACH_MAXSIZE_VALIDATE": "Розмір файлу має бути меншим за {0}", + "AUDIO_FILE_OR_URL_MSG": "Виберіть аудіофайл або введіть URL-адресу аудіофайлу!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Будь ласка, введіть або аудіофайл, або URL-адресу аудіо, а не обидва!", + "Blur": "Розмиття", + "Blur Radius": "Радіус розмиття", + "Brush": "Кисть", + "CAMERA": "Камера", + "CANCEL": "Скасувати", + "CHANGE_CASE": "Змінити регістр", + "CHANGE_IMAGE": "Змінити зображення", + "CHAR_LEFT": "Залишилося символів {0}", + "CHOOSE_AUDIO": "Виберіть аудіо", + "CHOOSE_COLOR": "Виберіть колір", + "CHOOSE_FILE": "Виберіть файл", + "CHOOSE_FILE_URL_MSG": "Виберіть файл або введіть URL-адресу файлу!", + "CHOOSE_IMAGE": "Виберіть зображення", + "CHOOSE_IMG_URL_MSG": "Виберіть зображення або введіть URL-адресу зображення!", + "CHOOSE_VIDEO": "Виберіть відео", + "CLEAR": "Ясно", + "CLOSE": "Закрити", + "COMPLETE_EDITING": "Повне редагування", + "COURIER": "Кур'єр", + "CROP": "Урожай", + "CUSTOM": "Користувальницькі", + "Color Opacity": "Непрозорість кольору", + "Crop": "Урожай", + "DEFAULT": "За замовчуванням", + "DELETE": "Видалити", + "DELETE_ATTACHMENT": "Видалити вкладення", + "DONE": "Зроблено", + "EDIT": "Редагувати", + "EDITOR_CIRCLE": "Коло", + "EDITOR_CODE": "код", + "EDITOR_DISC": "Диск", + "EDITOR_HEADER_1": "Заголовок 1", + "EDITOR_HEADER_2": "Заголовок 2", + "EDITOR_HEADER_3": "Заголовок 3", + "EDITOR_HEADER_4": "Заголовок 4", + "EDITOR_HEADER_5": "Заголовок 5", + "EDITOR_HEADER_6": "Заголовок 6", + "EDITOR_NORMAL": "нормально", + "EDITOR_PT": "пт", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "цитата", + "EDITOR_SQUARE": "Майдан", + "EDITOR_TEXT": "Текст", + "ENTER": "ENTER", + "ESCAPE": "Втеча", + "Emoji": "Емоджі", + "FILE": "Файл", + "FILE_COMPRESSING_ER": "Під час стиснення файлу сталася помилка", + "FILE_DUPLICATE_ER": "Файл уже прикріплено. Виберіть інший файл", + "FILE_OR_URL_MSG": "Введіть або файл, або URL-адресу файлу, а не обидва!", + "FILE_PIXEL_SIZE_ER": "Розмір файлу в пікселях має бути менше ніж {0}", + "FILE_PROCESSING_ER": "Під час обробки файлу сталася помилка", + "FILE_SIZE_CALCULATING_ER": "Під час розрахунку розміру файлу сталася помилка", + "FILE_UNSUPPORTED": "Цей тип файлу заборонений", + "FLIP": "Перевернути", + "FROM": "З", + "Filter": "Фільтр", + "Flip": "Перевернути", + "Freeform": "Довільна форма", + "GALLERY": "Галерея", + "GIF": "Gif", + "HELP": "Довідка", + "HIDE": "Сховати", + "IMAGE_OR_URL_MSG": "Введіть або зображення, або URL-адресу зображення, а не обидва!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Вибір зображень. Зображення вибрано", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Вибір зображень. Зображення не вибрано", + "IMG_URL_MSG": "Введіть URL-адресу зображення!", + "INSERT_AUDIO": "Вставити аудіо", + "INSERT_FILE": "Вставити файл", + "INSERT_IMAGE": "Вставити зображення", + "INSERT_LINK": "Вставити посилання", + "INSERT_PARAGRAPH": "Вставити абзац", + "INSERT_TABLE": "Вставити таблицю", + "INSERT_VIDEO": "Вставити відео", + "Insert Your Message": "Вставте своє повідомлення", + "KEY_COMBINATION": "Комбінація клавіш", + "LINK": "Посилання", + "LINK_NAME": "Назва посилання", + "LINK_URL": "URL-адреса посилання", + "LOWERCASE": "нижній регістр", + "LOWER_ALPHA": "Нижня Альфа", + "LOWER_ROMAN": "Нижній Роман", + "MAX_ATTACH_MSG": "Досягнуто максимальної кількості вкладених файлів", + "MONTH": "Місяць", + "NO_RESULTS": "Немає результатів", + "NUMBERED": "Пронумерований", + "OK": "в порядку", + "OPEN_IN_NEW_WINDOW": "Відкрити в новому вікні", + "PICK_GIF": "Виберіть Gif", + "PICK_IMAGE": "Виберіть зображення", + "REDO_MSG": "Повторити останню команду", + "REMOVE": "Видалити", + "RESET": "Скидання", + "REST_COLOR_MSG": "Відновити колір за замовчуванням", + "ROTATE_LEFT": "Повернути вліво", + "ROTATE_RIGHT": "Повернути вправо", + "Reset": "Скидання", + "Rotate left": "Повернути вліво", + "Rotate right": "Повернути праворуч", + "SANS_SERIF": "Без зарубок", + "SAVE": "Зберегти", + "SEARCH_GIF": "Пошук у всіх GIF-файлах", + "SELECT_FROM_FILES": "Виберіть із файлів", + "SELECT_STL_MSG": "Виберіть стиль списку", + "SENTENCE_CASE": "Відмінок речення", + "SET_COLOR": "Встановити колір", + "SHOW": "Показати", + "SWIPE_TO_REVEAL_SEMANTIC": "Проведіть пальцем ліворуч, щоб побачити дії", + "Select Emoji": "Виберіть Emoji", + "Slider Color": "Колір повзунка", + "Slider White Black Color": "Слайдер білий чорний колір", + "Square": "Майдан", + "TAB": "TAB", + "TEXT_TO_DISPLAY": "Текст для відображення", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Регістр назви", + "TO": "До", + "Text": "Текст", + "UNDO_MSG": "Скасувати останню команду", + "UNKNOWN": "Невідомо", + "UNTAB": "Зняти вкладку", + "UPPERCASE": "ВЕРХНИЙ РЕГІСТ", + "UPPER_ALPHA": "Верхня Альфа", + "UPPER_ROMAN": "Верхньоримський", + "URL": "URL-адреса", + "URL_DUPLICATE_ER": "URL-адресу вже додано. Введіть інший URL", + "URL_INVALID_ER": "Введіть дійсну URL-адресу", + "URL_MSG": "Будь ласка, введіть URL!", + "VIDEO": "Відео", + "VIEW": "Вид", + "WEEK": "Тиждень" +} \ No newline at end of file diff --git a/lib/assets/strings/vi.json b/lib/assets/strings/vi.json new file mode 100644 index 0000000..137a233 --- /dev/null +++ b/lib/assets/strings/vi.json @@ -0,0 +1,140 @@ +{ + "ACTION": "Hoạt động", + "ADD_URL": "Thêm URL", + "ALL": "Tất cả các", + "ATTACHED_FILE": "Tập tin đính kèm", + "ATTACH_MAXSIZE_VALIDATE": "Kích thước tệp phải nhỏ hơn {0}", + "AUDIO_FILE_OR_URL_MSG": "Vui lòng chọn tệp âm thanh hoặc nhập URL tệp âm thanh!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "Vui lòng nhập tệp âm thanh hoặc URL âm thanh, không phải cả hai!", + "Blur": "Mơ hồ", + "Blur Radius": "Bán kính mờ", + "Brush": "Chải", + "CAMERA": "Máy ảnh", + "CANCEL": "Hủy bỏ", + "CHANGE_CASE": "Thay đổi trường hợp", + "CHANGE_IMAGE": "Đổi hình ảnh", + "CHAR_LEFT": "Các ký tự còn lại {0}", + "CHOOSE_AUDIO": "Chọn âm thanh", + "CHOOSE_COLOR": "Chọn một màu", + "CHOOSE_FILE": "Chọn tập tin", + "CHOOSE_FILE_URL_MSG": "Vui lòng chọn tệp hoặc nhập URL tệp!", + "CHOOSE_IMAGE": "Chọn hình ảnh", + "CHOOSE_IMG_URL_MSG": "Vui lòng chọn một hình ảnh hoặc nhập một URL hình ảnh!", + "CHOOSE_VIDEO": "Chọn video", + "CLEAR": "Sạch", + "CLOSE": "Gần", + "COMPLETE_EDITING": "Hoàn thành chỉnh sửa", + "COURIER": "chuyển phát nhanh", + "CROP": "Mùa vụ", + "CUSTOM": "Tập quán", + "Color Opacity": "Độ mờ màu", + "Crop": "Mùa vụ", + "DEFAULT": "Mặc định", + "DELETE": "Xóa bỏ", + "DELETE_ATTACHMENT": "Xóa tệp đính kèm", + "DONE": "Làm xong", + "EDIT": "Chỉnh sửa", + "EDITOR_CIRCLE": "Vòng tròn", + "EDITOR_CODE": "mã số", + "EDITOR_DISC": "Đĩa", + "EDITOR_HEADER_1": "tiêu đề 1", + "EDITOR_HEADER_2": "tiêu đề 2", + "EDITOR_HEADER_3": "tiêu đề 3", + "EDITOR_HEADER_4": "tiêu đề 4", + "EDITOR_HEADER_5": "Tiêu đề 5", + "EDITOR_HEADER_6": "tiêu đề 6", + "EDITOR_NORMAL": "Bình thường", + "EDITOR_PT": "điểm", + "EDITOR_PX": "px", + "EDITOR_QUOTE": "trích dẫn", + "EDITOR_SQUARE": "Quảng trường", + "EDITOR_TEXT": "Bản văn", + "ENTER": "ĐI VÀO", + "ESCAPE": "Bỏ trốn", + "Emoji": "Biểu tượng cảm xúc", + "FILE": "Tập tin", + "FILE_COMPRESSING_ER": "Đã xảy ra lỗi khi nén tệp", + "FILE_DUPLICATE_ER": "Tập tin đã được đính kèm. Vui lòng chọn tệp khác", + "FILE_OR_URL_MSG": "Vui lòng nhập tệp hoặc URL tệp, không phải cả hai!", + "FILE_PIXEL_SIZE_ER": "Kích thước pixel của tệp phải nhỏ hơn {0}", + "FILE_PROCESSING_ER": "Đã xảy ra lỗi khi xử lý tệp", + "FILE_SIZE_CALCULATING_ER": "Đã xảy ra lỗi khi tính toán kích thước tệp", + "FILE_UNSUPPORTED": "Loại tệp này không được phép", + "FLIP": "Lật", + "FROM": "Từ", + "Filter": "Bộ lọc", + "Flip": "Lật", + "Freeform": "Hình thức miễn phí", + "GALLERY": "Bộ sưu tập", + "GIF": "Gif", + "HELP": "Cứu giúp", + "HIDE": "Ẩn giấu", + "IMAGE_OR_URL_MSG": "Vui lòng nhập một hình ảnh hoặc một URL hình ảnh, không phải cả hai!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "Bộ chọn Hình ảnh. Đã chọn hình ảnh", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "Bộ chọn Hình ảnh. Không có hình ảnh nào được chọn", + "IMG_URL_MSG": "Vui lòng nhập một URL hình ảnh!", + "INSERT_AUDIO": "Chèn âm thanh", + "INSERT_FILE": "Chèn tập tin", + "INSERT_IMAGE": "Chèn hình ảnh", + "INSERT_LINK": "Chèn đường dẫn", + "INSERT_PARAGRAPH": "Chèn đoạn văn", + "INSERT_TABLE": "Chèn bảng", + "INSERT_VIDEO": "Chèn video", + "Insert Your Message": "Chèn tin nhắn của bạn", + "KEY_COMBINATION": "tổ hợp phím", + "LINK": "liên kết", + "LINK_NAME": "Tên liên kết", + "LINK_URL": "Liên kết URL", + "LOWERCASE": "chữ thường", + "LOWER_ALPHA": "Alpha thấp hơn", + "LOWER_ROMAN": "Hạ La Mã", + "MAX_ATTACH_MSG": "Đã đạt đến giới hạn tối đa của tệp đính kèm", + "MONTH": "tháng", + "NO_RESULTS": "Ko có kết quả", + "NUMBERED": "đánh số", + "OK": "VÂNG", + "OPEN_IN_NEW_WINDOW": "Mở trong cửa sổ mới", + "PICK_GIF": "Chọn một Gif", + "PICK_IMAGE": "Chọn hình ảnh", + "REDO_MSG": "Làm lại lệnh cuối cùng", + "REMOVE": "Tẩy", + "RESET": "Cài lại", + "REST_COLOR_MSG": "Đặt lại về màu mặc định", + "ROTATE_LEFT": "Xoay trái", + "ROTATE_RIGHT": "Xoay phải", + "Reset": "Cài lại", + "Rotate left": "Xoay trái", + "Rotate right": "Xoay phải", + "SANS_SERIF": "Sans serif", + "SAVE": "Tiết kiệm", + "SEARCH_GIF": "Tìm kiếm tất cả các GIF", + "SELECT_FROM_FILES": "Chọn từ các tệp", + "SELECT_STL_MSG": "Chọn kiểu danh sách", + "SENTENCE_CASE": "trường hợp câu", + "SET_COLOR": "Đặt màu", + "SHOW": "Chỉ", + "SWIPE_TO_REVEAL_SEMANTIC": "Vuốt sang trái để hiển thị các hành động", + "Select Emoji": "Chọn biểu tượng cảm xúc", + "Slider Color": "Màu thanh trượt", + "Slider White Black Color": "Thanh trượt màu trắng đen", + "Square": "Quảng trường", + "TAB": "CHUYỂN HƯỚNG", + "TEXT_TO_DISPLAY": "Văn bản để hiển thị", + "TIMES_NEW_ROMAN": "Times New Roman", + "TITLE_CASE": "Trường hợp tiêu đề", + "TO": "Đến", + "Text": "Bản văn", + "UNDO_MSG": "Hoàn tác lệnh cuối cùng", + "UNKNOWN": "không xác định", + "UNTAB": "Bỏ tab", + "UPPERCASE": "CHỮ HOA", + "UPPER_ALPHA": "Alpha trên", + "UPPER_ROMAN": "Thượng La Mã", + "URL": "URL", + "URL_DUPLICATE_ER": "URL đã được đính kèm. Vui lòng nhập URL khác", + "URL_INVALID_ER": "Vui lòng nhập URL hợp lệ", + "URL_MSG": "Vui lòng nhập một URL!", + "VIDEO": "Video", + "VIEW": "Lượt xem", + "WEEK": "Tuần" +} \ No newline at end of file diff --git a/lib/assets/strings/zh.json b/lib/assets/strings/zh.json new file mode 100644 index 0000000..4d43cf4 --- /dev/null +++ b/lib/assets/strings/zh.json @@ -0,0 +1,140 @@ +{ + "ACTION": "行动", + "ADD_URL": "添加网址", + "ALL": "所有", + "ATTACHED_FILE": "附件", + "ATTACH_MAXSIZE_VALIDATE": "文件大小必须小于 {0}", + "AUDIO_FILE_OR_URL_MSG": "请选择音频文件或输入音频文件 URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "请输入音频文件或音频 URL,不能同时输入两者!", + "Blur": "模糊", + "Blur Radius": "模糊半径", + "Brush": "刷子", + "CAMERA": "相机", + "CANCEL": "取消", + "CHANGE_CASE": "改变大小写", + "CHANGE_IMAGE": "更改图像", + "CHAR_LEFT": "剩余 {0} 个字符", + "CHOOSE_AUDIO": "选择音频", + "CHOOSE_COLOR": "选择颜色", + "CHOOSE_FILE": "选择文件", + "CHOOSE_FILE_URL_MSG": "请选择文件或输入文件 URL!", + "CHOOSE_IMAGE": "选择图片", + "CHOOSE_IMG_URL_MSG": "请选择图片或输入图片网址!", + "CHOOSE_VIDEO": "选择视频", + "CLEAR": "清除", + "CLOSE": "关", + "COMPLETE_EDITING": "完成编辑", + "COURIER": "导游", + "CROP": "作物", + "CUSTOM": "习惯", + "Color Opacity": "颜色不透明度", + "Crop": "作物", + "DEFAULT": "默认", + "DELETE": "删除", + "DELETE_ATTACHMENT": "删除附件", + "DONE": "完成", + "EDIT": "编辑", + "EDITOR_CIRCLE": "圈", + "EDITOR_CODE": "代码", + "EDITOR_DISC": "光盘", + "EDITOR_HEADER_1": "标头 1", + "EDITOR_HEADER_2": "标头 2", + "EDITOR_HEADER_3": "标题 3", + "EDITOR_HEADER_4": "标题 4", + "EDITOR_HEADER_5": "标题 5", + "EDITOR_HEADER_6": "标题 6", + "EDITOR_NORMAL": "普通的", + "EDITOR_PT": "点", + "EDITOR_PX": "像素", + "EDITOR_QUOTE": "引用", + "EDITOR_SQUARE": "正方形", + "EDITOR_TEXT": "文本", + "ENTER": "进入", + "ESCAPE": "逃脱", + "Emoji": "表情符号", + "FILE": "文件", + "FILE_COMPRESSING_ER": "压缩文件时出错", + "FILE_DUPLICATE_ER": "文件已附加。请选择不同的文件", + "FILE_OR_URL_MSG": "请输入文件或文件 URL,不能同时输入!", + "FILE_PIXEL_SIZE_ER": "文件像素大小应小于 {0}", + "FILE_PROCESSING_ER": "处理文件时出错", + "FILE_SIZE_CALCULATING_ER": "计算文件大小时出错", + "FILE_UNSUPPORTED": "不允许使用此文件类型", + "FLIP": "翻动", + "FROM": "从", + "Filter": "过滤", + "Flip": "翻动", + "Freeform": "自由形式", + "GALLERY": "画廊", + "GIF": "动图", + "HELP": "救命", + "HIDE": "隐藏", + "IMAGE_OR_URL_MSG": "请输入图像或图像 URL,不能同时输入!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "图像选择器。已选择图片", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "图像选择器。未选择图像", + "IMG_URL_MSG": "请输入图片网址!", + "INSERT_AUDIO": "插入音频", + "INSERT_FILE": "插入文件", + "INSERT_IMAGE": "插入图片", + "INSERT_LINK": "插入链接", + "INSERT_PARAGRAPH": "插入段落", + "INSERT_TABLE": "插入表格", + "INSERT_VIDEO": "插入视频", + "Insert Your Message": "插入您的信息", + "KEY_COMBINATION": "组合键", + "LINK": "关联", + "LINK_NAME": "链接名称", + "LINK_URL": "连结网址", + "LOWERCASE": "小写", + "LOWER_ALPHA": "较低的阿尔法", + "LOWER_ROMAN": "下罗马语", + "MAX_ATTACH_MSG": "已达到附件最大限制", + "MONTH": "月", + "NO_RESULTS": "没有结果", + "NUMBERED": "编号", + "OK": "好的", + "OPEN_IN_NEW_WINDOW": "在新窗口中打开", + "PICK_GIF": "选择一个 Gif", + "PICK_IMAGE": "选择图片", + "REDO_MSG": "重做上一条命令", + "REMOVE": "去掉", + "RESET": "重启", + "REST_COLOR_MSG": "重置为默认颜色", + "ROTATE_LEFT": "向左旋转", + "ROTATE_RIGHT": "右旋", + "Reset": "重启", + "Rotate left": "向左旋转", + "Rotate right": "右旋", + "SANS_SERIF": "无衬线字体", + "SAVE": "保存", + "SEARCH_GIF": "搜索所有 GIF", + "SELECT_FROM_FILES": "从文件中选择", + "SELECT_STL_MSG": "选择列表样式", + "SENTENCE_CASE": "判例", + "SET_COLOR": "设置颜色", + "SHOW": "节目", + "SWIPE_TO_REVEAL_SEMANTIC": "向左滑动以显示操作", + "Select Emoji": "选择表情符号", + "Slider Color": "滑块颜色", + "Slider White Black Color": "滑块白色黑色", + "Square": "正方形", + "TAB": "标签", + "TEXT_TO_DISPLAY": "要显示的文本", + "TIMES_NEW_ROMAN": "英语字体格式一种", + "TITLE_CASE": "标题大小写", + "TO": "至", + "Text": "文本", + "UNDO_MSG": "撤消上一条命令", + "UNKNOWN": "未知", + "UNTAB": "取消制表", + "UPPERCASE": "大写字母", + "UPPER_ALPHA": "上阿尔法", + "UPPER_ROMAN": "上罗马语", + "URL": "网址", + "URL_DUPLICATE_ER": "网址已经附上。请输入不同的网址", + "URL_INVALID_ER": "请输入有效网址", + "URL_MSG": "请输入网址!", + "VIDEO": "视频", + "VIEW": "视图", + "WEEK": "周" +} \ No newline at end of file diff --git a/lib/assets/strings/zh_HK.json b/lib/assets/strings/zh_HK.json new file mode 100644 index 0000000..4d43cf4 --- /dev/null +++ b/lib/assets/strings/zh_HK.json @@ -0,0 +1,140 @@ +{ + "ACTION": "行动", + "ADD_URL": "添加网址", + "ALL": "所有", + "ATTACHED_FILE": "附件", + "ATTACH_MAXSIZE_VALIDATE": "文件大小必须小于 {0}", + "AUDIO_FILE_OR_URL_MSG": "请选择音频文件或输入音频文件 URL!", + "AUDIO_FILE_OR_URL_MSG_NOT_BOTH": "请输入音频文件或音频 URL,不能同时输入两者!", + "Blur": "模糊", + "Blur Radius": "模糊半径", + "Brush": "刷子", + "CAMERA": "相机", + "CANCEL": "取消", + "CHANGE_CASE": "改变大小写", + "CHANGE_IMAGE": "更改图像", + "CHAR_LEFT": "剩余 {0} 个字符", + "CHOOSE_AUDIO": "选择音频", + "CHOOSE_COLOR": "选择颜色", + "CHOOSE_FILE": "选择文件", + "CHOOSE_FILE_URL_MSG": "请选择文件或输入文件 URL!", + "CHOOSE_IMAGE": "选择图片", + "CHOOSE_IMG_URL_MSG": "请选择图片或输入图片网址!", + "CHOOSE_VIDEO": "选择视频", + "CLEAR": "清除", + "CLOSE": "关", + "COMPLETE_EDITING": "完成编辑", + "COURIER": "导游", + "CROP": "作物", + "CUSTOM": "习惯", + "Color Opacity": "颜色不透明度", + "Crop": "作物", + "DEFAULT": "默认", + "DELETE": "删除", + "DELETE_ATTACHMENT": "删除附件", + "DONE": "完成", + "EDIT": "编辑", + "EDITOR_CIRCLE": "圈", + "EDITOR_CODE": "代码", + "EDITOR_DISC": "光盘", + "EDITOR_HEADER_1": "标头 1", + "EDITOR_HEADER_2": "标头 2", + "EDITOR_HEADER_3": "标题 3", + "EDITOR_HEADER_4": "标题 4", + "EDITOR_HEADER_5": "标题 5", + "EDITOR_HEADER_6": "标题 6", + "EDITOR_NORMAL": "普通的", + "EDITOR_PT": "点", + "EDITOR_PX": "像素", + "EDITOR_QUOTE": "引用", + "EDITOR_SQUARE": "正方形", + "EDITOR_TEXT": "文本", + "ENTER": "进入", + "ESCAPE": "逃脱", + "Emoji": "表情符号", + "FILE": "文件", + "FILE_COMPRESSING_ER": "压缩文件时出错", + "FILE_DUPLICATE_ER": "文件已附加。请选择不同的文件", + "FILE_OR_URL_MSG": "请输入文件或文件 URL,不能同时输入!", + "FILE_PIXEL_SIZE_ER": "文件像素大小应小于 {0}", + "FILE_PROCESSING_ER": "处理文件时出错", + "FILE_SIZE_CALCULATING_ER": "计算文件大小时出错", + "FILE_UNSUPPORTED": "不允许使用此文件类型", + "FLIP": "翻动", + "FROM": "从", + "Filter": "过滤", + "Flip": "翻动", + "Freeform": "自由形式", + "GALLERY": "画廊", + "GIF": "动图", + "HELP": "救命", + "HIDE": "隐藏", + "IMAGE_OR_URL_MSG": "请输入图像或图像 URL,不能同时输入!", + "IMAGE_PICKER_SEMANTIC_SELECTED": "图像选择器。已选择图片", + "IMAGE_PICKER_SEMANTIC_UNSELECTED": "图像选择器。未选择图像", + "IMG_URL_MSG": "请输入图片网址!", + "INSERT_AUDIO": "插入音频", + "INSERT_FILE": "插入文件", + "INSERT_IMAGE": "插入图片", + "INSERT_LINK": "插入链接", + "INSERT_PARAGRAPH": "插入段落", + "INSERT_TABLE": "插入表格", + "INSERT_VIDEO": "插入视频", + "Insert Your Message": "插入您的信息", + "KEY_COMBINATION": "组合键", + "LINK": "关联", + "LINK_NAME": "链接名称", + "LINK_URL": "连结网址", + "LOWERCASE": "小写", + "LOWER_ALPHA": "较低的阿尔法", + "LOWER_ROMAN": "下罗马语", + "MAX_ATTACH_MSG": "已达到附件最大限制", + "MONTH": "月", + "NO_RESULTS": "没有结果", + "NUMBERED": "编号", + "OK": "好的", + "OPEN_IN_NEW_WINDOW": "在新窗口中打开", + "PICK_GIF": "选择一个 Gif", + "PICK_IMAGE": "选择图片", + "REDO_MSG": "重做上一条命令", + "REMOVE": "去掉", + "RESET": "重启", + "REST_COLOR_MSG": "重置为默认颜色", + "ROTATE_LEFT": "向左旋转", + "ROTATE_RIGHT": "右旋", + "Reset": "重启", + "Rotate left": "向左旋转", + "Rotate right": "右旋", + "SANS_SERIF": "无衬线字体", + "SAVE": "保存", + "SEARCH_GIF": "搜索所有 GIF", + "SELECT_FROM_FILES": "从文件中选择", + "SELECT_STL_MSG": "选择列表样式", + "SENTENCE_CASE": "判例", + "SET_COLOR": "设置颜色", + "SHOW": "节目", + "SWIPE_TO_REVEAL_SEMANTIC": "向左滑动以显示操作", + "Select Emoji": "选择表情符号", + "Slider Color": "滑块颜色", + "Slider White Black Color": "滑块白色黑色", + "Square": "正方形", + "TAB": "标签", + "TEXT_TO_DISPLAY": "要显示的文本", + "TIMES_NEW_ROMAN": "英语字体格式一种", + "TITLE_CASE": "标题大小写", + "TO": "至", + "Text": "文本", + "UNDO_MSG": "撤消上一条命令", + "UNKNOWN": "未知", + "UNTAB": "取消制表", + "UPPERCASE": "大写字母", + "UPPER_ALPHA": "上阿尔法", + "UPPER_ROMAN": "上罗马语", + "URL": "网址", + "URL_DUPLICATE_ER": "网址已经附上。请输入不同的网址", + "URL_INVALID_ER": "请输入有效网址", + "URL_MSG": "请输入网址!", + "VIDEO": "视频", + "VIEW": "视图", + "WEEK": "周" +} \ No newline at end of file diff --git a/lib/src/components/atoms.dart b/lib/src/components/atoms.dart new file mode 100644 index 0000000..331765a --- /dev/null +++ b/lib/src/components/atoms.dart @@ -0,0 +1,21 @@ +// Atoms are basic building blocks of matter. These are the most basic single components. + +export 'atoms/absorb_pointer.dart'; +export 'atoms/avatar.dart'; +export 'atoms/back_button.dart'; +export 'atoms/button.dart'; +export 'atoms/card.dart'; +export 'atoms/dashed_line.dart'; +export 'atoms/expandable.dart'; +export 'atoms/floating_action_button.dart'; +export 'atoms/icon_text_button.dart'; +export 'atoms/index.dart'; +export 'atoms/label.dart'; +export 'atoms/notification.dart'; +export 'atoms/popover.dart'; +export 'atoms/selection_pills.dart'; +export 'atoms/shake_animation.dart'; +export 'atoms/slidable_widget.dart'; +export 'atoms/tab.dart'; +export 'atoms/toggle_button.dart'; +export 'atoms/unread_badge_widget.dart'; diff --git a/lib/src/components/atoms/absorb_pointer.dart b/lib/src/components/atoms/absorb_pointer.dart new file mode 100644 index 0000000..d2f95bb --- /dev/null +++ b/lib/src/components/atoms/absorb_pointer.dart @@ -0,0 +1,68 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// A widget that absorbs pointers during hit testing. +/// +/// When [absorbing] is true, this widget prevents its subtree from receiving +/// pointer events by terminating hit testing at itself. It still consumes space +/// during layout and paints its child as usual. It just prevents its children +/// from being the target of located events, because it returns true from +/// [RenderBox.hitTest]. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=65HoWqBboI8} +/// +/// {@tool dartpad} +/// The following sample has an [AbsorbPointer] widget wrapping the button on +/// top of the stack, which absorbs pointer events, preventing its child button +/// __and__ the button below it in the stack from receiving the pointer events. +/// +/// ** See code in examples/api/lib/widgets/basic/absorb_pointer.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [IgnorePointer], which also prevents its children from receiving pointer +/// events but is itself invisible to hit testing. +class ZdsAbsorbPointer extends StatelessWidget { + /// Creates a widget that absorbs pointers during hit testing. + /// + /// The [absorbing] argument must not be null. + const ZdsAbsorbPointer({ + required this.child, + super.key, + this.absorbing = true, + this.duration = const Duration(milliseconds: 250), + }); + + /// Whether this widget absorbs pointers during hit testing. + /// + /// Regardless of whether this render object absorbs pointers during hit + /// testing, it will still consume space during layout and be visible during + /// painting. + final bool absorbing; + + /// The widget whose events should be prevented + final Widget child; + + /// Opacity duration + final Duration duration; + + @override + Widget build(BuildContext context) { + return AbsorbPointer( + absorbing: absorbing, + child: AnimatedOpacity( + duration: duration, + opacity: absorbing ? 0.4 : 1, + child: child, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('duration', duration)); + properties.add(DiagnosticsProperty('absorbing', absorbing)); + } +} diff --git a/lib/src/components/atoms/avatar.dart b/lib/src/components/atoms/avatar.dart new file mode 100644 index 0000000..3dcd74f --- /dev/null +++ b/lib/src/components/atoms/avatar.dart @@ -0,0 +1,102 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zds_flutter.dart'; + +/// A circular container used to display a user's profile picture or initials. +/// +/// When both [image] and [initials] are not null, this widget will prioritize showing the [image]. If both [image] and +/// [initials] are null, the widget will show a circle with a plain background using [backgroundColor]. +/// +/// It's possible to pass an [onTap] function that will be called whenever the user taps on the avatar. This is +/// typically used for accessing the user's profile page, or toggling selection in [ZdsSelectableListTile]. +/// +/// See also: +/// +/// * [ZdsNetworkAvatar], an avatar that fetches the image from an URL. +/// * [ZdsSelectableListTile], where [ZdsAvatar] is used as the [ZdsSelectableListTile.leading] widget. +/// * [ZdsProfile], where [ZdsAvatar] can be used for [ZdsProfile.avatar]. +/// * [computeForeground], a function used to calculate the text color for any background color. +class ZdsAvatar extends StatelessWidget implements PreferredSizeWidget { + /// An image that will fill the entire avatar. As the avatar is circular, a square image will not get its corners + /// shown, but the original image will be intact. + /// + /// If [image] and [initials] are both not null, [image] will get priority. + final Image? image; + + /// The user's initials. Typically up to 3 initials are used, although it can be higher if necessary by changing the + /// [textStyle] to use a smaller font size and weight. + /// + /// If [image] and [initials] are both not null, [image] will get priority. + final String? initials; + + /// A function called whenever the user taps on the avatar. + final VoidCallback? onTap; + + /// The avatar's size. + /// + /// Must be greater than 0. Defaults to 48 dp. + final double? size; + + /// The textStyle used for the [initials], if shown. + /// + /// Defaults to [TextTheme.displaySmall]. + final TextStyle? textStyle; + + /// The background color of the avatar if [initials] are used. + /// + /// Defaults to [ColorScheme.secondary]. + final Color? backgroundColor; + + /// Displays either initials or an image in an optionally tappable circular container. + /// If given both [initials] and [image], the avatar will always show [image]. + /// + /// If [size] is not null it must be greater than 0. + const ZdsAvatar({ + super.key, + this.image, + this.initials, + this.onTap, + this.size, + this.textStyle, + this.backgroundColor, + }) : assert(size != null ? size > 0 : size == null, 'Size must be greater than 0'); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + height: size ?? preferredSize.height, + width: size ?? preferredSize.width, + decoration: BoxDecoration( + color: backgroundColor ?? Theme.of(context).colorScheme.secondary, + image: image != null ? DecorationImage(image: image!.image, fit: BoxFit.cover) : null, + shape: BoxShape.circle, + ), + child: (image == null && initials != null) + ? Center( + child: Text( + initials!, + style: textStyle ?? + Theme.of(context).textTheme.displaySmall!.copyWith( + color: computeForeground(backgroundColor ?? Theme.of(context).colorScheme.secondary), + ), + ), + ) + : null, + ), + ); + } + + @override + Size get preferredSize => const Size(48, 48); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('initials', initials)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DoubleProperty('size', size)); + properties.add(DiagnosticsProperty('textStyle', textStyle)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/lib/src/components/atoms/back_button.dart b/lib/src/components/atoms/back_button.dart new file mode 100644 index 0000000..e13c042 --- /dev/null +++ b/lib/src/components/atoms/back_button.dart @@ -0,0 +1,40 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A back button that will, by default, call [Navigator.maybePop] when pressed. +class ZdsBackButton extends StatelessWidget { + /// The function to be called whenever the user presses this button. + /// + /// Calls [Navigator.maybePop] by default. + final VoidCallback? onPressed; + + /// A back button that will, by default, call [Navigator.maybePop] when pressed. + const ZdsBackButton({super.key, this.onPressed}); + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context), 'Localizations must be initialized'); + + return IconButton( + icon: const Icon(ZdsIcons.back), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + onPressed: () { + if (onPressed != null) { + onPressed!(); + } else { + unawaited(Navigator.maybePop(context)); + } + }, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('onPressed', onPressed)); + } +} diff --git a/lib/src/components/atoms/button.dart b/lib/src/components/atoms/button.dart new file mode 100644 index 0000000..440805c --- /dev/null +++ b/lib/src/components/atoms/button.dart @@ -0,0 +1,407 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Variants of ZdsButton. +enum _ZdsButtonVariant { + /// Filled button where the background is the defined color. + filled, + + /// Outlined button where the outline border is the defined color, and the background is transparent. + outlined, + + /// Text button equivalent with Zds styling. + text, + + /// Non filled button with a border and muted grey colors. + muted, +} + +// TODO(colors): Add Zeta. + +/// An ElevatedButton with pre-applied Zds styling. +/// +/// There are four variants: +/// * [ZdsButton], which returns a filled button. +/// * [ZdsButton.outlined], which returns a non-filled button with a border. +/// * [ZdsButton.text], which returns the equivalent of a TextButton with Zds styling. +/// * [ZdsButton.muted], which returns a non-filled button with a border and muted grey colors. +/// These variants can be thought of being the primary, secondary, tertiary, and quarternary buttons respectively. +/// +/// If [isDangerButton] is true, it will make the button red, used for destructive actions (deleting, removing, etc...). +/// [isDangerButton] can only be set to true if the variant used is either [ZdsButton] or [ZdsButton.outlined]. +/// +/// If [onTap] and [onLongPress] are null, the button will be disabled and change its appareance to a disabled state. +/// +/// The other parameters act the same way as the ones in [ElevatedButton]. +class ZdsButton extends StatelessWidget { + /// The Widget that will go inside the button, typically a [Text] with style [Theme.textTheme.titleMedium]. + /// + /// Must not be null. + final Widget child; + + /// Whether to use danger/red colors. If true, the variant used must be either [ZdsButton] or [ZdsButton.outlined]. + /// + /// Defaults to false. + final bool isDangerButton; + + /// Whether to use white text as the button is on a dark background. Can only be used with [ZdsButton.text] + final bool isOnDarkBackground; + + /// Whether to autofocus on this button. + /// + /// Defaults to false. + final bool autofocus; + + /// Whether to clip the contents of this button. + /// + /// Defaults to [Clip.none] + final Clip clipBehavior; + + /// FocusNode for the button. + /// + /// The [autofocus] and [clipBehavior] arguments must not be null. + final FocusNode? focusNode; + + /// If this and [onTap] are null, the button will be disabled. + final VoidCallback? onLongPress; + + /// Called whenever a pointer enters or exits the button response area, with true if a pointer has entered this + /// button and false if it has exited it. + final ValueChanged? onHover; + + /// Called when the focus changes, with true if this widget's node gains focus, and false if it loses focus. + final ValueChanged? onFocusChange; + + /// If this and [onLongPress] are null, the button will be disabled. + final VoidCallback? onTap; + + /// padding for the text within the button + final EdgeInsets? textPadding; + + /// Custom color override. + /// + /// Changes: + /// * [ZdsButton.filled] - background color. + /// * [ZdsButton.text] - text color. + /// * [ZdsButton.outlined - outline color. + /// * [ZdsButton.muted] - no change. + final Color? customColor; + + /// This is for talkback text on child. + final String? semanticLabel; + + final _ZdsButtonVariant _variant; + + /// Creates a filled ZdsButton. (Primary button). Use [ZdsButton.filled] until old buttons are fully removed. + /// Currently, this acts as a backward compatible constructor for the old buttons. + /// + /// The [child] argument, usually a [Text], must not be null. + const ZdsButton({ + required this.child, + super.key, + this.onTap, + this.isDangerButton = false, + this.autofocus = false, + this.clipBehavior = Clip.none, + this.focusNode, + this.onLongPress, + this.onHover, + this.onFocusChange, + this.textPadding, + this.semanticLabel, + }) : _variant = _ZdsButtonVariant.filled, + isOnDarkBackground = false, + customColor = null; + + /// Creates a filled ZdsButton. (Primary button). Will be removed when old button versions are fully removed. + /// Will be replaced with [ZdsButton] constructor. + /// + /// The [child] argument, usually a [Text], must not be null. + const ZdsButton.filled({ + required this.child, + super.key, + this.onTap, + this.isDangerButton = false, + this.autofocus = false, + this.clipBehavior = Clip.none, + this.focusNode, + this.onLongPress, + this.onHover, + this.onFocusChange, + this.textPadding, + this.customColor, + this.semanticLabel, + }) : _variant = _ZdsButtonVariant.filled, + isOnDarkBackground = false; + + /// Creates an outlined ZdsButton. (Secondary button) + /// + /// The [child] argument, usually a [Text], must not be null. + const ZdsButton.outlined({ + required this.child, + super.key, + this.onTap, + this.isDangerButton = false, + this.autofocus = false, + this.clipBehavior = Clip.none, + this.focusNode, + this.onLongPress, + this.onHover, + this.onFocusChange, + this.textPadding, + this.customColor, + this.semanticLabel, + }) : _variant = _ZdsButtonVariant.outlined, + isOnDarkBackground = false; + + /// Creates a ZdsButton that behaves as a TextButton. (Tertiary button) + /// + /// The [child] argument must not be null. + const ZdsButton.text({ + required this.child, + super.key, + this.isOnDarkBackground = false, + this.onTap, + this.autofocus = false, + this.clipBehavior = Clip.none, + this.focusNode, + this.onLongPress, + this.onHover, + this.onFocusChange, + this.textPadding, + this.customColor, + this.semanticLabel, + }) : _variant = _ZdsButtonVariant.text, + isDangerButton = false; + + /// Constructs a muted ZdsButton. (Quaternary button) + /// + /// The [child] argument must not be null. + const ZdsButton.muted({ + required this.child, + super.key, + this.onTap, + this.autofocus = false, + this.clipBehavior = Clip.none, + this.focusNode, + this.onLongPress, + this.onHover, + this.onFocusChange, + this.textPadding, + this.customColor, + this.semanticLabel, + }) : _variant = _ZdsButtonVariant.muted, + isOnDarkBackground = false, + isDangerButton = false; + + @override + Widget build(BuildContext context) { + final isChildText = child is Text; + return Semantics( + label: semanticLabel ?? (isChildText ? (child as Text).data : ''), + button: true, + onTap: onTap, + onLongPress: onLongPress, + excludeSemantics: true, + child: ElevatedButton( + key: key, + autofocus: autofocus, + focusNode: focusNode, + onPressed: onTap, + onLongPress: onLongPress, + onFocusChange: onFocusChange, + onHover: onHover, + style: _getStyle(context, textPadding), + clipBehavior: clipBehavior, + child: child, + ), + ); + } + + ButtonStyle _getStyle(BuildContext context, EdgeInsetsGeometry? tp) { + final textPadding = tp ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 6); + + const highlightBlueColor = Color(0xffB7DBFF); // TODO(colors): replace with theme color + final errorColor = Theme.of(context).colorScheme.error; + final onErrorColor = Theme.of(context).colorScheme.onError; + final defaultBackgroundColor = Theme.of(context).elevatedButtonTheme.style!.backgroundColor!.resolve({})!; + switch (_variant) { + case _ZdsButtonVariant.filled: + return ButtonStyle( + padding: MaterialStateProperty.all(textPadding), + textStyle: MaterialStateProperty.resolveWith( + (states) => Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), + ), + foregroundColor: MaterialStateProperty.resolveWith((Set states) { + return isDangerButton ? onErrorColor : computeForeground(defaultBackgroundColor); + }), + backgroundColor: MaterialStateProperty.resolveWith((Set states) { + if (customColor != null) { + return customColor!; + } + return isDangerButton + ? states.contains(MaterialState.disabled) + ? errorColor.withOpacity(0.33) + : errorColor + : states.contains(MaterialState.disabled) + ? defaultBackgroundColor.withOpacity(0.33) + : defaultBackgroundColor; + }), + overlayColor: MaterialStateProperty.resolveWith( + (Set states) { + final Color backgroundColor = isDangerButton ? errorColor : defaultBackgroundColor; + if (states.contains(MaterialState.pressed)) { + return backgroundColor.withLight(0.7, background: ZdsColors.black); + } + if (states.contains(MaterialState.hovered)) { + return backgroundColor.withLight(0.85, background: ZdsColors.black); + } + return null; // Use the component's default. + }, + ), + side: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.focused)) { + return const BorderSide(color: highlightBlueColor, width: 3); + } + return null; + }), + ); + case _ZdsButtonVariant.outlined: + return ButtonStyle( + padding: MaterialStateProperty.all(textPadding), + textStyle: MaterialStateProperty.resolveWith( + (states) => Theme.of(context).textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w500, + color: customColor, + ), + ), + foregroundColor: MaterialStateProperty.resolveWith((Set states) { + if (customColor != null) { + return customColor; + } + return isDangerButton + ? states.contains(MaterialState.disabled) + ? errorColor.withOpacity(0.33) + : errorColor + : states.contains(MaterialState.disabled) + ? defaultBackgroundColor.withOpacity(0.33) + : defaultBackgroundColor; + }), + backgroundColor: MaterialStateProperty.all(ZdsColors.transparent), + side: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.focused)) { + return const BorderSide(color: highlightBlueColor, width: 3); + } + if (customColor != null) { + return BorderSide(color: customColor!); + } + return isDangerButton + ? BorderSide(color: states.contains(MaterialState.disabled) ? errorColor.withOpacity(0.33) : errorColor) + : BorderSide( + color: states.contains(MaterialState.disabled) + ? defaultBackgroundColor.withOpacity(0.33) + : defaultBackgroundColor, + ); + }), + overlayColor: MaterialStateProperty.resolveWith( + (Set states) { + final Color backgroundColor = isDangerButton ? errorColor : defaultBackgroundColor; + if (states.contains(MaterialState.pressed)) { + return backgroundColor.withOpacity(0.2); + } + if (states.contains(MaterialState.hovered)) { + return backgroundColor.withOpacity(0.1); + } + return null; // Use the component's default. + }, + ), + ); + case _ZdsButtonVariant.text: + return ButtonStyle( + padding: MaterialStateProperty.all(textPadding), + textStyle: MaterialStateProperty.resolveWith( + (states) => Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w500), + ), + foregroundColor: MaterialStateProperty.resolveWith((Set states) { + final foregroundColor = isOnDarkBackground ? ZdsColors.white : defaultBackgroundColor; + if (customColor != null) { + return customColor; + } + if (states.contains(MaterialState.disabled)) { + return foregroundColor.withOpacity(0.33); + } + return foregroundColor; + }), + backgroundColor: MaterialStateProperty.all(ZdsColors.transparent), + overlayColor: MaterialStateProperty.resolveWith( + (Set states) { + final Color overlayColor = defaultBackgroundColor; + if (states.contains(MaterialState.pressed)) { + return overlayColor.withOpacity(0.2); + } + if (states.contains(MaterialState.hovered)) { + return overlayColor.withOpacity(0.1); + } + return null; // Use the component's default. + }, + ), + side: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.focused)) { + return const BorderSide(color: highlightBlueColor, width: 3); + } + return null; + }), + ); + case _ZdsButtonVariant.muted: + return ButtonStyle( + padding: MaterialStateProperty.all(textPadding), + textStyle: MaterialStateProperty.resolveWith( + (states) => Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w500), + ), + foregroundColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return ZdsColors.blueGrey.withOpacity(0.33); + } + return ZdsColors.blueGrey; + }), + backgroundColor: MaterialStateProperty.all(ZdsColors.transparent), + side: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.focused)) { + return const BorderSide(color: highlightBlueColor, width: 3); + } + return BorderSide(color: ZdsColors.lightGrey); + }), + overlayColor: MaterialStateProperty.resolveWith( + (Set states) { + if (states.contains(MaterialState.pressed)) { + return ZdsColors.greySwatch(context)[200]; + } + if (states.contains(MaterialState.hovered)) { + return ZdsColors.greySwatch(context)[100]; + } + return null; // Use the component's default. + }, + ), + ); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('isDangerButton', isDangerButton)); + properties.add(DiagnosticsProperty('isOnDarkBackground', isOnDarkBackground)); + properties.add(DiagnosticsProperty('autofocus', autofocus)); + properties.add(EnumProperty('clipBehavior', clipBehavior)); + properties.add(DiagnosticsProperty('focusNode', focusNode)); + properties.add(ObjectFlagProperty.has('onLongPress', onLongPress)); + properties.add(ObjectFlagProperty?>.has('onHover', onHover)); + properties.add(ObjectFlagProperty?>.has('onFocusChange', onFocusChange)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DiagnosticsProperty('textPadding', textPadding)); + properties.add(ColorProperty('customColor', customColor)); + properties.add(StringProperty('semanticLabel', semanticLabel)); + } +} diff --git a/lib/src/components/atoms/card.dart b/lib/src/components/atoms/card.dart new file mode 100644 index 0000000..db33a94 --- /dev/null +++ b/lib/src/components/atoms/card.dart @@ -0,0 +1,177 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Variants of [ZdsCard]. +enum ZdsCardVariant { + /// Creates a card with a border on all edges of color [ZdsColors.greySwatch.shade600]. + outlined, + + /// Creates a card with a box shadow around the edges with a radius of 4. + /// + /// Shadow color defaults to [CardTheme.shadowColor] but can be overridden in theme. + elevated, +} + +/// A card used to display grouped information and related actions. +/// +/// Cards provide a way to group information together and allow navigating large quantities of data. For example, +/// cards can be used to show information about an employee's shift, including the starting and ending time, the date, +/// and the current status. Cards make parsing information easier and faster than with a traditional table. +/// +/// For cards to be effective, an inner hierarchy is necessary. This can be achieved by organizing the information +/// shown in a card with a title, the supporting content, and actions: +/// +/// ```dart +/// ZdsCard( +/// child: Column( +/// children: [ +/// // Key information like the title can go here so the user understands the card's content in a glance +/// ZdsCardHeader(), +/// // The supporting content of the card (i.e. in a card about a delivery, its status would go here) +/// Text(), +/// // Related actions are placed at the end as they're only useful after the user has processed the rest of the +/// // card's information +/// Row(children:[ZdsButton(), ZdsButton()]) +/// ] +/// ) +/// ) +/// ``` +/// +/// This widget also integrates [Semantics] through [onTapHint], which is highly recommended to use so Talkback and +/// Voiceover users have an adequate experience. +/// +/// See also: +/// +/// * [ZdsCardHeader], used to create a title header in a card +/// * [ZdsCardWithActions], a [ZdsCard] variant with an actions/status bar at the bottom. +class ZdsCard extends StatelessWidget { + /// The card's contents. + /// + /// Typically a [Row] or a [Column] so information is organized in a hierarchy from start to end. + final Widget? child; + + /// The background color of the card. + /// + /// Defaults to [ColorScheme.surface]. If [gradient] is specified, this has no effect. + final Color? backgroundColor; + + /// A function called whenever the user taps on the card. + /// + /// If [onTap] is not null, it's highly recommended to use [onTapHint] as well for accessibility purposes. + final VoidCallback? onTap; + + /// A gradient to apply to the card's background + /// + /// If this is specified, [backgroundColor] has no effect. + final Gradient? gradient; + + /// A String used by [Semantics] to provide a hint of what tapping on this Card will do. + /// + /// For more information, look at the [Semantics] class. + final String? onTapHint; + + /// Empty space to inscribe inside this widget. + /// + /// Defaults to EdgeInsets.symmetric(horizontal: 24, vertical 20). + final EdgeInsets padding; + + /// {@template card-variant} + /// Whether to use an outlined or elevated card. + /// + /// Defaults to [ZdsCardVariant.elevated]. + /// {@endtemplate} + final ZdsCardVariant variant; + + /// Margin surrounding the outside of the card + /// + /// Defaults to [CardTheme.margin]. + final EdgeInsets? margin; + + /// The semantic label applied to the card. + /// + /// If not null, the semantics in the card will be excluded. + final String? semanticLabel; + + /// Creates a card to display information. + /// + /// [padding] and [variant] must not be null. + const ZdsCard({ + super.key, + this.child, + this.onTap, + this.backgroundColor, + this.gradient, + this.onTapHint, + this.padding = const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + this.variant = ZdsCardVariant.elevated, + this.margin, + this.semanticLabel, + }); + + @override + Widget build(BuildContext context) { + final borderRadius = (Theme.of(context).cardTheme.shape as RoundedRectangleBorder?)?.borderRadius as BorderRadius?; + final shadowColor = Theme.of(context).cardTheme.shadowColor; + final container = Container( + clipBehavior: Clip.antiAlias, + margin: margin ?? Theme.of(context).cardTheme.margin, + decoration: BoxDecoration( + color: backgroundColor ?? Theme.of(context).colorScheme.surface, + gradient: gradient, + borderRadius: borderRadius, + border: variant == ZdsCardVariant.outlined + ? Border.all( + color: ZdsColors.greySwatch( + context, + )[Theme.of(context).colorScheme.brightness == Brightness.dark ? 1000 : 600]!, + ) + : null, + boxShadow: [ + if (shadowColor != null && variant == ZdsCardVariant.elevated) BoxShadow(color: shadowColor, blurRadius: 4), + ], + ), + child: Material( + color: ZdsColors.transparent, + child: Semantics( + onTapHint: onTapHint, + child: InkWell( + splashColor: ZdsColors.splashColor, + hoverColor: Colors.transparent, + onTap: onTap ?? () {}, + child: Padding( + padding: padding, + child: child, + ), + ), + ), + ), + ); + + if (semanticLabel != null) { + return Semantics( + label: semanticLabel, + onTapHint: onTapHint, + onTap: onTap, + excludeSemantics: true, + child: container, + ); + } else { + return container; + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DiagnosticsProperty('gradient', gradient)); + properties.add(StringProperty('onTapHint', onTapHint)); + properties.add(DiagnosticsProperty('padding', padding)); + properties.add(EnumProperty('variant', variant)); + properties.add(DiagnosticsProperty('margin', margin)); + properties.add(StringProperty('semanticLabel', semanticLabel)); + } +} diff --git a/lib/src/components/atoms/dashed_line.dart b/lib/src/components/atoms/dashed_line.dart new file mode 100644 index 0000000..c331810 --- /dev/null +++ b/lib/src/components/atoms/dashed_line.dart @@ -0,0 +1,279 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Corner attributes for the [ZdsDashedLine]. +class ZdsDashedLineCorner { + /// Corner radius for top left corner. + final double topLeft; + + /// Corner radius for top right corner. + final double topRight; + + /// Corner radius for bottom right corner. + final double bottomRight; + + /// Corner radius for bottom left corner. + final double bottomLeft; + + /// Specify the size of each rounded corner. + const ZdsDashedLineCorner({ + this.topLeft = 0, + this.topRight = 0, + this.bottomRight = 0, + this.bottomLeft = 0, + }); + + /// Set all rounded corners to one size. + const ZdsDashedLineCorner.all(double radius) + : topLeft = radius, + topRight = radius, + bottomRight = radius, + bottomLeft = radius; +} + +/// A widget for creating dashed line or a container +/// +/// [ZdsDashedLine] provides developers with the ability to create dashed lines. It also supports creating a dashed +/// border for a [Widget]. Support for controlling the thickness, spacing, and corners of the dotted border. +/// +/// ```dart +/// ZdsDashedLine( +/// corner: const ZdsDashedLineCorner.all(8), +/// color: Colors.green, +/// strokeWidth: 1.5, +/// child: SizedBox( +/// height: 200, +/// width: 200, +/// ), +/// ) +/// ``` +/// +/// +/// This widget can be used as dashed line or dashed container. When supplied with [height] or [child] it will be a +/// Container. +class ZdsDashedLine extends StatefulWidget { + /// Dotted line color. + /// + /// Defaults to [ColorScheme.onSurface]. + final Color? color; + + /// Height. + /// + /// If there is only [height] and no [width], you will get a dotted line in the vertical direction. If there are both + /// [width] and [height], you will get a dotted border. + final double? height; + + /// Width. + /// + /// If there is only [width] and no [height], you will get a dotted line in the horizontal direction. If there are + /// both [width] and [height], you will get a dotted border. + final double? width; + + /// The thickness of the dotted line. + /// + /// Defaults to 1.0. + final double strokeWidth; + + /// The length of each small segment in the dotted line. + /// + /// Defaults to 5.0. + final double dottedLength; + + /// The distance between each segment in the dotted line. + /// + /// Defaults to 3.0. + final double space; + + /// The corners of the dotted border. See [ZdsDashedLineCorner] for details. + final ZdsDashedLineCorner? corner; + + /// If [child] is set, [ZdsDashedLine] will serve as the dotted border of [child]. + /// At this time, [width] and [height] will no longer be valid. + final Widget? child; + + /// A widget for creating dashed line or a container + /// + /// [color], [strokeWidth], [dottedLength], and [space] must not be null. + const ZdsDashedLine({ + super.key, + this.child, + this.color, + this.height, + this.width, + this.dottedLength = 5.0, + this.space = 3.0, + this.strokeWidth = 1.0, + this.corner, + }) : assert( + width != null || height != null || child != null, + 'Either width, height, or child must not be null else nothing would be rendered.', + ); + + @override + ZdsDashedLineState createState() => ZdsDashedLineState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('color', color)) + ..add(DoubleProperty('height', height)) + ..add(DoubleProperty('width', width)) + ..add(DoubleProperty('strokeWidth', strokeWidth)) + ..add(DoubleProperty('dottedLength', dottedLength)) + ..add(DoubleProperty('space', space)) + ..add(DiagnosticsProperty('corner', corner)); + } +} + +/// State for [ZdsDashedLine]. +class ZdsDashedLineState extends State { + static bool _isEmpty(double? d) { + return d == null || d == 0.0; + } + + double? _childWidth; + double? _childHeight; + final GlobalKey _childKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + if (_isEmpty(widget.width) && _isEmpty(widget.height) && widget.child == null) return const SizedBox(); + if (widget.child != null) { + tryToGetChildSize(); + final children = [ + Container( + clipBehavior: widget.corner == null ? Clip.none : Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(widget.corner?.topLeft ?? 0.0), + topRight: Radius.circular(widget.corner?.topRight ?? 0.0), + bottomLeft: Radius.circular(widget.corner?.bottomLeft ?? 0.0), + bottomRight: Radius.circular(widget.corner?.bottomRight ?? 0.0), + ), + ), + key: _childKey, + child: widget.child, + ), + ]; + + if (_childWidth != null && _childHeight != null) { + children.add(dashPath(width: _childWidth!, height: _childHeight!)); + } + + return Stack(children: children); + } else { + return dashPath(width: widget.width!, height: widget.height!); + } + } + + /// Attempts to get the [_childWidth] and [_childHeight] of the child to be wrapped with a dashed line. + void tryToGetChildSize() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + try { + final box = _childKey.currentContext?.findRenderObject() as RenderBox?; + final tempWidth = box?.size.width ?? 0.0; + final tempHeight = box?.size.height ?? 0.0; + final needUpdate = tempWidth != _childWidth || tempHeight != _childHeight; + if (needUpdate) { + setState(() { + _childWidth = tempWidth; + _childHeight = tempHeight; + }); + } + } catch (e) { + debugPrint(e.toString()); + } + }); + } + + /// Creates a dashed path line. + CustomPaint dashPath({required double width, required double height}) { + return CustomPaint( + size: Size(_isEmpty(width) ? widget.strokeWidth : width, _isEmpty(height) ? widget.strokeWidth : height), + foregroundPainter: _DashedLinePainter() + ..color = widget.color ?? Theme.of(context).colorScheme.onSurface + ..dottedLength = widget.dottedLength + ..space = widget.space + ..strokeWidth = widget.strokeWidth + ..corner = widget.corner + ..isShape = !_isEmpty(height) && !_isEmpty(width), + ); + } +} + +class _DashedLinePainter extends CustomPainter { + late Color color; + double dottedLength = 5; + double space = 3; + double strokeWidth = 1; + bool isShape = false; + ZdsDashedLineCorner? corner; + Radius topLeft = Radius.zero; + Radius topRight = Radius.zero; + Radius bottomRight = Radius.zero; + Radius bottomLeft = Radius.zero; + + @override + void paint(Canvas canvas, Size size) { + final isHorizontal = size.width > size.height; + final paint = Paint() + ..isAntiAlias = true + ..color = color + ..filterQuality = FilterQuality.high + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth; + + if (!isShape) { + /// line + final length = isHorizontal ? size.width : size.height; + final count = length / (dottedLength + space); + if (count < 2.0) return; + var startOffset = Offset.zero; + for (var i = 0; i < count.toInt(); i++) { + canvas.drawLine( + startOffset, + startOffset.translate(isHorizontal ? dottedLength : 0, isHorizontal ? 0 : dottedLength), + paint, + ); + startOffset = + startOffset.translate(isHorizontal ? (dottedLength + space) : 0, isHorizontal ? 0 : (dottedLength + space)); + } + } else { + /// shape + final path = Path() + ..addRRect( + RRect.fromLTRBAndCorners( + 0, + 0, + size.width, + size.height, + topLeft: Radius.circular(corner?.topLeft ?? 0.0), + topRight: Radius.circular(corner?.topRight ?? 0.0), + bottomLeft: Radius.circular(corner?.bottomLeft ?? 0.0), + bottomRight: Radius.circular(corner?.bottomRight ?? 0.0), + ), + ); + + final draw = buildDashPath(path, dottedLength, space); + canvas.drawPath(draw, paint); + } + } + + Path buildDashPath(Path path, double dottedLength, double space) { + final r = Path(); + for (final metric in path.computeMetrics()) { + var start = 0.0; + while (start < metric.length) { + final end = start + dottedLength; + r.addPath(metric.extractPath(start, end), Offset.zero); + start = end + space; + } + } + return r; + } + + @override + bool shouldRepaint(_DashedLinePainter oldDelegate) { + return true; + } +} diff --git a/lib/src/components/atoms/expandable.dart b/lib/src/components/atoms/expandable.dart new file mode 100644 index 0000000..6fcfe61 --- /dev/null +++ b/lib/src/components/atoms/expandable.dart @@ -0,0 +1,324 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; +import '../../utils/tools/measure.dart'; + +const Duration _kFadeDuration = Duration(milliseconds: 200); + +/// A component that can be collapsed and uncollapsed. +/// +/// This is typically used with long [Text] widgets, where their length might complicate navigation. +/// +/// When collapsed, the component will take the height of [minHeight]. If the [child]'s height does not exceed +/// [minHeight], then no button to collapse/expand the widget will be shown. +/// +/// See also: +/// +/// * [readMore], an alternative way of making a collapsible widget. +class ZdsExpandable extends StatelessWidget { + /// The text to show in the button when the [child] is collapsed. + final String collapsedButtonText; + + /// The text to show in the button when the [child] is expanded. + final String expandedButtonText; + + /// The [child]'s height when it's in its collapsed state. + /// + /// Defaults to 60. + final double minHeight; + + /// The child that will be collapsed and extended. + /// + /// Typically a [Text]. + final Widget child; + + /// The color to be used for the fadeout gradient indicating the widget is collapsed. + /// + /// Defaults to [ColorScheme.background]. + final Color? color; + + /// A widget that can be collapsed and expanded. + const ZdsExpandable({ + required this.child, + super.key, + this.collapsedButtonText = '', + this.expandedButtonText = '', + this.minHeight = 60, + this.color, + }); + + @override + Widget build(BuildContext context) { + return child.readMore( + collapsedButtonText: collapsedButtonText, + expandedButtonText: expandedButtonText, + color: color ?? Theme.of(context).colorScheme.background, + minHeight: minHeight, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('collapsedButtonText', collapsedButtonText)) + ..add(StringProperty('expandedButtonText', expandedButtonText)) + ..add(DoubleProperty('minHeight', minHeight)) + ..add(ColorProperty('color', color)); + } +} + +class _ExpandableContainer extends StatefulWidget { + final String collapsedButtonText; + final String expandedButtonText; + final double minHeight; + final Widget child; + final Color color; + + const _ExpandableContainer({ + required this.collapsedButtonText, + required this.expandedButtonText, + required this.minHeight, + required this.child, + required this.color, + }); + + @override + _ExpandableContainerState createState() => _ExpandableContainerState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('collapsedButtonText', collapsedButtonText)) + ..add(StringProperty('expandedButtonText', expandedButtonText)) + ..add(DoubleProperty('minHeight', minHeight)) + ..add(ColorProperty('color', color)); + } +} + +class _ExpandableContainerState extends State<_ExpandableContainer> with SingleTickerProviderStateMixin { + bool isExpanded = false; + double _textHeight = 0; + Animation? _sizeAnimation; + late AnimationController _controller; + final _keyText = GlobalKey(); + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: _kFadeDuration); + _controller.addListener(onControllerValue); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _afterLayout(_) { + final RenderBox renderBox = _keyText.currentContext!.findRenderObject()! as RenderBox; + _textHeight = renderBox.size.height; + _sizeAnimation = Tween(begin: widget.minHeight, end: _textHeight).animate(_controller); + } + + void onControllerValue() { + setState(() {}); + } + + double get textHeight => _sizeAnimation?.value ?? widget.minHeight; + + @override + Widget build(BuildContext context) { + return _ExpandableClip( + color: widget.color, + isExpanded: isExpanded, + contentKey: _keyText, + button: TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).elevatedButtonTheme.style!.backgroundColor!.resolve({}), + backgroundColor: Colors.transparent, + ), + onPressed: isExpanded ? collapse : expand, + child: AnimatedCrossFade( + duration: _kFadeDuration, + crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, + firstChild: Text(widget.collapsedButtonText), + secondChild: Text(widget.expandedButtonText), + ), + ).frame(alignment: Alignment.center), + height: textHeight, + child: widget.child, + ); + } + + void collapse() { + setState(() { + isExpanded = false; + }); + _controller.reverse(); + } + + void expand() { + setState(() { + isExpanded = true; + }); + _controller.forward(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('isExpanded', isExpanded)); + properties.add(DoubleProperty('textHeight', textHeight)); + } +} + +class _ExpandableClip extends StatelessWidget { + final Widget child; + final Widget button; + final double height; + final bool isExpanded; + final Color color; + final Key? contentKey; + + const _ExpandableClip({ + required this.height, + required this.button, + required this.color, + required this.child, + this.contentKey, + this.isExpanded = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ClipRect( + child: SizedOverflowBox( + // this is so that I can measure the real height + alignment: Alignment.topCenter, + size: Size(double.infinity, height), + child: Container( + key: contentKey, + child: child, + ), + ), + ), + // overflow + SizedBox( + height: 50, + child: Stack( + fit: StackFit.expand, + clipBehavior: Clip.none, + children: [ + Positioned( + top: -55, + height: 55, + left: 0, + right: 0, + child: AnimatedOpacity( + duration: _kFadeDuration, + opacity: isExpanded ? 0 : 1, + child: _FadeOpacity(color: color), + ), + ), + button, + ], + ), + ), + ], + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DoubleProperty('height', height)) + ..add(DiagnosticsProperty('isExpanded', isExpanded)) + ..add(ColorProperty('color', color)) + ..add(DiagnosticsProperty('contentKey', contentKey)); + } +} + +class _FadeOpacity extends StatelessWidget { + final Color color; + + const _FadeOpacity({required this.color}); + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: const [0.0, 1.0], + colors: [ + color, + color.withOpacity(0), + ], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('color', color)); + } +} + +/// A widget extension that enables the widget to be collapsed and uncollapsed. +extension ExpandableTextExtension on Widget { + /// A widget extension that enables the widget to be collapsed and uncollapsed. + /// + /// ```dart + /// Text( + /// 'Some very long text that will need to be collapsed.', + /// textAlign: TextAlign.justify, + /// ).readMore(collapsedButtonText: 'Read More', expandedButtonText: 'Collapse',) + /// ``` + /// + /// This is typically used with long [Text] widgets, where their length might complicate navigation. + /// + /// When collapsed, the component will take the height of [minHeight]. If the icon's height does not exceed + /// [minHeight], then no button to collapse/expand the widget will be shown. + /// + /// [collapsedButtonText] and [expandedButtonText] define the button's text for when the widget is collapsed and + /// expanded respectively. [color] defines the color to be used for the fadeout gradient indicating the widget is + /// collapsed, and defaults to [ColorScheme.background]. + /// + /// See also: + /// + /// * [ZdsExpandable], which wraps the widget to collapse/expand instead. + Widget readMore({ + String collapsedButtonText = '', + String expandedButtonText = '', + double minHeight = 60, + Color? color, + }) { + return MeasureSize( + child: this, + builder: (context, size) { + final ComponentStrings strings = ComponentStrings.of(context); + if (size.height < minHeight) { + return Padding(padding: const EdgeInsets.only(bottom: 16), child: this); + } + return _ExpandableContainer( + collapsedButtonText: + collapsedButtonText.isEmpty ? strings.get('READ_MORE', 'Read more') : collapsedButtonText, + expandedButtonText: expandedButtonText.isEmpty ? strings.get('COLLAPSE', 'Collapse') : expandedButtonText, + minHeight: minHeight, + color: color ?? Theme.of(context).colorScheme.background, + child: this, + ); + }, + ); + } +} diff --git a/lib/src/components/atoms/floating_action_button.dart b/lib/src/components/atoms/floating_action_button.dart new file mode 100644 index 0000000..280df2a --- /dev/null +++ b/lib/src/components/atoms/floating_action_button.dart @@ -0,0 +1,99 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +enum _FloatingActionButtonType { regular, extended } + +/// Widget that creates an FAB following Zds theming. +/// Note that MD3 introduced a new FAB not yet implemented in Flutter, so this integration might change in the future +/// +/// A regular FAB will always require an [icon] to be given, while this is optional for extended FABs. +/// This is because extended FABs require a `label` to be given and make the icon optional +/// +/// Flutter does not currently support shrinking or extending an extended FAB with integrated functions as is possible +/// on Android. It is possible that those functions will be integrated when Flutter FABs are updated to MD3. Until then, +/// an example of how to shrink on scroll is given in the example button.dart page. I do not recommend doing this +/// alternative method as, visually speaking, the result is not optimal (might be achievable with further fine tuning). +class ZdsFloatingActionButton extends StatelessWidget { + /// An icon to show in the FAB. + /// + /// If using [ZdsFloatingActionButton] this is required. + final Widget? icon; + + /// A function called whenever the user taps on the FAB. + final VoidCallback? onPressed; + + /// Text that describes what will occur when the button is pressed, displayed when the user long-presses + /// on the button or is using Talkback or VoiceOver. + /// + /// For more information, see [Semantics]. + final String? tooltip; + + final Widget? _extendedLabel; + + final double? _extendedIconLabelSpacing; + + final EdgeInsetsGeometry? _extendedPadding; + + final _FloatingActionButtonType _floatingActionButtonType; + + /// Creates a circular floating action button. + /// + /// The [icon] argument must not be null. + const ZdsFloatingActionButton({ + required this.icon, + super.key, + this.tooltip, + this.onPressed, + }) : _floatingActionButtonType = _FloatingActionButtonType.regular, + _extendedLabel = null, + _extendedIconLabelSpacing = null, + _extendedPadding = null; + + /// Creates a wider [StadiumBorder]-shaped floating action button with a [label] and an optional [icon]. + /// + /// The [label] argument, usually a [Text], must not be null. + const ZdsFloatingActionButton.extended({ + required Widget label, + super.key, + this.icon, + this.tooltip, + double? extendedIconLabelSpacing, + EdgeInsetsGeometry? extendedPadding, + this.onPressed, + }) : _floatingActionButtonType = _FloatingActionButtonType.extended, + _extendedLabel = label, + _extendedIconLabelSpacing = extendedIconLabelSpacing, + _extendedPadding = extendedPadding; + + @override + Widget build(BuildContext context) { + switch (_floatingActionButtonType) { + case _FloatingActionButtonType.regular: + return FloatingActionButton( + onPressed: onPressed, + backgroundColor: Theme.of(context).colorScheme.secondary, + tooltip: tooltip, + child: icon, + ); + case _FloatingActionButtonType.extended: + return FloatingActionButton.extended( + onPressed: onPressed, + backgroundColor: Theme.of(context).colorScheme.secondary, + label: _extendedLabel!, + icon: icon, + tooltip: tooltip, + extendedIconLabelSpacing: _extendedIconLabelSpacing, + extendedPadding: _extendedPadding, + extendedTextStyle: Theme.of(context).textTheme.bodyMedium, + ); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('onPressed', onPressed)) + ..add(StringProperty('tooltip', tooltip)); + } +} diff --git a/lib/src/components/atoms/icon_text_button.dart b/lib/src/components/atoms/icon_text_button.dart new file mode 100644 index 0000000..35f9980 --- /dev/null +++ b/lib/src/components/atoms/icon_text_button.dart @@ -0,0 +1,100 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A widget that creates a Large icon button with text. +/// +/// ```dart +/// ZdsIconTextButton( +/// onTap: () => {onButtonTapped(context)}, +/// icon: ZdsIcons.timecard, +/// label: 'Timecard', +/// iconColor: ZdsColors.white, +/// labelColor: ZdsColors.white, +/// backgroundColor: Theme.of(context).primaryColor, +/// ), +/// +/// ``` +class ZdsIconTextButton extends StatelessWidget { + /// The icon to be shown above the label + final IconData? icon; + + /// The background color of the [icon]. + final Color? iconColor; + + /// Function called whenever the user taps anywhere on the button + final VoidCallback? onTap; + + /// The label that will be shown at the below of the icon. + /// + /// If not null, it can't be empty. + final String label; + + /// The text color is used for [label]. + final Color? labelColor; + + /// The background color for this button. + /// + /// Defaults to [ColorScheme.primary] + final Color? backgroundColor; + + /// Constructs a [ZdsIconTextButton]. + const ZdsIconTextButton({ + required this.label, + super.key, + this.icon, + this.iconColor, + this.labelColor, + this.onTap, + this.backgroundColor, + }) : assert(label.length != 0, 'label must not be empty'); + + @override + Widget build(BuildContext context) { + final borderRadius = (Theme.of(context).cardTheme.shape as RoundedRectangleBorder?)?.borderRadius; + + return Container( + height: 112, + width: 112, + decoration: BoxDecoration( + borderRadius: borderRadius, + color: backgroundColor ?? Theme.of(context).colorScheme.primary, + boxShadow: [BoxShadow(blurRadius: 4, color: ZdsColors.blueGrey.withOpacity(0.1))], + ), + child: Material( + color: ZdsColors.transparent, + child: Semantics( + child: InkWell( + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 56, color: iconColor), + const SizedBox(height: 8), + Text( + label, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: + labelColor ?? computeForeground(backgroundColor ?? Theme.of(context).colorScheme.primary), + ), + ), + ], + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('icon', icon)); + properties.add(ColorProperty('iconColor', iconColor)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(StringProperty('label', label)); + properties.add(ColorProperty('labelColor', labelColor)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/lib/src/components/atoms/index.dart b/lib/src/components/atoms/index.dart new file mode 100644 index 0000000..536e343 --- /dev/null +++ b/lib/src/components/atoms/index.dart @@ -0,0 +1,67 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; +import '../../../zds_flutter.dart'; + +/// A component used to show status information, like index, order, or state, in a very small space. +/// +/// This component is typically used with a [Text] containing one letter or one number. This is meant to show important +/// information, like priority (using numbers), status (using a letter code like 'D' = 'Done', 'P' = 'In Progress'), or +/// alerts (showing '!' and using a red color) in a small area of the screen. Generally, this is used in lists where +/// each element has a status that needs to be understood quickly and transmitted in a succinct manner. +/// +/// See also: +/// +/// * [ZdsTag], which uses this component in its prefix. +class ZdsIndex extends StatelessWidget { + /// The background color of the circle. + /// + /// Defaults to [ColorScheme.primaryContainer]. + final Color? color; + + /// The widget that will be shown in the circle. + /// + /// Typically a [Text] widget. + final Widget? child; + + /// This is an option to have a small circle or not on the leading element of a tag. + /// + /// If rectangular boolean is false in [ZdsTag], this defaults to true. + final bool useBoxDecoration; + + /// Creates a small circle used to show status information at a glance. + /// This circle is optional to cater for when a leading icon is required, without a circle. + /// An example of this is 'Approved' with a leading check icon. + const ZdsIndex({super.key, this.child, this.color, this.useBoxDecoration = true}); + + @override + Widget build(BuildContext context) { + return Container( + width: 20, + height: 20, + margin: !useBoxDecoration ? const EdgeInsets.only(left: 6) : EdgeInsets.zero, + decoration: useBoxDecoration + ? BoxDecoration( + color: color ?? Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ) + : const BoxDecoration(), + child: Center( + child: DefaultTextStyle( + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ZetaColors.of(context).white, + ), + child: child ?? const SizedBox(), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('color', color)) + ..add(DiagnosticsProperty('useBoxDecoration', useBoxDecoration)); + } +} diff --git a/lib/src/components/atoms/label.dart b/lib/src/components/atoms/label.dart new file mode 100644 index 0000000..1e59616 --- /dev/null +++ b/lib/src/components/atoms/label.dart @@ -0,0 +1,64 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A label typically used to showcase status information. +class ZdsLabel extends StatelessWidget { + /// The icon to be shown at the start of this component. + final IconData? icon; + + /// The icon's size. + /// + /// Must be equal or greater than 0. Defaults to 16. + final double? size; + + /// This label's main content + /// + /// Typically a [Text]. + final Widget? child; + + /// Empty space to surround this widget. + /// + /// Defaults to EdgeInsets.only(right: 16). + final EdgeInsets padding; + + /// Creates a label. + const ZdsLabel({super.key, this.icon, this.child, this.size = 16, this.padding = const EdgeInsets.only(right: 16)}) + : assert(size != null ? size >= 0 : size == null, 'Size must be greater than or equal to 0'); + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: Row( + children: [ + if (icon != null) ...[ + Icon( + icon, + size: size, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: 4), + ], + if (child != null) + DefaultTextStyle( + style: Theme.of(context).textTheme.titleSmall!.copyWith( + color: ZdsColors.blueGrey, + ), + child: child!, + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('icon', icon)) + ..add(DoubleProperty('size', size)) + ..add(DiagnosticsProperty('padding', padding)); + } +} diff --git a/lib/src/components/atoms/notification.dart b/lib/src/components/atoms/notification.dart new file mode 100644 index 0000000..2814711 --- /dev/null +++ b/lib/src/components/atoms/notification.dart @@ -0,0 +1,96 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A component used to show a notification with read/unread message details. +/// +/// +/// ```dart +/// ZdsNotificationTile( +/// onTap: () {}, +/// title: getNotificationDate(), +/// subtitle: 'PTO Request approved for Mon, Jan 16 - Tue, Jan 17', +/// isUnread: isUnread, +// ), +/// ``` + +class ZdsNotificationTile extends StatelessWidget { + /// The text that is header of the notification tile. + /// + final String dateLabel; + + /// Additional content displayed below the date label. + /// + final String? content; + + /// Whether the notification is unread or read. + /// + /// Defaults to true and its unread. + final bool isUnread; + + /// Called when the user taps this list tile. + final VoidCallback? onTap; + + /// Widget displayed at the beginning of a title and subTitle like icon. + final Widget? leadingData; + + /// Whether the notification tiles are closely packed together or separated + /// + /// Defaults to true + /// + /// It will be used in [ZdsListTile]. + final bool? shrinkWrap; + + /// Width of leading widget. + /// + /// If set to null, leading widget will determine its own width. + final double? leadingWidth; + + /// Constructs a [ZdsNotificationTile]. + const ZdsNotificationTile({ + required this.dateLabel, + super.key, + this.content, + this.onTap, + this.isUnread = true, + this.leadingData, + this.leadingWidth = 12, + this.shrinkWrap = true, + }) : assert(dateLabel.length != 0, 'dateLabel must not be empty'); + + @override + Widget build(BuildContext context) { + return ZdsListTile( + shrinkWrap: shrinkWrap, + contentPadding: const EdgeInsets.all(8), + onTap: onTap, + title: Text( + dateLabel, + style: Theme.of(context).textTheme.bodyMedium, + ), + subtitle: Text( + content!, + style: Theme.of(context).textTheme.bodyMedium, + ), + leading: Container( + margin: const EdgeInsets.only(bottom: 28), + width: leadingWidth, + height: 12, + child: isUnread && leadingData == null ? const CircleAvatar() : leadingData, + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('dateLabel', dateLabel)) + ..add(StringProperty('content', content)) + ..add(DiagnosticsProperty('isUnread', isUnread)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('shrinkWrap', shrinkWrap)) + ..add(DoubleProperty('leadingWidth', leadingWidth)); + } +} diff --git a/lib/src/components/atoms/popover.dart b/lib/src/components/atoms/popover.dart new file mode 100644 index 0000000..e1f27cb --- /dev/null +++ b/lib/src/components/atoms/popover.dart @@ -0,0 +1,241 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:popover/popover.dart'; + +import '../../../zds_flutter.dart'; + +/// An icon button with a popover. +/// +/// ```dart +///ZdsPopOverIconButton( +/// icon: const Icon(ZdsIcons.info), +/// popOverBuilder: (context) { +/// return const Text('Popover text!'); +/// }, +/// ), +/// ``` +class ZdsPopOverIconButton extends StatelessWidget { + /// Icon for the icon button. + final Icon icon; + + /// A [WidgetBuilder] that builds the content of the popover. + /// + /// This will be wrapped in a popover of Zds style. + /// + /// Usually a function that returns a [Text]. + final WidgetBuilder popOverBuilder; + + /// Callback function called when popover is dismissed. + final void Function(dynamic)? onDismissed; + + /// Size of the [IconButton]. + /// + /// Defaults to 24. + final double iconSize; + + /// Defines the visual density of the [IconButton]. + final VisualDensity? visualDensity; + + /// Defines the inset padding of the [IconButton]. + final EdgeInsetsGeometry padding; + + /// Defines the of the [IconButton]. + final AlignmentGeometry alignment; + + /// Defines the splashRadius of the [IconButton]. + final double? splashRadius; + + /// Defines the focusColor of the [IconButton]. + final Color? focusColor; + + /// Defines the hoverColor of the [IconButton]. + final Color? hoverColor; + + /// Defines the color of the [IconButton]. + final Color? color; + + /// Defines the splashColor of the [IconButton]. + final Color? splashColor; + + /// Defines the highlightColor of the [IconButton]. + final Color? highlightColor; + + /// Defines the disabledColor of the [IconButton]. + final Color? disabledColor; + + /// Defines the mouseCursor of the [IconButton]. + final MouseCursor mouseCursor; + + /// Defines the focusNode of the [IconButton]. + final FocusNode? focusNode; + + /// Defines the autofocus of the [IconButton]. + final bool autofocus; + + /// Defines the enableFeedback of the [IconButton]. + final bool enableFeedback; + + /// Defines the buttonConstraints of the [IconButton]. + final BoxConstraints? buttonConstraints; + + /// Defines the constraints for the child of the popover. + /// + /// Defaults to `BoxConstraints(maxWidth: size.width * 0.94)` + final BoxConstraints? popOverConstraints; + + /// Background color of the popover. + /// + /// Defaults to [ColorScheme.surface]. + final Color? backgroundColor; + + /// Defines the vertical offset of the popover from the icon. + final double contentDyOffset; + + /// Semantic label used for a dismissible barrier. + final String? barrierLabel; + + /// Constructs an icon button with a popover. + const ZdsPopOverIconButton({ + required this.icon, + required this.popOverBuilder, + super.key, + this.onDismissed, + this.iconSize = 24.0, + this.visualDensity, + this.padding = const EdgeInsets.all(8), + this.alignment = Alignment.center, + this.splashRadius, + this.color, + this.focusColor, + this.hoverColor, + this.highlightColor, + this.splashColor, + this.disabledColor, + this.mouseCursor = SystemMouseCursors.click, + this.focusNode, + this.autofocus = false, + this.enableFeedback = true, + this.buttonConstraints, + this.popOverConstraints, + this.backgroundColor, + this.contentDyOffset = 0.0, + this.barrierLabel, + }) : assert(splashRadius == null || splashRadius > 0, 'Splash radius must be greater than 0'); + + @override + Widget build(BuildContext context) { + final Size size = MediaQuery.of(context).size; + final BoxConstraints defaultPopOverConstraints = BoxConstraints(maxWidth: size.width * 0.94); + + Widget popOverWrapper = popOverBuilder(context); + + if (popOverConstraints?.minHeight == null || popOverConstraints?.minHeight == 0) { + popOverWrapper = IntrinsicHeight( + child: popOverBuilder(context), + ); + } + + return IconButton( + iconSize: iconSize, + visualDensity: visualDensity, + padding: padding, + alignment: alignment, + splashRadius: splashRadius, + color: color, + focusColor: focusColor, + hoverColor: hoverColor, + highlightColor: highlightColor, + splashColor: splashColor, + disabledColor: disabledColor, + mouseCursor: mouseCursor, + focusNode: focusNode, + autofocus: autofocus, + enableFeedback: enableFeedback, + constraints: buttonConstraints, + onPressed: () async { + final result = await showZdsPopOver( + context: context, + backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.surface, + contentDyOffset: contentDyOffset, + barrierLabel: barrierLabel, + constraints: popOverConstraints ?? defaultPopOverConstraints, + builder: (_) { + return popOverWrapper; + }, + ); + onDismissed?.call(result); + }, + icon: icon, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('popOverBuilder', popOverBuilder)) + ..add(ObjectFlagProperty.has('onDismissed', onDismissed)) + ..add(DoubleProperty('iconSize', iconSize)) + ..add(DiagnosticsProperty('visualDensity', visualDensity)) + ..add(DiagnosticsProperty('padding', padding)) + ..add(DiagnosticsProperty('alignment', alignment)) + ..add(DoubleProperty('splashRadius', splashRadius)) + ..add(ColorProperty('focusColor', focusColor)) + ..add(ColorProperty('hoverColor', hoverColor)) + ..add(ColorProperty('color', color)) + ..add(ColorProperty('splashColor', splashColor)) + ..add(ColorProperty('highlightColor', highlightColor)) + ..add(ColorProperty('disabledColor', disabledColor)) + ..add(DiagnosticsProperty('mouseCursor', mouseCursor)) + ..add(DiagnosticsProperty('focusNode', focusNode)) + ..add(DiagnosticsProperty('autofocus', autofocus)) + ..add(DiagnosticsProperty('enableFeedback', enableFeedback)) + ..add(DiagnosticsProperty('buttonConstraints', buttonConstraints)) + ..add(DiagnosticsProperty('popOverConstraints', popOverConstraints)) + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(DoubleProperty('contentDyOffset', contentDyOffset)) + ..add(StringProperty('barrierLabel', barrierLabel)); + } +} + +/// Future that creates a popover. +/// +/// * [context] (required) The context of the popover. +/// * [builder] (required) A [WidgetBuilder] that builds the content of the popover. This will be wrapped in a popover of Zds style. Usually a function that returns a [Text]. +/// * [backgroundColor] The background color of the popover. Defaults to [ColorScheme.surface]. +/// * [radius] Radius of the popover's body. Defaults to 6. +/// * [contentDyOffset] The vertical offset of the popover from the icon. +/// * [constraints] Defines the constraints for the child of the popover. +/// * [onPop] VoidCallback function that is called when the popover is popped. +/// * [barrierLabel] Semantic label used for a dismissible barrier. +Future showZdsPopOver({ + required BuildContext context, + required WidgetBuilder builder, + Color? backgroundColor, + double radius = 6, + double contentDyOffset = 0.0, + Duration transitionDuration = const Duration(milliseconds: 100), + BoxConstraints? constraints, + VoidCallback? onPop, + String? barrierLabel, +}) { + final Offset parentPosition = context.widgetGlobalPosition; + final bool parentIsAtTopHandSide = (parentPosition.dy + contentDyOffset) < MediaQuery.of(context).size.height / 2; + final PopoverDirection direction = parentIsAtTopHandSide ? PopoverDirection.bottom : PopoverDirection.top; + + return showPopover( + context: context, + transitionDuration: transitionDuration, + bodyBuilder: builder, + onPop: onPop, + direction: direction, + arrowHeight: 10, + arrowWidth: 25, + radius: radius, + backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.surface, + barrierColor: Colors.transparent, + contentDyOffset: contentDyOffset, + constraints: constraints, + barrierLabel: barrierLabel, + ); +} diff --git a/lib/src/components/atoms/selection_pills.dart b/lib/src/components/atoms/selection_pills.dart new file mode 100644 index 0000000..1e67450 --- /dev/null +++ b/lib/src/components/atoms/selection_pills.dart @@ -0,0 +1,185 @@ +import 'package:expand_tap_area/expand_tap_area.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// A text button that can be selectable and that accepts a string and isButtonSelected boolean value. +/// +/// +/// /// ```dart +/// ZdsSelectionPills( +/// isButtonSelected: isButtonSelected, +/// label: 'Approved', +/// onChanged: () => +/// setState(() => isButtonSelected = !isButtonSelected) +/// ), +/// +/// /// ``` +class ZdsSelectionPill extends StatelessWidget { + /// The button's label. + /// + /// Prefer to use short strings. + final String label; + + /// Whether the button is selected or not. + /// + /// Defaults to false. + final bool selected; + + /// The icon to be displayed in the chip before the label. + final Icon? leadingIcon; + + /// A callback to call whenever the user taps on the button. + /// + /// Typically used to setState on [selected]. + final VoidCallback? onTap; + + /// A callback to call when the pill's close button is pressed. + /// + /// If this argument is null, the close button will be hidden. + final VoidCallback? onClose; + + /// Padding that wraps the selection pill. + /// + /// Defaults to `EdgeInsets.all(9)`. + final EdgeInsets padding; + + /// Color swatch + /// + /// Defaults to primary. + final ZetaColorSwatch? color; + + ///Use [color] instead. Will be deprecated in future release. + + /// + /// Custom color to override pill background color. + /// + ///Defaults to `colorScheme.secondary.withOpacity(0.1)` + final Color? selectedColor; + + ///Use [color] instead. Will be deprecated in future release. + /// + /// Custom color to override unselected pill border color. + /// + /// Defaults to `ZdsColors.greyCoolSwatch[100]`. + final Color? borderColor; + + /// Constructs a circular, checkable button. + const ZdsSelectionPill({ + required this.label, + super.key, + this.selected = false, + this.onTap, + this.leadingIcon, + this.onClose, + this.padding = const EdgeInsets.all(9), + this.color, + this.selectedColor, + this.borderColor, + }); + + @override + Widget build(BuildContext context) { + final Color background = + color?.surface ?? selectedColor ?? Theme.of(context).colorScheme.secondary.withOpacity(0.1); + final Color disabledColor = color?.disabled ?? ZdsColors.greyWarmSwatch[100]!; + final Color border = color?.border ?? borderColor ?? ZdsColors.greyCoolSwatch[100]!; + + final Color selectedForeground = color?.icon ?? + (selectedColor != null ? computeForeground(selectedColor!) : Theme.of(context).colorScheme.secondary); + + final disabled = onTap == null; + + return ExpandTapWidget( + onTap: onTap ?? () {}, + tapPadding: padding, + child: MergeSemantics( + child: Semantics( + checked: selected, + onTap: onTap, + child: Container( + constraints: const BoxConstraints(minWidth: 50), + padding: padding, + child: Material( + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(19)), + onTap: onTap, + child: AnimatedContainer( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(50)), + border: Border.fromBorderSide( + BorderSide( + color: selected ? border : Colors.transparent, + ), + ), + color: disabled + ? disabledColor + : selected + ? background + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (leadingIcon != null) + IconTheme( + data: IconThemeData(color: selectedForeground), + child: Row(children: [leadingIcon!, const SizedBox(width: 8)]), + ), + Text( + label, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: disabled + ? ZdsColors.greyWarmSwatch[1000] + : selected + ? color?.text ?? selectedForeground + : Theme.of(context).colorScheme.onSurface, + fontWeight: selected && !disabled ? FontWeight.w600 : null, + ), + ), + if (onClose != null) + Row( + children: [ + const SizedBox(width: 10), + IconButton( + constraints: const BoxConstraints(maxHeight: 24, maxWidth: 24), + onPressed: onClose, + icon: Icon( + ZdsIcons.close, + color: selected ? Theme.of(context).colorScheme.secondary : ZdsColors.blueGrey, + ), + splashRadius: 16, + iconSize: 16, + padding: EdgeInsets.zero, + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('label', label)); + properties.add(DiagnosticsProperty('selected', selected)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(ObjectFlagProperty.has('onClose', onClose)); + properties.add(DiagnosticsProperty('padding', padding)); + properties.add(ColorProperty('selectedColor', selectedColor)); + properties.add(ColorProperty('borderColor', borderColor)); + properties.add(ColorProperty('color', color)); + } +} diff --git a/lib/src/components/atoms/shake_animation.dart b/lib/src/components/atoms/shake_animation.dart new file mode 100644 index 0000000..f48282c --- /dev/null +++ b/lib/src/components/atoms/shake_animation.dart @@ -0,0 +1,147 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// The shake animation for the widgets. +/// +/// Use this widget as a container in [StatefulWidget] where shake animation is needed. +/// This widget could be used where we need the users attention on some events. +/// +/// ```dart +/// class ShakeExample extends StatefulWidget { +/// const ShakeExample({Key? key}) : super(key: key); +/// +/// @override +/// State createState() => _ShakeExampleState(); +/// } +/// +/// class _ShakeExampleState extends State { +/// late final _shakeKey = GlobalKey(); +/// +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// appBar: AppBar( +/// title: const Text('Shake Animation'), +/// ), +/// body: ZdsShakeAnimation( +/// key: _shakeKey, +/// shakeCount: 2, +/// shakeOffset: 5, +/// shakeDuration: const Duration(milliseconds: 350), +/// child: ZdsButton.filled( +/// child: const Text('Shake me!'), +/// onTap: () { +/// _shakeKey.currentState?.shake(); +/// }, +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +class ZdsShakeAnimation extends StatefulWidget { + /// Constructs a [ZdsShakeAnimation]. + const ZdsShakeAnimation({ + required this.child, + required this.shakeOffset, + super.key, + this.shakeCount = 3, + this.shakeDuration = const Duration(milliseconds: 400), + this.onAnimationUpdate, + }) : assert(shakeOffset > 0, "'shakeOffset' should not be zero"), + assert(shakeCount > 0, "'shakeCount' should not be zero"); + + /// A child widget to animate. + final Widget child; + + /// Horizontal shake offset + /// ...<- [Widget] ->... + final double shakeOffset; + + /// No of repetitions. + final int shakeCount; + + /// Animation duration. + final Duration shakeDuration; + + /// Animation status change call backs. + final void Function(AnimationStatus)? onAnimationUpdate; + + @override + ZdsShakeAnimationState createState() => ZdsShakeAnimationState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DoubleProperty('shakeOffset', shakeOffset)) + ..add(IntProperty('shakeCount', shakeCount)) + ..add(DiagnosticsProperty('shakeDuration', shakeDuration)) + ..add(ObjectFlagProperty.has('onAnimationUpdate', onAnimationUpdate)); + } +} + +/// State for [ZdsShakeAnimation], used to create a key: +/// +/// ```dart +/// late final _shakeKey = GlobalKey(); +/// ``` +/// +/// See also: +/// * [ZdsShakeAnimation]. +class ZdsShakeAnimationState extends State with SingleTickerProviderStateMixin { + /// Animation Controller to control the shake. + late final animationController = AnimationController(vsync: this, duration: widget.shakeDuration); + + @override + void dispose() { + animationController.removeStatusListener(_updateStatus); + super.dispose(); + } + + @override + void initState() { + super.initState(); + animationController.addStatusListener(_updateStatus); + } + + void _updateStatus(AnimationStatus status) { + widget.onAnimationUpdate?.call(status); + + if (status == AnimationStatus.completed) { + animationController.reset(); + } + } + + /// Triggers the shaking animation. + void shake() { + animationController.forward(); + } + + @override + Widget build(BuildContext context) { + // 1. return an AnimatedBuilder + return AnimatedBuilder( + // 2. pass our custom animation as an argument + animation: animationController, + // 3. optimization: pass the given child as an argument + child: widget.child, + builder: (context, child) { + final sineValue = sin(widget.shakeCount * 2 * pi * animationController.value); + return Transform.translate( + // 4. apply a translation as a function of the animation value + offset: Offset(sineValue * widget.shakeOffset, 0), + // 5. use the child widget + child: child, + ); + }, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('animationController', animationController)); + } +} diff --git a/lib/src/components/atoms/slidable_widget.dart b/lib/src/components/atoms/slidable_widget.dart new file mode 100644 index 0000000..7fcffab --- /dev/null +++ b/lib/src/components/atoms/slidable_widget.dart @@ -0,0 +1,195 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zds_flutter.dart'; + +/// A [ZdsSlidableWidget] with pre-applied Zds styling. This widget is used specifically in [ZdsSlidableButton] to create a SlidableButton. +class ZdsSlidableWidget extends StatefulWidget { + /// The `Widget` on which we want to detect the slide movement. + final Widget child; + + /// The height of the widget that will be drawn, required. + final double height; + + /// The width of the widget that will be drawn, required. + final double handleWidth; + + /// The `VoidCallback` that will be called once a slide with certain percentage is detected. + final VoidCallback onSlide; + + /// The `VoidCallback` that will be called once the toggle is tapped. + final void Function()? onTapDown; + + /// The `VoidCallback` that will be called once the toggle tap is released. + final void Function()? onTapUp; + + /// The `onSlideValue` as a callback provided to the parent [ZdsSlidableButton]. + final void Function(double value)? onSlideValueCallback; + + /// The decimal percentage of swiping in order for the callbacks to get called, defaults to 0.75 (75%) of the total width of the children. + final double slidePercentageNeeded; + + /// Button is active value default : true. + final bool isActive; + + /// Button animation after slide, this is defaulted to false for no animation. + final bool animate; + + /// Keeps the toggle at one end after completion. + final bool stayCompleted; + + /// Displays button on the slidable button in [ZdsSlidableButton], responsible for detecting slide gestures and animating movement. + const ZdsSlidableWidget({ + required this.child, + required this.height, + required this.handleWidth, + required this.onSlide, + this.onSlideValueCallback, + this.onTapDown, + this.onTapUp, + super.key, + this.isActive = true, + this.animate = false, + this.slidePercentageNeeded = 0.75, + this.stayCompleted = false, + }); + + @override + ZdsSlidableWidgetState createState() => ZdsSlidableWidgetState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DoubleProperty('height', height)) + ..add(DiagnosticsProperty('stayCompleted', stayCompleted)) + ..add(DoubleProperty('handleWidth', handleWidth)) + ..add(ObjectFlagProperty.has('onSlide', onSlide)) + ..add(ObjectFlagProperty.has('onTapDown', onTapDown)) + ..add(ObjectFlagProperty.has('onTapUp', onTapUp)) + ..add(ObjectFlagProperty.has('onSlideValueCallback', onSlideValueCallback)) + ..add(DoubleProperty('slidePercentageNeeded', slidePercentageNeeded)) + ..add(DiagnosticsProperty('isActive', isActive)) + ..add(DiagnosticsProperty('animate', animate)); + } +} + +/// State is required for the animation and updating the position on [ZdsSlidableButton]. +class ZdsSlidableWidgetState extends State with SingleTickerProviderStateMixin { + late AnimationController _controller; + + double _dxStartPosition = 0; + double _dxEndsPosition = 0; + + bool _isComplete = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 800)) + ..addListener(() { + setState(() {}); + }); + + _controller.value = 1; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + /// Resets the slideable widget. + void reset() { + _controller.animateTo( + 1, + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + ); + _isComplete = false; + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + /// This onTap is included to show bounce slide suggestion to the user, this indicates the intended action of this button. + onTap: () { + if (widget.isActive && !_isComplete) { + _controller.animateTo(0.6, duration: const Duration(milliseconds: 800), curve: Curves.fastOutSlowIn); + Future.delayed(const Duration(milliseconds: 500), () { + _controller.animateTo(1, duration: const Duration(milliseconds: 800), curve: Curves.fastOutSlowIn); + }); + } + }, + onTapDown: (_) { + if (widget.isActive && !_isComplete) { + widget.onTapDown?.call(); + } + }, + onTapUp: (_) => widget.onTapUp?.call(), + onPanStart: (details) { + if (!_isComplete) { + widget.onTapDown?.call(); + setState(() { + _dxStartPosition = details.localPosition.dx - widget.handleWidth; + }); + } + }, + onPanUpdate: (details) { + if (widget.isActive && !_isComplete) { + setState(() { + _dxEndsPosition = details.localPosition.dx - widget.handleWidth; + }); + + final trackWidth = context.size!.width; + final slideValue = widget.handleWidth / trackWidth; + final val = 1 - (((details.localPosition.dx) / trackWidth) - slideValue / 2); + final isHandleAtEnd = val < slideValue; + _controller.value = isHandleAtEnd ? slideValue : val; + widget.onSlideValueCallback?.call(_controller.value - slideValue); + } + }, + onPanEnd: (details) { + widget.onTapUp?.call(); + if (widget.isActive && !_isComplete) { + final delta = _dxEndsPosition - _dxStartPosition; + final trackWidth = context.size!.width; + var deltaNeededToBeSlided = trackWidth * widget.slidePercentageNeeded; + deltaNeededToBeSlided -= 80.0; + + if (delta > deltaNeededToBeSlided) { + _controller.animateTo( + widget.handleWidth / trackWidth, + duration: const Duration(milliseconds: 200), + curve: Curves.fastOutSlowIn, + ); + if (!widget.animate) { + _controller.animateTo( + trackWidth, + duration: const Duration(milliseconds: 800), + curve: Curves.fastOutSlowIn, + ); + } else { + _isComplete = widget.stayCompleted; + + widget.onSlide(); + } + } else { + _controller.animateTo(1, duration: const Duration(milliseconds: 800), curve: Curves.fastOutSlowIn); + widget.onSlideValueCallback?.call(1); + } + } + }, + child: SizedBox( + height: widget.height, + child: Align( + alignment: Alignment.centerRight, + child: FractionallySizedBox( + widthFactor: _controller.value, + heightFactor: 1, + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/src/components/atoms/tab.dart b/lib/src/components/atoms/tab.dart new file mode 100644 index 0000000..971964a --- /dev/null +++ b/lib/src/components/atoms/tab.dart @@ -0,0 +1,40 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zds_flutter.dart'; + +/// A [Tab] with Zds styling. +class ZdsTab extends StatelessWidget { + /// The icon shown in this tab. If [label] is also provided, the icon will be shown above it. + final Icon? icon; + + /// The text that will be shown in this tab. If [icon] is also provided, the text will be shown below it. + /// Must not be used at the same time than [child]. + final String? label; + + /// A widget to display below the [icon], if any. If used, [label] must be null. + final Widget? child; + + /// Creates a [ZdsResponsiveTabBar] or [ZdsTabBar] tab. At least one of [icon], [label], or [child] must not be null. + const ZdsTab({super.key, this.icon, this.label, this.child}) + : assert( + icon != null || label != null || child != null, + 'At least one of icon, label, or child must be defined.', + ), + assert(label == null || child == null, 'One of either label or child must be defined.'); + + @override + Widget build(BuildContext context) { + return Tab( + icon: icon, + text: label, + iconMargin: const EdgeInsets.only(bottom: 4), + child: child, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('label', label)); + } +} diff --git a/lib/src/components/atoms/toggle_button.dart b/lib/src/components/atoms/toggle_button.dart new file mode 100644 index 0000000..b1af8a4 --- /dev/null +++ b/lib/src/components/atoms/toggle_button.dart @@ -0,0 +1,156 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// A large text button that can toggle between multiple values with Zds style. +class ZdsToggleButton extends StatefulWidget { + /// The name of the option that will be displayed on the toggle. + /// + /// It is recommended that this does not exceed 4 as the toggle would be very small. + final List values; + + /// Called when the user clicks on toggle options. + final ValueChanged onToggleCallback; + + /// The background color for the selected value from toggle bottom. + /// + /// Defaults to [ColorScheme.primary]. + final Color? backgroundColor; + + /// The foreground color for the selected value from toggle button. + /// + /// Defaults to [ColorScheme.onPrimary]. + final Color? foregroundColor; + + /// Initial value for the toggle to be loaded with. + final int initialValue; + + /// Margin around the outside of the button. + /// + /// Defaults to EdgeInsets.all(18). + final EdgeInsets margin; + + /// Constructs a ZdsToggleButton. + const ZdsToggleButton({ + required this.values, + required this.onToggleCallback, + super.key, + this.backgroundColor, + this.initialValue = 0, + this.margin = const EdgeInsets.all(kBigTogglePadding), + this.foregroundColor, + }); + + @override + ZdsToggleButtonState createState() => ZdsToggleButtonState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty('values', values)); + properties.add(ObjectFlagProperty>.has('onToggleCallback', onToggleCallback)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(ColorProperty('foregroundColor', foregroundColor)); + properties.add(IntProperty('initialValue', initialValue)); + properties.add(DiagnosticsProperty('margin', margin)); + } +} + +/// State for [ZdsToggleButton]. +class ZdsToggleButtonState extends State { + int _selectedValue = 0; + + @override + void initState() { + _selectedValue = widget.initialValue; + super.initState(); + } + + void _setSelectedValueFromGesture(double dx, double width) { + int newSelected = (dx / (width / widget.values.length)).truncate().clamp(0, widget.values.length - 1); + if (newSelected > widget.values.length - 1) { + newSelected = widget.values.length - 1; + } + + if (newSelected != _selectedValue) { + widget.onToggleCallback(newSelected); + setState(() { + _selectedValue = newSelected; + }); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth - kBigTogglePadding * 2; + return Container( + margin: widget.margin, + height: kBigToggleHeight, + child: GestureDetector( + onTapDown: (TapDownDetails details) => _setSelectedValueFromGesture(details.localPosition.dx, width), + onPanUpdate: (DragUpdateDetails details) => _setSelectedValueFromGesture(details.localPosition.dx, width), + child: Stack( + children: [ + Container( + height: kBigToggleHeight, + decoration: ShapeDecoration( + color: ZdsColors.greySwatch( + context, + )[Theme.of(context).colorScheme.brightness == Brightness.dark ? 1100 : 200], + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28))), + ), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 250), + curve: Curves.ease, + left: _selectedValue * (width / widget.values.length), + right: (widget.values.length - _selectedValue - 1) * (width / widget.values.length), + top: 0, + bottom: 0, + child: Container( + width: width / widget.values.length - 1, + decoration: ShapeDecoration( + color: widget.backgroundColor ?? Theme.of(context).colorScheme.primary, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28))), + ), + alignment: Alignment.center, + ), + ), + SizedBox( + height: kBigToggleHeight, + child: Row( + children: List.generate( + widget.values.length, + (index) => Expanded( + child: Center( + child: AnimatedDefaultTextStyle( + curve: Curves.ease, + duration: const Duration(milliseconds: 250), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: (index == _selectedValue) + ? widget.foregroundColor ?? + ZetaColors.computeForeground( + input: widget.backgroundColor ?? Theme.of(context).colorScheme.primary, + ) + : Theme.of(context).colorScheme.onBackground, + ), + child: Text(widget.values[index]), + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/src/components/atoms/unread_badge_widget.dart b/lib/src/components/atoms/unread_badge_widget.dart new file mode 100644 index 0000000..928866a --- /dev/null +++ b/lib/src/components/atoms/unread_badge_widget.dart @@ -0,0 +1,132 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; +import '../../../zds_flutter.dart'; + +/// A widget that shows a badge to display how many unread notifications there are. +/// +/// See also: +/// +/// * [IconWithBadge], an icon that uses [UnreadBadge]. +class UnreadBadge extends StatelessWidget { + /// The number to show in the badge. + final int unread; + + /// Optional text to replace the default [Semantics] behavior of reading the number in this badge. + /// + /// If not null, [Semantics] will ignore [unread], so this parameter can be used for making [Semantics] read "3 + /// unread emails" instead of just "3". If null, [Semantics] will just read [unread]. + final String? semanticsLabel; + + /// How many digits long the unread amount can be. For example, if set to 3, any number over 999 will be shown as + /// 999+ or +999 depending on the locale used and the text direction. + /// + /// Must be equal or greater than 1. Defaults to 3. + final int maximumDigits; + + /// Foreground color for the text. + /// Defaults to theme's onError color. + final Color? foregroundColor; + + /// Background color for the widget + /// Defaults to theme's error color. + final Color? backgroundColor; + + /// Min width of the unread bubble. + /// Defaults to 16. + final double minWidth; + + /// Min height of the unread bubble. + /// Defaults to 16. + final double minHeight; + + /// The color of the surface where this badge is being drawn on. Typically, this will be the surface color. However, in cases + /// where this widget is used in a context with a different surface color, such as in an AppBar, this value should + /// be set to the AppBar's background color. + /// + /// This color is later used to draw a border around the count bubble. + final Color? badgeContainerColor; + + /// Displays the [unread] parameter in a red circle. + /// + /// If not null, [semanticsLabel] is read instead of the [unread] amount if given. + /// + /// [maximumDigits] represents how many digits will be shown. It must be equal or greater than 1 + /// (if [maximumDigits] is 3, any number over 999 will be shown as 999+). + /// + /// [maximumDigits] and [unread] must not be null. + const UnreadBadge({ + required this.unread, + super.key, + this.semanticsLabel, + this.maximumDigits = 3, + this.foregroundColor, + this.backgroundColor, + this.minWidth = 16, + this.minHeight = 16, + this.badgeContainerColor, + }) : assert(maximumDigits >= 1, 'Maximum digits must be greater than 1'); + + @override + Widget build(BuildContext context) { + final String maximumNumber = '9' * maximumDigits; + final themeData = Theme.of(context); + + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: badgeContainerColor ?? themeData.colorScheme.surface, + borderRadius: BorderRadius.circular(minHeight + 2), + ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + constraints: BoxConstraints(minWidth: minWidth, minHeight: minHeight), + decoration: BoxDecoration( + color: backgroundColor ?? themeData.colorScheme.error, + borderRadius: BorderRadius.circular(minHeight), + ), + child: Center( + child: Semantics( + label: semanticsLabel, + child: ExcludeSemantics( + excluding: semanticsLabel != null, + child: Text( + unread.toString().length <= maximumDigits + ? unread.toString() + : Directionality.of(context) == TextDirection.ltr + ? '$maximumNumber+' + : '+$maximumNumber', + textScaleFactor: MediaQuery.of(context).textScaleFactor > 1.35 ? 1.35 : null, + style: themeData.textTheme.bodySmall?.copyWith( + color: foregroundColor ?? + ZetaColors.computeForeground( + input: backgroundColor ?? themeData.colorScheme.error, + ), + // TODO(colors): determine why onError doesnt work in darkmode themeData.colorScheme.onError, + fontSize: max(themeData.textTheme.bodySmall?.fontSize ?? 0, minHeight * 0.65), + ), + ), + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('unread', unread)) + ..add(StringProperty('semanticsLabel', semanticsLabel)) + ..add(IntProperty('maximumDigits', maximumDigits)) + ..add(ColorProperty('foregroundColor', foregroundColor)) + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(DoubleProperty('minWidth', minWidth)) + ..add(DoubleProperty('minHeight', minHeight)) + ..add(ColorProperty('badgeContainerColor', badgeContainerColor)); + } +} diff --git a/lib/src/components/atoms/ximage.dart b/lib/src/components/atoms/ximage.dart new file mode 100644 index 0000000..3355a7e --- /dev/null +++ b/lib/src/components/atoms/ximage.dart @@ -0,0 +1,84 @@ +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Created [Image] widget from [XFile] +extension XImage on Image { + /// Created [Image] widget from [XFile] + static Image file( + XFile file, { + BoxFit fit = BoxFit.cover, + Key? key, + double scale = 1.0, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? width, + double? height, + Color? color, + Animation? opacity, + BlendMode? colorBlendMode, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + FilterQuality filterQuality = FilterQuality.low, + int? cacheWidth, + int? cacheHeight, + }) { + return kIsWeb + ? Image.network( + file.path, + key: key, + scale: scale, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ) + : Image.file( + File(file.path), + key: key, + scale: scale, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: width, + height: height, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); + } +} diff --git a/lib/src/components/molecules.dart b/lib/src/components/molecules.dart new file mode 100644 index 0000000..46e802a --- /dev/null +++ b/lib/src/components/molecules.dart @@ -0,0 +1,34 @@ +// Molecules are groups of atoms bonded together and are the smallest fundamental units of a compound. + +export 'molecules/block_table.dart'; +export 'molecules/bottom_sheet.dart'; +export 'molecules/card_actions.dart'; +export 'molecules/card_header.dart'; +export 'molecules/check_button.dart'; +export 'molecules/date_range_picker.dart'; +export 'molecules/date_time_picker.dart'; +export 'molecules/dropdown.dart'; +export 'molecules/empty.dart'; +export 'molecules/expansion_tile.dart'; +export 'molecules/fields_list_tile.dart'; +export 'molecules/icon_badge_widget.dart'; +export 'molecules/information_bar.dart'; +export 'molecules/input_dialog.dart'; +export 'molecules/list.dart'; +export 'molecules/list_tile_wrapper.dart'; +export 'molecules/menu.dart'; +export 'molecules/menu_item.dart'; +export 'molecules/network_avatar.dart'; +export 'molecules/resizeable_text_area.dart'; +export 'molecules/responsive_tab_bar.dart'; +export 'molecules/search.dart'; +export 'molecules/selectable_list_tile.dart'; +export 'molecules/sheet_header.dart'; +export 'molecules/slidable_button.dart'; +export 'molecules/slidable_list_tile.dart'; +export 'molecules/stats_card.dart'; +export 'molecules/tab_bar.dart'; +export 'molecules/tag.dart'; +export 'molecules/toast.dart'; +export 'molecules/toolbar.dart'; +export 'molecules/vertical_nav.dart'; diff --git a/lib/src/components/molecules/block_table.dart b/lib/src/components/molecules/block_table.dart new file mode 100644 index 0000000..78a1996 --- /dev/null +++ b/lib/src/components/molecules/block_table.dart @@ -0,0 +1,455 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:linked_scroll_controller/linked_scroll_controller.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// Defines a header for a [ZdsBlockTable] +class ZdsBlockTableHeader { + /// The text displayed on the header + final String text; + + /// The color of the text + final Color? textColor; + + /// Creates a new [ZdsBlockTableHeader] + ZdsBlockTableHeader({ + required this.text, + this.textColor, + }); +} + +/// Defines a row in a [ZdsBlockTable] +class ZdsBlockTableRow { + /// The header text displayed above the row + final String? header; + + /// The cell that is used as the title cell for the row + final ZdsBlockTableCellData titleCell; + + /// The data displayed in the table + final List data; + + /// Creates a new [ZdsBlockTableRow] + ZdsBlockTableRow({ + required this.data, + required this.titleCell, + this.header, + }); +} + +/// Defines a cell in a [ZdsBlockTable] +class ZdsBlockTableCellData { + /// The child of the cell. Cannot be set if [text] is defined + final Widget? child; + + /// The text displayed in the cell. Cannot be set if [child] is defined + final String? text; + + /// The color of the text + final Color? textColor; + + /// The background color of the cell + final Color? backgroundColor; + + /// The style of the text + final TextStyle? textStyle; + + /// The function called when the cell is tapped + final void Function()? onTap; + + /// If set to true, gives the cell a highlight background and border + bool? isSelected; + + /// Creates a new [ZdsBlockTableCellData] + ZdsBlockTableCellData({ + this.text, + this.child, + this.textColor, + this.backgroundColor, + this.textStyle, + this.isSelected, + this.onTap, + }); +} + +/// A scrollable table with floating headers +class ZdsBlockTable extends StatefulWidget { + /// The table headers + final List headers; + + /// The table rows + final List rows; + + /// The width of each column + final double columnWidth; + + /// The height of each cell + final double cellHeight; + + /// The vertical padding within each cell + final double cellPadding; + + /// The multiple for height of row header + final double rowHeaderHeight; + + /// Creates a new [ZdsBlockTable] + const ZdsBlockTable({ + required this.headers, + required this.rows, + this.columnWidth = 80, + this.cellHeight = 34, + this.cellPadding = 18, + this.rowHeaderHeight = 24, + super.key, + }); + + @override + State createState() => _BlockTable(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty('headers', headers)); + properties.add(IterableProperty('rows', rows)); + properties.add(DoubleProperty('columnWidth', columnWidth)); + properties.add(DoubleProperty('cellHeight', cellHeight)); + properties.add(DoubleProperty('cellPadding', cellPadding)); + properties.add(DoubleProperty('rowHeaderHeight', rowHeaderHeight)); + } +} + +class _BlockTable extends State with WidgetsBindingObserver { + final LinkedScrollControllerGroup _controllers = LinkedScrollControllerGroup(); + late final double headerHeight; + + late ScrollController _tableHeader; + late ScrollController _tableBody; + + Widget zeroCellWidget = const SizedBox(); + List headerCellsWidgets = []; + List firstColumnCellsWidgets = []; + List bodyCellsWidgets = []; + + @override + void initState() { + super.initState(); + _tableHeader = _controllers.addAndGet(); + _tableBody = _controllers.addAndGet(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + headerHeight = widget.rowHeaderHeight * MediaQuery.of(context).textScaleFactor; + buildTable(); + }); + + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeMetrics() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + buildTable(); + }); + } + + void buildTable() { + if (!mounted) return; + setState(() { + zeroCellWidget = _getZeroCellWidget(); + headerCellsWidgets = _buildHeaderCells(widget.headers); + firstColumnCellsWidgets = _buildFirstColumnCells(widget.rows); + bodyCellsWidgets = _buildBodyRows(); + }); + } + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: ZdsColors.lightGrey, + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + zeroCellWidget, + Flexible( + flex: 3, + child: SingleChildScrollView( + controller: _tableHeader, + scrollDirection: Axis.horizontal, + child: Row( + children: headerCellsWidgets, + ), + ), + ), + ], + ).paddingOnly(bottom: 1), + Flexible( + child: _workingFixedColScrollable(), + ), + ], + ), + ), + ); + } + + Widget _workingFixedColScrollable() { + return SingleChildScrollView( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: firstColumnCellsWidgets, + ), + Flexible( + child: SingleChildScrollView( + controller: _tableBody, + scrollDirection: Axis.horizontal, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: bodyCellsWidgets, + ), + ), + ), + ], + ), + ); + } + + List _buildHeaderCells(List items) { + return List.generate( + items.length, + (index) => Row( + children: [ + Container( + alignment: Alignment.center, + width: _getDayColumnWidth(), + height: 28 * MediaQuery.of(context).textScaleFactor, + color: Theme.of(context).colorScheme.surface, + child: Text( + items[index].text, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: items[index].textColor, + ), + ), + ), + const SizedBox( + width: 1, + ), + ], + ), + growable: false, + ); + } + + List _buildFirstColumnCells(List rows) { + return List.generate( + rows.length, + (index) { + final row = rows[index]; + final cellItem = row.titleCell; + return Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (row.header != null) + Container( + height: headerHeight, + width: _getAssocColumnWidth(), + decoration: BoxDecoration( + color: ZetaColors.of(context).warm.shade10, + border: Border( + bottom: BorderSide( + color: ZdsColors.lightGrey, + ), + ), + ), + padding: const EdgeInsets.only(left: 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + row.header!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + Container( + alignment: Alignment.center, + width: _getAssocColumnWidth(), + height: widget.cellHeight + widget.cellPadding, + color: cellItem.backgroundColor ?? Theme.of(context).colorScheme.surface, + margin: const EdgeInsets.only(bottom: 1), + child: cellItem.child ?? + Text( + cellItem.text ?? '', + style: cellItem.textStyle ?? + Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 12, + fontWeight: FontWeight.w400, + color: cellItem.textColor ?? Theme.of(context).colorScheme.onSurface, + ), + ).paddingOnly(left: 8), + ), + ], + ), + const SizedBox( + width: 1, + ), + ], + ); + }, + growable: false, + ); + } + + List _buildRowElements(int index) { + final List cells = []; + final List rows = widget.rows; + final double cellHeight = widget.cellHeight + widget.cellPadding; + + for (int j = 0; j < rows[index].data.length; j++) { + final List columnWidgets = []; + + if (rows[index].header != null) { + columnWidgets.add( + Container( + height: headerHeight, + decoration: BoxDecoration( + color: ZetaColors.of(context).warm.shade10, + border: Border( + bottom: BorderSide(color: ZdsColors.lightGrey), + ), + ), + ), + ); + } + final tableCell = rows[index].data[j]; + final isSelected = tableCell.isSelected != null && tableCell.isSelected!; + + columnWidgets.add( + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + border: isSelected ? Border.all(color: Theme.of(context).colorScheme.secondary, width: 2) : null, + color: isSelected + ? Theme.of(context) + .colorScheme + .secondary + .withLight(0.1, background: Theme.of(context).colorScheme.background) + : tableCell.backgroundColor ?? Theme.of(context).colorScheme.surface, + ), + child: Align( + child: tableCell.child ?? + Text( + tableCell.text!, + textAlign: TextAlign.center, + style: tableCell.textStyle ?? + Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 12, + fontWeight: FontWeight.w400, + color: tableCell.textColor ?? Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ), + ), + ); + + final Widget cellBody = Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: columnWidgets, + ); + + final Widget cell = tableCell.onTap == null + ? cellBody + : GestureDetector( + onTap: tableCell.onTap, + child: cellBody, + ); + + cells.add(cell); + } + + return List.generate( + cells.length, + (indx) => Row( + children: [ + Container( + alignment: Alignment.center, + width: _getDayColumnWidth(), + height: cellHeight + (rows[index].header != null ? headerHeight : 0), + child: cells[indx], + ), + const SizedBox( + width: 1, + ), + ], + ), + growable: false, + ); + } + + List _buildBodyRows() { + return List.generate( + widget.rows.length, + (index) { + return Row( + children: _buildRowElements(index), + ).paddingOnly(bottom: 1); + }, + growable: false, + ); + } + + Widget _getZeroCellWidget() { + return Container( + alignment: Alignment.center, + width: _getAssocColumnWidth(), + height: 28 * MediaQuery.of(context).textScaleFactor, + color: Theme.of(context).colorScheme.surface, + ).paddingOnly(right: 1); + } + + double _getAssocColumnWidth() { + final bool isLandscape = MediaQuery.of(context).orientation == Orientation.landscape; + if (!isLandscape) { + return _firstColMinWidth(); + } + final double screenWidth = _screenWidthSafe(); + return screenWidth - _getDayColumnWidth() * 7; + } + + double _firstColMinWidth() { + return widget.columnWidth + 40; + } + + double _screenWidthSafe() { + final double screenWidth = MediaQuery.of(context).size.width; + final double leftSafe = MediaQuery.of(context).padding.left; + return screenWidth - leftSafe; + } + + double _getDayColumnWidth() { + final bool isLandscape = MediaQuery.of(context).orientation == Orientation.landscape; + if (!isLandscape && (!context.isTablet())) { + return widget.columnWidth; + } + double screenWidth = _screenWidthSafe().floorToDouble(); + screenWidth -= _firstColMinWidth(); // remove first column + final double suggestedWidth = (screenWidth / 7).floorToDouble(); + return suggestedWidth > widget.columnWidth ? suggestedWidth : widget.columnWidth; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('headerHeight', headerHeight)); + } +} diff --git a/lib/src/components/molecules/bottom_sheet.dart b/lib/src/components/molecules/bottom_sheet.dart new file mode 100644 index 0000000..9651400 --- /dev/null +++ b/lib/src/components/molecules/bottom_sheet.dart @@ -0,0 +1,370 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +import '../../../zds_flutter.dart'; + +/// Use [showZdsBottomSheet] to display any bottom sheets instead of using this widget directly. +/// +/// Defines the contents to be shown in a bottom sheet. +/// +/// See also: +/// +/// * [showZdsBottomSheet], which uses this widget for its contents and is the recommended way to show bottom sheets +class ZdsBottomSheet extends StatelessWidget { + /// The widget that contains the main content of this bottom sheet. If [header] and [bottom] are not null, it will be + /// shown between those two widgets + final Widget child; + + /// The widget that will be shown at the top of this bottom sheet. Typically a [ZdsSheetHeader] + final PreferredSizeWidget? header; + + /// The widget that will be shown at the bottom of this bottom sheet. Typically a [ZdsBottomBar] + final PreferredSizeWidget? bottom; + + /// The background color for this bottom sheet. + /// + /// Defaults to [ColorScheme.background] + final Color? backgroundColor; + + /// How high this bottom sheet will be allowed to grow. If not null, it must be greater than 0. The bottom sheet will + /// not grow beyond the screen height excluding the top viewPadding even if a greater maxHeight value is declared. + /// + /// Defaults to `MediaQuery.size.height - MediaQuery.viewPadding.top` + final double? maxHeight; + + /// How much of the bottom part of the display will be avoided when showing this ZdsBottomSheet. + /// + /// Defaults to `MediaQuery.viewPadding.bottom` + final double? bottomInset; + + /// Defines the contents of the bottom sheet. It's recommended to not use this widget directly and instead call + /// [showZdsBottomSheet] + /// + /// [child] must not be null. + const ZdsBottomSheet({ + required this.child, + super.key, + this.header, + this.bottom, + this.backgroundColor, + this.maxHeight, + this.bottomInset, + }) : assert(maxHeight != null ? maxHeight > 0 : maxHeight == null, 'Max height must be greater than 0'); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final sheetBackgroundColor = backgroundColor ?? colorScheme.background; + final headerColor = header != null ? colorScheme.surface : sheetBackgroundColor; + final headerWidget = _BottomSheetHeader(bottom: header, backgroundColor: headerColor); + final media = MediaQuery.of(context); + final maxScreenHeight = media.size.height - media.viewPadding.top; + final height = maxHeight != null && (maxHeight! > 0 && maxHeight! < maxScreenHeight) ? maxHeight! : maxScreenHeight; + final setBottomInset = bottomInset ?? MediaQuery.of(context).viewPadding.bottom; + + return MediaQuery.removePadding( + removeTop: true, + context: context, + child: Container( + constraints: BoxConstraints(maxHeight: height), + color: sheetBackgroundColor, + child: Stack( + children: [ + Padding( + padding: EdgeInsets.only( + top: headerWidget.preferredSize.height, + bottom: (bottom?.preferredSize.height ?? 0) + setBottomInset, + ), + child: Semantics(sortKey: const OrdinalSortKey(2), child: child), + ), + SizedBox( + height: headerWidget.preferredSize.height, + child: Semantics( + sortKey: const OrdinalSortKey(1), + child: headerWidget, + ), + ), + if (bottom != null) + Positioned( + bottom: 0, + left: 0, + right: 0, + height: bottom!.preferredSize.height + setBottomInset, + child: Semantics(sortKey: const OrdinalSortKey(3), child: bottom), + ), + ], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(DoubleProperty('maxHeight', maxHeight)); + properties.add(DoubleProperty('bottomInset', bottomInset)); + } +} + +/// Method to build widgets for headerBuilder & bottomBuilder of showZdsBottomSheet +/// This return an array of widgets which contains first element as ZdsSheetHeader and second element as ZdsBottomBar +/// Here widgets gets computed based on the device(Mobile or Tablet). +/// +/// * [primaryActionText] Text for primary button (like Save button) +/// * [primaryActionOnTap] On tap event for primary button +/// * [secondaryActionText] Text for secondary button (like Cancel button) +/// * [secondaryActionOnTap] On tap event for secondary button +/// * [ternaryActionText] Text for ternary button (like Reset button) +/// * [ternaryActionOnTap] On tap event for ternary button +/// * [title] Title for the ZdsSheetHeader +/// * [showClose] Close icon button for the ZdsSheetHeader +List buildSheetBars({ + required BuildContext context, + String primaryActionText = '', + VoidCallback? primaryActionOnTap, + String secondaryActionText = '', + VoidCallback? secondaryActionOnTap, + String ternaryActionText = '', + VoidCallback? ternaryActionOnTap, + String? title = '', + bool showClose = false, +}) { + final bool isTablet = context.isTablet(); + final offset = isTablet ? 0 : MediaQuery.of(context).viewPadding.bottom; + + return [ + ZdsSheetHeader( + headerText: title!, + leading: isTablet && secondaryActionText.isNotEmpty + ? ZdsButton.text( + child: Text(secondaryActionText), + ) + : !isTablet && showClose + ? IconButton( + onPressed: () { + secondaryActionOnTap != null ? secondaryActionOnTap() : Navigator.of(context).pop(); + }, + icon: const Icon(ZdsIcons.close), + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + ) + : null, + trailing: isTablet && primaryActionText.isNotEmpty + ? ZdsButton.text( + onTap: primaryActionOnTap, + child: Text(primaryActionText), + ) + : null, + ), + if (_isBottomBarRequired(isTablet, primaryActionText, secondaryActionText, ternaryActionText)) + ZdsBottomBar( + minHeight: kBottomBarHeight + offset, + child: Row( + children: [ + if (isTablet) ...[ + if (ternaryActionText.isNotEmpty) + ZdsButton.text( + onTap: ternaryActionOnTap, + child: Text(ternaryActionText), + ), + ] else ...[ + if (ternaryActionText.isNotEmpty) + ZdsButton.text( + onTap: ternaryActionOnTap, + child: Text(ternaryActionText), + ), + const Spacer(), + if (secondaryActionText.isNotEmpty && !showClose) + ZdsButton.outlined( + child: Text(secondaryActionText), + onTap: () { + secondaryActionOnTap != null ? secondaryActionOnTap() : Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 9), + if (primaryActionText.isNotEmpty) + ZdsButton.filled( + onTap: primaryActionOnTap, + child: Text(primaryActionText), + ), + ], + ], + ), + ), + ]; +} + +bool _isBottomBarRequired( + bool isTablet, + String primaryActionText, + String secondaryActionText, + String ternaryActionText, +) { + if (isTablet) { + return ternaryActionText.isNotEmpty; + } else if (primaryActionText.isNotEmpty || secondaryActionText.isNotEmpty || ternaryActionText.isNotEmpty) { + return true; + } + return false; +} + +/// Shows a bottom sheet with Zds styling. +/// +/// Uses a [ZdsBottomSheet] to build the bottom sheet contents. [context] and [builder] must not be null. +/// +/// * [builder] creates the [ZdsBottomSheet.child]. +/// * [backgroundColor] sets [ZdsBottomSheet.backgroundColor]. +/// * [barrierColor] argument is used to specify the color of the modal +/// barrier that darkens everything below the dialog. If `null`, the default +/// color `Colors.black54` is used. +/// * [maxHeight] sets [ZdsBottomSheet.maxHeight]. If not null, it must be greater than 0. The bottom sheet will +/// not grow beyond the screen height excluding the top viewPadding even if a greater maxHeight value is declared. +/// Defaults to `MediaQuery.size.height - MediaQuery.viewPadding.top`. +/// * [maxWidth] sets maxWidth for the dialog. +/// * [bottomInset] sets [ZdsBottomSheet.bottomInset]. Defaults to `MediaQuery.viewPadding.bottom`. +/// * [headerBuilder] creates the [ZdsBottomSheet.header]. Typically a [ZdsSheetHeader]. +/// * [bottomBuilder] creates the [ZdsBottomSheet.bottom]. Typically a [ZdsBottomBar]. +/// * If [isDismissible] is set to true, then clicking outside the sheet content will close the sheet else nothing +/// happens. Defaults to `true`. +/// * If [enableDrag] is set to true, then sheet could be dragged down to close it, provided that the immediate child +/// * [contentPadding] can be used to apply additional padding to the body +/// is a scrollable content. Defaults to `true`. +/// * [useRootNavigator] The useRootNavigator argument is used to determine whether to push the dialog to the +/// Navigator furthest from or nearest to the given context. By default, useRootNavigator is false and the dialog +/// route created by this method is pushed to the nearest navigator. It can not be null. +/// * [enforceSheet] argument is used to show bottom sheet irrespective of the device type. Defaults to false. +Future showZdsBottomSheet({ + required BuildContext context, + required WidgetBuilder builder, + Color? backgroundColor, + Color? barrierColor = ZdsColors.barrierColor, + double? maxHeight, + double? maxWidth, + double? bottomInset, + bool isDismissible = true, + bool enableDrag = true, + bool useRootNavigator = false, + bool enforceSheet = false, + EdgeInsetsGeometry? contentPadding, + PreferredSizeWidget Function(BuildContext)? headerBuilder, + PreferredSizeWidget? Function(BuildContext)? bottomBuilder, +}) { + return enforceSheet || !context.isTablet() + ? showMaterialModalBottomSheet( + context: context, + barrierColor: barrierColor, + isDismissible: isDismissible, + closeProgressThreshold: 0.8, + bounce: true, + enableDrag: enableDrag, + clipBehavior: Clip.antiAlias, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(15)), + ), + builder: (context) { + final header = headerBuilder?.call(context); + final bottom = bottomBuilder?.call(context); + final child = builder.call(context); + + return ZdsBottomSheet( + header: header, + bottom: bottom, + maxHeight: maxHeight, + bottomInset: bottomInset, + backgroundColor: backgroundColor, + child: child, + ); + }, + ) + : showDialog( + context: context, + barrierColor: barrierColor, + barrierDismissible: isDismissible, + useRootNavigator: useRootNavigator, + builder: (context) { + final header = headerBuilder?.call(context); + final bottom = bottomBuilder?.call(context); + Widget child = builder.call(context); + + if (contentPadding != null) { + child = Padding( + padding: contentPadding, + child: child, + ); + } + + final shortestSide = MediaQuery.of(context).size.shortestSide; + + return Dialog( + backgroundColor: backgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(15), + child: SizedBox( + width: maxWidth ?? (shortestSide * 0.75), + height: maxHeight ?? (shortestSide * 0.70), + child: Column( + children: [ + if (header != null) header, + Expanded( + child: child, + ), + if (bottom != null) bottom, + ], + ), + ), + ), + ); + }, + ); +} + +class _BottomSheetHeader extends StatelessWidget implements PreferredSizeWidget { + final PreferredSizeWidget? bottom; + final Color? backgroundColor; + + const _BottomSheetHeader({this.bottom, this.backgroundColor}); + static const _dragAreaHeight = 20.0; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + border: bottom != null ? Border(bottom: BorderSide(color: ZdsColors.lightGrey.withOpacity(0.5))) : null, + ), + child: Column( + children: [ + Container( + width: double.infinity, + height: _dragAreaHeight, + alignment: Alignment.center, + color: backgroundColor ?? Theme.of(context).colorScheme.surface, + child: Container( + width: 120, + height: 4, + decoration: BoxDecoration( + color: ZdsColors.lightGrey, + borderRadius: BorderRadius.circular(19), + ), + ), + ), + if (bottom != null) bottom!, + ], + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight( + _dragAreaHeight + (bottom?.preferredSize.height ?? 0) + kHeaderBoarderSize, + ); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/lib/src/components/molecules/card_actions.dart b/lib/src/components/molecules/card_actions.dart new file mode 100644 index 0000000..063d7b9 --- /dev/null +++ b/lib/src/components/molecules/card_actions.dart @@ -0,0 +1,141 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A widget used to show actions at the bottom of a [ZdsCard]. +/// It's recommended to use [ZdsCardWithActions] instead of using this widget directly. +/// +/// See also: +/// +/// * [ZdsCardWithActions], which uses this widget to show actions at the bottom of the card. +class ZdsCardActions extends StatelessWidget { + /// The widgets that will be laid out in a [Row]. + /// + /// Typically [ZdsTag] and [ZdsLabel]. + final List? children; + + /// If [children] only contains 1 widget, the alignment of said widget. Ignored if [children] contains more than 1 + /// widget, and MainAxisAlignment.spaceBetween will be used + /// + /// Defaults to [MainAxisAlignment.end]. + final MainAxisAlignment alignment; + + /// Constructs a [ZdsCardActions]. + const ZdsCardActions({ + super.key, + this.children, + this.alignment = MainAxisAlignment.end, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: ZdsColors.lightGrey.withOpacity(0.5))), + ), + child: Row( + mainAxisAlignment: alignment, + children: children ?? [], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('alignment', alignment)); + } +} + +/// The direction in which a [ZdsCardWithActions.children] will be laid out. +enum ZdsCardDirection { + /// Sets cards in a horizontal row in [ZdsCardWithActions.children]. + horizontal, + + /// Sets cards in a vertical column in [ZdsCardWithActions.children]. + vertical, +} + +/// Creates a [ZdsCard] with two sections, one for the main card content, and another one for status/actions +/// information. +/// +/// This component can be used to display information that has a status (e.g., an equipment order with delivery status, +/// a task with completion information, an objective with a target date...). +/// +/// The [children] will be laid out in a [Row] if [direction] is [ZdsCardDirection.horizontal], and in a [Column] if +/// [ZdsCardDirection.vertical] +/// +/// ```dart +/// ZdsCardWithActions( +/// children: [ +/// LeadingCardMainContent(), +/// TrailingCardMainContent() +/// ], +/// actions: [ +/// ZdsTag(child: Text("Incomplete")), +/// ], +/// ), +/// ``` +/// +/// +/// See also: +/// +/// * [ZdsCard] for a card without the bottom actions/state row +/// * [ZdsCardHeader], used to create a title in cards. +/// * [ZdsCardActions], the widget used to lay out actions. +class ZdsCardWithActions extends StatelessWidget { + /// Function called whenever the user taps anywhere on the card. + final VoidCallback? onTap; + + /// The widgets shown as the card's main content, above the actions. + final List? children; + + /// Whether the [children] widgets will be laid out horizontally or vertically. + final ZdsCardDirection direction; + + /// The widgets to show in the bottom part of the card. Typically contains [ZdsTag] and [ZdsLabel]. + final List? actions; + + /// Creates a card with a bottom section to display status/action information. + const ZdsCardWithActions({ + super.key, + this.actions, + this.children, + this.direction = ZdsCardDirection.horizontal, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final Widget content = direction == ZdsCardDirection.horizontal + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: children?.divide(const SizedBox(width: 16)).toList() ?? [], + ) + : Column( + children: children?.divide(const SizedBox(height: 16)).toList() ?? [], + ); + return ZdsCard( + padding: EdgeInsets.zero, + onTap: onTap, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16).copyWith(top: 20), + child: content, + ), + if (actions != null && actions!.isNotEmpty) ZdsCardActions(children: actions), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(EnumProperty('direction', direction)); + } +} diff --git a/lib/src/components/molecules/card_header.dart b/lib/src/components/molecules/card_header.dart new file mode 100644 index 0000000..0f55194 --- /dev/null +++ b/lib/src/components/molecules/card_header.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A widget used to create header in a [ZdsCard]. +/// +/// This is typically used to create a title. The [leading], [child], and [trailing] widget are displayed in a [Row] in +/// that order. [child] can't be null. +/// +/// See also: +/// +/// * [ZdsCard] where this widget is typically used. +/// * [ZdsCardWithActions], a [ZdsCard] variant with a bottom status/information bar. +class ZdsCardHeader extends StatelessWidget { + /// An optional widget that will be placed before [child]. + /// + /// Typically an [Icon]. + final Widget? leading; + + /// An optional widget that will be placed after [child]. + /// + /// Typically an [IconButton] used to create a [ZdsPopupMenu] to display more options. + final Widget? trailing; + + /// The main widget of this header. Can't be null. + /// + /// Typically a [Text]. If [leading] and [trailing] are not null, this widget will be set between them. + final Widget child; + + /// Creates a card header. + /// + /// [child] can't be null. + const ZdsCardHeader({ + required this.child, + super.key, + this.leading, + this.trailing, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16, top: 20, bottom: 8), + child: Row( + children: [ + ...[ + if (leading != null) leading!, + child.textStyle(Theme.of(context).textTheme.titleLarge), + ].divide(const SizedBox(width: 8)), + const Spacer(), + ], + ), + ), + if (trailing != null) + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(right: 5, top: 5), + child: trailing, + ), + ), + ], + ); + } +} diff --git a/lib/src/components/molecules/check_button.dart b/lib/src/components/molecules/check_button.dart new file mode 100644 index 0000000..057aec4 --- /dev/null +++ b/lib/src/components/molecules/check_button.dart @@ -0,0 +1,101 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// A circular button that can be checked and that accepts either a string or an icon as a child. +class ZdsCheckableButton extends StatelessWidget { + /// The widget that will be shown inside the button. + /// + /// If this parameter is not null, [label] must be null. + /// + /// Typically an [Icon], or a [Text]. + final IconData? icon; + + /// The button's label. + /// + /// If the text is too long, it will be clipped using ellipsis. Prefer to use short strings. + /// + /// If this parameter is not null, [icon] must be null. + final String? label; + + /// Whether the button is checked or not. + /// + /// Defaults to false. + final bool isChecked; + + /// A callback to call whenever the user taps on the button. + /// + /// Typically used to setState on [isChecked]. + final VoidCallback? onChanged; + + /// Constructs a circular, checkable button. + const ZdsCheckableButton({ + super.key, + this.icon, + this.label, + this.isChecked = false, + this.onChanged, + }) : assert( + icon != null && label == null || icon == null && label != null, + 'Icon and label cannot be used at the same time, either of them must be null', + ); + + @override + Widget build(BuildContext context) { + final ZetaColors colors = ZetaColors.of(context); + final bool enabled = onChanged != null; + final Color foreground = + isChecked ? ZetaColors.computeForeground(input: Theme.of(context).colorScheme.secondary) : colors.warm.shade60; + return MergeSemantics( + child: Semantics( + checked: isChecked, + enabled: enabled, + child: Material( + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(checkableButtonSize)), + onTap: onChanged, + // SizedBox declaring the dimension instead of the container allows the Text to overflow correctly + // instead of just being clipped + child: SizedBox.square( + dimension: checkableButtonSize, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: enabled && !isChecked ? Border.fromBorderSide(BorderSide(color: colors.warm.shade50)) : null, + color: isChecked + ? Theme.of(context).colorScheme.secondary.withOpacity(enabled ? 1.0 : 0.5) + : enabled + ? null + : Theme.of(context).colorScheme.background, + ), + child: icon != null + ? Icon(icon, color: foreground) + : Text( + label ?? '', + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: foreground, + ), + ), + ), + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('icon', icon)) + ..add(StringProperty('label', label)) + ..add(DiagnosticsProperty('isChecked', isChecked)) + ..add(ObjectFlagProperty.has('onChanged', onChanged)); + } +} diff --git a/lib/src/components/molecules/date_range_picker.dart b/lib/src/components/molecules/date_range_picker.dart new file mode 100644 index 0000000..16c208b --- /dev/null +++ b/lib/src/components/molecules/date_range_picker.dart @@ -0,0 +1,2953 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart' show DragStartBehavior; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import '../../../zds_flutter.dart'; + +/// A Material-style date picker dialog. +/// +/// It is used internally by [showDatePicker] or can be directly pushed +/// onto the [Navigator] stack to enable state restoration. See +/// [showDatePicker] for a state restoration app example. +/// +/// See also: +/// +/// * [showDatePicker], which is a way to display the date picker. +class DatePickerDialog extends StatefulWidget { + static const Size _calendarPortraitDialogSize = Size(330, 518); + static const Size _calendarLandscapeDialogSize = Size(496, 346); + static const Size _inputPortraitDialogSize = Size(330, 270); + static const Size _inputLandscapeDialogSize = Size(496, 160); + static const Size _inputRangeLandscapeDialogSize = Size(496, 164); + static const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200); + static const double _inputFormPortraitHeight = 98; + static const double _inputFormLandscapeHeight = 108; + + /// A Material-style date picker dialog. + DatePickerDialog({ + required DateTime initialDate, + required DateTime firstDate, + required DateTime lastDate, + super.key, + DateTime? currentDate, + this.initialEntryMode = DatePickerEntryMode.calendar, + this.selectableDayPredicate, + this.cancelText, + this.confirmText, + this.helpText, + this.initialCalendarMode = DatePickerMode.day, + this.errorFormatText, + this.errorInvalidText, + this.fieldHintText, + this.fieldLabelText, + this.restorationId, + }) : initialDate = DateUtils.dateOnly(initialDate), + firstDate = DateUtils.dateOnly(firstDate), + lastDate = DateUtils.dateOnly(lastDate), + currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) { + assert( + !this.lastDate.isBefore(this.firstDate), + 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.', + ); + assert( + !this.initialDate.isBefore(this.firstDate), + 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.', + ); + assert( + !this.initialDate.isAfter(this.lastDate), + 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.', + ); + assert( + selectableDayPredicate == null || selectableDayPredicate!(this.initialDate), + 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate', + ); + } + + /// The initially selected [DateTime] that the picker should display. + final DateTime initialDate; + + /// The earliest allowable [DateTime] that the user can select. + final DateTime firstDate; + + /// The latest allowable [DateTime] that the user can select. + final DateTime lastDate; + + /// The [DateTime] representing today. It will be highlighted in the day grid. + final DateTime currentDate; + + /// The initial mode of date entry method for the date picker dialog. + /// + /// See [DatePickerEntryMode] for more details on the different data entry + /// modes available. + final DatePickerEntryMode initialEntryMode; + + /// Function to provide full control over which [DateTime] can be selected. + final SelectableDayPredicate? selectableDayPredicate; + + /// The text that is displayed on the cancel button. + final String? cancelText; + + /// The text that is displayed on the confirm button. + final String? confirmText; + + /// The text that is displayed at the top of the header. + /// + /// This is used to indicate to the user what they are selecting a date for. + final String? helpText; + + /// The initial display of the calendar picker. + final DatePickerMode initialCalendarMode; + + /// The error text displayed if the entered date is not in the correct format. + final String? errorFormatText; + + /// The error text displayed if the date is not valid. + /// + /// A date is not valid if it is earlier than [firstDate], later than + /// [lastDate], or doesn't pass the [selectableDayPredicate]. + final String? errorInvalidText; + + /// The hint text displayed in the [TextField]. + /// + /// If this is null, it will default to the date format string. For example, + /// 'mm/dd/yyyy' for en_US. + final String? fieldHintText; + + /// The label text displayed in the [TextField]. + /// + /// If this is null, it will default to the words representing the date format + /// string. For example, 'Month, Day, Year' for en_US. + final String? fieldLabelText; + + /// Restoration ID to save and restore the state of the [DatePickerDialog]. + /// + /// If it is non-null, the date picker will persist and restore the + /// date selected on the dialog. + /// + /// The state of this widget is persisted in a [RestorationBucket] claimed + /// from the surrounding [RestorationScope] using the provided restoration ID. + /// + /// See also: + /// + /// * [RestorationManager], which explains how state restoration works in + /// Flutter. + final String? restorationId; + + @override + State createState() => _DatePickerDialogState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('initialDate', initialDate)) + ..add(DiagnosticsProperty('firstDate', firstDate)) + ..add(DiagnosticsProperty('lastDate', lastDate)) + ..add(DiagnosticsProperty('currentDate', currentDate)) + ..add(EnumProperty('initialEntryMode', initialEntryMode)) + ..add(ObjectFlagProperty.has('selectableDayPredicate', selectableDayPredicate)) + ..add(StringProperty('cancelText', cancelText)) + ..add(StringProperty('confirmText', confirmText)) + ..add(StringProperty('helpText', helpText)) + ..add(EnumProperty('initialCalendarMode', initialCalendarMode)) + ..add(StringProperty('errorFormatText', errorFormatText)) + ..add(StringProperty('errorInvalidText', errorInvalidText)) + ..add(StringProperty('fieldHintText', fieldHintText)) + ..add(StringProperty('fieldLabelText', fieldLabelText)) + ..add(StringProperty('restorationId', restorationId)); + } +} + +class _DatePickerDialogState extends State with RestorationMixin { + late final RestorableDateTime _selectedDate = RestorableDateTime(widget.initialDate); + late final _RestorableDatePickerEntryMode _entryMode = _RestorableDatePickerEntryMode(widget.initialEntryMode); + final RestorableBool _autoValidate = RestorableBool(false); + + @override + String? get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_selectedDate, 'selected_date'); + registerForRestoration(_autoValidate, 'autovalidate'); + registerForRestoration(_entryMode, 'calendar_entry_mode'); + } + + final GlobalKey _calendarPickerKey = GlobalKey(); + final GlobalKey _formKey = GlobalKey(); + + void _handleOk() { + if (_entryMode.value == DatePickerEntryMode.input || _entryMode.value == DatePickerEntryMode.inputOnly) { + final FormState form = _formKey.currentState!; + if (!form.validate()) { + setState(() => _autoValidate.value = true); + return; + } + form.save(); + } + Navigator.pop(context, _selectedDate.value); + } + + void _handleCancel() { + Navigator.pop(context); + } + + void _handleEntryModeToggle() { + setState(() { + switch (_entryMode.value) { + case DatePickerEntryMode.calendar: + _autoValidate.value = false; + _entryMode.value = DatePickerEntryMode.input; + case DatePickerEntryMode.input: + _formKey.currentState!.save(); + _entryMode.value = DatePickerEntryMode.calendar; + case DatePickerEntryMode.calendarOnly: + case DatePickerEntryMode.inputOnly: + assert(false, 'Can not change entry mode from _entryMode'); + } + }); + } + + void _handleDateChanged(DateTime date) { + setState(() { + _selectedDate.value = date; + }); + } + + Size _dialogSize(BuildContext context) { + final Orientation orientation = MediaQuery.of(context).orientation; + switch (_entryMode.value) { + case DatePickerEntryMode.calendar: + case DatePickerEntryMode.calendarOnly: + switch (orientation) { + case Orientation.portrait: + return DatePickerDialog._calendarPortraitDialogSize; + case Orientation.landscape: + return DatePickerDialog._calendarLandscapeDialogSize; + } + case DatePickerEntryMode.input: + case DatePickerEntryMode.inputOnly: + switch (orientation) { + case Orientation.portrait: + return DatePickerDialog._inputPortraitDialogSize; + case Orientation.landscape: + return DatePickerDialog._inputLandscapeDialogSize; + } + } + } + + static const Map _formShortcutMap = { + // Pressing enter on the field will move focus to the next field or control. + SingleActivator(LogicalKeyboardKey.enter): NextFocusIntent(), + }; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final Orientation orientation = MediaQuery.of(context).orientation; + final TextTheme textTheme = theme.textTheme; + // Constrain the textScaleFactor to the largest supported value to prevent + // layout issues. + final double textScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 1.3); + + final String dateText = localizations.formatMediumDate(_selectedDate.value); + final Color onPrimarySurface = + colorScheme.brightness == Brightness.light ? colorScheme.onPrimary : colorScheme.onSurface; + final TextStyle? dateStyle = orientation == Orientation.landscape + ? textTheme.headlineSmall?.copyWith(color: onPrimarySurface) + : textTheme.headlineMedium?.copyWith(color: onPrimarySurface); + + final Widget actions = Container( + alignment: AlignmentDirectional.centerEnd, + constraints: const BoxConstraints(minHeight: 52), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: OverflowBar( + spacing: 8, + children: [ + ZdsButton.text( + onTap: _handleCancel, + child: Text(widget.cancelText ?? localizations.cancelButtonLabel), + ), + ZdsButton.text( + onTap: _handleOk, + child: Text(widget.confirmText ?? localizations.okButtonLabel), + ), + ], + ), + ); + + CalendarDatePicker calendarDatePicker() { + return CalendarDatePicker( + key: _calendarPickerKey, + initialDate: _selectedDate.value, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + currentDate: widget.currentDate, + onDateChanged: _handleDateChanged, + selectableDayPredicate: widget.selectableDayPredicate, + initialCalendarMode: widget.initialCalendarMode, + ); + } + + Form inputDatePicker() { + return Form( + key: _formKey, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24), + height: orientation == Orientation.portrait + ? DatePickerDialog._inputFormPortraitHeight + : DatePickerDialog._inputFormLandscapeHeight, + child: Shortcuts( + shortcuts: _formShortcutMap, + child: Column( + children: [ + const Spacer(), + InputDatePickerFormField( + initialDate: _selectedDate.value, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + onDateSubmitted: _handleDateChanged, + onDateSaved: _handleDateChanged, + selectableDayPredicate: widget.selectableDayPredicate, + errorFormatText: widget.errorFormatText, + errorInvalidText: widget.errorInvalidText, + fieldHintText: widget.fieldHintText, + fieldLabelText: widget.fieldLabelText, + autofocus: true, + ), + const Spacer(), + ], + ), + ), + ), + ); + } + + final Widget picker; + final Widget? entryModeButton; + switch (_entryMode.value) { + case DatePickerEntryMode.calendar: + picker = calendarDatePicker(); + entryModeButton = IconButton( + icon: const Icon(Icons.edit), + color: onPrimarySurface, + tooltip: localizations.inputDateModeButtonLabel, + onPressed: _handleEntryModeToggle, + ); + + case DatePickerEntryMode.calendarOnly: + picker = calendarDatePicker(); + entryModeButton = null; + + case DatePickerEntryMode.input: + picker = inputDatePicker(); + entryModeButton = IconButton( + icon: const Icon(Icons.calendar_today), + color: onPrimarySurface, + tooltip: localizations.calendarModeButtonLabel, + onPressed: _handleEntryModeToggle, + ); + + case DatePickerEntryMode.inputOnly: + picker = inputDatePicker(); + entryModeButton = null; + } + + final Widget header = _DatePickerHeader( + helpText: widget.helpText ?? localizations.datePickerHelpText, + titleText: dateText, + titleStyle: dateStyle, + orientation: orientation, + isShort: orientation == Orientation.landscape, + entryModeButton: entryModeButton, + ); + + final Size dialogSize = _dialogSize(context) * textScaleFactor; + return Dialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + clipBehavior: Clip.antiAlias, + child: AnimatedContainer( + width: dialogSize.width, + height: dialogSize.height, + duration: DatePickerDialog._dialogSizeAnimationDuration, + curve: Curves.easeIn, + child: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: textScaleFactor, + ), + child: Builder( + builder: (BuildContext context) { + switch (orientation) { + case Orientation.portrait: + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + header, + Expanded(child: picker), + actions, + ], + ); + case Orientation.landscape: + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + header, + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: picker), + actions, + ], + ), + ), + ], + ); + } + }, + ), + ), + ), + ); + } +} + +// A restorable [DatePickerEntryMode] value. +// +// This serializes each entry as a unique `int` value. +class _RestorableDatePickerEntryMode extends RestorableValue { + _RestorableDatePickerEntryMode( + DatePickerEntryMode defaultValue, + ) : _defaultValue = defaultValue; + + final DatePickerEntryMode _defaultValue; + + @override + DatePickerEntryMode createDefaultValue() => _defaultValue; + + @override + void didUpdateValue(DatePickerEntryMode? oldValue) { + assert(debugIsSerializableForRestoration(value.index), 'value.index must be serializable for state restoration.'); + notifyListeners(); + } + + @override + DatePickerEntryMode fromPrimitives(Object? data) => DatePickerEntryMode.values[data! as int]; + + @override + Object? toPrimitives() => value.index; +} + +/// Re-usable widget that displays the selected date (in large font) and the +/// help text above it. +/// +/// These types include: +/// +/// * Single Date picker with calendar mode. +/// * Single Date picker with text input mode. +/// * Date Range picker with text input mode. +/// +/// [helpText], [orientation], [titleStyle], [titleText] are required and must be non-null. +class _DatePickerHeader extends StatelessWidget { + /// Creates a header for use in a date picker dialog. + const _DatePickerHeader({ + required this.titleStyle, + required this.orientation, + required this.helpText, + required this.titleText, + this.titleSemanticsLabel, + this.isShort = false, + this.entryModeButton, + }); + + static const double _datePickerHeaderLandscapeWidth = 152; + static const double _datePickerHeaderPortraitHeight = 120; + static const double _headerPaddingLandscape = 16; + + /// The text that is displayed at the top of the header. + /// + /// This is used to indicate to the user what they are selecting a date for. + final String helpText; + + /// The text that is displayed at the center of the header. + final String titleText; + + /// The semantic label associated with the [titleText]. + final String? titleSemanticsLabel; + + /// The [TextStyle] that the title text is displayed with. + final TextStyle? titleStyle; + + /// The orientation is used to decide how to layout its children. + final Orientation orientation; + + /// Indicates the header is being displayed in a shorter/narrower context. + /// + /// This will be used to tighten up the space between the help text and date + /// text if `true`. Additionally, it will use a smaller typography style if + /// `true`. + /// + /// This is necessary for displaying the manual input mode in + /// landscape orientation, in order to account for the keyboard height. + final bool isShort; + + final Widget? entryModeButton; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + final TextTheme textTheme = theme.textTheme; + + // The header should use the primary color in light themes and surface color in dark + final bool isDark = colorScheme.brightness == Brightness.dark; + final Color primarySurfaceColor = isDark ? colorScheme.surface : colorScheme.primary; + final Color onPrimarySurfaceColor = isDark ? colorScheme.onSurface : colorScheme.onPrimary; + + final TextStyle? helpStyle = textTheme.labelSmall?.copyWith( + color: onPrimarySurfaceColor, + ); + + final Text help = Text( + helpText, + style: helpStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + final Text title = Text( + titleText, + semanticsLabel: titleSemanticsLabel ?? titleText, + style: titleStyle, + maxLines: orientation == Orientation.portrait ? 1 : 2, + overflow: TextOverflow.ellipsis, + ); + + switch (orientation) { + case Orientation.portrait: + return SizedBox( + height: _datePickerHeaderPortraitHeight, + child: Material( + color: primarySurfaceColor, + child: Padding( + padding: const EdgeInsetsDirectional.only( + start: 24, + end: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + help, + const Flexible(child: SizedBox(height: 38)), + Row( + children: [ + Expanded(child: title), + if (entryModeButton != null) entryModeButton!, + ], + ), + ], + ), + ), + ), + ); + case Orientation.landscape: + return SizedBox( + width: _datePickerHeaderLandscapeWidth, + child: Material( + color: primarySurfaceColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: _headerPaddingLandscape, + ), + child: help, + ), + SizedBox(height: isShort ? 16 : 56), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: _headerPaddingLandscape, + ), + child: title, + ), + ), + if (entryModeButton != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: entryModeButton, + ), + ], + ), + ), + ); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('helpText', helpText)) + ..add(StringProperty('titleText', titleText)) + ..add(StringProperty('titleSemanticsLabel', titleSemanticsLabel)) + ..add(DiagnosticsProperty('titleStyle', titleStyle)) + ..add(EnumProperty('orientation', orientation)) + ..add(DiagnosticsProperty('isShort', isShort)); + } +} + +/// Shows a full screen modal dialog containing a Material Design date range +/// picker. +/// +/// The returned [Future] resolves to the [DateTimeRange] selected by the user +/// when the user saves their selection. If the user cancels the dialog, null is +/// returned. +/// +/// If [initialDateRange] is non-null, then it will be used as the initially +/// selected date range. If it is provided, `initialDateRange.start` must be +/// before or on `initialDateRange.end`. +/// +/// The [firstDate] is the earliest allowable date. The [lastDate] is the latest +/// allowable date. Both must be non-null. +/// +/// If an initial date range is provided, `initialDateRange.start` +/// and `initialDateRange.end` must both fall between or on [firstDate] and +/// [lastDate]. For all of these [DateTime] values, only their dates are +/// considered. Their time fields are ignored. +/// +/// The [currentDate] represents the current day (ie today). This +/// date will be highlighted in the day grid. If null, the date of +/// `DateTime.now()` will be used. +/// +/// The following optional string parameters allow you to override the default +/// text used for various parts of the dialog: +/// +/// * [helpText], the label displayed at the top of the dialog. +/// * [cancelText], the label on the cancel button for the text input mode. +/// * [confirmText],the label on the ok button for the text input mode. +/// * [saveText], the label on the save button for the fullscreen calendar +/// mode. +/// * [errorFormatText], the message used when an input text isn't in a proper +/// date format. +/// * [errorInvalidText], the message used when an input text isn't a +/// selectable date. +/// * [errorInvalidRangeText], the message used when the date range is +/// invalid (eg start date is after end date). +/// * [fieldStartHintText], the text used to prompt the user when no text has +/// been entered in the start field. +/// * [fieldEndHintText], the text used to prompt the user when no text has +/// been entered in the end field. +/// * [fieldStartLabelText], the label for the start date text input field. +/// * [fieldEndLabelText], the label for the end date text input field. +/// +/// An optional [locale] argument can be used to set the locale for the date +/// picker. It defaults to the ambient locale provided by [Localizations]. +/// +/// An optional [textDirection] argument can be used to set the text direction +/// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It +/// defaults to the ambient text direction provided by [Directionality]. If both +/// [locale] and [textDirection] are non-null, [textDirection] overrides the +/// direction chosen for the [locale]. +/// +/// The [context], [useRootNavigator] and [routeSettings] arguments are passed +/// to [showDialog], the documentation for which discusses how it is used. +/// [context] and [useRootNavigator] must be non-null. +/// +/// The [builder] parameter can be used to wrap the dialog widget +/// to add inherited widgets like [Theme]. +/// +/// ### State Restoration +/// +/// Using this method will not enable state restoration for the date range picker. +/// In order to enable state restoration for a date range picker, use +/// [Navigator.restorablePush] or [Navigator.restorablePushNamed] with +/// [ZdsDateRangePickerDialog]. +/// +/// For more information about state restoration, see [RestorationManager]. +/// +/// {@macro flutter.widgets.RestorationManager} +/// +/// {@tool sample --template=stateful_widget_restoration_material} +/// +/// This sample demonstrates how to create a restorable Material date range picker. +/// This is accomplished by enabling state restoration by specifying +/// [MaterialApp.restorationScopeId] and using [Navigator.restorablePush] to +/// push [ZdsDateRangePickerDialog] when the button is tapped. +/// +/// ```dart +/// final RestorableDateTimeN _startDate = RestorableDateTimeN(DateTime(2021, 1, 1)); +/// final RestorableDateTimeN _endDate = RestorableDateTimeN(DateTime(2021, 1, 5)); +/// late final RestorableRouteFuture _restorableDateRangePickerRouteFuture = RestorableRouteFuture( +/// onComplete: _selectDateRange, +/// onPresent: (NavigatorState navigator, Object? arguments) { +/// return navigator.restorablePush( +/// _dateRangePickerRoute, +/// arguments: { +/// 'initialStartDate': _startDate.value?.millisecondsSinceEpoch, +/// 'initialEndDate': _endDate.value?.millisecondsSinceEpoch, +/// } +/// ); +/// }, +/// ); +/// +/// void _selectDateRange(DateTimeRange? newSelectedDate) { +/// if (newSelectedDate != null) { +/// setState(() { +/// _startDate.value = newSelectedDate.start; +/// _endDate.value = newSelectedDate.end; +/// }); +/// } +/// } +/// +/// @override +/// void restoreState(RestorationBucket? oldBucket, bool initialRestore) { +/// registerForRestoration(_startDate, 'start_date'); +/// registerForRestoration(_endDate, 'end_date'); +/// registerForRestoration(_restorableDateRangePickerRouteFuture, 'date_picker_route_future'); +/// } +/// +/// static Route _dateRangePickerRoute( +/// BuildContext context, +/// Object? arguments, +/// ) { +/// return DialogRoute( +/// context: context, +/// builder: (BuildContext context) { +/// return DateRangePickerDialog( +/// restorationId: 'date_picker_dialog', +/// initialDateRange: _initialDateTimeRange(arguments! as Map), +/// firstDate: DateTime(2021, 1, 1), +/// currentDate: DateTime(2021, 1, 25), +/// lastDate: DateTime(2022, 1, 1), +/// ); +/// }, +/// ); +/// } +/// +/// static DateTimeRange? _initialDateTimeRange(Map arguments) { +/// if (arguments['initialStartDate'] != null && arguments['initialEndDate'] != null) { +/// return DateTimeRange( +/// start: DateTime.fromMillisecondsSinceEpoch(arguments['initialStartDate'] as int), +/// end: DateTime.fromMillisecondsSinceEpoch(arguments['initialEndDate'] as int), +/// ); +/// } +/// +/// return null; +/// } +/// +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// body: Center( +/// child: OutlinedButton( +/// onPressed: () { _restorableDateRangePickerRouteFuture.present(); }, +/// child: const Text('Open Date Range Picker'), +/// ), +/// ), +/// ); +/// } +/// ``` +/// +/// {@end-tool} +/// +/// See also: +/// +/// * [showDatePicker], which shows a material design date picker used to +/// select a single date. +/// * [DateTimeRange], which is used to describe a date range. +Future showZdsDateRangePicker({ + required DateTime firstDate, + required DateTime lastDate, + required BuildContext context, + DateTimeRange? initialDateRange, + DateTime? currentDate, + DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar, + String? helpText, + String? cancelText, + String? confirmText, + String? saveText, + String? errorFormatText, + String? errorInvalidText, + String? errorInvalidRangeText, + String? fieldStartHintText, + String? fieldEndHintText, + String? fieldStartLabelText, + String? fieldEndLabelText, + String? clearButtonString, + String? applyButtonString, + Locale? locale, + bool useRootNavigator = true, + RouteSettings? routeSettings, + TextDirection? textDirection, + TransitionBuilder? builder, + List? actions, + bool isWeekMode = false, + int startingDayOfWeek = 0, +}) async { + assert( + initialDateRange == null || !initialDateRange.start.isAfter(initialDateRange.end), + "initialDateRange's start date must not be after it's end date.", + ); + initialDateRange = initialDateRange == null ? null : DateUtils.datesOnly(initialDateRange); + firstDate = DateUtils.dateOnly(firstDate); + lastDate = DateUtils.dateOnly(lastDate); + assert( + !lastDate.isBefore(firstDate), + 'lastDate $lastDate must be on or after firstDate $firstDate.', + ); + assert( + initialDateRange == null || !initialDateRange.start.isBefore(firstDate), + "initialDateRange's start date must be on or after firstDate $firstDate.", + ); + assert( + initialDateRange == null || !initialDateRange.end.isBefore(firstDate), + "initialDateRange's end date must be on or after firstDate $firstDate.", + ); + assert( + initialDateRange == null || !initialDateRange.start.isAfter(lastDate), + "initialDateRange's start date must be on or before lastDate $lastDate.", + ); + assert( + initialDateRange == null || !initialDateRange.end.isAfter(lastDate), + "initialDateRange's end date must be on or before lastDate $lastDate.", + ); + currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()); + assert(debugCheckHasMaterialLocalizations(context), 'Localizations must be initialized'); + + Widget dialog = ZdsDateRangePickerDialog( + initialDateRange: initialDateRange, + clearButtonString: clearButtonString, + applyButtonString: applyButtonString, + firstDate: firstDate, + lastDate: lastDate, + currentDate: currentDate, + initialEntryMode: initialEntryMode, + helpText: helpText, + cancelText: cancelText, + confirmText: confirmText, + saveText: saveText, + actions: actions, + errorFormatText: errorFormatText, + errorInvalidText: errorInvalidText, + errorInvalidRangeText: errorInvalidRangeText, + fieldStartHintText: fieldStartHintText, + fieldEndHintText: fieldEndHintText, + fieldStartLabelText: fieldStartLabelText, + fieldEndLabelText: fieldEndLabelText, + isWeekMode: isWeekMode, + startDayOfWeek: startingDayOfWeek, + ); + + if (textDirection != null) { + dialog = Directionality( + textDirection: textDirection, + child: dialog, + ); + } + + if (locale != null) { + dialog = Localizations.override( + context: context, + locale: locale, + child: dialog, + ); + } + return showDialog( + context: context, + useRootNavigator: useRootNavigator, + routeSettings: routeSettings, + useSafeArea: false, + builder: (BuildContext context) { + return builder == null ? dialog : builder(context, dialog); + }, + ); +} + +/// Returns a locale-appropriate string to describe the start of a date range. +/// +/// If `startDate` is null, then it defaults to 'Start Date', otherwise if it +/// is in the same year as the `endDate` then it will use the short month +/// day format (i.e. 'Jan 21'). Otherwise it will return the short date format +/// (i.e. 'Jan 21, 2020'). +String _formatRangeStartDate(MaterialLocalizations localizations, DateTime? startDate, DateTime? endDate) { + return startDate == null + ? localizations.dateRangeStartLabel + : (endDate == null || startDate.year == endDate.year) + ? localizations.formatShortMonthDay(startDate) + : localizations.formatShortDate(startDate); +} + +/// Returns an locale-appropriate string to describe the end of a date range. +/// +/// If `endDate` is null, then it defaults to 'End Date', otherwise if it +/// is in the same year as the `startDate` and the `currentDate` then it will +/// just use the short month day format (i.e. 'Jan 21'), otherwise it will +/// include the year (i.e. 'Jan 21, 2020'). +String _formatRangeEndDate( + MaterialLocalizations localizations, + DateTime? startDate, + DateTime? endDate, + DateTime currentDate, +) { + return endDate == null + ? localizations.dateRangeEndLabel + : (startDate != null && startDate.year == endDate.year && startDate.year == currentDate.year) + ? localizations.formatShortMonthDay(endDate) + : localizations.formatShortDate(endDate); +} + +/// A Material-style date range picker dialog. +/// +/// It is used internally by [showDateRangePicker] or can be directly pushed +/// onto the [Navigator] stack to enable state restoration. See +/// [showDateRangePicker] for a state restoration app example. +/// +/// See also: +/// +/// * [showDateRangePicker], which is a way to display the date picker. +class ZdsDateRangePickerDialog extends StatefulWidget { + /// A Material-style date range picker dialog. + const ZdsDateRangePickerDialog({ + required this.firstDate, + required this.lastDate, + super.key, + this.initialDateRange, + this.currentDate, + this.initialEntryMode = DatePickerEntryMode.calendar, + this.helpText, + this.cancelText, + this.confirmText, + this.saveText, + this.errorInvalidRangeText, + this.errorFormatText, + this.errorInvalidText, + this.fieldStartHintText, + this.fieldEndHintText, + this.fieldStartLabelText, + this.fieldEndLabelText, + this.restorationId, + this.actions, + this.clearButtonString, + this.applyButtonString, + this.isWeekMode = false, + this.startDayOfWeek = 0, + }) : assert( + !isWeekMode || (startDayOfWeek >= 0 && startDayOfWeek <= 6), + 'startingDayOfWeek must be an int between 0 and 6 inclusive', + ); + + /// Optional list of widgets to display within the top action bar of the widget. + final List? actions; + + /// The date range that the date range picker starts with when it opens. + /// + /// If an initial date range is provided, `initialDateRange.start` + /// and `initialDateRange.end` must both fall between or on [firstDate] and + /// [lastDate]. For all of these [DateTime] values, only their dates are + /// considered. Their time fields are ignored. + /// + /// If [initialDateRange] is non-null, then it will be used as the initially + /// selected date range. If it is provided, `initialDateRange.start` must be + /// before or on `initialDateRange.end`. + final DateTimeRange? initialDateRange; + + /// The earliest allowable date on the date range. + final DateTime firstDate; + + /// The latest allowable date on the date range. + final DateTime lastDate; + + /// The [currentDate] represents the current day (ie today). + /// + /// This date will be highlighted in the day grid. + /// + /// If `null`, the date of `DateTime.now()` will be used. + final DateTime? currentDate; + + /// The initial date range picker entry mode. + /// + /// The date range has two main modes: [DatePickerEntryMode.calendar] (a + /// scrollable calendar month grid) or [DatePickerEntryMode.input] (two text + /// input fields) mode. + /// + /// It defaults to [DatePickerEntryMode.calendar] and must be non-null. + final DatePickerEntryMode initialEntryMode; + + /// The label on the cancel button for the text input mode. + /// + /// If null, the localized value of + /// [MaterialLocalizations.cancelButtonLabel] is used. + final String? cancelText; + + /// The label on the "OK" button for the text input mode. + /// + /// If null, the localized value of + /// [MaterialLocalizations.okButtonLabel] is used. + final String? confirmText; + + /// The label on the save button for the fullscreen calendar mode. + /// + /// If null, the localized value of + /// [MaterialLocalizations.saveButtonLabel] is used. + final String? saveText; + + /// The label displayed at the top of the dialog. + /// + /// If null, the localized value of + /// [MaterialLocalizations.dateRangePickerHelpText] is used. + final String? helpText; + + /// The message used when the date range is invalid (eg start date is after + /// end date). + /// + /// If null, the localized value of + /// [MaterialLocalizations.invalidDateRangeLabel] is used. + final String? errorInvalidRangeText; + + /// The message used when an input text isn't in a proper date format. + /// + /// If null, the localized value of + /// [MaterialLocalizations.invalidDateFormatLabel] is used. + final String? errorFormatText; + + /// The message used when an input text isn't a selectable date. + /// + /// If null, the localized value of + /// [MaterialLocalizations.dateOutOfRangeLabel] is used. + final String? errorInvalidText; + + /// The text used to prompt the user when no text has been entered in the + /// start field. + /// + /// If null, the localized value of + /// [MaterialLocalizations.dateHelpText] is used. + final String? fieldStartHintText; + + /// The text used to prompt the user when no text has been entered in the + /// end field. + /// + /// If null, the localized value of [MaterialLocalizations.dateHelpText] is + /// used. + final String? fieldEndHintText; + + /// The label for the start date text input field. + /// + /// If null, the localized value of [MaterialLocalizations.dateRangeStartLabel] + /// is used. + final String? fieldStartLabelText; + + /// The label for the end date text input field. + /// + /// If null, the localized value of [MaterialLocalizations.dateRangeEndLabel] + /// is used. + final String? fieldEndLabelText; + + /// Restoration ID to save and restore the state of the [ZdsDateRangePickerDialog]. + /// + /// If it is non-null, the date range picker will persist and restore the + /// date range selected on the dialog. + /// + /// The state of this widget is persisted in a [RestorationBucket] claimed + /// from the surrounding [RestorationScope] using the provided restoration ID. + /// + /// See also: + /// + /// * [RestorationManager], which explains how state restoration works in + /// Flutter. + final String? restorationId; + + /// String to display on UI for clear button. + /// + /// Used to pass in custom strings such as for localization. + final String? clearButtonString; + + /// String to display on UI for apply button. + /// + /// Used to pass in custom strings such as for localization. + final String? applyButtonString; + + /// If true, user can only select weeks, not days in popup. + /// + /// If being used to show weeks, user should not be able to select a random selection of days. + /// + /// Defaults to false. + final bool isWeekMode; + + /// 0 indexed where Sunday is 0 and Saturday is 6 + /// + /// Defaults to 0. + final int startDayOfWeek; + + @override + State createState() => _ZdsDateRangePickerDialogState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('initialDateRange', initialDateRange)) + ..add(DiagnosticsProperty('firstDate', firstDate)) + ..add(DiagnosticsProperty('lastDate', lastDate)) + ..add(DiagnosticsProperty('currentDate', currentDate)) + ..add(EnumProperty('initialEntryMode', initialEntryMode)) + ..add(StringProperty('cancelText', cancelText)) + ..add(StringProperty('confirmText', confirmText)) + ..add(StringProperty('saveText', saveText)) + ..add(StringProperty('helpText', helpText)) + ..add(StringProperty('errorInvalidRangeText', errorInvalidRangeText)) + ..add(StringProperty('errorFormatText', errorFormatText)) + ..add(StringProperty('errorInvalidText', errorInvalidText)) + ..add(StringProperty('fieldStartHintText', fieldStartHintText)) + ..add(StringProperty('fieldEndHintText', fieldEndHintText)) + ..add(StringProperty('fieldStartLabelText', fieldStartLabelText)) + ..add(StringProperty('fieldEndLabelText', fieldEndLabelText)) + ..add(StringProperty('restorationId', restorationId)) + ..add(StringProperty('clearButtonString', clearButtonString)) + ..add(StringProperty('applyButtonString', applyButtonString)) + ..add(DiagnosticsProperty('isWeekMode', isWeekMode)) + ..add(IntProperty('startDayOfWeek', startDayOfWeek)); + } +} + +class _ZdsDateRangePickerDialogState extends State with RestorationMixin { + late final _RestorableDatePickerEntryMode _entryMode = _RestorableDatePickerEntryMode(widget.initialEntryMode); + late final RestorableDateTimeN _selectedStart = RestorableDateTimeN(widget.initialDateRange?.start); + late final RestorableDateTimeN _selectedEnd = RestorableDateTimeN(widget.initialDateRange?.end); + final RestorableBool _autoValidate = RestorableBool(false); + final GlobalKey _calendarPickerKey = GlobalKey(); + final GlobalKey<_InputDateRangePickerState> _inputPickerKey = GlobalKey<_InputDateRangePickerState>(); + + @override + String? get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_entryMode, 'entry_mode'); + registerForRestoration(_selectedStart, 'selected_start'); + registerForRestoration(_selectedEnd, 'selected_end'); + registerForRestoration(_autoValidate, 'autovalidate'); + } + + void _handleOk() { + if (_entryMode.value == DatePickerEntryMode.input || _entryMode.value == DatePickerEntryMode.inputOnly) { + final _InputDateRangePickerState picker = _inputPickerKey.currentState!; + if (!picker.validate()) { + setState(() { + _autoValidate.value = true; + }); + return; + } + } + final DateTimeRange? selectedRange = + _hasSelectedDateRange ? DateTimeRange(start: _selectedStart.value!, end: _selectedEnd.value!) : null; + + Navigator.pop(context, selectedRange); + } + + void _handleCancel() { + Navigator.pop(context); + } + + void _handleEntryModeToggle() { + setState(() { + switch (_entryMode.value) { + case DatePickerEntryMode.calendar: + _autoValidate.value = false; + _entryMode.value = DatePickerEntryMode.input; + + case DatePickerEntryMode.input: + // Validate the range dates + if (_selectedStart.value != null && + (_selectedStart.value!.isBefore(widget.firstDate) || _selectedStart.value!.isAfter(widget.lastDate))) { + _selectedStart.value = null; + // With no valid start date, having an end date makes no sense for the UI. + _selectedEnd.value = null; + } + if (_selectedEnd.value != null && + (_selectedEnd.value!.isBefore(widget.firstDate) || _selectedEnd.value!.isAfter(widget.lastDate))) { + _selectedEnd.value = null; + } + // If invalid range (start after end), then just use the start date + if (_selectedStart.value != null && + _selectedEnd.value != null && + _selectedStart.value!.isAfter(_selectedEnd.value!)) { + _selectedEnd.value = null; + } + _entryMode.value = DatePickerEntryMode.calendar; + + case DatePickerEntryMode.calendarOnly: + case DatePickerEntryMode.inputOnly: + assert(false, 'Can not change entry mode from $_entryMode'); + } + }); + } + + void _handleStartDateChanged(DateTime? date) { + setState(() => _selectedStart.value = date); + } + + void _handleEndDateChanged(DateTime? date) { + setState(() => _selectedEnd.value = date); + } + + bool get _hasSelectedDateRange => _selectedStart.value != null && _selectedEnd.value != null; + + @override + Widget build(BuildContext context) { + final MediaQueryData mediaQuery = MediaQuery.of(context); + final Orientation orientation = mediaQuery.orientation; + final double textScaleFactor = math.min(mediaQuery.textScaleFactor, 1.3); + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final Color onPrimarySurface = + colorScheme.brightness == Brightness.light ? colorScheme.onPrimary : colorScheme.onSurface; + + final Widget contents; + final Size size; + ShapeBorder? shape; + final double elevation; + final EdgeInsets insetPadding; + final bool showEntryModeButton = + _entryMode.value == DatePickerEntryMode.calendar || _entryMode.value == DatePickerEntryMode.input; + switch (_entryMode.value) { + case DatePickerEntryMode.calendar: + case DatePickerEntryMode.calendarOnly: + contents = _CalendarRangePickerDialog( + key: _calendarPickerKey, + clearButtonString: widget.clearButtonString, + applyButtonString: widget.applyButtonString, + selectedStartDate: _selectedStart.value, + selectedEndDate: _selectedEnd.value, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + currentDate: widget.currentDate, + onStartDateChanged: _handleStartDateChanged, + onEndDateChanged: _handleEndDateChanged, + onConfirm: _hasSelectedDateRange ? _handleOk : null, + onCancel: _handleCancel, + actions: widget.actions, + isWeekMode: widget.isWeekMode, + startDayOfWeek: widget.startDayOfWeek, + entryModeButton: showEntryModeButton + ? IconButton( + icon: const Icon(Icons.edit), + padding: EdgeInsets.zero, + color: onPrimarySurface, + tooltip: localizations.inputDateModeButtonLabel, + onPressed: _handleEntryModeToggle, + ) + : null, + confirmText: widget.saveText ?? localizations.saveButtonLabel, + helpText: widget.helpText ?? localizations.dateRangePickerHelpText, + ); + size = mediaQuery.size; + insetPadding = EdgeInsets.zero; + shape = const RoundedRectangleBorder(); + elevation = 0; + + case DatePickerEntryMode.input: + case DatePickerEntryMode.inputOnly: + contents = _InputDateRangePickerDialog( + selectedStartDate: _selectedStart.value, + selectedEndDate: _selectedEnd.value, + currentDate: widget.currentDate, + picker: Container( + padding: const EdgeInsets.symmetric(horizontal: 24), + height: orientation == Orientation.portrait + ? DatePickerDialog._inputFormPortraitHeight + : DatePickerDialog._inputFormLandscapeHeight, + child: Column( + children: [ + const Spacer(), + _InputDateRangePicker( + key: _inputPickerKey, + initialStartDate: _selectedStart.value, + initialEndDate: _selectedEnd.value, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + onStartDateChanged: _handleStartDateChanged, + onEndDateChanged: _handleEndDateChanged, + autofocus: true, + autovalidate: _autoValidate.value, + helpText: widget.helpText, + errorInvalidRangeText: widget.errorInvalidRangeText, + errorFormatText: widget.errorFormatText, + errorInvalidText: widget.errorInvalidText, + fieldStartHintText: widget.fieldStartHintText, + fieldEndHintText: widget.fieldEndHintText, + fieldStartLabelText: widget.fieldStartLabelText, + fieldEndLabelText: widget.fieldEndLabelText, + ), + const Spacer(), + ], + ), + ), + onConfirm: _handleOk, + onCancel: _handleCancel, + entryModeButton: showEntryModeButton + ? IconButton( + icon: const Icon(Icons.calendar_today), + padding: EdgeInsets.zero, + color: onPrimarySurface, + tooltip: localizations.calendarModeButtonLabel, + onPressed: _handleEntryModeToggle, + ) + : null, + confirmText: widget.confirmText ?? localizations.okButtonLabel, + cancelText: widget.cancelText ?? localizations.cancelButtonLabel, + helpText: widget.helpText ?? localizations.dateRangePickerHelpText, + ); + final DialogTheme dialogTheme = Theme.of(context).dialogTheme; + size = orientation == Orientation.portrait + ? DatePickerDialog._inputPortraitDialogSize + : DatePickerDialog._inputRangeLandscapeDialogSize; + insetPadding = const EdgeInsets.symmetric(horizontal: 16, vertical: 24); + shape = dialogTheme.shape; + elevation = dialogTheme.elevation ?? 24; + } + + return Dialog( + insetPadding: insetPadding, + shape: shape, + elevation: elevation, + clipBehavior: Clip.antiAlias, + child: AnimatedContainer( + width: size.width, + height: size.height, + duration: DatePickerDialog._dialogSizeAnimationDuration, + curve: Curves.easeIn, + child: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: textScaleFactor, + ), + child: Builder( + builder: (BuildContext context) { + return contents; + }, + ), + ), + ), + ); + } +} + +class _CalendarRangePickerDialog extends StatelessWidget { + const _CalendarRangePickerDialog({ + required this.selectedStartDate, + required this.selectedEndDate, + required this.firstDate, + required this.lastDate, + required this.currentDate, + required this.onStartDateChanged, + required this.onEndDateChanged, + required this.onConfirm, + required this.onCancel, + required this.confirmText, + required this.helpText, + super.key, + this.actions, + this.entryModeButton, + this.clearButtonString, + this.applyButtonString, + this.isWeekMode = false, + this.startDayOfWeek = 0, + }) : assert( + !isWeekMode || (startDayOfWeek >= 0 && startDayOfWeek <= 6), + 'startingDayOfWeek must be an int between 0 and 6 inclusive', + ); + + final List? actions; + final DateTime? selectedStartDate; + final DateTime? selectedEndDate; + final DateTime firstDate; + final DateTime lastDate; + final DateTime? currentDate; + final ValueChanged onStartDateChanged; + final ValueChanged onEndDateChanged; + final VoidCallback? onConfirm; + final VoidCallback? onCancel; + final String confirmText; + final String helpText; + final Widget? entryModeButton; + final String? clearButtonString; + final String? applyButtonString; + final bool isWeekMode; + final int startDayOfWeek; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final Orientation orientation = MediaQuery.of(context).orientation; + final TextTheme textTheme = theme.textTheme; + final Color headerForeground = colorScheme.onPrimary; + final Color headerDisabledForeground = headerForeground.withOpacity(0.38); + final String startDateText = _formatRangeStartDate(localizations, selectedStartDate, selectedEndDate); + final String endDateText = _formatRangeEndDate(localizations, selectedStartDate, selectedEndDate, DateTime.now()); + final TextStyle? headlineStyle = textTheme.headlineSmall; + final TextStyle? startDateStyle = headlineStyle?.apply( + color: selectedStartDate != null ? headerForeground : headerDisabledForeground, + ); + final TextStyle? endDateStyle = headlineStyle?.apply( + color: selectedEndDate != null ? headerForeground : headerDisabledForeground, + ); + final TextStyle saveButtonStyle = textTheme.labelLarge!.apply( + color: onConfirm != null ? headerForeground : headerDisabledForeground, + ); + + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + leading: CloseButton( + onPressed: onCancel, + ), + actions: [ + if (orientation == Orientation.portrait && entryModeButton != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: entryModeButton, + ), + ], + title: PreferredSize( + preferredSize: const Size.fromHeight(46), + child: SizedBox( + width: double.infinity, + height: 46, + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Text( + startDateText, + style: startDateStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text(' - ', style: startDateStyle), + Flexible( + child: Text( + endDateText, + style: endDateStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + if (actions != null) + DefaultTextStyle( + style: saveButtonStyle, + child: Row( + children: actions!, + ), + ), + ], + ), + ), + ), + ), + body: _CalendarDateRangePicker( + initialStartDate: selectedStartDate, + initialEndDate: selectedEndDate, + firstDate: firstDate, + lastDate: lastDate, + currentDate: currentDate, + onStartDateChanged: onStartDateChanged, + onEndDateChanged: onEndDateChanged, + isWeekMode: isWeekMode, + startDayOfWeek: startDayOfWeek, + ), + bottomNavigationBar: ZdsBottomBar( + child: Row( + children: [ + const Spacer(), + ZdsButton.outlined( + onTap: onCancel, + child: Text(clearButtonString ?? ComponentStrings.of(context).get('CLEAR', 'Clear')), + ), + const SizedBox(width: 8), + ZdsButton( + onTap: onConfirm, + child: Text(applyButtonString ?? ComponentStrings.of(context).get('APPLY', 'Apply')), + ), + ], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('selectedStartDate', selectedStartDate)) + ..add(DiagnosticsProperty('selectedEndDate', selectedEndDate)) + ..add(DiagnosticsProperty('firstDate', firstDate)) + ..add(DiagnosticsProperty('lastDate', lastDate)) + ..add(DiagnosticsProperty('currentDate', currentDate)) + ..add(ObjectFlagProperty>.has('onStartDateChanged', onStartDateChanged)) + ..add(ObjectFlagProperty>.has('onEndDateChanged', onEndDateChanged)) + ..add(ObjectFlagProperty.has('onConfirm', onConfirm)) + ..add(ObjectFlagProperty.has('onCancel', onCancel)) + ..add(StringProperty('confirmText', confirmText)) + ..add(StringProperty('helpText', helpText)) + ..add(StringProperty('clearButtonString', clearButtonString)) + ..add(StringProperty('applyButtonString', applyButtonString)) + ..add(DiagnosticsProperty('isWeekMode', isWeekMode)) + ..add(IntProperty('startDayOfWeek', startDayOfWeek)); + } +} + +const Duration _monthScrollDuration = Duration(milliseconds: 200); + +const double _monthItemHeaderHeight = 58; +const double _monthItemFooterHeight = 12; +const double _monthItemRowHeight = 48; +const double _monthItemSpaceBetweenRows = 8; +const double _horizontalPadding = 8; +const double _maxCalendarWidthLandscape = 384; +const double _maxCalendarWidthPortrait = 480; + +/// Displays a scrollable calendar grid that allows a user to select a range +/// of dates. +class _CalendarDateRangePicker extends StatefulWidget { + /// Creates a scrollable calendar grid for picking date ranges. + _CalendarDateRangePicker({ + required DateTime firstDate, + required DateTime lastDate, + required this.onStartDateChanged, + required this.onEndDateChanged, + DateTime? initialStartDate, + DateTime? initialEndDate, + DateTime? currentDate, + this.isWeekMode = false, + this.startDayOfWeek = 0, + }) : initialStartDate = initialStartDate != null ? DateUtils.dateOnly(initialStartDate) : null, + initialEndDate = initialEndDate != null ? DateUtils.dateOnly(initialEndDate) : null, + firstDate = DateUtils.dateOnly(firstDate), + lastDate = DateUtils.dateOnly(lastDate), + currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) { + assert( + this.initialStartDate == null || this.initialEndDate == null || !this.initialStartDate!.isAfter(initialEndDate!), + 'initialStartDate must be on or before initialEndDate.', + ); + assert( + !this.lastDate.isBefore(this.firstDate), + 'firstDate must be on or before lastDate.', + ); + } + + /// The [DateTime] that represents the start of the initial date range selection. + final DateTime? initialStartDate; + + /// The [DateTime] that represents the end of the initial date range selection. + final DateTime? initialEndDate; + + /// The earliest allowable [DateTime] that the user can select. + final DateTime firstDate; + + /// The latest allowable [DateTime] that the user can select. + final DateTime lastDate; + + /// The [DateTime] representing today. It will be highlighted in the day grid. + final DateTime currentDate; + + /// Called when the user changes the start date of the selected range. + final ValueChanged? onStartDateChanged; + + /// Called when the user changes the end date of the selected range. + final ValueChanged? onEndDateChanged; + + /// If true, user can only select weeks, not days in popup. + /// + /// If being used to show weeks, user should not be able to select a random selection of days. + /// + /// Defaults to false. + final bool isWeekMode; + + /// 0 indexed where Sunday is 0 and Saturday is 6 + /// + /// Defaults to 0. + final int startDayOfWeek; + + @override + _CalendarDateRangePickerState createState() => _CalendarDateRangePickerState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('initialStartDate', initialStartDate)) + ..add(DiagnosticsProperty('initialEndDate', initialEndDate)) + ..add(DiagnosticsProperty('firstDate', firstDate)) + ..add(DiagnosticsProperty('lastDate', lastDate)) + ..add(DiagnosticsProperty('currentDate', currentDate)) + ..add(ObjectFlagProperty?>.has('onStartDateChanged', onStartDateChanged)) + ..add(ObjectFlagProperty?>.has('onEndDateChanged', onEndDateChanged)) + ..add(DiagnosticsProperty('isWeekMode', isWeekMode)) + ..add(IntProperty('startDayOfWeek', startDayOfWeek)); + } +} + +class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> { + final GlobalKey _scrollViewKey = GlobalKey(); + DateTime? _startDate; + DateTime? _endDate; + int _initialMonthIndex = 0; + late ScrollController _controller; + late bool _showWeekBottomDivider; + + @override + void initState() { + super.initState(); + _controller = ScrollController(); + _controller.addListener(_scrollListener); + + _startDate = widget.initialStartDate; + _endDate = widget.initialEndDate; + + // Calculate the index for the initially displayed month. This is needed to + // divide the list of months into two `SliverList`s. + final DateTime initialDate = widget.initialStartDate ?? widget.currentDate; + if (!initialDate.isBefore(widget.firstDate) && !initialDate.isAfter(widget.lastDate)) { + _initialMonthIndex = DateUtils.monthDelta(widget.firstDate, initialDate); + } + + _showWeekBottomDivider = _initialMonthIndex != 0; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (_controller.offset <= _controller.position.minScrollExtent) { + setState(() { + _showWeekBottomDivider = false; + }); + } else if (!_showWeekBottomDivider) { + setState(() { + _showWeekBottomDivider = true; + }); + } + } + + int get _numberOfMonths => DateUtils.monthDelta(widget.firstDate, widget.lastDate) + 1; + + void _vibrate() { + switch (Theme.of(context).platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + unawaited(HapticFeedback.vibrate()); + case TargetPlatform.iOS: + break; + case TargetPlatform.linux: + break; + case TargetPlatform.macOS: + break; + case TargetPlatform.windows: + break; + } + } + + // This updates the selected date range using this logic: + // + // * From the unselected state, selecting one date creates the start date. + // * If the next selection is before the start date, reset date range and + // set the start date to that selection. + // * If the next selection is on or after the start date, set the end date + // to that selection. + // * After both start and end dates are selected, any subsequent selection + // resets the date range and sets start date to that selection. + void _updateSelection(DateTime date) { + _vibrate(); + setState(() { + if (_startDate != null && _endDate == null && !date.isBefore(_startDate!)) { + _endDate = date; + widget.onEndDateChanged?.call(_endDate); + } else { + _startDate = date; + widget.onStartDateChanged?.call(_startDate!); + if (_endDate != null) { + _endDate = null; + widget.onEndDateChanged?.call(_endDate); + } + } + + if (widget.isWeekMode) { + widget.onStartDateChanged?.call(_startDate = date.getFirstDayOfWeek(weekStartDay: widget.startDayOfWeek)); + widget.onEndDateChanged?.call(_endDate = date.getLastDayOfWeek(weekStartDay: widget.startDayOfWeek)); + } + }); + } + + Widget _buildMonthItem(int index, bool beforeInitialMonth) { + final int monthIndex = beforeInitialMonth ? _initialMonthIndex - index - 1 : _initialMonthIndex + index; + final DateTime month = DateUtils.addMonthsToMonthDate(widget.firstDate, monthIndex); + return MonthItem( + selectedDateStart: _startDate, + selectedDateEnd: _endDate, + currentDate: widget.currentDate, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + displayedMonth: month, + onChanged: _updateSelection, + ); + } + + @override + Widget build(BuildContext context) { + const Key sliverAfterKey = Key('sliverAfterKey'); + + return Column( + children: [ + _DayHeaders(), + if (_showWeekBottomDivider) const Divider(height: 0), + Expanded( + child: _CalendarKeyboardNavigator( + firstDate: widget.firstDate, + lastDate: widget.lastDate, + initialFocusedDay: _startDate ?? widget.initialStartDate ?? widget.currentDate, + // In order to prevent performance issues when displaying the + // correct initial month, 2 `SliverList`s are used to split the + // months. The first item in the second SliverList is the initial + // month to be displayed. + child: CustomScrollView( + key: _scrollViewKey, + controller: _controller, + center: sliverAfterKey, + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => _buildMonthItem(index, true), + childCount: _initialMonthIndex, + ), + ), + SliverList( + key: sliverAfterKey, + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => _buildMonthItem(index, false), + childCount: _numberOfMonths - _initialMonthIndex, + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _CalendarKeyboardNavigator extends StatefulWidget { + const _CalendarKeyboardNavigator({ + required this.child, + required this.firstDate, + required this.lastDate, + required this.initialFocusedDay, + }); + + final Widget child; + final DateTime firstDate; + final DateTime lastDate; + final DateTime initialFocusedDay; + + @override + _CalendarKeyboardNavigatorState createState() => _CalendarKeyboardNavigatorState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('firstDate', firstDate)) + ..add(DiagnosticsProperty('lastDate', lastDate)) + ..add(DiagnosticsProperty('initialFocusedDay', initialFocusedDay)); + } +} + +class _CalendarKeyboardNavigatorState extends State<_CalendarKeyboardNavigator> { + final Map _shortcutMap = const { + SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left), + SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right), + SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down), + SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up), + }; + late Map> _actionMap; + late FocusNode _dayGridFocus; + TraversalDirection? _dayTraversalDirection; + DateTime? _focusedDay; + + @override + void initState() { + super.initState(); + + _actionMap = >{ + NextFocusIntent: CallbackAction(onInvoke: _handleGridNextFocus), + PreviousFocusIntent: CallbackAction(onInvoke: _handleGridPreviousFocus), + DirectionalFocusIntent: CallbackAction(onInvoke: _handleDirectionFocus), + }; + _dayGridFocus = FocusNode(debugLabel: 'Day Grid'); + } + + @override + void dispose() { + _dayGridFocus.dispose(); + super.dispose(); + } + + void _handleGridFocusChange(bool focused) { + setState(() { + if (focused) { + _focusedDay ??= widget.initialFocusedDay; + } + }); + } + + /// Move focus to the next element after the day grid. + void _handleGridNextFocus(_) { + _dayGridFocus + ..requestFocus() + ..nextFocus(); + } + + /// Move focus to the previous element before the day grid. + void _handleGridPreviousFocus(_) { + _dayGridFocus + ..requestFocus() + ..previousFocus(); + } + + /// Move the internal focus date in the direction of the given intent. + /// + /// This will attempt to move the focused day to the next selectable day in + /// the given direction. If the new date is not in the current month, then + /// the page view will be scrolled to show the new date's month. + /// + /// For horizontal directions, it will move forward or backward a day (depending + /// on the current [TextDirection]). For vertical directions it will move up and + /// down a week at a time. + void _handleDirectionFocus(DirectionalFocusIntent intent) { + assert(_focusedDay != null, '_focusedDay must not be null.'); + setState(() { + final DateTime? nextDate = _nextDateInDirection(_focusedDay!, intent.direction); + if (nextDate != null) { + _focusedDay = nextDate; + _dayTraversalDirection = intent.direction; + } + }); + } + + static const Map _directionOffset = { + TraversalDirection.up: -DateTime.daysPerWeek, + TraversalDirection.right: 1, + TraversalDirection.down: DateTime.daysPerWeek, + TraversalDirection.left: -1, + }; + + int _dayDirectionOffset(TraversalDirection traversalDirection, TextDirection textDirection) { + TraversalDirection td = traversalDirection; + // Swap left and right if the text direction if RTL + if (textDirection == TextDirection.rtl) { + if (traversalDirection == TraversalDirection.left) { + td = TraversalDirection.right; + } else if (traversalDirection == TraversalDirection.right) { + td = TraversalDirection.left; + } + } + return _directionOffset[td] ?? 0; + } + + DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) { + final TextDirection textDirection = Directionality.of(context); + final DateTime nextDate = DateUtils.addDaysToDate(date, _dayDirectionOffset(direction, textDirection)); + if (!nextDate.isBefore(widget.firstDate) && !nextDate.isAfter(widget.lastDate)) { + return nextDate; + } + return null; + } + + @override + Widget build(BuildContext context) { + return FocusableActionDetector( + shortcuts: _shortcutMap, + actions: _actionMap, + focusNode: _dayGridFocus, + onFocusChange: _handleGridFocusChange, + child: _FocusedDate( + date: _dayGridFocus.hasFocus ? _focusedDay : null, + scrollDirection: _dayGridFocus.hasFocus ? _dayTraversalDirection : null, + child: widget.child, + ), + ); + } +} + +/// InheritedWidget indicating what the current focused date is for its children. +/// +/// This is used by the [MonthItem] to let its children know what the currently focused date (if any) should be. +class _FocusedDate extends InheritedWidget { + const _FocusedDate({ + required super.child, + this.date, + this.scrollDirection, + }); + + final DateTime? date; + final TraversalDirection? scrollDirection; + + @override + bool updateShouldNotify(_FocusedDate oldWidget) { + return !DateUtils.isSameDay(date, oldWidget.date) || scrollDirection != oldWidget.scrollDirection; + } + + static _FocusedDate? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_FocusedDate>(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('date', date)) + ..add(EnumProperty('scrollDirection', scrollDirection)); + } +} + +class _DayHeaders extends StatelessWidget { + /// Builds widgets showing abbreviated days of week. The first widget in the + /// returned list corresponds to the first day of week for the current locale. + /// + /// Examples: + /// + /// ``` + /// ┌ Sunday is the first day of week in the US (en_US) + /// | + /// S M T W T F S <-- the returned list contains these widgets + /// _ _ _ _ _ 1 2 + /// 3 4 5 6 7 8 9 + /// + /// ┌ But it's Monday in the UK (en_GB) + /// | + /// M T W T F S S <-- the returned list contains these widgets + /// _ _ _ _ 1 2 3 + /// 4 5 6 7 8 9 10 + /// ``` + List _getDayHeaders(TextStyle headerStyle, MaterialLocalizations localizations) { + final List result = []; + bool foundFlag = false; + int iterator = localizations.firstDayOfWeekIndex; + while (!foundFlag) { + final String weekday = localizations.narrowWeekdays[iterator]; + result.add( + ExcludeSemantics( + child: Center(child: Text(weekday, style: headerStyle)), + ), + ); + if (iterator == (localizations.firstDayOfWeekIndex - 1) % 7) foundFlag = true; + iterator = (iterator + 1) % 7; + } + return result; + } + + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + final ColorScheme colorScheme = themeData.colorScheme; + final TextStyle textStyle = themeData.textTheme.titleSmall!.apply(color: colorScheme.onSurface); + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + + // Add leading and trailing containers for edges of the custom grid layout. + final List labels = _getDayHeaders(textStyle, localizations) + ..insert(0, const SizedBox()) + ..add(const SizedBox()); + + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).orientation == Orientation.landscape + ? _maxCalendarWidthLandscape + : _maxCalendarWidthPortrait, + maxHeight: _monthItemRowHeight, + ), + child: GridView.custom( + shrinkWrap: true, + gridDelegate: _monthItemGridDelegate, + childrenDelegate: SliverChildListDelegate( + labels, + addRepaintBoundaries: false, + ), + ), + ); + } +} + +class _MonthItemGridDelegate extends SliverGridDelegate { + const _MonthItemGridDelegate(); + + @override + SliverGridLayout getLayout(SliverConstraints constraints) { + final double tileWidth = (constraints.crossAxisExtent - _horizontalPadding * 2) / DateTime.daysPerWeek; + return _MonthSliverGridLayout( + crossAxisCount: DateTime.daysPerWeek + 2, + dayChildWidth: tileWidth, + edgeChildWidth: _horizontalPadding, + reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection), + ); + } + + @override + bool shouldRelayout(_MonthItemGridDelegate oldDelegate) => false; +} + +const _MonthItemGridDelegate _monthItemGridDelegate = _MonthItemGridDelegate(); + +class _MonthSliverGridLayout extends SliverGridLayout { + /// Creates a layout that uses equally sized and spaced tiles for each day of + /// the week and an additional edge tile for padding at the start and end of + /// each row. + /// + /// This is necessary to facilitate the painting of the range highlight + /// correctly. + const _MonthSliverGridLayout({ + required this.crossAxisCount, + required this.dayChildWidth, + required this.edgeChildWidth, + required this.reverseCrossAxis, + }) : assert(crossAxisCount > 0, 'crossAxisCount must be greater than 0.'), + assert(dayChildWidth >= 0, 'dayChildWidth must be greater than or equal to 0.'), + assert(edgeChildWidth >= 0, 'edgeChildWith must be greater than or equal to 0.'); + + /// The number of children in the cross axis. + final int crossAxisCount; + + /// The width in logical pixels of the day child widgets. + final double dayChildWidth; + + /// The width in logical pixels of the edge child widgets. + final double edgeChildWidth; + + /// Whether the children should be placed in the opposite order of increasing + /// coordinates in the cross axis. + /// + /// For example, if the cross axis is horizontal, the children are placed from + /// left to right when [reverseCrossAxis] is false and from right to left when + /// [reverseCrossAxis] is true. + /// + /// Typically set to the return value of [axisDirectionIsReversed] applied to + /// the [SliverConstraints.crossAxisDirection]. + final bool reverseCrossAxis; + + /// The number of logical pixels from the leading edge of one row to the + /// leading edge of the next row. + double get _rowHeight { + return _monthItemRowHeight + _monthItemSpaceBetweenRows; + } + + /// The height in logical pixels of the children widgets. + double get _childHeight { + return _monthItemRowHeight; + } + + @override + int getMinChildIndexForScrollOffset(double scrollOffset) { + return crossAxisCount * (scrollOffset ~/ _rowHeight); + } + + @override + int getMaxChildIndexForScrollOffset(double scrollOffset) { + final int mainAxisCount = (scrollOffset / _rowHeight).ceil(); + return math.max(0, crossAxisCount * mainAxisCount - 1); + } + + double _getCrossAxisOffset(double crossAxisStart, bool isPadding) { + if (reverseCrossAxis) { + return ((crossAxisCount - 2) * dayChildWidth + edgeChildWidth * 2) - + crossAxisStart - + (isPadding ? edgeChildWidth : dayChildWidth); + } + return crossAxisStart; + } + + @override + SliverGridGeometry getGeometryForChildIndex(int index) { + final int adjustedIndex = index % crossAxisCount; + final bool isEdge = adjustedIndex == 0 || adjustedIndex == crossAxisCount - 1; + final double crossAxisStart = math.max(0, (adjustedIndex - 1) * dayChildWidth + edgeChildWidth); + + return SliverGridGeometry( + scrollOffset: (index ~/ crossAxisCount) * _rowHeight, + crossAxisOffset: _getCrossAxisOffset(crossAxisStart, isEdge), + mainAxisExtent: _childHeight, + crossAxisExtent: isEdge ? edgeChildWidth : dayChildWidth, + ); + } + + @override + double computeMaxScrollOffset(int childCount) { + assert(childCount >= 0, 'childCount must be greater than or equal to 0.'); + final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1; + final double mainAxisSpacing = _rowHeight - _childHeight; + return _rowHeight * mainAxisCount - mainAxisSpacing; + } +} + +/// Displays the days of a given month and allows choosing a date range. +/// +/// The days are arranged in a rectangular grid with one column for each day of +/// the week. +class MonthItem extends StatefulWidget { + /// Creates a month item. + MonthItem({ + required this.selectedDateStart, + required this.selectedDateEnd, + required this.currentDate, + required this.onChanged, + required this.firstDate, + required this.lastDate, + required this.displayedMonth, + super.key, + this.dragStartBehavior = DragStartBehavior.start, + }) : assert(!firstDate.isAfter(lastDate), 'firstDate must be before lastDate.'), + assert( + selectedDateStart == null || !selectedDateStart.isBefore(firstDate), + 'selectedDateStart must be after firstDate.', + ), + assert( + selectedDateEnd == null || !selectedDateEnd.isBefore(firstDate), + 'selectedDateEnd must be after firstDate.', + ), + assert( + selectedDateStart == null || !selectedDateStart.isAfter(lastDate), + 'selectedDateStart must be before lastDate.', + ), + assert( + selectedDateEnd == null || !selectedDateEnd.isAfter(lastDate), + 'selectedDateEnd must be before lastDate.', + ), + assert( + selectedDateStart == null || selectedDateEnd == null || !selectedDateStart.isAfter(selectedDateEnd), + 'selectedDateStart must be before selectedDateEnd.', + ); + + /// The currently selected start date. + /// + /// This date is highlighted in the picker. + final DateTime? selectedDateStart; + + /// The currently selected end date. + /// + /// This date is highlighted in the picker. + final DateTime? selectedDateEnd; + + /// The current date at the time the picker is displayed. + final DateTime currentDate; + + /// Called when the user picks a day. + final ValueChanged onChanged; + + /// The earliest date the user is permitted to pick. + final DateTime firstDate; + + /// The latest date the user is permitted to pick. + final DateTime lastDate; + + /// The month whose days are displayed by this picker. + final DateTime displayedMonth; + + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], the drag gesture used to scroll a + /// date picker wheel will begin at the position where the drag gesture won + /// the arena. If set to [DragStartBehavior.down] it will begin at the position + /// where a down event is first detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragStartBehavior], which gives an example for the different behaviors. + final DragStartBehavior dragStartBehavior; + + @override + MonthItemState createState() => MonthItemState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('selectedDateStart', selectedDateStart)) + ..add(DiagnosticsProperty('selectedDateEnd', selectedDateEnd)) + ..add(DiagnosticsProperty('currentDate', currentDate)) + ..add(ObjectFlagProperty>.has('onChanged', onChanged)) + ..add(DiagnosticsProperty('firstDate', firstDate)) + ..add(DiagnosticsProperty('lastDate', lastDate)) + ..add(DiagnosticsProperty('displayedMonth', displayedMonth)) + ..add(EnumProperty('dragStartBehavior', dragStartBehavior)); + } +} + +/// State for [MonthItem]. +class MonthItemState extends State { + /// List of [FocusNode]s, one for each day of the month. + late List _dayFocusNodes; + + @override + void initState() { + super.initState(); + final int daysInMonth = DateUtils.getDaysInMonth(widget.displayedMonth.year, widget.displayedMonth.month); + _dayFocusNodes = List.generate( + daysInMonth, + (int index) => FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}'), + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Check to see if the focused date is in this month, if so focus it. + final DateTime? focusedDate = _FocusedDate.of(context)?.date; + if (focusedDate != null && DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) { + _dayFocusNodes[focusedDate.day - 1].requestFocus(); + } + } + + @override + void dispose() { + for (final FocusNode node in _dayFocusNodes) { + node.dispose(); + } + super.dispose(); + } + + Color _highlightColor(BuildContext context) { + return Theme.of(context).colorScheme.secondary.withOpacity(0.12); + } + + void _dayFocusChanged(bool focused) { + if (focused) { + final TraversalDirection? focusDirection = _FocusedDate.of(context)?.scrollDirection; + if (focusDirection != null) { + ScrollPositionAlignmentPolicy policy = ScrollPositionAlignmentPolicy.explicit; + switch (focusDirection) { + case TraversalDirection.up: + case TraversalDirection.left: + policy = ScrollPositionAlignmentPolicy.keepVisibleAtStart; + + case TraversalDirection.right: + case TraversalDirection.down: + policy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd; + } + unawaited( + Scrollable.ensureVisible( + primaryFocus!.context!, + duration: _monthScrollDuration, + alignmentPolicy: policy, + ), + ); + } + } + } + + Widget _buildDayItem(BuildContext context, DateTime dayToBuild) { + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + final TextTheme textTheme = theme.textTheme; + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final TextDirection textDirection = Directionality.of(context); + final Color highlightColor = _highlightColor(context); + final int day = dayToBuild.day; + + final bool isDisabled = dayToBuild.isAfter(widget.lastDate) || dayToBuild.isBefore(widget.firstDate); + + BoxDecoration decoration = const BoxDecoration(); + TextStyle? itemStyle = textTheme.bodyMedium; + + final bool isRangeSelected = widget.selectedDateStart != null && widget.selectedDateEnd != null; + final bool isSelectedDayStart = + widget.selectedDateStart != null && dayToBuild.isAtSameMomentAs(widget.selectedDateStart!); + final bool isSelectedDayEnd = + widget.selectedDateEnd != null && dayToBuild.isAtSameMomentAs(widget.selectedDateEnd!); + final bool isInRange = isRangeSelected && + dayToBuild.isAfter(widget.selectedDateStart!) && + dayToBuild.isBefore(widget.selectedDateEnd!); + + _HighlightPainter? highlightPainter; + + if (isSelectedDayStart || isSelectedDayEnd) { + // The selected start and end dates gets a circle background + // highlight, and a contrasting text color. + itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onSecondary); + decoration = BoxDecoration( + color: colorScheme.secondary, + shape: BoxShape.circle, + ); + + if (isRangeSelected && widget.selectedDateStart != widget.selectedDateEnd) { + final _HighlightPainterStyle style = + isSelectedDayStart ? _HighlightPainterStyle.highlightTrailing : _HighlightPainterStyle.highlightLeading; + highlightPainter = _HighlightPainter( + color: highlightColor, + style: style, + textDirection: textDirection, + ); + } + } else if (isInRange) { + // The days within the range get a light background highlight. + highlightPainter = _HighlightPainter( + color: highlightColor, + style: _HighlightPainterStyle.highlightAll, + textDirection: textDirection, + ); + } else if (isDisabled) { + itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.onSurface.withOpacity(0.38)); + } else if (DateUtils.isSameDay(widget.currentDate, dayToBuild)) { + // The current day gets a different text color and a circle stroke + // border. + itemStyle = textTheme.bodyMedium?.apply(color: colorScheme.secondary); + decoration = BoxDecoration( + border: Border.fromBorderSide(BorderSide(color: colorScheme.secondary)), + shape: BoxShape.circle, + ); + } + + // We want the day of month to be spoken first irrespective of the + // locale-specific preferences or TextDirection. This is because + // an accessibility user is more likely to be interested in the + // day of month before the rest of the date, as they are looking + // for the day of month. To do that we prepend day of month to the + // formatted full date. + String semanticLabel = '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}'; + if (isSelectedDayStart) { + semanticLabel = localizations.dateRangeStartDateSemanticLabel(semanticLabel); + } else if (isSelectedDayEnd) { + semanticLabel = localizations.dateRangeEndDateSemanticLabel(semanticLabel); + } + + Widget dayWidget = DecoratedBox( + decoration: decoration, + child: Center( + child: Semantics( + label: semanticLabel, + selected: isSelectedDayStart || isSelectedDayEnd, + child: ExcludeSemantics( + child: Text(localizations.formatDecimal(day), style: itemStyle), + ), + ), + ), + ); + + if (highlightPainter != null) { + dayWidget = CustomPaint( + painter: highlightPainter, + child: dayWidget, + ); + } + + if (!isDisabled) { + dayWidget = InkResponse( + focusNode: _dayFocusNodes[day - 1], + onTap: () => widget.onChanged(dayToBuild), + radius: _monthItemRowHeight / 2 + 4, + splashColor: colorScheme.primary.withOpacity(0.38), + onFocusChange: _dayFocusChanged, + child: dayWidget, + ); + } + + return dayWidget; + } + + Widget _buildEdgeContainer(BuildContext context, bool isHighlighted) { + return Container(color: isHighlighted ? _highlightColor(context) : null); + } + + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + final TextTheme textTheme = themeData.textTheme; + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final int year = widget.displayedMonth.year; + final int month = widget.displayedMonth.month; + final int daysInMonth = DateUtils.getDaysInMonth(year, month); + final int dayOffset = DateUtils.firstDayOffset(year, month, localizations); + final int weeks = ((daysInMonth + dayOffset) / DateTime.daysPerWeek).ceil(); + final double gridHeight = weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows; + final List dayItems = []; + + bool foundFlag = false; + int iterator = 0; + while (!foundFlag) { + final int day = iterator - dayOffset + 1; + if (day >= daysInMonth) foundFlag = true; + if (day < 1) { + dayItems.add(const SizedBox()); + } else { + final DateTime dayToBuild = DateTime(year, month, day); + final Widget dayItem = _buildDayItem(context, dayToBuild); + dayItems.add(dayItem); + } + iterator += 1; + } + + // Add the leading/trailing edge containers to each week in order to + // correctly extend the range highlight. + final List paddedDayItems = []; + for (int i = 0; i < weeks; i++) { + final int start = i * DateTime.daysPerWeek; + final int end = math.min( + start + DateTime.daysPerWeek, + dayItems.length, + ); + final List weekList = dayItems.sublist(start, end); + + final DateTime dateAfterLeadingPadding = DateTime(year, month, start - dayOffset + 1); + // Only color the edge container if it is after the start date and + // on/before the end date. + final bool isLeadingInRange = !(dayOffset > 0 && i == 0) && + widget.selectedDateStart != null && + widget.selectedDateEnd != null && + dateAfterLeadingPadding.isAfter(widget.selectedDateStart!) && + !dateAfterLeadingPadding.isAfter(widget.selectedDateEnd!); + weekList.insert(0, _buildEdgeContainer(context, isLeadingInRange)); + + // Only add a trailing edge container if it is for a full week and not a + // partial week. + if (end < dayItems.length || (end == dayItems.length && dayItems.length % DateTime.daysPerWeek == 0)) { + final DateTime dateBeforeTrailingPadding = DateTime(year, month, end - dayOffset); + // Only color the edge container if it is on/after the start date and + // before the end date. + final bool isTrailingInRange = widget.selectedDateStart != null && + widget.selectedDateEnd != null && + !dateBeforeTrailingPadding.isBefore(widget.selectedDateStart!) && + dateBeforeTrailingPadding.isBefore(widget.selectedDateEnd!); + weekList.add(_buildEdgeContainer(context, isTrailingInRange)); + } + + paddedDayItems.addAll(weekList); + } + + final double maxWidth = MediaQuery.of(context).orientation == Orientation.landscape + ? _maxCalendarWidthLandscape + : _maxCalendarWidthPortrait; + return Column( + children: [ + Container( + constraints: BoxConstraints(maxWidth: maxWidth), + height: _monthItemHeaderHeight, + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: AlignmentDirectional.centerStart, + child: ExcludeSemantics( + child: Text( + localizations.formatMonthYear(widget.displayedMonth), + style: textTheme.bodyMedium!.apply(color: themeData.colorScheme.onSurface), + ), + ), + ), + Container( + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: gridHeight, + ), + child: GridView.custom( + physics: const NeverScrollableScrollPhysics(), + gridDelegate: _monthItemGridDelegate, + childrenDelegate: SliverChildListDelegate( + paddedDayItems, + addRepaintBoundaries: false, + ), + ), + ), + const SizedBox(height: _monthItemFooterHeight), + ], + ); + } +} + +/// Determines which style to use to paint the highlight. +enum _HighlightPainterStyle { + /// Paints nothing. + none, + + /// Paints a rectangle that occupies the leading half of the space. + highlightLeading, + + /// Paints a rectangle that occupies the trailing half of the space. + highlightTrailing, + + /// Paints a rectangle that occupies all available space. + highlightAll, +} + +/// This custom painter will add a background highlight to its child. +/// +/// This highlight will be drawn depending on the [style], [color], and +/// [textDirection] supplied. It will either paint a rectangle on the +/// left/right, a full rectangle, or nothing at all. This logic is determined by +/// a combination of the [style] and [textDirection]. +class _HighlightPainter extends CustomPainter { + _HighlightPainter({ + required this.color, + this.style = _HighlightPainterStyle.none, + this.textDirection, + }); + + final Color color; + final _HighlightPainterStyle style; + final TextDirection? textDirection; + + @override + void paint(Canvas canvas, Size size) { + if (style == _HighlightPainterStyle.none) { + return; + } + + final Paint paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + final Rect rectLeft = Rect.fromLTWH(0, 0, size.width / 2, size.height); + final Rect rectRight = Rect.fromLTWH(size.width / 2, 0, size.width / 2, size.height); + + switch (style) { + case _HighlightPainterStyle.highlightTrailing: + canvas.drawRect( + textDirection == TextDirection.ltr ? rectRight : rectLeft, + paint, + ); + case _HighlightPainterStyle.highlightLeading: + canvas.drawRect( + textDirection == TextDirection.ltr ? rectLeft : rectRight, + paint, + ); + + case _HighlightPainterStyle.highlightAll: + canvas.drawRect( + Rect.fromLTWH(0, 0, size.width, size.height), + paint, + ); + case _HighlightPainterStyle.none: + break; + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} + +class _InputDateRangePickerDialog extends StatelessWidget { + const _InputDateRangePickerDialog({ + required this.selectedStartDate, + required this.selectedEndDate, + required this.currentDate, + required this.picker, + required this.onConfirm, + required this.onCancel, + required this.confirmText, + required this.cancelText, + required this.helpText, + required this.entryModeButton, + }); + + final DateTime? selectedStartDate; + final DateTime? selectedEndDate; + final DateTime? currentDate; + final Widget picker; + final VoidCallback onConfirm; + final VoidCallback onCancel; + final String? confirmText; + final String? cancelText; + final String? helpText; + final Widget? entryModeButton; + + String _formatDateRange(BuildContext context, DateTime? start, DateTime? end, DateTime now) { + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final String startText = _formatRangeStartDate(localizations, start, end); + final String endText = _formatRangeEndDate(localizations, start, end, now); + if (start == null || end == null) { + return localizations.unspecifiedDateRange; + } + return Directionality.of(context) == TextDirection.ltr ? '$startText - $endText' : '$endText - $startText'; + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final ColorScheme colorScheme = theme.colorScheme; + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final Orientation orientation = MediaQuery.of(context).orientation; + final TextTheme textTheme = theme.textTheme; + + final Color onPrimarySurfaceColor = + colorScheme.brightness == Brightness.light ? colorScheme.onPrimary : colorScheme.onSurface; + final TextStyle? dateStyle = orientation == Orientation.landscape + ? textTheme.headlineSmall?.apply(color: onPrimarySurfaceColor) + : textTheme.headlineMedium?.apply(color: onPrimarySurfaceColor); + final String dateText = _formatDateRange(context, selectedStartDate, selectedEndDate, currentDate!); + final String semanticDateText = selectedStartDate != null && selectedEndDate != null + ? '${localizations.formatMediumDate(selectedStartDate!)} - ${localizations.formatMediumDate(selectedEndDate!)}' + : ''; + + final Widget header = _DatePickerHeader( + helpText: helpText ?? localizations.dateRangePickerHelpText, + titleText: dateText, + titleSemanticsLabel: semanticDateText, + titleStyle: dateStyle, + orientation: orientation, + isShort: orientation == Orientation.landscape, + entryModeButton: entryModeButton, + ); + + final Widget actions = Container( + alignment: AlignmentDirectional.centerEnd, + constraints: const BoxConstraints(minHeight: 52), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: OverflowBar( + spacing: 8, + children: [ + ZdsButton.text( + onTap: onCancel, + child: Text(cancelText ?? localizations.cancelButtonLabel), + ), + ZdsButton.text( + onTap: onConfirm, + child: Text(confirmText ?? localizations.okButtonLabel), + ), + ], + ), + ); + + switch (orientation) { + case Orientation.portrait: + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + header, + Expanded(child: picker), + actions, + ], + ); + + case Orientation.landscape: + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + header, + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: picker), + actions, + ], + ), + ), + ], + ); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('selectedStartDate', selectedStartDate)) + ..add(DiagnosticsProperty('selectedEndDate', selectedEndDate)) + ..add(DiagnosticsProperty('currentDate', currentDate)) + ..add(ObjectFlagProperty.has('onConfirm', onConfirm)) + ..add(ObjectFlagProperty.has('onCancel', onCancel)) + ..add(StringProperty('confirmText', confirmText)) + ..add(StringProperty('cancelText', cancelText)) + ..add(StringProperty('helpText', helpText)); + } +} + +/// Provides a pair of text fields that allow the user to enter the start and +/// end dates that represent a range of dates. +class _InputDateRangePicker extends StatefulWidget { + /// Creates a row with two text fields configured to accept the start and end dates + /// of a date range. + _InputDateRangePicker({ + required DateTime firstDate, + required DateTime lastDate, + required this.onStartDateChanged, + required this.onEndDateChanged, + super.key, + DateTime? initialStartDate, + DateTime? initialEndDate, + this.helpText, + this.errorFormatText, + this.errorInvalidText, + this.errorInvalidRangeText, + this.fieldStartHintText, + this.fieldEndHintText, + this.fieldStartLabelText, + this.fieldEndLabelText, + this.autofocus = false, + this.autovalidate = false, + }) : initialStartDate = initialStartDate == null ? null : DateUtils.dateOnly(initialStartDate), + initialEndDate = initialEndDate == null ? null : DateUtils.dateOnly(initialEndDate), + firstDate = DateUtils.dateOnly(firstDate), + lastDate = DateUtils.dateOnly(lastDate); + + /// The [DateTime] that represents the start of the initial date range selection. + final DateTime? initialStartDate; + + /// The [DateTime] that represents the end of the initial date range selection. + final DateTime? initialEndDate; + + /// The earliest allowable [DateTime] that the user can select. + final DateTime firstDate; + + /// The latest allowable [DateTime] that the user can select. + final DateTime lastDate; + + /// Called when the user changes the start date of the selected range. + final ValueChanged? onStartDateChanged; + + /// Called when the user changes the end date of the selected range. + final ValueChanged? onEndDateChanged; + + /// The text that is displayed at the top of the header. + /// + /// This is used to indicate to the user what they are selecting a date for. + final String? helpText; + + /// Error text used to indicate the text in a field is not a valid date. + final String? errorFormatText; + + /// Error text used to indicate the date in a field is not in the valid range + /// of [firstDate] - [lastDate]. + final String? errorInvalidText; + + /// Error text used to indicate the dates given don't form a valid date + /// range (ie the start date is after the end date). + final String? errorInvalidRangeText; + + /// Hint text shown when the start date field is empty. + final String? fieldStartHintText; + + /// Hint text shown when the end date field is empty. + final String? fieldEndHintText; + + /// Label used for the start date field. + final String? fieldStartLabelText; + + /// Label used for the end date field. + final String? fieldEndLabelText; + + /// {@macro flutter.widgets.editableText.autofocus} + final bool autofocus; + + /// If true, this the date fields will validate and update their error text + /// immediately after every change. Otherwise, you must call + /// [_InputDateRangePickerState.validate] to validate. + final bool autovalidate; + + @override + _InputDateRangePickerState createState() => _InputDateRangePickerState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('initialStartDate', initialStartDate)) + ..add(DiagnosticsProperty('initialEndDate', initialEndDate)) + ..add(DiagnosticsProperty('firstDate', firstDate)) + ..add(DiagnosticsProperty('lastDate', lastDate)) + ..add(ObjectFlagProperty?>.has('onStartDateChanged', onStartDateChanged)) + ..add(ObjectFlagProperty?>.has('onEndDateChanged', onEndDateChanged)) + ..add(StringProperty('helpText', helpText)) + ..add(StringProperty('errorFormatText', errorFormatText)) + ..add(StringProperty('errorInvalidText', errorInvalidText)) + ..add(StringProperty('errorInvalidRangeText', errorInvalidRangeText)) + ..add(StringProperty('fieldStartHintText', fieldStartHintText)) + ..add(StringProperty('fieldEndHintText', fieldEndHintText)) + ..add(StringProperty('fieldStartLabelText', fieldStartLabelText)) + ..add(StringProperty('fieldEndLabelText', fieldEndLabelText)) + ..add(DiagnosticsProperty('autofocus', autofocus)) + ..add(DiagnosticsProperty('autovalidate', autovalidate)); + } +} + +/// The current state of an [_InputDateRangePicker]. Can be used to +/// [validate] the date field entries. +class _InputDateRangePickerState extends State<_InputDateRangePicker> { + late String _startInputText; + late String _endInputText; + DateTime? _startDate; + DateTime? _endDate; + late TextEditingController _startController; + late TextEditingController _endController; + String? _startErrorText; + String? _endErrorText; + bool _autoSelected = false; + + @override + void initState() { + super.initState(); + _startDate = widget.initialStartDate; + _startController = TextEditingController(); + _endDate = widget.initialEndDate; + _endController = TextEditingController(); + } + + @override + void dispose() { + _startController.dispose(); + _endController.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + if (_startDate != null) { + _startInputText = localizations.formatCompactDate(_startDate!); + final bool selectText = widget.autofocus && !_autoSelected; + _updateController(_startController, _startInputText, selectText); + _autoSelected = selectText; + } + + if (_endDate != null) { + _endInputText = localizations.formatCompactDate(_endDate!); + _updateController(_endController, _endInputText, false); + } + } + + /// Validates that the text in the start and end fields represent a valid + /// date range. + /// + /// Will return true if the range is valid. If not, it will + /// return false and display an appropriate error message under one of the + /// text fields. + bool validate() { + String? startError = _validateDate(_startDate); + final String? endError = _validateDate(_endDate); + if (startError == null && endError == null) { + if (_startDate!.isAfter(_endDate!)) { + startError = widget.errorInvalidRangeText ?? MaterialLocalizations.of(context).invalidDateRangeLabel; + } + } + setState(() { + _startErrorText = startError; + _endErrorText = endError; + }); + return startError == null && endError == null; + } + + DateTime? _parseDate(String? text) { + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + return localizations.parseCompactDate(text); + } + + String? _validateDate(DateTime? date) { + if (date == null) { + return widget.errorFormatText ?? MaterialLocalizations.of(context).invalidDateFormatLabel; + } else if (date.isBefore(widget.firstDate) || date.isAfter(widget.lastDate)) { + return widget.errorInvalidText ?? MaterialLocalizations.of(context).dateOutOfRangeLabel; + } + return null; + } + + void _updateController(TextEditingController controller, String text, bool selectText) { + TextEditingValue textEditingValue = controller.value.copyWith(text: text); + if (selectText) { + textEditingValue = textEditingValue.copyWith( + selection: TextSelection( + baseOffset: 0, + extentOffset: text.length, + ), + ); + } + controller.value = textEditingValue; + } + + void _handleStartChanged(String text) { + setState(() { + _startInputText = text; + _startDate = _parseDate(text); + widget.onStartDateChanged?.call(_startDate); + }); + if (widget.autovalidate) { + validate(); + } + } + + void _handleEndChanged(String text) { + setState(() { + _endInputText = text; + _endDate = _parseDate(text); + widget.onEndDateChanged?.call(_endDate); + }); + if (widget.autovalidate) { + validate(); + } + } + + @override + Widget build(BuildContext context) { + final MaterialLocalizations localizations = MaterialLocalizations.of(context); + final InputDecorationTheme inputTheme = Theme.of(context).inputDecorationTheme; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: _startController, + decoration: InputDecoration( + border: inputTheme.border ?? const UnderlineInputBorder(), + filled: inputTheme.filled, + hintText: widget.fieldStartHintText ?? localizations.dateHelpText, + labelText: widget.fieldStartLabelText ?? localizations.dateRangeStartLabel, + errorText: _startErrorText, + ), + keyboardType: TextInputType.datetime, + onChanged: _handleStartChanged, + autofocus: widget.autofocus, + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: _endController, + decoration: InputDecoration( + border: inputTheme.border ?? const UnderlineInputBorder(), + filled: inputTheme.filled, + hintText: widget.fieldEndHintText ?? localizations.dateHelpText, + labelText: widget.fieldEndLabelText ?? localizations.dateRangeEndLabel, + errorText: _endErrorText, + ), + keyboardType: TextInputType.datetime, + onChanged: _handleEndChanged, + ), + ), + ], + ); + } +} diff --git a/lib/src/components/molecules/date_time_picker.dart b/lib/src/components/molecules/date_time_picker.dart new file mode 100644 index 0000000..9adc0de --- /dev/null +++ b/lib/src/components/molecules/date_time_picker.dart @@ -0,0 +1,400 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide DatePickerDialog; +import 'package:interval_time_picker/interval_time_picker.dart' as interval_picker; +import 'package:intl/intl.dart'; + +import '../../../zds_flutter.dart'; + +/// Variants of [ZdsDateTimePicker]. +enum DateTimePickerMode { + /// Creates and shows a [DatePickerDialog] with Zds style and behavior. + date, + + /// Creates and shows a [TimePickerDialog] with Zds style and behavior. + time, + + /// Creates and shows first a [DatePickerDialog] and then a [TimePickerDialog] with Zds style and behavior. + dateAndTime +} + +/// A widget that allow to select a date, a time, or both. +/// +/// A [controller] can be used to handle this date and use it to manage state. +/// +/// This widget is typically used as a non-editable textfield by using [ZdsInputDecoration] for [inputDecoration]. +/// It can also be used as a button on its own by not assigning any [inputDecoration]. +/// +/// ```dart +/// // As a textfield +/// ZdsDateTimePicker( +/// emptyLabel: 'select date', +/// minDate: DateTime.now(), +/// mode: DateTimePickerMode.date, +/// controller: _controller, +/// inputDecoration: ZdsInputDecoration( +/// labelText: 'Optional Date', +/// suffixIcon: IconButton( +/// icon: const Icon(ZdsIcons.close_circle, size: 24,), +/// onPressed: () => _controller.value = null, +/// ), +/// ), +/// onChange: (dateTime) => handleTime(dateTime), +/// ), +/// +/// // As a button +/// ZdsDateTimePicker( +/// emptyLabel: 'select time', +/// textAlign: TextAlign.center, +/// minDate: DateTime.now(), +/// mode: DateTimePickerMode.time, +/// ), +/// ``` +/// See also: +/// +/// * [ZdsDateRangePickerTile], which allows to select a date range's start and end time separately. +/// * [showDatePicker] to show a date picker directly +class ZdsDateTimePicker extends StatefulWidget { + /// The type of picker to show - `date`, `time` or `dateAndTime`. + final DateTimePickerMode mode; + + /// The format in which the [DateTime] will be formatted. + /// + /// See [DateFormat] for more details. + final String format; + + /// Text that will appear when no date has been selected. + final String emptyLabel; + + /// The text displayed at the top of the date picker window. + final String? helpText; + + /// The style applied to the help text. Defaults to [Theme.of(context).textTheme.labelSmall]. + final TextStyle? helpTextStyle; + + /// How the text will be aligned in this widget. + /// + /// Defaults to [TextAlign.start]. + final TextAlign textAlign; + + /// The earliest date that can be selected. Must be before the [maxDate]. + final DateTime? minDate; + + /// The latest date that can be selected. Must be after [minDate]. + final DateTime? maxDate; + + /// to enable the click of calendar icon. + final bool readOnly; + + /// A pre-selected date. + /// + /// If null, no selected date will appear and [emptyLabel] will be shown instead. + /// + /// If [interval] is set, the selected date's minute will be rounded to the nearest denomination of that interval. + final DateTime? selectedDate; + + /// The style used for this button's or field's appearance. + final TextStyle? textStyle; + + /// Empty space to surround this widget. + /// + /// Defaults to EdgeInsets.all(16) if [inputDecoration] is null, const EdgeInsets.all(0) if it isn't. + final EdgeInsets? padding; + + /// Optional decoration for this widget. + /// + /// Typically [ZdsInputDecoration]. + final InputDecoration? inputDecoration; + + /// A function called whenever the date or time selected has changed. + final void Function(DateTime? dateTime)? onChange; + + /// A controller used to keep track of the selected date. + final ZdsValueController? controller; + + /// The interval that the picker will step up in. Only valid for time pickers. + final int? interval; + + /// The minute labels that are visible on the ring of the picker. Only valid for time pickers. + final interval_picker.VisibleStep visibleStep; + + /// The error text used on the time picker. + final String? timePickerErrorText; + + /// Overrides the 24 hour format of the locale. + final bool? use24HourFormat; + + /// Initial entry mode for time pickers. + final TimePickerEntryMode timePickerEntryMode; + + /// Constructs a [ZdsDateTimePicker]. + ZdsDateTimePicker({ + required this.emptyLabel, + super.key, + String? format, + this.mode = DateTimePickerMode.date, + this.textAlign = TextAlign.start, + this.helpText, + this.helpTextStyle, + this.minDate, + this.maxDate, + this.selectedDate, + this.textStyle, + this.onChange, + this.padding, + this.inputDecoration, + this.controller, + this.readOnly = false, + this.interval, + this.visibleStep = interval_picker.VisibleStep.fifths, + this.timePickerErrorText, + this.use24HourFormat, + this.timePickerEntryMode = TimePickerEntryMode.inputOnly, + }) : format = (format?.isNotEmpty ?? false) + ? format! + : (mode == DateTimePickerMode.date + ? 'MMM dd, yyyy' + : mode == DateTimePickerMode.time + ? 'hh:mm a' + : 'MMM dd, yyyy hh:mm a'), + assert( + (minDate != null && maxDate != null) ? minDate.isBefore(maxDate) : (minDate == null || maxDate == null), + 'minDate must be before maxDate.', + ); + + @override + ZdsDateTimePickerState createState() => ZdsDateTimePickerState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('mode', mode)); + properties.add(StringProperty('format', format)); + properties.add(StringProperty('emptyLabel', emptyLabel)); + properties.add(StringProperty('helpText', helpText)); + properties.add(DiagnosticsProperty('helpTextStyle', helpTextStyle)); + properties.add(EnumProperty('textAlign', textAlign)); + properties.add(DiagnosticsProperty('minDate', minDate)); + properties.add(DiagnosticsProperty('maxDate', maxDate)); + properties.add(DiagnosticsProperty('readOnly', readOnly)); + properties.add(DiagnosticsProperty('selectedDate', selectedDate)); + properties.add(DiagnosticsProperty('textStyle', textStyle)); + properties.add(DiagnosticsProperty('padding', padding)); + properties.add(DiagnosticsProperty('inputDecoration', inputDecoration)); + properties.add(ObjectFlagProperty.has('onChange', onChange)); + properties.add(DiagnosticsProperty?>('controller', controller)); + properties.add(IntProperty('interval', interval)); + properties.add(EnumProperty('visibleStep', visibleStep)); + properties.add(StringProperty('timePickerErrorText', timePickerErrorText)); + properties.add(DiagnosticsProperty('use24HourFormat', use24HourFormat)); + properties.add(EnumProperty('timePickerEntryMode', timePickerEntryMode)); + } +} + +/// State for [ZdsDateTimePicker]. +class ZdsDateTimePickerState extends State { + /// Constructs a [ZdsDateTimePickerState]. + ZdsDateTimePickerState(); + + late DateTime? _dateTime; + + String get _formattedDate => _dateTime == null ? widget.emptyLabel : DateFormat(widget.format).format(_dateTime!); + + @override + void initState() { + if (widget.interval != null && widget.mode != DateTimePickerMode.date && widget.selectedDate != null) { + setState(() => _dateTime = _roundDate(widget.selectedDate!)); + } else { + setState(() => _dateTime = widget.selectedDate); + } + + widget.controller?.updateListener = (value) { + setState(() { + _dateTime = value; + }); + widget.controller?.notifyListeners(value); + }; + super.initState(); + } + + DateTime _roundDate(DateTime date) { + DateTime roundedDate = date; + final diff = roundedDate.minute % widget.interval!; + + if (diff > (widget.interval! / 2)) { + roundedDate = roundedDate.add(Duration(minutes: widget.interval! - diff)); + } else { + roundedDate = roundedDate.subtract(Duration(minutes: diff)); + } + + return roundedDate; + } + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + Widget child = Text( + _formattedDate.toLowerCase(), + textAlign: widget.textAlign, + style: widget.textStyle ?? + textTheme.bodyLarge?.copyWith( + color: _dateTime == null ? ZdsColors.greySwatch(context)[600] : textTheme.bodyLarge?.color, + ), + ); + + if (widget.inputDecoration != null) { + child = InputDecorator( + decoration: widget.inputDecoration!, + child: child, + ); + } + + return Material( + color: ZdsColors.transparent, + child: InkWell( + splashColor: ZdsColors.splashColor, + hoverColor: ZdsColors.hoverColor, + radius: MediaQuery.of(context).size.width, + onTap: () { + if (!widget.readOnly) unawaited(onShowPicker(context, _dateTime)); + }, + borderRadius: BorderRadius.circular( + widget.inputDecoration == null ? 30 : 15, + ), + child: Semantics( + label: _formattedDate == widget.emptyLabel ? '' : _formattedDate, + excludeSemantics: true, + onTap: () async { + if (!widget.readOnly) await onShowPicker(context, _dateTime); + }, + child: Padding( + padding: widget.padding ?? (widget.inputDecoration == null ? const EdgeInsets.all(16) : EdgeInsets.zero), + child: child, + ), + ), + ), + ); + } + + /// Shows correct date or time picker for component. + Future onShowPicker( + BuildContext context, + DateTime? currentValue, + ) async { + DateTime? newValue; + + if (widget.mode == DateTimePickerMode.date) { + newValue = await _showDatePicker(context, currentValue); + } else if (widget.mode == DateTimePickerMode.time) { + final newTime = await _showTimePicker(context, currentValue); + newValue = newTime != null ? _convert(newTime) : null; + } else { + final date = await _showDatePicker(context, currentValue); + if (date != null) { + if (mounted) { + final time = await _showTimePicker(context, date); + newValue = _combine(date, time); + } + } + } + + setState(() { + _dateTime = newValue ?? currentValue; + widget.onChange?.call(_dateTime); + widget.controller?.notifyListeners(_dateTime); + }); + } + + Future _showDatePicker( + BuildContext context, + DateTime? currentValue, + ) { + return showDatePicker( + context: context, + initialDate: currentValue ?? + (widget.minDate != null && DateTime.now().isBefore(widget.minDate!) ? widget.minDate! : DateTime.now()), + firstDate: widget.minDate ?? DateTime(1900), + lastDate: widget.maxDate ?? DateTime(2100), + initialEntryMode: DatePickerEntryMode.calendarOnly, + helpText: widget.helpText, + builder: (context, child) { + return Theme( + data: Theme.of(context).zdsDateTimePickerTheme.copyWith( + textTheme: Theme.of(context).textTheme.copyWith(labelSmall: widget.helpTextStyle), + ), + child: child!, + ); + }, + ); + } + + Future _showTimePicker(BuildContext context, DateTime? currentValue) async { + Widget timePickerBuilder(BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: widget.use24HourFormat), + child: Theme( + data: Theme.of(context).zdsDateTimePickerTheme, + child: child!, + ), + ); + } + + final timePickerResult = widget.interval == null && mounted + ? await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(currentValue ?? DateTime.now()), + initialEntryMode: widget.timePickerEntryMode, + errorInvalidText: widget.timePickerErrorText, + helpText: widget.helpText, + builder: timePickerBuilder, + ) + : mounted + ? await interval_picker.showIntervalTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(currentValue ?? _roundDate(DateTime.now())), + interval: widget.interval!, + visibleStep: widget.visibleStep, + errorInvalidText: widget.timePickerErrorText, + initialEntryMode: _getIntervalPickerMode(), + builder: timePickerBuilder, + helpText: widget.helpText, + ) + : null; + + final selectedTime = timePickerResult ?? (currentValue != null ? TimeOfDay.fromDateTime(currentValue) : null); + + return selectedTime; + } + + interval_picker.TimePickerEntryMode _getIntervalPickerMode() { + switch (widget.timePickerEntryMode) { + case TimePickerEntryMode.dial: + return interval_picker.TimePickerEntryMode.dial; + case TimePickerEntryMode.input: + return interval_picker.TimePickerEntryMode.input; + case TimePickerEntryMode.dialOnly: + return interval_picker.TimePickerEntryMode.dialOnly; + case TimePickerEntryMode.inputOnly: + return interval_picker.TimePickerEntryMode.inputOnly; + } + } + + /// Sets the hour and minute of a [DateTime] from a [TimeOfDay]. + DateTime _combine(DateTime date, TimeOfDay? time) => DateTime( + date.year, + date.month, + date.day, + time?.hour ?? 0, + time?.minute ?? 0, + ); + + DateTime? _convert(TimeOfDay? time) => time == null + ? null + : DateTime( + 1, + 1, + 1, + time.hour, + time.minute, + ); +} diff --git a/lib/src/components/molecules/dropdown.dart b/lib/src/components/molecules/dropdown.dart new file mode 100644 index 0000000..cbc2d45 --- /dev/null +++ b/lib/src/components/molecules/dropdown.dart @@ -0,0 +1,226 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Defines an item to be used in a [ZdsDropdownList] +class ZdsDropdownListItem { + /// The value of the item + final T value; + + /// The name of the item + final String name; + + /// Creates a new [ZdsDropdownListItem] + ZdsDropdownListItem({ + required this.value, + required this.name, + }); +} + +/// A [DropdownButtonFormField] with Zds style and behavior. +class ZdsDropdownList extends StatefulWidget { + /// The label that will be shown above the dropdown. + final String? label; + + /// A function called whenever the selected items change. + final void Function(T selectedValue)? onChange; + + /// A function called whenever no items selected. + final void Function()? onReset; + + /// A function called whenever the dropdown is tapped. + /// + /// This will disable the functionality of the dropdown and the list will no longer open. + /// You will neeed to manage the state yourself by changing [value]. + final void Function()? onTap; + + /// The list of options for the dropdown. + final List> options; + + /// The value of the selected item. + /// + /// The value must match the value property of one of the [ZdsDropdownListItem]s in [options]. + /// If two options have the same value, the first one in the list will be used. + final T? value; + + /// The textStyle used for dropdown label. + /// + /// Defaults to [TextTheme.headlineSmall]. + final TextStyle? labelStyle; + + /// The hint shown when the dropdown has no value. + final String? hint; + + /// The border color of the dropdown. + final Color? borderColor; + + /// Constructs a [ZdsDropdownList]. + const ZdsDropdownList({ + this.onChange, + this.onReset, + this.value, + this.label, + this.onTap, + this.hint, + this.labelStyle, + this.borderColor, + this.options = const [], + super.key, + }); + + @override + ZdsDropdownListState createState() => ZdsDropdownListState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('label', label)); + properties.add(ObjectFlagProperty.has('onChange', onChange)); + properties.add(ObjectFlagProperty.has('onReset', onReset)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(IterableProperty>('options', options)); + properties.add(DiagnosticsProperty('value', value)); + properties.add(DiagnosticsProperty('labelStyle', labelStyle)); + properties.add(StringProperty('hint', hint)); + properties.add(ColorProperty('borderColor', borderColor)); + } +} + +/// State for [ZdsDropdownList]. +class ZdsDropdownListState extends State> { + ZdsDropdownListItem? _selectedItem; + final TextEditingController _formFieldController = TextEditingController(); + bool _isOpen = false; + + @override + void initState() { + super.initState(); + _setValue(); + } + + void _setValue() { + try { + _selectedItem = widget.options.where((element) => element.value == widget.value).first; + _formFieldController.text = _selectedItem?.name ?? ''; + } catch (e) { + _selectedItem = null; + } + } + + @override + void didUpdateWidget(ZdsDropdownList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.value != widget.value) { + setState(_setValue); + } + } + + @override + void dispose() { + super.dispose(); + _formFieldController.dispose(); + } + + /// Resets the value of the dropdown + void reset() { + setState(() { + _selectedItem = null; + }); + _formFieldController.clear(); + } + + @override + Widget build(BuildContext context) { + final border = widget.borderColor != null + ? ZdsInputBorder( + borderSide: BorderSide(color: widget.borderColor!), + space: 2, + borderRadius: BorderRadius.circular(6), + ) + : null; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.label != null) ...[ + Text( + widget.label!, + style: widget.labelStyle ?? + Theme.of(context).textTheme.headlineSmall!.copyWith( + color: ZdsColors.greySwatch(context)[900], + ), + ), + const SizedBox(height: 4), + ], + Semantics( + onTap: widget.onTap != null ? () => widget.onTap : null, + button: true, + label: _selectedItem?.name ?? widget.hint, + enabled: widget.options.isNotEmpty || widget.onTap != null, + excludeSemantics: true, + child: GestureDetector( + onTap: () => widget.onTap?.call(), + child: DropdownButton2( + value: _selectedItem?.value, + onMenuStateChange: (isOpen) => setState(() { + _isOpen = isOpen; + }), + customButton: TextFormField( + controller: _formFieldController, + decoration: ZdsInputDecoration.withNoLabel( + suffixPadding: const EdgeInsets.only(right: 8), + suffixIcon: Icon( + _isOpen ? ZdsIcons.chevron_up : ZdsIcons.chevron_down, + color: ZdsColors.greySwatch(context)[700], + ), + border: border, + errorBorder: border, + focusedBorder: border, + disabledBorder: border, + hintText: widget.hint, + enabled: false, + ), + ), + underline: const SizedBox(), + items: widget.onTap == null + ? widget.options.map( + (item) { + return DropdownMenuItem( + value: item.value, + child: Text( + item.name, + style: const TextStyle(overflow: TextOverflow.ellipsis), + ), + ); + }, + ).toList() + : [], + menuItemStyleData: MenuItemStyleData( + selectedMenuItemBuilder: (context, child) { + return ColoredBox( + color: Theme.of(context).colorScheme.secondary.withOpacity(0.1), + child: child, + ); + }, + ), + dropdownStyleData: const DropdownStyleData(elevation: 4), + onChanged: (T? value) { + if (value != null && value == _selectedItem?.value) { + reset(); + widget.onReset?.call(); + } else if (value != null) { + setState(() { + _selectedItem = widget.options.firstWhere((element) => element.value == value); + }); + _formFieldController.text = _selectedItem?.name ?? ''; + widget.onChange?.call(value); + } + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/components/molecules/empty.dart b/lib/src/components/molecules/empty.dart new file mode 100644 index 0000000..953684f --- /dev/null +++ b/lib/src/components/molecules/empty.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Creates a message about no results being returned. +/// +/// This widget can be used to show a message for, when example, a search returns no results, or when no options are +/// available. Typically, this widget is shown instead of the results list when the list is empty. +class ZdsEmpty extends StatelessWidget { + /// The icon used for this message. + /// + /// Typically an [Icon]. + final Widget? icon; + + /// The message to display. + /// + /// Typically a [Text]. + final Widget? message; + + /// Creates a message about no results being returned. + const ZdsEmpty({super.key, this.icon, this.message}); + + @override + Widget build(BuildContext context) { + return Align( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Icon + if (!(context.isShortScreen() || context.isSmallScreen() || (context.isPhone() && context.isLandscape()))) + ExcludeSemantics( + child: IconTheme( + data: IconThemeData( + color: Theme.of(context).colorScheme.secondary, + size: 72, + ), + child: icon ?? + ConstrainedBox( + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width / 3), + child: ZdsImages.emptyBox, + ), + ).space(40), + ), + // Message + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: MergeSemantics( + child: message ?? + Text( + ComponentStrings.of(context).get('NO_RESULTS', 'No results'), + textAlign: TextAlign.center, + ), + ).textStyle( + Theme.of(context).textTheme.bodyLarge, + overflow: TextOverflow.visible, + textAlign: TextAlign.center, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/components/molecules/expansion_tile.dart b/lib/src/components/molecules/expansion_tile.dart new file mode 100644 index 0000000..835c71d --- /dev/null +++ b/lib/src/components/molecules/expansion_tile.dart @@ -0,0 +1,454 @@ +import 'dart:async'; +import 'dart:core'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// Types of expansion tile +enum ExpansionTileType { + /// When expansion tile can be expanded/collapsed, but is not selectable. + regular, + + /// When expansion tile can be expanded/collapsed, and is selectable. + selectable +} + +const Duration _kExpand = Duration(milliseconds: 200); + +/// A tile that can be expanded and collapsed to reveal further information. +/// +/// Typically used to hide information that could clutter the page, like detailed information. +/// +/// When using this component in a [ZdsCard], the semantics of all widgets will be grouped together (expected Card +/// behaviour to be read in one go). This means that when in a card, reading the expanded children using TalkBack or +/// VoiceOver is difficult. For accessibility purposes, if you want to use this widget within a card, we instead +/// recommend wrapping the [ZdsExpansionTile] in a container with custom decoration so the children remain +/// individually accessible through TalkBack like so: +/// +/// ```dart +/// Container( +/// decoration: CustomDecoration(), +/// child: ZdsExpansionTile( +/// title: const Text('Tile outside of a card'), +/// child: Column( +/// crossAxisAlignment: CrossAxisAlignment.start, +/// children: [ +/// Text('Child'), +/// Text('Another child'), +/// ], +/// ), +/// ), +/// ) +/// ``` + +class ZdsExpansionTile extends StatefulWidget { + /// The title of this expansion tile. This title will always be shown. + /// + /// Typically a [Text] widget. + final Widget title; + + /// Additional content displayed below the title. + /// + /// Typically a [Text] widget. + final Widget? subtitle; + + /// The widget to show when the tile is in the expanded state. + /// + /// Typically a [Column] with other children. + final Widget child; + + /// The bottom widget similiar to list tile so the widgets do not interfere with expansion button + /// + /// Typically a [Row] with other children or single widget. + final Widget? bottom; + + /// Whether the tile is expanded when it's first drawn or not. + /// + /// Defaults to false. + final bool initiallyExpanded; + + /// Specifies whether the state of the children is maintained when the tile expands and collapses. + /// + /// When true (default), the children are kept in the tree while the tile is collapsed. + /// When false, the children are removed from the tree when the tile is + /// collapsed and recreated upon expansion. + final bool maintainState; + + /// Empty space to inscribe inside this widget. + /// + /// Defaults to `EdgeInsets.symmetric(horizontal: 24, vertical: 2)`. + final EdgeInsets contentPadding; + + /// Padding surrounding the title of the widget. + /// + /// Defaults to `EdgeInsets.symmetric(horizontal: 24, vertical: 16)`. + final EdgeInsets titlePadding; + + /// A function called whenever the tile is toggled to its expanded state. + final VoidCallback? onExpanded; + + /// A function called whenever the tile is toggled to its collapsed state. + final VoidCallback? onCollapse; + + /// A function called whenever the tile is toggled to its collapsed state and animation is finished. + final VoidCallback? onCollapsed; + + /// Whether the expansion tile is selected or not. Defaults to false. + final bool selected; + + /// A function called whenever an item is selected. + // ignore: avoid_positional_boolean_parameters + final void Function(bool)? onSelected; + + /// Boolean to show divider between expansion tiles. + /// + /// Defaults to true. + final bool showDivider; + + /// If true, tapping the down chevron will toggle the tile expansion, if false tapping anywhere on the tile will. + /// + /// Defaults to false. + final bool expandWithIconOnly; + + /// If true, a semantics service such as Talkback / VoiceOver will ignore the expand button. + /// + /// Typically used if the content of the expansion is provided to this service in another way. + final bool hideExpansionSemantics; + + /// Determines if the user can expand the tile, or if it should behave like a regular tile. + /// + /// Typically used in a list when some tiles are expandable but some are not for UI consistencny. + final bool isExpandable; + + /// Type of expansion tile. + /// + /// Defaults to [ExpansionTileType.regular]. + final ExpansionTileType expansionTileType; + + /// Determines the background color of the title part of the expansion tile. + /// + /// Defaults to [ZdsColors.transparent]. + final Color titleColor; + + /// A tile that can be expanded and collapsed to reveal further information. + const ZdsExpansionTile({ + super.key, + required this.title, + required this.child, + this.subtitle, + this.bottom, + this.initiallyExpanded = false, + this.maintainState = true, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 24, vertical: 2), + this.titlePadding = const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + this.onExpanded, + this.onCollapse, + this.onCollapsed, + this.showDivider = true, + this.expandWithIconOnly = false, + this.hideExpansionSemantics = false, + this.isExpandable = true, + this.titleColor = ZdsColors.transparent, + }) : expansionTileType = ExpansionTileType.regular, + selected = false, + onSelected = null; + + /// A selectable tile that can be expanded and collapsed to reveal further information. + const ZdsExpansionTile.selectable({ + super.key, + required this.title, + required this.child, + this.subtitle, + this.bottom, + this.initiallyExpanded = false, + this.maintainState = true, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 24, vertical: 2), + this.titlePadding = const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + this.onExpanded, + this.onCollapse, + this.onCollapsed, + this.selected = false, + this.onSelected, + this.showDivider = true, + this.expandWithIconOnly = false, + this.hideExpansionSemantics = false, + this.isExpandable = true, + this.titleColor = ZdsColors.transparent, + }) : expansionTileType = ExpansionTileType.selectable; + + @override + ZdsExpansionTileState createState() => ZdsExpansionTileState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('initiallyExpanded', initiallyExpanded)); + properties.add(DiagnosticsProperty('maintainState', maintainState)); + properties.add(DiagnosticsProperty('contentPadding', contentPadding)); + properties.add(DiagnosticsProperty('titlePadding', titlePadding)); + properties.add(ObjectFlagProperty.has('onExpanded', onExpanded)); + properties.add(ObjectFlagProperty.has('onCollapse', onCollapse)); + properties.add(ObjectFlagProperty.has('onCollapsed', onCollapsed)); + properties.add(DiagnosticsProperty('selected', selected)); + properties.add(ObjectFlagProperty.has('onSelected', onSelected)); + properties.add(DiagnosticsProperty('showDivider', showDivider)); + properties.add(DiagnosticsProperty('expandWithIconOnly', expandWithIconOnly)); + properties.add(DiagnosticsProperty('hideExpansionSemantics', hideExpansionSemantics)); + properties.add(DiagnosticsProperty('isExpandable', isExpandable)); + properties.add(EnumProperty('expansionTileType', expansionTileType)); + properties.add(ColorProperty('titleColor', titleColor)); + } +} + +/// State for [ZdsExpansionTile], used to handle the on demand expansion and collapsing of the tile +/// +/// ```dart +/// late final expansionKey = GlobalKey(); +/// ``` +/// +/// See also: +/// * [ZdsExpansionTile] +class ZdsExpansionTileState extends State with SingleTickerProviderStateMixin { + static final Animatable _easeInTween = CurveTween(curve: Curves.easeIn); + late Animatable _halfTween; + late AnimationController _controller; + late Animation _iconTurns; + late Animation _heightFactor; + + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + _halfTween = Tween(begin: 0, end: widget.expansionTileType == ExpansionTileType.selectable ? -0.25 : 0.5); + _controller = AnimationController(duration: _kExpand, vsync: this); + + _iconTurns = _controller.drive(_halfTween.chain(_easeInTween)); + _heightFactor = _controller.drive(_easeInTween); + + _isExpanded = widget.initiallyExpanded; + _selected = widget.selected; + if (_isExpanded) _controller.value = 1.0; + } + + @override + void didUpdateWidget(covariant ZdsExpansionTile oldWidget) { + if (oldWidget.selected != widget.selected) { + _selected = widget.selected; + } + super.didUpdateWidget(oldWidget); + } + + /// Collapses the expanded tile. + Future collapse() async { + _isExpanded = false; + widget.onCollapse?.call(); + await _controller.reverse(); + if (!mounted) return; + widget.onCollapsed?.call(); + setState(() { + // Rebuild without widget.children. + }); + + PageStorage.of(context).writeState(context, _isExpanded); + } + + /// Expands the collapsed tile. + void expand() { + _isExpanded = true; + _controller.forward(); + + if (!mounted) return; + widget.onExpanded?.call(); + setState(() { + // Rebuild without widget.children. + }); + PageStorage.of(context).writeState(context, _isExpanded); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + /// Toggles the expansion tile between collapsed and expanded. + void toggle() { + setState(() { + _isExpanded = !_isExpanded; + if (_isExpanded) { + _controller.forward(); + widget.onExpanded?.call(); + } else { + unawaited(collapse()); + } + PageStorage.of(context).writeState(context, _isExpanded); + }); + } + + /// Function to select and unselect an expansion tile + void _toggleSelect() { + setState(() { + _selected = !_selected; + widget.onSelected?.call(_selected); + }); + } + + /// True if the tile is expanded + bool get isExpanded => _isExpanded; + bool _selected = false; + + @override + Widget build(BuildContext context) { + final bool closed = !_isExpanded && _controller.isDismissed; + final bool shouldRemoveChildren = closed && !widget.maintainState; + return AnimatedBuilder( + animation: _controller.view, + builder: (BuildContext context, Widget? child) { + final card = context.findAncestorWidgetOfExactType(); + final chevronIcon = IconButton( + onPressed: toggle, + icon: RotationTransition( + turns: _iconTurns, + child: Icon( + widget.expansionTileType == ExpansionTileType.regular ? ZdsIcons.chevron_down : ZdsIcons.chevron_right, + color: ZdsColors.greySwatch(context)[800], + size: 24, + ), + ), + ); + + return Stack( + clipBehavior: Clip.none, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + color: widget.titleColor, + child: Semantics( + checked: _selected, + child: InkWell( + onTap: widget.expandWithIconOnly + ? null + : widget.expansionTileType == ExpansionTileType.selectable + ? _toggleSelect + : toggle, + child: Column( + children: [ + Padding( + padding: widget.expansionTileType == ExpansionTileType.selectable + ? const EdgeInsets.fromLTRB(8, 4, 20, 4) + : widget.titlePadding, + child: Row( + children: [ + if (widget.expansionTileType == ExpansionTileType.selectable) + Semantics( + onTapHint: _isExpanded + ? ComponentStrings.of(context).get('HIDE', 'Hide') + : ComponentStrings.of(context).get('SHOW', 'Show'), + child: chevronIcon, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ZetaColors.of(context).textDefault), + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: widget.title, + ), + if (widget.subtitle != null) ...[ + const SizedBox(height: 5), + DefaultTextStyle( + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: ZetaColors.of(context).textSubtle, + ), + child: widget.subtitle!, + ), + ], + ], + ), + ), + if (widget.isExpandable && widget.expansionTileType == ExpansionTileType.regular) + Semantics( + excludeSemantics: widget.hideExpansionSemantics, + onTapHint: _isExpanded + ? ComponentStrings.of(context).get('HIDE', 'Hide') + : ComponentStrings.of(context).get('SHOW', 'Show'), + child: widget.expandWithIconOnly + ? chevronIcon + : IconTheme( + data: IconThemeData(color: ZetaColors.of(context).textSubtle, size: 24), + child: RotationTransition( + turns: _iconTurns, + child: const Icon(ZdsIcons.chevron_down), + ), + ), + ), + if (widget.selected) + IconTheme( + data: IconThemeData( + color: Theme.of(context).colorScheme.secondary, + size: 24, + ), + child: const Icon(ZdsIcons.check), + ), + ], + ), + ), + if (widget.bottom != null) widget.bottom!, + ], + ), + ), + ), + ), + ClipRect( + child: Align( + alignment: Alignment.topLeft, + heightFactor: _heightFactor.value, + child: child, + ), + ), + ], + ), + if (widget.showDivider) + Positioned( + top: -1, + left: 0, + right: 0, + child: Container( + height: 1, + color: card != null ? ZdsColors.lightGrey.withOpacity(0.5) : ZdsColors.transparent, + ), + ), + ], + ); + }, + child: shouldRemoveChildren || !widget.isExpandable + ? null + : Offstage( + offstage: closed, + child: TickerMode( + enabled: !closed, + child: Padding( + padding: widget.contentPadding, + child: widget.child, + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('isExpanded', isExpanded)); + } +} diff --git a/lib/src/components/molecules/fields_list_tile.dart b/lib/src/components/molecules/fields_list_tile.dart new file mode 100644 index 0000000..608063a --- /dev/null +++ b/lib/src/components/molecules/fields_list_tile.dart @@ -0,0 +1,201 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A tile showing a list of properties with their respective values. +/// +/// This component can be used instead of a table to show a lot of data at once in an easy to scan format. +/// +/// ```dart +/// ZdsFieldsListTile( +/// title: const Text('Project Title'), +/// fields: const [ +/// TileField( +/// start: Text('Start/End'), +/// end: Text('07/05/2021 - 07/05/2021'), +/// ), +/// TileField( +/// start: Text('Approval Date'), +/// end: Text('07/06/2021 16:45 PST'), +/// ), +/// ], +/// data: ProjectDataObject(), +/// onTap: (data) => manageProjectDataObject(), +/// ), +/// ``` +/// +/// See also: +/// +/// * [ZdsPropertiesList], another way to show table-like data. +/// * [TileField], which defines the fields. +class ZdsFieldsListTile extends StatelessWidget { + /// The title, shown at the top of this tile. + /// + /// Typically a [Text]. + final Widget? title; + + /// A list of pairs of data. + /// + /// Typically, [TileField.start] is that pair's title, while [TileField.end] is that pair's value. + final List? fields; + + /// The textStyle used for the starting elements of each field. + /// + /// Defaults to [TextTheme.titleSmall] with [ZdsColors.blueGrey] color. + final TextStyle? fieldsStartTextStyle; + + /// The textStyle used for the end elements of each field. + /// + /// Defaults to [TextTheme.bodyMedium]. + final TextStyle? fieldsEndTextStyle; + + /// Data called in [onTap] argument. + final T? data; + + /// The additional information, shown at the bottom of this tile. + /// + /// Typically a [Text]. + final Widget? footnote; + + /// The function to call whenever the user taps on this tile. + /// + /// [data] is passed as an argument. + final void Function(T?)? onTap; + + /// Whether the fields are closely packed together or separated with more space. + /// + /// Defaults to true. + final bool shrink; + + /// Determines how much space the start field should take up in the [TileField] row. + /// + /// Default is 0. + final int? startFieldFlexFactor; + + /// The card's internal padding. + /// + /// Defaults to EdgeInsets.symmetric(horizontal: 14, vertical: 18). + final EdgeInsets cardPadding; + + /// Creates a tile showing a list of properties with their respective values. + const ZdsFieldsListTile({ + super.key, + this.title, + this.fields, + this.fieldsStartTextStyle, + this.fieldsEndTextStyle, + this.data, + this.footnote, + this.onTap, + this.shrink = true, + this.startFieldFlexFactor, + this.cardPadding = const EdgeInsets.symmetric(horizontal: 14, vertical: 18), + }); + + @override + Widget build(BuildContext context) { + return ZdsCard( + padding: cardPadding, + onTap: onTap != null ? () => onTap!.call(data) : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (title != null) + DefaultTextStyle( + style: Theme.of(context).textTheme.bodyLarge!, + child: title!, + ).space(), + if (fields != null) + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, index) => buildDetailRow( + context: context, + field: fields![index], + fieldsStartDefaultStyle: fieldsStartTextStyle, + fieldsEndDefaultStyle: fieldsEndTextStyle, + startFieldFlexFactor: startFieldFlexFactor, + ), + separatorBuilder: (_, index) => + SizedBox(height: fields?[index].start != null && fields?[index].end != null ? (shrink ? 10 : 18) : 0), + itemCount: fields!.length, + ), + if (footnote != null) + Column( + children: [ + const SizedBox(height: 8), + DefaultTextStyle( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: ZdsColors.greySwatch(context)[900], + ), + child: footnote!, + ), + ], + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty('fields', fields)); + properties.add(DiagnosticsProperty('fieldsStartTextStyle', fieldsStartTextStyle)); + properties.add(DiagnosticsProperty('fieldsEndTextStyle', fieldsEndTextStyle)); + properties.add(DiagnosticsProperty('data', data)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DiagnosticsProperty('shrink', shrink)); + properties.add(IntProperty('startFieldFlexFactor', startFieldFlexFactor)); + properties.add(DiagnosticsProperty('cardPadding', cardPadding)); + } +} + +extension _UIBuilder on ZdsFieldsListTile { + Widget buildDetailRow({ + required BuildContext context, + required TileField field, + TextStyle? fieldsStartDefaultStyle, + TextStyle? fieldsEndDefaultStyle, + int? startFieldFlexFactor, + }) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (field.start != null) + DefaultTextStyle( + style: + fieldsStartDefaultStyle ?? Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.blueGrey), + textAlign: TextAlign.start, + child: Flexible(flex: startFieldFlexFactor ?? 0, child: field.start!), + ) + else + const Spacer(), + const SizedBox(width: 12), + if (field.end != null) + DefaultTextStyle( + style: fieldsEndDefaultStyle ?? Theme.of(context).textTheme.bodyMedium!, + textAlign: TextAlign.end, + child: Flexible(child: field.end!), + ), + ], + ); + } +} + +/// Pairs of data used with [ZdsFieldsListTile]. +class TileField { + /// Start widget of the data. + final Widget? start; + + /// End widget of the data. + final Widget? end; + + /// Constructs a [TileField]. + const TileField({ + this.start, + this.end, + }); +} diff --git a/lib/src/components/molecules/icon_badge_widget.dart b/lib/src/components/molecules/icon_badge_widget.dart new file mode 100644 index 0000000..638526d --- /dev/null +++ b/lib/src/components/molecules/icon_badge_widget.dart @@ -0,0 +1,271 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter/rendering.dart'; +import '../../../zds_flutter.dart'; + +/// Shows an icon with an unread badge. +/// +/// Typically used as a way of indicating how many unread messages/notifications an app section has. +/// +/// See also: +/// +/// * [UnreadBadge], used to show the unread badge in this +/// * [ZdsNavItem], where this widget can be used as the icon. +class IconWithBadge extends StatelessWidget { + /// The number to show in the badge. If it's equal to 0, only the icon will be shown and no badge will show. + /// + /// Defaults to 0. + final int unread; + + /// The icon to display. The available icons are described in [Icons]. + /// + /// The icon can be null, in which case the widget will render as an empty + /// space of the specified [size]. + final IconData? icon; + + /// The size of the icon in logical pixels. + /// + /// Icons occupy a square with width and height equal to size. + /// + /// Defaults to the nearest [IconTheme]'s [IconThemeData.size]. + /// + /// If this [Icon] is being placed inside an [IconButton], then use + /// [IconButton.iconSize] instead, so that the [IconButton] can make the splash + /// area the appropriate size as well. The [IconButton] uses an [IconTheme] to + /// pass down the size to the [Icon]. + /// Defaults to 24. + final double size; + + /// The fill for drawing the icon. + /// + /// Requires the underlying icon font to support the `FILL` [FontVariation] + /// axis, otherwise has no effect. Variable font filenames often indicate + /// the supported axes. Must be between 0.0 (unfilled) and 1.0 (filled), + /// inclusive. + /// + /// Can be used to convey a state transition for animation or interaction. + /// + /// Defaults to nearest [IconTheme]'s [IconThemeData.fill]. + /// + /// See also: + /// * [weight], for controlling stroke weight. + /// * [grade], for controlling stroke weight in a more granular way. + /// * [opticalSize], for controlling optical size. + final double? fill; + + /// The stroke weight for drawing the icon. + /// + /// Requires the underlying icon font to support the `weight` [FontVariation] + /// axis, otherwise has no effect. Variable font filenames often indicate + /// the supported axes. Must be greater than 0. + /// + /// Defaults to nearest [IconTheme]'s [IconThemeData.weight]. + /// + /// See also: + /// * [fill], for controlling fill. + /// * [grade], for controlling stroke weight in a more granular way. + /// * [opticalSize], for controlling optical size. + /// * https://fonts.google.com/knowledge/glossary/weight_axis. + final double? weight; + + /// The grade (granular stroke weight) for drawing the icon. + /// + /// Requires the underlying icon font to support the `GRAD` [FontVariation] + /// axis, otherwise has no effect. Variable font filenames often indicate + /// the supported axes. Can be negative. + /// + /// Grade and [weight] both affect a symbol's stroke weight (thickness), but + /// grade has a smaller impact on the size of the symbol. + /// + /// Grade is also available in some text fonts. One can match grade levels + /// between text and symbols for a harmonious visual effect. For example, if + /// the text font has a -25 grade value, the symbols can match it with a + /// suitable value, say -25. + /// + /// Defaults to nearest [IconTheme]'s [IconThemeData.grade]. + /// + /// See also: + /// * [fill], for controlling fill. + /// * [weight], for controlling stroke weight in a less granular way. + /// * [opticalSize], for controlling optical size. + /// * https://fonts.google.com/knowledge/glossary/grade_axis. + final double? grade; + + /// The optical size for drawing the icon. + /// + /// Requires the underlying icon font to support the `optical size` [FontVariation] + /// axis, otherwise has no effect. Variable font filenames often indicate + /// the supported axes. Must be greater than 0. + /// + /// For an icon to look the same at different sizes, the stroke weight + /// (thickness) must change as the icon size scales. Optical size offers a way + /// to automatically adjust the stroke weight as icon size changes. + /// + /// Defaults to nearest [IconTheme]'s [IconThemeData.opticalSize]. + /// + /// See also: + /// * [fill], for controlling fill. + /// * [weight], for controlling stroke weight. + /// * [grade], for controlling stroke weight in a more granular way. + /// * https://fonts.google.com/knowledge/glossary/optical_size_axis. + final double? opticalSize; + + /// The color to use when drawing the icon. + /// + /// Defaults to the nearest [IconTheme]'s [IconThemeData.color]. + /// + /// The color (whether specified explicitly here or obtained from the + /// [IconTheme]) will be further adjusted by the nearest [IconTheme]'s + /// [IconThemeData.opacity]. + /// + /// {@tool snippet} + /// Typically, a Material Design color will be used, as follows: + /// + /// ```dart + /// Icon( + /// Icons.widgets, + /// color: Colors.blue.shade400, + /// ) + /// ``` + /// {@end-tool} + final Color? color; + + /// A list of [Shadow]s that will be painted underneath the icon. + /// + /// Multiple shadows are supported to replicate lighting from multiple light + /// sources. + /// + /// Shadows must be in the same order for [Icon] to be considered as + /// equivalent as order produces differing transparency. + /// + /// Defaults to the nearest [IconTheme]'s [IconThemeData.shadows]. + final List? shadows; + + /// Semantic label for the icon. + /// + /// Announced in accessibility modes (e.g TalkBack/VoiceOver). + /// This label does not show in the UI. + /// + /// * [SemanticsProperties.label], which is set to [semanticLabel] in the + /// underlying [Semantics] widget. + final String? semanticLabel; + + /// The text direction to use for rendering the icon. + /// + /// If this is null, the ambient [Directionality] is used instead. + /// + /// Some icons follow the reading direction. For example, "back" buttons point + /// left in left-to-right environments and right in right-to-left + /// environments. Such icons have their [IconData.matchTextDirection] field + /// set to true, and the [Icon] widget uses the [textDirection] to determine + /// the orientation in which to draw the icon. + /// + /// This property has no effect if the [icon]'s [IconData.matchTextDirection] + /// field is false, but for consistency a text direction value must always be + /// specified, either directly using this property or using [Directionality]. + final TextDirection? textDirection; + + /// Optional text to replace the default [Semantics] behavior of reading the number in this badge. + /// Can be used for making [Semantics] read "3 unread emails" instead of just "3" + /// + /// See [UnreadBadge.semanticsLabel] for more details. + final String? semanticsLabel; + + /// How many digits long the unread amount can be. For example, if set to 3, any number over 999 will be shown as + /// 999+ or +999 depending on the locale used and the text direction. + /// + /// Must be equal or greater than 1. Defaults to 3. + final int maximumDigits; + + /// The color of the surface where this icon is being drawn on. Typically, this will be the surface color. However, in cases + /// where this widget is used in a context with a different surface color, such as in an AppBar, this value should + /// be set to the AppBar's background color. + /// + /// This color is later used to draw a border around the [UnreadBadge]. + final Color? iconContainerColor; + + /// Displays an icon with an optional unread badge if the [unread] parameter is given. The optional [semanticsLabel] + /// parameter is used by the unreadBadge child. + /// + /// No badge will show if [unread] is 0. + /// + /// All parameters except [semanticsLabel] must not be null. + const IconWithBadge( + this.icon, { + super.key, + this.color, + this.fill, + this.grade, + this.maximumDigits = 3, + this.opticalSize, + this.semanticLabel, + this.semanticsLabel, + this.shadows, + this.size = 24, + this.textDirection, + this.unread = 0, + this.weight, + this.iconContainerColor, + }) : assert(maximumDigits >= 1, 'maximumDigits must be greater than or equal to 1.'); + + @override + Widget build(BuildContext context) { + final badgeSize = size * 0.6; + return Center( + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Icon( + icon, + size: size, + fill: fill, + weight: weight, + grade: grade, + opticalSize: opticalSize, + color: color, + shadows: shadows, + semanticLabel: semanticLabel, + textDirection: textDirection, + ), + if (unread > 0) + Positioned( + top: -badgeSize * 0.4, + left: badgeSize * 0.75, + child: ExcludeSemantics( + child: UnreadBadge( + unread: unread, + minWidth: badgeSize, + minHeight: badgeSize, + badgeContainerColor: iconContainerColor, + ), + ), + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('unread', unread)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(DoubleProperty('size', size)) + ..add(DoubleProperty('fill', fill)) + ..add(DoubleProperty('weight', weight)) + ..add(DoubleProperty('grade', grade)) + ..add(DoubleProperty('opticalSize', opticalSize)) + ..add(ColorProperty('color', color)) + ..add(IterableProperty('shadows', shadows)) + ..add(StringProperty('semanticLabel', semanticLabel)) + ..add(EnumProperty('textDirection', textDirection)) + ..add(StringProperty('semanticsLabel', semanticsLabel)) + ..add(IntProperty('maximumDigits', maximumDigits)) + ..add(ColorProperty('iconContainerColor', iconContainerColor)); + } +} diff --git a/lib/src/components/molecules/information_bar.dart b/lib/src/components/molecules/information_bar.dart new file mode 100644 index 0000000..3cf814c --- /dev/null +++ b/lib/src/components/molecules/information_bar.dart @@ -0,0 +1,96 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// A bar used to display status information. +/// +/// Typically used at the top of the page, below the appbar, to display status information. +class ZdsInformationBar extends StatelessWidget { + /// The icon shown before the [label]. + /// + /// The color used will be determined by [zetaColorSwatch]. + final IconData? icon; + + /// The label shown after the [icon]. + final String? label; + + /// Custom foreground color. + /// + /// Overrides [zetaColorSwatch]. + /// + /// Will be deprecated and replaced with ZetaColorSwatch in future release. + final Color? customForeground; + + /// Custom background color. + /// + /// Overrides [zetaColorSwatch]. + /// + /// Will be deprecated and replaced with ZetaColorSwatch in future release. + final Color? customBackground; + + /// Color for the information bar. + /// + /// * Background: `color.surface`. + /// * Icon: `color.icon`. + /// * Text: default text color. + /// + /// Defaults to primary color swatch. + final ZetaColorSwatch? zetaColorSwatch; + + /// Creates a bar used to display status information. + const ZdsInformationBar({ + super.key, + this.icon, + this.label, + this.customBackground, + this.customForeground, + this.zetaColorSwatch, + }); + + @override + Widget build(BuildContext context) { + final Color bg = zetaColorSwatch?.shade20 ?? customBackground ?? ZetaColors.of(context).primary.surface; + return Container( + height: 42, + width: double.maxFinite, + color: bg, + child: SafeArea( + top: false, + bottom: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) + Icon( + icon, + color: zetaColorSwatch?.icon ?? customForeground ?? ZetaColors.of(context).primary.icon, + size: 24, + ).paddingOnly(right: 8), + if (label != null) + Flexible( + child: Text( + label!, + overflow: TextOverflow.ellipsis, + style: + Theme.of(context).textTheme.titleSmall?.copyWith(color: ZetaColors.computeForeground(input: bg)), + ), + ), + ], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + + properties.add(DiagnosticsProperty('icon', icon)); + properties.add(StringProperty('label', label)); + properties.add(ColorProperty('customForeground', customForeground)); + properties.add(ColorProperty('customBackground', customBackground)); + properties.add(ColorProperty('zetaColorSwatch', zetaColorSwatch)); + } +} diff --git a/lib/src/components/molecules/input_dialog.dart b/lib/src/components/molecules/input_dialog.dart new file mode 100644 index 0000000..500ce78 --- /dev/null +++ b/lib/src/components/molecules/input_dialog.dart @@ -0,0 +1,236 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../zds_flutter.dart'; + +/// A dialog with a textfield and built-in validation. +/// +/// This dialog can be used to retrieve 1 String value through a Future. Validation is built-in, so the Future will +/// only return a valid value. +/// +/// ```dart +/// final filterName = await showDialog( +/// context: context, +/// builder: (context) => ZdsInputDialog( +/// title: 'Save Filter', +/// hint: 'Enter filter name', +/// primaryAction: 'Save', +/// secondaryAction: 'Cancel', +/// onValidate: (value) async { +/// if (value.isEmpty) { +/// return 'This field is mandatory'; +/// } else if (state.filters.contains(value) { +/// return 'This filter already exists'; +/// } else { +/// return null; +/// } +/// }, +/// ), +/// ); +/// ``` +/// +/// See also: +/// +/// * [ZdsModal], to create a general purpose dialog. +class ZdsInputDialog extends StatefulWidget { + /// The title of this dialog, shown at the top. + final String? title; + + /// The label of the textField. Recommended to use [title] instead. + final String? labelText; + + /// The hint of the textField. + final String? hint; + + /// How many characters can be input in the textField. + /// + /// If null, there will be no limit. + final int? characterCount; + + /// The text for primary action of this dialog, which will validate the input and then close the dialog and return + /// the Future with the value. + /// + /// Typically 'Save' or 'Apply'. + final String primaryAction; + + /// The text for the button that will close this dialog and cancel any value retrieval. + /// + /// Typically 'Cancel'. + final String? secondaryAction; + + /// The [TextInputAction] that will be shown in the keyboard when the user types on the textField. + /// + /// Defaults to [TextInputAction.done]. + final TextInputAction inputAction; + + /// Whether to autofocus on this dialog. + /// + /// Defaults to true. + final bool autoFocus; + + /// Optional initial text that will pre-populate the textField. + /// + /// If null, [hint] will be shown. + final String? initialText; + + /// An optional input formatter to provide validation as the user types. + final List? inputFormatters; + + /// The function to be called when the user taps on the [primaryAction] button to return the value. + /// + /// If it returns null, validation has gone through and the Future with the value will be returned. Returning a + /// String means validation failed, with said String being the error text. + final Future Function(String text)? onValidate; + + /// A dialog used to retrieve 1 String value with built-in validation. + const ZdsInputDialog({ + required this.primaryAction, + super.key, + this.title, + this.labelText, + this.hint, + this.initialText, + this.secondaryAction, + this.inputAction = TextInputAction.done, + this.autoFocus = true, + this.inputFormatters, + this.onValidate, + this.characterCount, + }); + + @override + ZdsInputDialogState createState() => ZdsInputDialogState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('title', title)); + properties.add(StringProperty('labelText', labelText)); + properties.add(StringProperty('hint', hint)); + properties.add(IntProperty('characterCount', characterCount)); + properties.add(StringProperty('primaryAction', primaryAction)); + properties.add(StringProperty('secondaryAction', secondaryAction)); + properties.add(EnumProperty('inputAction', inputAction)); + properties.add(DiagnosticsProperty('autoFocus', autoFocus)); + properties.add(StringProperty('initialText', initialText)); + properties.add(IterableProperty('inputFormatters', inputFormatters)); + properties.add(ObjectFlagProperty Function(String text)?>.has('onValidate', onValidate)); + } +} + +/// State for [ZdsInputDialog]. +class ZdsInputDialogState extends State { + late final TextEditingController _controller = TextEditingController(); + String? _error; + int? _characterLeftCount; + + @override + void initState() { + _controller.text = widget.initialText ?? ''; + if (widget.characterCount != null) { + _characterLeftCount = widget.characterCount! - _controller.text.length; + } + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Dialog( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.title != null) Text(widget.title!, style: theme.textTheme.headlineMedium), + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 30), + child: Row( + children: [ + Expanded( + child: TextFormField( + maxLength: widget.characterCount, + autofocus: widget.autoFocus, + textInputAction: widget.inputAction, + controller: _controller, + inputFormatters: widget.inputFormatters, + onChanged: (value) async { + await _validateText(); + }, + onFieldSubmitted: (_) async { + final error = await _validateText(); + if ((error?.isEmpty ?? true) && mounted) { + await Navigator.maybePop(context, _controller.text); + } + }, + decoration: ZdsInputDecoration( + labelText: widget.labelText, + counterText: widget.characterCount != null && _error == null + ? ComponentStrings.of(context) + .get('CHAR_LEFT', 'Characters Left {0}', args: ['$_characterLeftCount']) + : null, + hintText: widget.hint, + errorText: _error, + errorStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(color: ZdsColors.red), + ), + ), + ), + ], + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 48), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (widget.secondaryAction != null) + ZdsButton.muted( + key: const Key('secondary_button'), + onTap: () async => Navigator.maybePop(context), + child: Text(widget.secondaryAction!), + ), + if (widget.secondaryAction != null) const SizedBox(width: 8), + ZdsButton.filled( + key: const Key('primary_button'), + onTap: _error == null + ? () async { + final error = await _validateText(); + if ((error?.isEmpty ?? true) && mounted) { + await Navigator.maybePop(context, _controller.text); + } + } + : null, + child: Text(widget.primaryAction), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Future _validateText() async { + final error = await widget.onValidate?.call(_controller.text); + if (mounted) { + setState(() { + _error = error; + if (widget.characterCount != null) { + _characterLeftCount = widget.characterCount! - _controller.text.length; + } + }); + return error; + } + return null; + } +} diff --git a/lib/src/components/molecules/list.dart b/lib/src/components/molecules/list.dart new file mode 100644 index 0000000..dcdb12f --- /dev/null +++ b/lib/src/components/molecules/list.dart @@ -0,0 +1,228 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; +import '../../utils/tools/measure.dart'; + +const _emptyChildLength = 30; + +final List _emptyChildren = + List.generate(_emptyChildLength, (_) => _emptyChild).divide(const Divider()).toList(); + +const Widget _emptyChild = SizedBox(height: 60, width: double.infinity); + +Widget _emptyBuilder(_, __) => _emptyChild; + +/// Creates a [ListView] with predefined parameters that match Zds styling and behavior. +/// +/// See also: +/// +/// * [ListView]. +class ZdsList extends ListView { + /// {@template ZdsList.showEmpty} + /// Whether to show an empty list with a divider if the `children` list is empty. + /// + /// Defaults to false. + /// {@endtemplate} + final bool showEmpty; + + /// Creates a [ZdsList]. + ZdsList({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + ScrollPhysics? physics, + super.shrinkWrap, + super.padding = const EdgeInsets.symmetric( + horizontal: kDefaultHorizontalPadding, + vertical: kDefaultVerticalPadding, + ), + super.itemExtent, + super.addAutomaticKeepAlives, + super.addRepaintBoundaries, + super.addSemanticIndexes, + super.cacheExtent, + List children = const [], + super.semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + this.showEmpty = false, + }) : super( + physics: showEmpty && children.isEmpty ? const NeverScrollableScrollPhysics() : physics, + children: showEmpty && children.isEmpty ? _emptyChildren : children, + ); + + /// Creates a ZdsList with an itemBuilder. + /// + /// Extends [ListView.builder] with Zds styling and behavior. + /// + /// To create a list builder with the items compacted together: + /// ```dart + ///ZdsListGroup( + /// child: ZdsList.builder( + /// padding: EdgeInsets.zero, + /// itemBuilder: (context, index) { + /// return ZdsListTile( + /// title: Text(index.toString()), + /// onTap: () {}, + /// ); + /// }, + /// itemCount: 10, + /// ), + /// ``` + ZdsList.builder({ + required IndexedWidgetBuilder itemBuilder, + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + ScrollPhysics? physics, + super.shrinkWrap, + super.padding = const EdgeInsets.symmetric(horizontal: kDefaultHorizontalPadding), + int? itemCount, + super.addAutomaticKeepAlives, + super.addRepaintBoundaries, + super.addSemanticIndexes, + super.cacheExtent, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + this.showEmpty = false, + }) : super.separated( + physics: showEmpty && (itemCount == null || itemCount == 0) ? const NeverScrollableScrollPhysics() : physics, + itemBuilder: showEmpty && (itemCount == null || itemCount == 0) ? _emptyBuilder : itemBuilder, + itemCount: showEmpty && (itemCount == null || itemCount == 0) ? _emptyChildLength : itemCount ?? 0, + separatorBuilder: (_, __) { + if (showEmpty && !(itemCount == null || itemCount == 0)) { + return const Divider(height: 1); + } + return const SizedBox.shrink(); + }, + ); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('showEmpty', showEmpty)); + } +} + +/// Makes a horizontal [ListView] that sizes itself to its children. +class ZdsHorizontalList extends _ZdsHorizontalList { + /// Creates a [ZdsHorizontalList]. + ZdsHorizontalList({super.key, List? children, super.caption, super.isReducedHeight}) + : super( + delegate: _ZdsHorizontalListChildrenDelegate(children), + ); + + /// Makes a horizontal [ListView.builder] that sizes itself to its children. + ZdsHorizontalList.builder({ + super.key, + BuilderCallback? itemBuilder, + int? itemCount, + super.caption, + super.isReducedHeight, + }) : super( + delegate: _ZdsHorizontalListBuilderDelegate(itemBuilder, itemCount), + ); +} + +class _ZdsHorizontalList extends StatelessWidget { + final Widget? caption; + final _ZdsHorizontalChildDelegate? delegate; + final bool isReducedHeight; + + const _ZdsHorizontalList({super.key, this.delegate, this.caption, this.isReducedHeight = false}); + + @override + Widget build(BuildContext context) { + final firstItem = delegate!.build(context, 0); + + return MeasureSize( + child: firstItem, + builder: (context, size) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.displayMedium!, + child: caption != null ? caption!.paddingOnly(top: 20, left: 20, right: 20) : const SizedBox(), + ), + SizedBox( + width: double.infinity, + height: isReducedHeight ? size.height : size.height + 20, + child: CustomScrollView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(10), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + delegate!.build, + childCount: delegate!.estimatedChildCount, + ), + ), + ), + ], + ), + ), + ], + ); + }, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty<_ZdsHorizontalChildDelegate?>('delegate', delegate)) + ..add(DiagnosticsProperty('isReducedHeight', isReducedHeight)); + } +} + +class _ZdsHorizontalListChildrenDelegate extends _ZdsHorizontalChildDelegate { + final List? children; + + _ZdsHorizontalListChildrenDelegate(this.children) : super(); + + @override + int get estimatedChildCount => children!.length; + + @override + Widget build(BuildContext context, int index) { + return children![index]; + } +} + +class _ZdsHorizontalListBuilderDelegate extends _ZdsHorizontalChildDelegate { + final int? itemCount; + final BuilderCallback? builder; + + _ZdsHorizontalListBuilderDelegate(this.builder, this.itemCount) : super(); + + @override + int? get estimatedChildCount => itemCount; + + @override + Widget build(BuildContext context, int index) { + return builder!(context, index); + } +} + +abstract class _ZdsHorizontalChildDelegate { + const _ZdsHorizontalChildDelegate(); + + int? get estimatedChildCount => null; + + Widget build(BuildContext context, int index); +} + +/// Callback function for [ZdsHorizontalList.builder] method. +typedef BuilderCallback = Widget Function(BuildContext context, int index); diff --git a/lib/src/components/molecules/list_tile_wrapper.dart b/lib/src/components/molecules/list_tile_wrapper.dart new file mode 100644 index 0000000..4817d0e --- /dev/null +++ b/lib/src/components/molecules/list_tile_wrapper.dart @@ -0,0 +1,93 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +/// Component to add additional styling to a ZdsListTile. +/// +/// Removes the gap between ZdsTiles when used in a list and adds dividing lines between the tiles and rounds the corners. +class ZdsListTileWrapper extends StatelessWidget { + /// Typically a ZdsListTile for the styling to be applied to. + final Widget child; + + /// Whether the tile is the first (at the top) of the list. + /// + /// Defaults to false. + final bool top; + + /// Whether the tile is the last (at the bottom) of the list. + /// + /// Defaults to false. + final bool bottom; + + /// Constructs a [ZdsListTileWrapper]. + const ZdsListTileWrapper({required this.child, super.key, this.top = false, this.bottom = false}); + + @override + Widget build(BuildContext context) { + final ZetaColors colors = ZetaColors.of(context); + return ClipRect( + clipper: _PaddingRect( + const EdgeInsets.symmetric(horizontal: 10).copyWith(top: top ? 10 : 0, bottom: bottom ? 10 : 0), + ), + child: Padding( + padding: EdgeInsets.only(top: top ? 10 : 0, bottom: bottom ? 10 : 0), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + top: top ? BorderSide.none : BorderSide(color: colors.cool.shade40), + bottom: bottom ? BorderSide.none : BorderSide(color: colors.cool.shade40), + ), + boxShadow: [ + BoxShadow( + blurRadius: 4, + color: colors.shadow, + ), + ], + ), + child: Material( + clipBehavior: Clip.antiAlias, + borderRadius: BorderRadius.only( + topLeft: top ? const Radius.circular(6) : Radius.zero, + topRight: top ? const Radius.circular(6) : Radius.zero, + bottomLeft: bottom ? const Radius.circular(6) : Radius.zero, + bottomRight: bottom ? const Radius.circular(6) : Radius.zero, + ), + color: ZetaColors.of(context).background, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + top: top ? BorderSide.none : BorderSide(color: Theme.of(context).dividerColor), + ), + ), + child: child, + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('top', top)) + ..add(DiagnosticsProperty('bottom', bottom)); + } +} + +class _PaddingRect extends CustomClipper { + _PaddingRect(this.padding); + + final EdgeInsets padding; + + @override + Rect getClip(Size size) { + return Rect.fromLTRB(padding.left * -1, 0, size.width + (padding.horizontal), size.height); + } + + @override + bool shouldReclip(_PaddingRect oldClipper) { + return oldClipper.padding != padding; + } +} diff --git a/lib/src/components/molecules/menu.dart b/lib/src/components/molecules/menu.dart new file mode 100644 index 0000000..713f1dc --- /dev/null +++ b/lib/src/components/molecules/menu.dart @@ -0,0 +1,170 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zds_flutter.dart'; + +/// Creates a popup menu. +/// +/// This component is typically used to display more options that do not fit in a [ZdsAppBar], or to show more +/// options in a specific widget, like a [ZdsListTile] +/// +/// This popup menu should typically be populated with [ZdsPopupMenuItem]. +/// +/// ```dart +/// ZdsPopupMenu( +/// onSelected: (String value) => manageValue(value), +/// builder: (context, open) => IconButton( +/// onPressed: open, +/// icon: Icon(ZdsIcons.more_vert), +/// ), +/// items: [ +/// ZdsPopupMenuItem( +/// value: 'Week', +/// child: ListTile(title: Text('Week')), +/// ), +/// ZdsPopupMenuItem( +/// value: 'Month', +/// child: ListTile(title: Text('Month')), +/// ), +/// ], +/// ), +/// ``` +/// +/// See also: +/// +/// * [ZdsPopupMenuItem], used to create the options that appear in this menu. +/// * [ZdsAppBar], where this component is used to show more actions that do not typically fit. +class ZdsPopupMenu extends StatefulWidget { + /// Defines how this component will appear on screen. + /// + /// Typically builds an [IconButton]. + final Widget Function(BuildContext, VoidCallback) builder; + + /// The options that will appear when this menu is tapped. + /// + /// See [ZdsPopupMenuItem]. + /// + /// Must not be empty. + final List> items; + + /// A function called whenever an item is selected. + final PopupMenuItemSelected? onSelected; + + /// A function called whenever the user doesn't select an item and instead closes the menu. + final PopupMenuCanceled? onCanceled; + + /// Creates a pop up menu. + const ZdsPopupMenu({ + required this.builder, + required this.items, + super.key, + this.onCanceled, + this.onSelected, + }) : assert(items.length > 0, 'Must have at least 1 item'); + + @override + ZdsPopupMenuState createState() => ZdsPopupMenuState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has('builder', builder)) + ..add(ObjectFlagProperty?>.has('onSelected', onSelected)) + ..add(ObjectFlagProperty.has('onCanceled', onCanceled)); + } +} + +/// State for [ZdsPopupMenu]. +class ZdsPopupMenuState extends State> { + final GlobalKey _key = GlobalKey(); + + Null Function() _showButtonMenu(BuildContext context) => () { + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + if (_key.currentContext == null || + _key.currentContext?.findRenderObject() == null || + Navigator.of(context).overlay == null || + Navigator.of(context).overlay?.context.findRenderObject() == null) { + return; + } + final RenderBox button = _key.currentContext!.findRenderObject()! as RenderBox; + final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox; + final RelativeRect position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(Offset(0, button.size.height), ancestor: overlay), + button.localToGlobal( + button.size.bottomRight(Offset.zero) + Offset(0, button.size.height), + ancestor: overlay, + ), + ), + Offset.zero & overlay.size, + ); + final List> items = widget.items; + // Only show the menu if there is something to show + if (items.isNotEmpty) { + unawaited( + showMenu( + context: context, + elevation: popupMenuTheme.elevation, + items: items, + position: position, + shape: popupMenuTheme.shape, + color: popupMenuTheme.color, + ).then((T? newValue) { + if (!mounted) return null; + if (newValue == null) { + widget.onCanceled?.call(); + return null; + } + widget.onSelected?.call(newValue); + }), + ); + } + }; + + @override + Widget build(BuildContext context) { + return ListTileTheme( + contentPadding: EdgeInsets.zero, + minVerticalPadding: 0, + dense: true, + child: Material( + color: Colors.transparent, + child: Builder( + key: _key, + builder: (context) => IconTheme( + data: Theme.of(context).primaryIconTheme, + child: widget.builder(context, _showButtonMenu(context)), + ), + ), + ), + ); + } +} + +/// A component used to create the options that appear when using [ZdsPopupMenu]. +/// +/// The contents of an item are typically created using [ListTile]. +/// +/// ```dart +/// ZdsPopupMenuItem( +/// value: 'Week', +/// child: ListTile(title: Text('Week')), +/// ), +/// ``` +/// +/// See also: +/// +/// * [ZdsPopupMenu], used to create popup menus. +class ZdsPopupMenuItem extends PopupMenuItem { + /// Constructs a [ZdsPopupMenuItem]. + const ZdsPopupMenuItem({ + required Widget super.child, + super.key, + super.value, + super.enabled, + super.height, + super.padding, + super.textStyle, + }); +} diff --git a/lib/src/components/molecules/menu_item.dart b/lib/src/components/molecules/menu_item.dart new file mode 100644 index 0000000..2a393a8 --- /dev/null +++ b/lib/src/components/molecules/menu_item.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +const double _itemHeight = 56; + +/// A menu item used in [ZdsNavigationMenu]. +/// +/// This component is typically used to create buttons in a drawer menu to navigate to other apps, other parts of the +/// current app, and to change settings +/// +/// While this component is typically used to redirect to other parts of the app or to other apps, it's possible to add +/// a toggle button to the [trailing] parameter to enable changing settings in the drawer itself. +/// +/// ```dart +/// ZdsMenuItem( +/// title: const Text('About'), +/// leading: const Icon(ZdsIcons.info), +/// onTap: () => openAboutPage(), +/// trailing: const Icon(ZdsIcons.chevron_right), +/// ), +/// ZdsMenuItem( +/// title: const Text('Enable notifications'), +/// trailing: Switch( +/// onChanged: (value) => manageValue(value), +/// value: true, +/// ), +/// ), +/// ``` +/// +/// See also: +/// +/// * [ZdsNavigationMenu], used to create drawer navigation menus. +class ZdsMenuItem extends StatelessWidget { + /// A widget that will be shown before the title. + /// + /// Typically an [Icon]. + final Widget? leading; + + /// The widget that describes this item's function. + /// + /// Typically a [Text]. + final Widget? title; + + /// A widget that will be shown at the end of the tile. + /// + /// Typically an [Icon]. + final Widget? trailing; + + /// A widget that will be shown above the [leading] and [title] widgets for secondary information. + /// + /// Typically a [Text]. + final Widget? label; + + /// A function called whenever a user taps on this component. + final VoidCallback? onTap; + + /// Creates a menu item for navigation. + const ZdsMenuItem({super.key, this.label, this.leading, this.title, this.trailing, this.onTap}); + + @override + Widget build(BuildContext context) { + return Semantics( + button: onTap != null, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: _itemHeight), + child: InkWell( + onTap: onTap, + child: IconTheme.merge( + data: IconThemeData(size: 24, color: ZdsColors.greySwatch(context)[800]), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (label != null) ZdsNavigationMenuLabel(child: label!), + Row( + children: [ + if (leading != null) + IconTheme.merge( + child: leading!, + data: IconThemeData( + color: Theme.of(context).colorScheme.secondary, + ), + ), + if (title != null) Expanded(child: title!), + ].divide(const SizedBox(width: 15)).toList(), + ), + ], + ).textStyle( + Theme.of(context).textTheme.bodyMedium!.copyWith(color: Theme.of(context).colorScheme.onBackground), + ), + ), + if (trailing != null) trailing!, + ], + ), + ).paddingInsets(kMenuHorizontalPadding), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + } +} diff --git a/lib/src/components/molecules/network_avatar.dart b/lib/src/components/molecules/network_avatar.dart new file mode 100644 index 0000000..a4c64d0 --- /dev/null +++ b/lib/src/components/molecules/network_avatar.dart @@ -0,0 +1,141 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import '../../../zds_flutter.dart'; + +/// A circular container used to display a user's profile picture fetched from the internet. +/// +/// This widget will prioritize showing the image fetched from [url]. If an image can't be loaded, the avatar will show +/// a circle with a plain background using [backgroundColor] and the user's [initials]. +/// +/// The image fetched from [url] is not loaded again every time, but is rather cached to reduce network usage. +/// +/// See also: +/// +/// * [ZdsAvatar], an avatar used to display an image that is stored locally, or just the initials. +/// * [CachedNetworkImage], which this avatar uses to not have to download the image every time. +class ZdsNetworkAvatar extends StatelessWidget implements PreferredSizeWidget { + /// The URL from which to fetch the image. + final String url; + + /// Fallback text shown while the image is loading, or if the image couldn't be loaded. + /// + /// The image will always get priority. + final String initials; + + /// A function called whenever the user taps on this avatar. + final VoidCallback? onTap; + + /// The avatar's size. + /// + /// Must be greater than 0. Defaults to 48 dp. + final double? size; + + /// The textStyle used for the [initials], if shown. + /// + /// Defaults to [TextTheme.displaySmall]. + final TextStyle? textStyle; + + /// The background color of this avatar if the [initials] are shown. + /// + /// Defaults to [ColorScheme.secondary]. + final Color? backgroundColor; + + /// Semantic label for description of image + final String? semanticLabelAvatarDescription; + + /// It the key which will be used in the CachedNetworkImage as cachekey for refreshing image. + final String? imgCacheKey; + + /// Optional headers for the http request of the image url. + final Map? headers; + + /// How to inscribe the image into the space allocated during layout. + final BoxFit? fit; + + /// An avatar that gets its image from an URL. + const ZdsNetworkAvatar({ + required this.initials, + this.url = '', + this.onTap, + this.size, + this.textStyle, + this.backgroundColor, + this.semanticLabelAvatarDescription, + this.imgCacheKey, + this.headers, + this.fit, + super.key, + }) : assert(size != null ? size >= 0 : size == null, 'Size must be greater than 0'); + + @override + Widget build(BuildContext context) { + final initialsWidget = Center( + child: Text( + initials, + textScaleFactor: 1, + style: textStyle ?? + Theme.of(context).textTheme.bodyLarge?.copyWith( + color: calculateTextColor(backgroundColor ?? Theme.of(context).colorScheme.secondary), + ), + ), + ); + + return GestureDetector( + onTap: onTap, + child: Container( + height: size ?? preferredSize.height, + width: size ?? preferredSize.width, + decoration: BoxDecoration( + color: backgroundColor ?? Theme.of(context).colorScheme.secondary, + shape: BoxShape.circle, + ), + child: Semantics( + label: semanticLabelAvatarDescription, + child: ExcludeSemantics( + child: Uri.tryParse(url)?.hasAbsolutePath ?? false + ? CachedNetworkImage( + cacheKey: imgCacheKey, + imageBuilder: (context, imageProvider) { + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage(image: imageProvider, fit: fit), + ), + ); + }, + imageUrl: url, + httpHeaders: headers, + placeholder: (context, _) => initialsWidget, + errorWidget: (context, _, error) => initialsWidget, + ) + : initialsWidget, + ), + ), + ), + ); + } + + @override + Size get preferredSize => const Size(48, 48); + + /// Sets the default text color based on the background color. + Color calculateTextColor(Color background) { + return ThemeData.estimateBrightnessForColor(background) == Brightness.light ? ZdsColors.darkGrey : ZdsColors.white; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('url', url)); + properties.add(StringProperty('initials', initials)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DoubleProperty('size', size)); + properties.add(DiagnosticsProperty('textStyle', textStyle)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(StringProperty('semanticLabelAvatarDescription', semanticLabelAvatarDescription)); + properties.add(StringProperty('imgCacheKey', imgCacheKey)); + properties.add(DiagnosticsProperty?>('headers', headers)); + properties.add(EnumProperty('fit', fit)); + } +} diff --git a/lib/src/components/molecules/resizeable_text_area.dart b/lib/src/components/molecules/resizeable_text_area.dart new file mode 100644 index 0000000..217cd3f --- /dev/null +++ b/lib/src/components/molecules/resizeable_text_area.dart @@ -0,0 +1,212 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A text input area with handle icon in the bottom right corner that can be used to resize the typing input area. +class ZdsResizableTextArea extends StatefulWidget { + /// The [textInputAction]'s input action button of keyboard of the text field/box + /// + /// Defaults to [TextInputAction.none]. + final TextInputAction textInputAction; + + /// The [height]'s height of the text field/box + /// + /// Defaults to 100. + final double height; + + /// This is maximum height of the text field/box + final double maxHeight; + + /// The minimum height of the text field. + /// + /// Defaults to 48 (1 line) + final double minHeight; + + /// Hint text that will appear when the user hasn't written anything inside text area + final String? hintText; + + /// The [label] that will be shown at the above of text area. + final String? label; + + /// The [semanticLabel] that will be shown at the above of text area. + final String? semanticLabel; + + /// This is the maximum number of lines that user can enter inside text area + final int? maxLines; + + // The text style used for the text being edited. + /// + /// Defaults to [TextTheme.bodyMedium]. + final TextStyle? textStyle; + + /// A controller that can be used to notify listeners when the text changes. + final TextEditingController? controller; + + /// Callback called whenever the resizable textarea's text changes. + final ValueChanged? onChanged; + + /// The focus node given to the text area. + final FocusNode? focusNode; + + /// Whether the text is disabled. + /// + /// Defaults to true (editable). + final bool enabled; + + /// shown at the bottom of the text editor + final String? footerText; + + /// Input decoration used for underlying TextField. + final InputDecoration? decoration; + + /// Constructs a [ZdsResizableTextArea]. + const ZdsResizableTextArea({ + super.key, + this.textInputAction = TextInputAction.none, + this.hintText, + this.label, + this.maxLines, + this.height = 100, + this.maxHeight = double.infinity, + this.minHeight = 48, + this.textStyle, + this.controller, + this.onChanged, + this.focusNode, + this.enabled = true, + this.footerText, + this.decoration, + this.semanticLabel, + }); + + @override + State createState() => _ZdsResizableTextAreaState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('textInputAction', textInputAction)); + properties.add(DoubleProperty('height', height)); + properties.add(DoubleProperty('maxHeight', maxHeight)); + properties.add(DoubleProperty('minHeight', minHeight)); + properties.add(StringProperty('hintText', hintText)); + properties.add(StringProperty('label', label)); + properties.add(StringProperty('semanticLabel', semanticLabel)); + properties.add(IntProperty('maxLines', maxLines)); + properties.add(DiagnosticsProperty('textStyle', textStyle)); + properties.add(DiagnosticsProperty('controller', controller)); + properties.add(ObjectFlagProperty?>.has('onChanged', onChanged)); + properties.add(DiagnosticsProperty('focusNode', focusNode)); + properties.add(DiagnosticsProperty('enabled', enabled)); + properties.add(StringProperty('footerText', footerText)); + properties.add(DiagnosticsProperty('decoration', decoration)); + } +} + +class _ZdsResizableTextAreaState extends State { + late double _height; + final TextEditingController _textEditingController = TextEditingController(); + final GlobalKey key = GlobalKey(); + + TextEditingController get textEditingController { + return widget.controller ?? _textEditingController; + } + + @override + void initState() { + super.initState(); + _height = widget.height; + } + + @override + void dispose() { + textEditingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Semantics( + textField: true, + readOnly: !widget.enabled, + onTap: () => widget.focusNode?.requestFocus(), + excludeSemantics: true, + label: '${widget.label ?? widget.semanticLabel} ${textEditingController.text}', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + SizedBox( + height: _height, + child: TextField( + textInputAction: widget.textInputAction, + style: widget.textStyle ?? Theme.of(context).textTheme.bodyMedium, + decoration: widget.decoration ?? + ZdsInputDecoration( + labelText: widget.label, + hintText: widget.hintText, + fillColor: !widget.enabled ? ZdsColors.greySwatch(context).shade300 : null, + filled: !widget.enabled, + ), + maxLines: widget.maxLines, + controller: textEditingController, + onChanged: widget.onChanged, + focusNode: widget.focusNode, + enabled: widget.enabled, + key: key, + ), + ), + Positioned( + bottom: 12, + right: 4, + child: GestureDetector( + child: Container( + alignment: Alignment.bottomRight, + height: 48, + width: 48, + child: Icon( + ZdsIcons.expand, + size: 18, + color: ZdsColors.greySwatch(context).shade800, + ).padding(4), + ), + onVerticalDragUpdate: (details) { + setState(() { + _height += details.delta.dy; + // prevent overflow if height is more/less than available space + // Min height is 1 line + final minLimit = (widget.minHeight / 2) * MediaQuery.of(context).devicePixelRatio + 24; + if (_height > widget.maxHeight) { + _height = widget.maxHeight; + } else if (_height < minLimit) { + _height = minLimit; + } + }); + }, + ), + ), + ], + ), + if (widget.footerText != null) + Transform.translate( + offset: const Offset(0, -8), + child: Text( + widget.footerText!, + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: ZdsColors.greySwatch(context)[1000]), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty>>('key', key)); + properties.add(DiagnosticsProperty('textEditingController', textEditingController)); + } +} diff --git a/lib/src/components/molecules/responsive_tab_bar.dart b/lib/src/components/molecules/responsive_tab_bar.dart new file mode 100644 index 0000000..99161b2 --- /dev/null +++ b/lib/src/components/molecules/responsive_tab_bar.dart @@ -0,0 +1,158 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A TabBar that solves certain issues with Material's TabBar. When using isScrollable is set to true in TabBar, the +/// tabs will not fit the device's width. When isScrollable is set to false, the tabs all have the same size and do not +/// change size to fit their children. This makes it awkward for certain situations where a non-scrollable tab bar is +/// appropriate for certain big devices but not small ones, and where scrollable tab bars do not cover the entire width +/// in big devices. +/// +/// This widget solves this issue and makes the tabbar have responsive tab sizes and, if the tabbar does not need to be +/// scrollable, acts as a non-scrollable tab bar fitting exactly the device width. This is done automatically, without +/// needing to specify which tabbar version to use. +/// +/// Typically used with a [TabController] and a [TabBarView] +/// ```dart +/// return DefaultTabController( +/// length: 3, +/// child: Column( +/// children: [ +/// ZdsResponsiveTabBar(tabs: [ZdsTab(), ZdsTab(),ZdsTab(),],), +/// const Expanded( +/// child: TabBarView(children: [View1(), View2(), View3()],), +/// ), +/// ], +/// ), +/// ); +/// ``` +/// +/// If a [controller] is not provided, then a [DefaultTabController] ancestor must be provided instead. The tab +/// controller's [TabController.length] must equal the length of the [tabs] list and the length of the +/// [TabBarView.children] list. +/// +/// See also: +/// * [ZdsTab], which defines the individual tabs to be used in this tab bar. +class ZdsResponsiveTabBar extends StatelessWidget { + /// Constructs a [ZdsResponsiveTabBar]. + const ZdsResponsiveTabBar({ + required this.tabs, + super.key, + this.controller, + this.color = ZdsTabBarColor.surface, + this.selectedLabelFontWeight, + this.topSafeArea = true, + this.indicatorColor, + this.selectedTextColor, + this.textColor, + }); + + /// The list of tabs that will be displayed in this TabBar. The length of this list must match the [controller] + /// [TabController.length] and the length of the [TabBarView.children] list. + final List tabs; + + /// An optional controller. If this is not provided, a [DefaultTabController] ancestor must be provided instead. + final TabController? controller; + + /// The font weight of the label which has a textTheme of [TextTheme.bodyLarge]. + final FontWeight? selectedLabelFontWeight; + + /// Sets the color scheme for each of the tabs and the tab bar itself. + final ZdsTabBarColor color; + + /// Custom color for selected tab text. + /// + /// Overrides [color]. + /// + /// This will be deprecated in future release, as the value should be autogenerated based on the [color]. + final Color? selectedTextColor; + + /// Custom color for tab text. + /// + /// Overrides [color]. + /// + /// This will be deprecated in future release, as the value should be autogenerated based on the [color]. + final Color? textColor; + + /// Indicator color. + /// + /// Overrides [color]. + /// + /// This will be deprecated in future release, as the value should be autogenerated based on the [color]. + final Color? indicatorColor; + + /// Determine's whether component observes safe area of screen. + /// + /// See also: + /// * [SafeArea]. + final bool topSafeArea; + + @override + Widget build(BuildContext context) { + final int numberOfTabs = tabs.length; + final appBar = context.findAncestorWidgetOfExactType(); + + final customThemeContainer = Theme.of(context).zdsTabBarThemeData( + context, + hasIcons: hasIcons(tabs), + )[appBar != null ? appBar.color : color]!; + final customTheme = customThemeContainer.customTheme; + + return Container( + color: (customThemeContainer.customTheme.decoration as BoxDecoration).color, + child: SafeArea( + bottom: false, + top: topSafeArea, + child: LayoutBuilder( + builder: (context, constraints) { + // 16*2 refers to kTabLabelPadding (on both sides of the label) + var minWidth = constraints.maxWidth / numberOfTabs - (16 * 2); + minWidth = minWidth > 0 ? minWidth : 12; + + return Container( + height: customTheme.height + 1, + decoration: customTheme.decoration, + child: Theme( + data: customThemeContainer.theme, + child: TabBar( + controller: controller, + indicatorWeight: 5, + isScrollable: true, + labelStyle: + (hasIcons(tabs) ? Theme.of(context).textTheme.bodySmall : Theme.of(context).textTheme.bodyLarge) + ?.copyWith( + fontWeight: selectedLabelFontWeight, + ), + labelColor: selectedTextColor, + unselectedLabelColor: textColor, + indicatorColor: indicatorColor, + tabs: [ + for (final tab in tabs) + ConstrainedBox( + constraints: BoxConstraints(minWidth: minWidth), + child: tab, + ), + ], + ), + ), + ); + }, + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('selectedLabelFontWeight', selectedLabelFontWeight)) + ..add(EnumProperty('color', color)) + ..add(DiagnosticsProperty('topSafeArea', topSafeArea)) + ..add(ColorProperty('selectedTextColor', selectedTextColor)) + ..add(ColorProperty('textColor', textColor)) + ..add(ColorProperty('indicatorColor', indicatorColor)); + } +} diff --git a/lib/src/components/molecules/search.dart b/lib/src/components/molecules/search.dart new file mode 100644 index 0000000..c59b93b --- /dev/null +++ b/lib/src/components/molecules/search.dart @@ -0,0 +1,136 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Variants of [ZdsSearchField]. +enum ZdsSearchFieldVariant { + /// Creates a Search field with a border on all edges of color [ZetaColors.warm.60]. + outlined, + + /// Creates a Search field with a box shadow around the edges with a radius of 4. + /// + /// Shadow color defaults to [CardTheme.shadowColor] which can be changed in [ThemeData]. + elevated, +} + +/// A search field that can be used in a Form. +/// +/// This component can be used to update search results in two ways. [onChange] can be used to update the search +/// results as the user types their query without needing to be submitted through the press of a button. If the search +/// requires a lookup that will take time, you can instead only use [onSubmit] to only query results when the user +/// presses the search button on their keyboard. +/// +/// See also: +/// +/// * [ZdsSearchAppBar], an appBar that uses this component. +/// * [ZdsEmpty], which can be used to show a no results message. +class ZdsSearchField extends StatelessWidget { + /// The Key to use for the underlying [TextFormField]. + final Key? textFormFieldKey; + + /// Which variant to use. + /// + /// Defaults to [ZdsSearchFieldVariant.elevated]. + final ZdsSearchFieldVariant variant; + + /// Optional pre-filled text. + /// + /// If null, [hintText] will be shown instead. + final String? initValue; + + /// Hint text that will appear when the user hasn't written anything in the search field. + final String? hintText; + + /// Callback called whenever the search field's text changes. + final void Function(String value)? onChange; + + /// Callback called whenever the user presses the 'Search' button on their keyboard. + final void Function(String value)? onSubmit; + + /// The focusNode for the search field. + final FocusNode? focusNode; + + /// Empty space to inscribe around this widget. + /// + /// Defaults to EdgeInsets.all(12). + final EdgeInsets padding; + + /// A widget to show at the end of the search field, after the editable text area. + /// + /// Typically an [Icon] or an [IconButton]. + final Widget? suffixIcon; + + /// The input action that will be shown on the keyboard when the user interacts with this search field. + /// + /// Defaults to [TextInputAction.search]. + final TextInputAction inputAction; + + /// A controller that can be used to notify listeners when the text changes. + final TextEditingController? controller; + + /// A search field that can be used in a form. + const ZdsSearchField({ + super.key, + this.textFormFieldKey, + this.variant = ZdsSearchFieldVariant.elevated, + this.onChange, + this.initValue, + this.hintText, + this.onSubmit, + this.focusNode, + this.padding = const EdgeInsets.all(12), + this.suffixIcon, + this.controller, + this.inputAction = TextInputAction.search, + }); + + @override + Widget build(BuildContext context) { + final defaultTheme = Theme.of(context); + final effectiveTheme = defaultTheme.zdsSearchThemeData[variant] ?? defaultTheme; + return Theme( + data: effectiveTheme, + child: Padding( + padding: padding, + child: ZdsCard( + padding: EdgeInsets.zero, + backgroundColor: effectiveTheme.colorScheme.surface, + child: TextFormField( + style: effectiveTheme.textTheme.bodyLarge?.copyWith(color: effectiveTheme.colorScheme.onSurface), + autocorrect: false, + textInputAction: inputAction, + key: textFormFieldKey, + focusNode: focusNode, + controller: controller, + initialValue: controller == null ? initValue : null, + onChanged: onChange, + onFieldSubmitted: onSubmit, + decoration: InputDecoration( + constraints: const BoxConstraints(minHeight: 48, minWidth: 48), + hintText: hintText, + prefixIcon: effectiveTheme.prefixIcon, + contentPadding: EdgeInsets.zero, + suffixIcon: suffixIcon, + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('textFormFieldKey', textFormFieldKey)); + properties.add(EnumProperty('variant', variant)); + properties.add(StringProperty('initValue', initValue)); + properties.add(StringProperty('hintText', hintText)); + properties.add(ObjectFlagProperty.has('onChange', onChange)); + properties.add(ObjectFlagProperty.has('onSubmit', onSubmit)); + properties.add(DiagnosticsProperty('focusNode', focusNode)); + properties.add(DiagnosticsProperty('padding', padding)); + properties.add(EnumProperty('inputAction', inputAction)); + properties.add(DiagnosticsProperty('controller', controller)); + } +} diff --git a/lib/src/components/molecules/selectable_list_tile.dart b/lib/src/components/molecules/selectable_list_tile.dart new file mode 100644 index 0000000..baa9f8e --- /dev/null +++ b/lib/src/components/molecules/selectable_list_tile.dart @@ -0,0 +1,165 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A list tile with circular edges that can be toggled between a selected and unselected state. +/// +/// There are two variants: +/// * [ZdsSelectableListTile], which returns a list tile that changes its color when selected. +/// When a listTile is marked as selected, its background will be colored with the [Theme.colorScheme.secondary] at 10% +/// opacity. When it's unselected, the background will use [Theme.colorScheme.surface] +/// * [ZdsSelectableListTile.checkable], which returns a list tile that adds a check at the end of the tile when selected. +/// +/// This widget does not manage its own state, but should rather be rebuilt by the parent widget's state through +/// [onTap]. +class ZdsSelectableListTile extends StatelessWidget { + /// A widget shown before the title. + /// + /// Usually an indicator of whether the tile is selected or not. + final Widget? leading; + + /// The tile's title. + /// + /// Typically a [Text]. + final Widget? title; + + /// The tile's subTitle. + /// + /// Typically a [Text]. + final Widget? subTitle; + + /// A widget shown at the end of the tile. + /// + /// Usually an indicator of whether the tile is selected or not. + final Widget? trailing; + + /// Whether the tile is selected. + /// + /// Defaults to false. + final bool selected; + + /// A function called whenever the user taps on this tile. + /// + /// Used to set the parent's state and update [selected]. + final VoidCallback? onTap; + + final bool _checkable; + + /// this is for talk back text + final String? semanticLabel; + + /// A tile with rounded edges that can be toggled as selected or unselected. + const ZdsSelectableListTile({ + super.key, + this.leading, + this.title, + this.subTitle, + this.trailing, + this.onTap, + this.selected = false, + this.semanticLabel, + }) : _checkable = false; + + /// A tile with rounded edges that can be toggled as selected or unselected and shows a check icon when selected. + const ZdsSelectableListTile.checkable({ + super.key, + this.leading, + this.title, + this.subTitle, + this.selected = false, + this.onTap, + this.semanticLabel, + }) : trailing = const Icon(ZdsIcons.check), + _checkable = true; + + @override + Widget build(BuildContext context) { + const padding = EdgeInsets.symmetric(horizontal: 24, vertical: 12); + const innerPadding = EdgeInsets.symmetric(horizontal: 14, vertical: 12); + + final showSelected = (_checkable && selected) || (!_checkable && selected); + + return IconTheme( + data: Theme.of(context).iconTheme.copyWith(size: 24, color: Theme.of(context).colorScheme.secondary), + child: Padding( + padding: kZdsSelectableListTilePadding, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(kZdsSelectableListTileBorderRadius)), + child: Material( + color: showSelected + ? Theme.of(context).colorScheme.secondary.withOpacity(0.1) + : Theme.of(context).colorScheme.surface, + child: DecoratedBox( + decoration: BoxDecoration( + border: showSelected + ? Border.all(color: Theme.of(context).colorScheme.secondary) + : Border.all(color: ZdsColors.lightGrey), + borderRadius: const BorderRadius.all(Radius.circular(kZdsSelectableListTileBorderRadius)), + ), + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(kZdsSelectableListTileBorderRadius)), + onTap: onTap, + child: Semantics( + selected: selected, + enabled: onTap != null, + onTap: onTap, + label: semanticLabel, + excludeSemantics: semanticLabel != null, + child: SizedBox( + width: double.infinity, + child: Padding( + padding: padding.copyWith( + top: 0, + left: leading == null ? 0 : null, + right: trailing == null ? 0 : null, + bottom: 0, + ), + child: Row( + children: [ + if (leading != null) leading!, + if (title != null) + Expanded( + child: Padding( + padding: padding.copyWith( + left: leading != null ? innerPadding.left : null, + right: trailing != null ? innerPadding.right : null, + top: subTitle != null ? 14 : innerPadding.top, + bottom: subTitle != null ? 14 : innerPadding.bottom, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title!, + Container(child: subTitle).textStyle( + Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: ZdsColors.blueGrey, fontSize: 12), + ), + ], + ), + ).textStyle(Theme.of(context).textTheme.titleSmall), + ), + if ((trailing != null && !_checkable) || (_checkable && selected)) trailing!, + ], + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('selected', selected)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(StringProperty('semanticLabel', semanticLabel)); + } +} diff --git a/lib/src/components/molecules/sheet_header.dart b/lib/src/components/molecules/sheet_header.dart new file mode 100644 index 0000000..73b5da0 --- /dev/null +++ b/lib/src/components/molecules/sheet_header.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; + +import '../../../zds_flutter.dart'; + +/// Creates the header component for a bottom sheet with Zds style. +class ZdsSheetHeader extends StatelessWidget implements PreferredSizeWidget { + static const _kSheetHeight = 54.0; + + /// Sheet header title of type [String]. + final String headerText; + + /// The Widget that is displayed before the title, typically an [IconButton] widget. + final Widget? leading; + + /// The widget that is displayed after the title, typically a clickable widget. + final Widget? trailing; + + /// Indicates whether the application is busy or not. + /// + /// The default value is false. + final bool busy; + + /// The text style for the sheet header + /// + /// Defaults to [TextTheme.headlineMedium]. + final TextStyle? headerTextStyle; + + /// Constructs a [ZdsSheetHeader]. + const ZdsSheetHeader({ + required this.headerText, + super.key, + this.leading, + this.trailing, + this.busy = false, + this.headerTextStyle, + }); + + @override + Widget build(BuildContext context) { + return Semantics( + child: Container( + color: Theme.of(context).colorScheme.surface, + height: _kSheetHeight, + width: double.infinity, + child: Material( + color: Colors.transparent, + child: SafeArea( + top: false, + bottom: false, + child: Stack( + children: [ + Center( + child: Text( + headerText, + style: headerTextStyle ?? Theme.of(context).textTheme.titleLarge, + overflow: TextOverflow.ellipsis, + textScaleFactor: MediaQuery.of(context).textScaleFactor > 2 ? 2 : null, + ), + ).paddingOnly(bottom: 5), + if (leading != null) + leading.runtimeType == IconButton + ? Padding( + padding: EdgeInsets.only(left: context.isTablet() ? 0 : 16), + child: Material( + shape: const CircleBorder(), + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: Semantics( + sortKey: const OrdinalSortKey(1), + child: IconTheme( + data: Theme.of(context).iconTheme.copyWith( + color: ZdsColors.greySwatch(context)[1000], + size: 24, + ), + child: leading!, + ), + ), + ), + ) + : Semantics( + sortKey: const OrdinalSortKey(1), + child: SizedBox(height: _kSheetHeight, child: UnconstrainedBox(child: leading)), + ).paddingOnly(left: 10), + if (trailing != null) + Positioned( + right: 8, + child: Semantics( + sortKey: const OrdinalSortKey(2), + child: SizedBox(height: _kSheetHeight, child: UnconstrainedBox(child: trailing)), + ), + ), + ], + ), + ), + ), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(_kSheetHeight); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('headerText', headerText)); + properties.add(DiagnosticsProperty('busy', busy)); + properties.add(DiagnosticsProperty('headerTextStyle', headerTextStyle)); + } +} diff --git a/lib/src/components/molecules/slidable_button.dart b/lib/src/components/molecules/slidable_button.dart new file mode 100644 index 0000000..74c309d --- /dev/null +++ b/lib/src/components/molecules/slidable_button.dart @@ -0,0 +1,307 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +// TODO(colors): Fix colors on track. + +/// A SlidableButton with pre-applied Zds styling. +/// This grows to the width of its container. +class ZdsSlidableButton extends StatefulWidget { + /// The future that is called when the button has been slid. + /// While it is being awaited, a loading indicator will show. + /// If the future returns true, the text and colors will be switched out for their 'end' equivalents + /// If included the button is active, if not it will be disabled. + final Future Function()? onSlideComplete; + + /// Called when the handle is tapped. + final void Function()? onTapDown; + + /// Button background color. + final Color? buttonSliderColor; + + /// Button color default. + /// + /// Defaults to `colors.primary`. + final Color? buttonColor; + + /// Button center text. + final String? buttonText; + + /// Button Icon when toggled. + final IconData? buttonIcon; + + /// Button background color when toggled. + final Color? buttonSliderColorEnd; + + /// Button color when toggled default. + /// + /// Defaults to `buttonColor.withOpacity(0.5)`, or if [buttonColor] is null, `colors.subtle`. + final Color? buttonColorEnd; + + /// Button center text when toggled. + final String? buttonTextEnd; + + /// Button Icon when toggled. + final IconData? buttonIconEnd; + + /// Button animation after slide, this is defaulted to false for no animation. + final bool animate; + + /// Circle indicator color. + final Animation? indicatorColor; + + /// Keeps the toggle at one end after completion. + final bool stayCompleted; + + /// The duration to show the completed widget before the toggle resets. + /// Will not do anything if [stayCompleted] is set to true. + final Duration completedDisplayDuration; + + /// The message to be displayed when the button is disabled. + /// Only shows if [onSlideComplete] is null. + final String? disabledMessage; + + /// The height of the button. + final double height; + + /// The width of the button handle. + final double handleWidth; + + /// Base color used for button. + final ZetaColorSwatch? colors; + + /// A button slider with a [ZdsSlidableWidget] as a child, the [ZdsSlidableWidget] is the slidable element. + const ZdsSlidableButton({ + this.buttonText, + this.buttonIcon, + this.buttonSliderColor, + this.buttonColor, + this.onTapDown, + this.onSlideComplete, + this.animate = true, + this.indicatorColor = const AlwaysStoppedAnimation(Colors.white), + this.buttonSliderColorEnd, + this.buttonColorEnd, + this.buttonTextEnd, + this.buttonIconEnd, + this.stayCompleted = false, + this.disabledMessage, + this.completedDisplayDuration = const Duration(seconds: 2), + this.handleWidth = 160, + this.height = 64, + this.colors, + super.key, + }); + + @override + ZdsSlidableButtonState createState() => ZdsSlidableButtonState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty Function()?>.has('onSlideComplete', onSlideComplete)); + properties.add(DiagnosticsProperty('onTapDown', onTapDown)); + properties.add(ColorProperty('buttonSliderColor', buttonSliderColor)); + properties.add(ColorProperty('buttonColor', buttonColor)); + properties.add(StringProperty('buttonText', buttonText)); + properties.add(DiagnosticsProperty('buttonIcon', buttonIcon)); + properties.add(ColorProperty('buttonSliderColorEnd', buttonSliderColorEnd)); + properties.add(ColorProperty('buttonColorEnd', buttonColorEnd)); + properties.add(StringProperty('buttonTextEnd', buttonTextEnd)); + properties.add(DiagnosticsProperty('buttonIconEnd', buttonIconEnd)); + properties.add(DiagnosticsProperty('animate', animate)); + properties.add(DiagnosticsProperty?>('indicatorColor', indicatorColor)); + properties.add(DiagnosticsProperty('stayCompleted', stayCompleted)); + properties.add(DiagnosticsProperty('completedDisplayDuration', completedDisplayDuration)); + properties.add(StringProperty('disabledMessage', disabledMessage)); + properties.add(DoubleProperty('height', height)); + properties.add(DoubleProperty('handleWidth', handleWidth)); + properties.add(ColorProperty('colors', colors)); + } +} + +/// State for [ZdsSlidableButton]. +class ZdsSlidableButtonState extends State { + bool _isComplete = false; + bool _isLoading = false; + bool _isTapedDown = false; + + final GlobalKey _slideableKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + Future _onSlideComplete() async { + if (widget.onSlideComplete != null) { + setState(() { + _isLoading = true; + }); + final isSuccessful = await widget.onSlideComplete!.call(); + if (mounted) { + setState(() { + _isLoading = false; + _isTapedDown = false; + }); + } + if (isSuccessful) { + setState(() { + _isComplete = true; + }); + if (!widget.stayCompleted) { + await Future.delayed(widget.completedDisplayDuration); + } + } + } + if (!widget.stayCompleted) reset(); + } + + /// Resets the slider. + void reset() { + if (_slideableKey.currentWidget is ZdsSlidableWidgetState) { + (_slideableKey.currentState! as ZdsSlidableWidgetState).reset(); + setState(() { + _isTapedDown = false; + _isComplete = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final ZetaColorSwatch colors = widget.colors ?? ZetaColors.of(context).primary; + + final Color buttonSliderColorEnd = widget.buttonSliderColorEnd ?? colors.shade20; + final Color buttonSliderColor = widget.buttonSliderColorEnd ?? colors.shade20; + final Color buttonColor = widget.buttonColor ?? colors.primary; + final Color buttonColorComplete = widget.buttonColor?.withOpacity(0.5) ?? colors.subtle; + + final showDisabledMessage = widget.disabledMessage != null && widget.onSlideComplete == null; + return Wrap( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: widget.height, + decoration: BoxDecoration( + color: showDisabledMessage + ? null + : widget.buttonSliderColorEnd != null && _isComplete + ? buttonSliderColorEnd + : buttonSliderColor, + borderRadius: BorderRadius.circular(widget.height), + ), + child: Stack( + children: [ + ZdsSlidableWidget( + key: _slideableKey, + animate: widget.animate, + isActive: widget.onSlideComplete != null && !_isLoading && !_isComplete, + height: widget.height, + onTapDown: () { + widget.onTapDown?.call(); + setState(() { + _isTapedDown = true; + }); + }, + onTapUp: () { + setState(() { + _isTapedDown = false; + }); + }, + slidePercentageNeeded: 0.55, + handleWidth: widget.handleWidth, + stayCompleted: widget.stayCompleted, + onSlide: _onSlideComplete, + child: SizedBox( + height: widget.height, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Material( + elevation: 2, + borderRadius: BorderRadius.all(Radius.circular(widget.height)), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: (_isLoading || _isTapedDown || _isComplete) && + widget.animate && + widget.buttonColorEnd != null + ? widget.buttonColorEnd + : widget.onSlideComplete != null + ? buttonColor + : buttonColorComplete, + borderRadius: BorderRadius.all(Radius.circular(widget.height)), + ), + height: widget.height, + width: widget.handleWidth, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: _isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + valueColor: widget.indicatorColor, + strokeWidth: 2, + ), + ) + : Icon( + _isComplete && widget.animate + ? widget.buttonIconEnd ?? ZdsIcons.check_circle + : widget.buttonIcon, + color: widget.colors != null ? widget.colors?.on : ZdsColors.white, + size: 20, + ), + ), + Text( + ((_isComplete || _isTapedDown || _isLoading) && widget.buttonTextEnd != null + ? widget.buttonTextEnd! + : widget.buttonText) ?? + '', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + color: () { + return widget.buttonColor != null + ? computeForeground(widget.buttonColor!) + : colors.on; + }(), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + if (showDisabledMessage) + Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: 320 - widget.handleWidth, + child: Row( + children: [ + Expanded( + child: Text( + widget.disabledMessage!, + style: Theme.of(context).textTheme.labelMedium, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/src/components/molecules/slidable_list_tile.dart b/lib/src/components/molecules/slidable_list_tile.dart new file mode 100644 index 0000000..1be7ee5 --- /dev/null +++ b/lib/src/components/molecules/slidable_list_tile.dart @@ -0,0 +1,512 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; + +import 'package:flutter_slidable/flutter_slidable.dart'; + +/// A widget that creates a slidable list tile, which can be slid right-to-left to reveal further actions. +/// Takes a [child] which can be any widget, although a [Row] is recommended to use like so: +/// ```dart +/// ZdsSlidableListTile( +/// child: Row( +/// children: [ +/// LeadingWidget(), +/// Spacer(), +/// TrailingWidget() +/// ], +/// ), +/// ); +/// ``` +/// +/// Although this widget has predefined styling, it can be adjusted with [backgroundColor] and [slideButtonWidth]. +/// This is recommended for example to indicate state change. +/// +/// It is also possible that the actions text will not fit within the default action width and will be ellipsized, in +/// which case you can increase [slideButtonWidth]. [slideButtonWidth] * [actions].length must not exceed [width]. +/// +/// If we don't want the slide actions to be shown (i.e. the tile is in a selected state), then [slideEnabled] +/// must be set to false. This will not allow the user to slide to see more actions, but they will still be able to +/// interact with the tile's main contents. To disable the user interacting with the tile, you can set the [onTap] to +/// null. +/// +/// This widget's actions are defined through [ZdsSlidableAction]. +class ZdsSlidableListTile extends StatelessWidget { + /// A tile that can be slid to reveal further actions. + /// + /// [width], [child], and [actions] are required and can't be null. + /// [slideButtonWidth] and [slideEnabled] can't be null. + const ZdsSlidableListTile({ + required this.width, + required this.child, + super.key, + this.actions, + this.leadingActions, + this.backgroundColor, + this.slideButtonWidth = 100, + this.minHeight = 80, + this.onTap, + this.slideEnabled = true, + this.semanticDescription, + }) : assert(actions == null || slideButtonWidth * actions.length <= width, ''); + + /// The tile's main content. Usually a [Row] + final Widget child; + + /// The length of the tile. On vertical displays this usually is `MediaQuery.of(context).size.width`. + /// Must exceed or be equal to [slideButtonWidth] * [actions].length. + final double width; + + /// The actions that will be revealed when the user swipes left on the tile. Must not be empty for the slidable action to occur. + final List? actions; + + /// The actions that will be revealed when the user swipes right on the tile. Must not be empty for the slidable action to occur. + final List? leadingActions; + + /// The background color of the tile's main content. Defaults to [ColorScheme.surface] + final Color? backgroundColor; + + /// How wide each action button is. Text that does not fit this width will be ellipsized. + final double slideButtonWidth; + + /// Will be called whenever the user taps on the tile's main content. + final VoidCallback? onTap; + + /// Whether to show the slide actions. + final bool slideEnabled; + + /// Minimum height of the list tile. + /// + /// Defaults to 80. + final double minHeight; + + /// Description used as semantic label for the tile. + /// + /// Typically will contain all the text displayed on the card UI. + final String? semanticDescription; + + @override + Widget build(BuildContext context) { + final Map semanticActions = {}; + + for (final action in [...?actions, ...?leadingActions]) { + semanticActions[CustomSemanticsAction(label: action.label)] = () { + action.onPressed!(context); + }; + } + + return Semantics( + label: semanticDescription, + customSemanticsActions: semanticActions, + excludeSemantics: semanticDescription != null, + child: Slidable( + enabled: slideEnabled, + startActionPane: leadingActions != null && leadingActions!.isNotEmpty + ? ActionPane( + motion: const DrawerMotion(), + extentRatio: (slideButtonWidth * leadingActions!.length) / width, + children: [for (final action in leadingActions!) _ActionBuilder(action: action)], + ) + : null, + endActionPane: actions != null && actions!.isNotEmpty + ? ActionPane( + motion: const DrawerMotion(), + extentRatio: (slideButtonWidth * (actions!.length)) / width, + children: [for (final action in actions!) _ActionBuilder(action: action)], + ) + : null, + child: Card( + shape: const ContinuousRectangleBorder(), + color: backgroundColor ?? Theme.of(context).colorScheme.surface, + margin: EdgeInsets.zero, + child: InkWell( + onTap: onTap, + child: Container( + constraints: BoxConstraints(minHeight: minHeight), + width: MediaQuery.of(context).size.width, + child: child, + ), + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('width', width)); + properties.add(IterableProperty('actions', actions)); + properties.add(IterableProperty('leadingActions', leadingActions)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(DoubleProperty('slideButtonWidth', slideButtonWidth)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DiagnosticsProperty('slideEnabled', slideEnabled)); + properties.add(DoubleProperty('minHeight', minHeight)); + properties.add(StringProperty('semanticDescription', semanticDescription)); + } +} + +class _ActionBuilder extends StatefulWidget { + const _ActionBuilder({required this.action}); + final ZdsSlidableAction action; + + @override + State<_ActionBuilder> createState() => _ActionBuilderState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('action', action)); + } +} + +class _ActionBuilderState extends State<_ActionBuilder> { + final _key = GlobalKey(); + + Size _size = Size.zero; + Size get size => _size; + set size(Size size) { + if (_size == size) return; + setState(() { + _size = size; + }); + } + + @override + void initState() { + super.initState(); + unawaited( + WidgetsBinding.instance.endOfFrame.then( + (_) => size = _key.currentContext?.size ?? Size.zero, + ), + ); + } + + @override + Widget build(BuildContext context) { + return FlutterSlidableAction( + key: _key, + onPressed: widget.action.onPressed, + label: size.height < 60 && widget.action.icon != null ? null : widget.action.label, + icon: widget.action.icon, + backgroundColor: widget.action.backgroundColor ?? Theme.of(context).colorScheme.background, + foregroundColor: widget.action.foregroundColor ?? Theme.of(context).colorScheme.onBackground, + autoClose: widget.action.autoclose, + spacing: 16, + padding: EdgeInsets.zero, + textOverflow: widget.action.textOverflow, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('size', size)); + } +} + +/// Defines an action that will be shown when sliding on a ZdsSlidableListTile. +class ZdsSlidableAction { + /// Function called on press of the widget. + final void Function(BuildContext)? onPressed; + + /// The text that will be shown above the icon. It can't be empty. + final String label; + + /// An optional icon that will be shown below the label. + final IconData? icon; + + /// Background color of the widget. + /// + /// Should be visually different to the background color of the parent [ZdsSlidableListTile] and any other [ZdsSlidableAction]. + /// + /// If null, uses default background color. + final Color? backgroundColor; + + /// The label and icon color. + /// + /// If null, uses default text color. + final Color? foregroundColor; + + /// Whether to automatically close the actions after tapping on this action. Defaults to true. + final bool autoclose; + + ///{@macro slidable.actions.padding} + final EdgeInsets padding; + + /// Optional parameter to handle overflowing text + /// + /// if null, the default value is [TextOverflow.ellipsis] + final TextOverflow? textOverflow; + + /// Defines an action that will be shown when sliding on a ZdsSlidableListTile. + /// [label] must not be empty. + /// [backgroundColor], [foregroundColor], and [autoclose] must not be null + ZdsSlidableAction({ + required this.label, + this.onPressed, + this.icon, + this.backgroundColor, + this.foregroundColor, + this.autoclose = true, + this.padding = EdgeInsets.zero, + this.textOverflow, + }) : assert(label.isNotEmpty, 'Label must have content as it acts as the semantic button description'); +} + +// Modified version of SlidableAction from flutter_slidable package + +/// Signature for [CustomSlidableAction.onPressed]. +typedef SlidableActionCallback = void Function(BuildContext context); + +const int _kFlex = 1; +const Color _kBackgroundColor = Colors.white; +const bool _kAutoClose = true; + +/// Represents an action of an [ActionPane]. +class CustomSlidableAction extends StatelessWidget { + /// Creates a [CustomSlidableAction]. + /// + /// The [flex], [backgroundColor], [autoClose] and [child] arguments must not + /// be null. + /// + /// The [flex] argument must also be greater than 0. + const CustomSlidableAction({ + required this.onPressed, + required this.child, + this.flex = _kFlex, + this.backgroundColor = _kBackgroundColor, + this.foregroundColor, + this.autoClose = _kAutoClose, + this.borderRadius = BorderRadius.zero, + this.padding, + super.key, + }); + + /// {@template slidable.actions.flex} + /// The flex factor to use for this child. + /// + /// The amount of space the child's can occupy in the main axis is + /// determined by dividing the free space according to the flex factors of the + /// other [CustomSlidableAction]s. + /// {@endtemplate} + final int flex; + + /// {@template slidable.actions.backgroundColor} + /// The background color of this action. + /// + /// Defaults to [Colors.white]. + /// {@endtemplate} + final Color backgroundColor; + + /// {@template slidable.actions.foregroundColor} + /// The foreground color of this action. + /// + /// Defaults to [Colors.black] if [backgroundColor]'s brightness is + /// [Brightness.light], or to [Colors.white] if [backgroundColor]'s brightness is + /// [Brightness.dark]. + /// {@endtemplate} + final Color? foregroundColor; + + /// {@template slidable.actions.autoClose} + /// Whether the enclosing [Slidable] will be closed after [onPressed] + /// occurred. + /// {@endtemplate} + final bool autoClose; + + /// {@template slidable.actions.onPressed} + /// Called when the action is tapped or otherwise activated. + /// + /// If this callback is null, then the action will be disabled. + /// {@endtemplate} + final SlidableActionCallback? onPressed; + + /// {@template slidable.actions.borderRadius} + /// The borderRadius of this action + /// + /// Defaults to [BorderRadius.zero]. + /// {@endtemplate} + final BorderRadius borderRadius; + + /// {@template slidable.actions.padding} + /// The padding of the OutlinedButton + /// {@endtemplate} + final EdgeInsets? padding; + + /// Typically the action's icon or label. + final Widget child; + + @override + Widget build(BuildContext context) { + final effectiveForegroundColor = foregroundColor ?? + (ThemeData.estimateBrightnessForColor(backgroundColor) == Brightness.light ? Colors.black : Colors.white); + + return Expanded( + flex: flex, + child: SizedBox.expand( + child: OutlinedButton( + onPressed: () => _handleTap(context), + style: OutlinedButton.styleFrom( + padding: padding, + backgroundColor: backgroundColor, + foregroundColor: effectiveForegroundColor, + disabledForegroundColor: effectiveForegroundColor, + shape: RoundedRectangleBorder( + borderRadius: borderRadius, + ), + side: BorderSide.none, + ), + child: child, + ), + ), + ); + } + + void _handleTap(BuildContext context) { + onPressed?.call(context); + if (autoClose) { + unawaited(Slidable.of(context)?.close()); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('flex', flex)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(ColorProperty('foregroundColor', foregroundColor)); + properties.add(DiagnosticsProperty('autoClose', autoClose)); + properties.add(ObjectFlagProperty.has('onPressed', onPressed)); + properties.add(DiagnosticsProperty('borderRadius', borderRadius)); + properties.add(DiagnosticsProperty('padding', padding)); + } +} + +/// An action for [Slidable] which can show an icon, a label, or both. +class FlutterSlidableAction extends StatelessWidget { + /// The [flex], [backgroundColor], [autoClose] and [spacing] arguments + /// must not be null. + /// + /// You must set either an [icon] or a [label]. + /// + /// The [flex] argument must also be greater than 0. + const FlutterSlidableAction({ + required this.onPressed, + this.flex = _kFlex, + this.backgroundColor = _kBackgroundColor, + this.foregroundColor, + this.autoClose = _kAutoClose, + this.icon, + this.spacing = 4, + this.label, + this.borderRadius = BorderRadius.zero, + this.padding, + this.textOverflow, + super.key, + }); + + /// {@macro slidable.actions.flex} + final int flex; + + /// {@macro slidable.actions.backgroundColor} + final Color backgroundColor; + + /// {@macro slidable.actions.foregroundColor} + final Color? foregroundColor; + + /// {@macro slidable.actions.autoClose} + final bool autoClose; + + /// {@macro slidable.actions.onPressed} + final SlidableActionCallback? onPressed; + + /// An icon to display above the [label]. + final IconData? icon; + + /// The space between [icon] and [label] if both set. + /// + /// Defaults to 4. + final double spacing; + + /// A label to display below the [icon]. + final String? label; + + /// Padding of the OutlinedButton + final BorderRadius borderRadius; + + /// Padding of the OutlinedButton + final EdgeInsets? padding; + + /// Optional parameter to handle overflowing text + /// + /// if null, the default value is [TextOverflow.ellipsis] + final TextOverflow? textOverflow; + + @override + Widget build(BuildContext context) { + final children = []; + + if (icon != null) { + children.add( + Icon(icon), + ); + } + + if (label != null) { + if (children.isNotEmpty) { + children.add( + SizedBox(height: spacing), + ); + } + + children.add( + Text( + label!, + overflow: textOverflow ?? TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ); + } + + final child = children.length == 1 + ? children.first + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...children.map( + (child) => Flexible( + child: child, + ), + ), + ], + ); + + return CustomSlidableAction( + borderRadius: borderRadius, + padding: padding, + onPressed: onPressed, + autoClose: autoClose, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + flex: flex, + child: child, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('flex', flex)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(ColorProperty('foregroundColor', foregroundColor)); + properties.add(DiagnosticsProperty('autoClose', autoClose)); + properties.add(ObjectFlagProperty.has('onPressed', onPressed)); + properties.add(DiagnosticsProperty('icon', icon)); + properties.add(DoubleProperty('spacing', spacing)); + properties.add(StringProperty('label', label)); + properties.add(DiagnosticsProperty('borderRadius', borderRadius)); + properties.add(DiagnosticsProperty('padding', padding)); + properties.add(EnumProperty('textOverflow', textOverflow)); + } +} diff --git a/lib/src/components/molecules/stats_card.dart b/lib/src/components/molecules/stats_card.dart new file mode 100644 index 0000000..8f27a7c --- /dev/null +++ b/lib/src/components/molecules/stats_card.dart @@ -0,0 +1,302 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +enum _ListElement { first, middle, last } + +/// A card used to display value-key pairs. +/// +/// This is typically used to display key information that needs top be processed at a glance, shown in a table-like +/// format. +/// +/// See also: +/// +/// * [ZdsStat], used to define each statistic's properties. +class ZdsStatCard extends StatelessWidget { + static const double _padding = 16; + static const double _dividerWidth = 0.5; + + /// The stats that will be displayed in this card. + /// + /// Its length must be between 1 and 4. + final List stats; + + /// The title that will be shown at the top of the card. + /// + /// If not null, it can't be empty. + final String? title; + + /// The subtitle is the additional information, will be shown at the top-right side of the card. + final String? subtitle; + + /// Whether the stats card should be shown in a horizontal row, or a vertical column. + /// + /// Typical behavior is horizontal, and vertical is mainly used for accessability on smaller screens. + /// + /// By default, screen size will be used to determine if horizontal will fit, otherwise will display vertically. + final bool? isHorizontal; + + /// {@macro card-variant} + final ZdsCardVariant? cardVariant; + + /// Displays a card with [ZdsStat] values. + const ZdsStatCard({ + required this.stats, + this.subtitle, + super.key, + this.title, + this.isHorizontal, + this.cardVariant = ZdsCardVariant.elevated, + }) : assert(stats.length > 0 && stats.length < 5, 'Only 1 to 4 stats can be used.'), + assert(title?.length != 0, "Title can't be empty if not null."); + + bool _isVertical(BuildContext context, BoxConstraints constraints) { + final scale = MediaQuery.of(context).textScaleFactor; + final totalPadding = stats.length * 2 * _padding; + final totalDividers = _dividerWidth * 0.5 * (stats.length - 1); + final double width = ((totalDividers + totalPadding - constraints.maxWidth) / -stats.length) / scale; + for (int i = 0; i < stats.length; i++) { + final bool description = hasTextOverflow(stats[i].description, Theme.of(context).textTheme.bodySmall!, width); + final bool value = hasTextOverflow( + stats[i]._valueString, + Theme.of(context).textTheme.headlineSmall!.copyWith(fontSize: 28), + width, + ); + if (description || value) { + return true; + } + } + return false; + } + + double _columnWidth(BuildContext context) { + final List counter = []; + for (int i = 0; i < stats.length; i++) { + counter.add( + textWidth(stats[i]._valueString, Theme.of(context).textTheme.headlineSmall!.copyWith(fontSize: 28)), + ); + } + return counter.max; + } + + @override + Widget build(BuildContext context) { + final Color textColor = Theme.of(context).colorScheme.onSurface; + + return ZdsCard( + variant: cardVariant ?? ZdsCardVariant.elevated, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null || subtitle != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (title != null) + Expanded( + child: Text( + title ?? '', + style: Theme.of(context).textTheme.titleLarge?.copyWith(color: textColor), + ), + ) + else + const SizedBox(), + const SizedBox(width: _padding), + if (subtitle != null) + Expanded( + flex: title != null ? 1 : 0, + child: Text( + subtitle ?? '', + textAlign: TextAlign.end, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: ZdsColors.greySwatch(context)[1000], + ), + ), + ) + else + const SizedBox(), + ], + ).paddingOnly(bottom: 18), + LayoutBuilder( + builder: (context, constraints) { + final bool horizontal = isHorizontal ?? !_isVertical(context, constraints); + final double horWidth = !horizontal ? _columnWidth(context) : 0; + return horizontal + ? Row( + children: stats + .mapIndexed( + (index, stat) => _StatElement( + stat: stat, + width: (constraints.maxWidth / stats.length) - (_dividerWidth * (stats.length - 1)), + type: index == 0 + ? _ListElement.first + : index == stats.length - 1 + ? _ListElement.last + : _ListElement.middle, + ), + ) + .toList() + .divide(Container(color: ZdsColors.lightGrey, height: 43, width: _dividerWidth)) + .toList(), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: stats + .map( + (stat) => _HorizontalStatElement(statsList: stats, stat: stat, width: horWidth), + ) + .toList(), + ), + ], + ); + }, + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty('stats', stats)); + properties.add(StringProperty('title', title)); + properties.add(StringProperty('subtitle', subtitle)); + properties.add(DiagnosticsProperty('isHorizontal', isHorizontal)); + properties.add(DiagnosticsProperty('cardVariant', cardVariant)); + } +} + +class _HorizontalStatElement extends StatelessWidget { + final ZdsStat stat; + final List statsList; + final double width; + + const _HorizontalStatElement({ + required this.statsList, + required this.stat, + required this.width, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + width: width, + child: Text( + stat._valueString, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 28, + color: stat.color ?? Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.end, + ), + ), + const SizedBox(width: 16, height: 40), + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Text( + stat.description, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('stat', stat)); + properties.add(IterableProperty('statsList', statsList)); + properties.add(DoubleProperty('width', width)); + } +} + +class _StatElement extends StatelessWidget { + const _StatElement({required this.stat, required this.width, required this.type}); + + final ZdsStat stat; + final double width; + final _ListElement type; + + @override + Widget build(BuildContext context) { + return Container( + width: width, + padding: type == _ListElement.middle + ? const EdgeInsets.symmetric(horizontal: ZdsStatCard._padding) + : EdgeInsets.only( + left: type == _ListElement.first ? ZdsStatCard._padding / 2 : ZdsStatCard._padding, + right: type == _ListElement.last ? ZdsStatCard._padding / 2 : ZdsStatCard._padding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + stat._valueString, + style: Theme.of(context) + .textTheme + .headlineMedium! + .copyWith(fontSize: 28, color: stat.color ?? Theme.of(context).colorScheme.onSurface), + ), + const SizedBox(height: 2), + Text( + stat.description, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith(color: stat.color ?? Theme.of(context).colorScheme.onSurface), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('stat', stat)) + ..add(DoubleProperty('width', width)) + ..add(EnumProperty<_ListElement>('type', type)); + } +} + +/// A statistic definition to be used in [ZdsStatCard]. +/// +/// See also: +/// +/// * [ZdsStatCard], used to display multiple ZdsStat. +class ZdsStat { + /// The color with which this stat's value will be displayed in a [ZdsStatCard]. + /// + /// Defaults to [ColorScheme.onSurface]. + final Color? color; + + /// This statistic's value. + final double value; + + /// The description of this statistic. + /// + /// If using in [ZdsStatCard], this description should be as concise as possible. + final String description; + + /// Creates a statistic to be used in [ZdsStatCard]. + const ZdsStat({required this.value, required this.description, this.color}); + + String get _valueString => value.toString().replaceAll(RegExp(r'([.]*0)(?!.*\d)'), ''); +} diff --git a/lib/src/components/molecules/tab_bar.dart b/lib/src/components/molecules/tab_bar.dart new file mode 100644 index 0000000..365614f --- /dev/null +++ b/lib/src/components/molecules/tab_bar.dart @@ -0,0 +1,127 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Theme colors for [ZdsTabBar]. +enum ZdsTabBarColor { + /// * Background color: `ZetaColors.primary`. + /// * Foreground color: `ZetaColors.onPrimary`. + /// * Unselected foreground color: `ZetaColors.cool.40`. + /// * Indicator color: `ZetaColors.primary.20`. + primary, + + /// * Background color: `ZetaColors.cool.90`. + /// * Foreground color: `ZetaColors.cool.20`. + /// * Unselected foreground color: `ZetaColors.cool.40`. + /// * Indicator color: `ZetaColors.primary`. + basic, + + /// * Background color: `ZetaColors.surface`. + /// * Foreground color: `ZetaColors.onSurface`. + /// * Unselected foreground color: `ZetaColors.cool.70`. + /// * Indicator color: `ZetaColors.primary`. + surface, +} + +/// Returns a [TabBar] with Zds styling. However, this widget has a number of issues that make it less useful in +/// varying screen sizes and resizable screens. It's recommended to instead use [ZdsResponsiveTabBar]. +class ZdsTabBar extends StatelessWidget implements PreferredSizeWidget { + /// Sets the color scheme for each of the tabs and the tab bar itself. + /// + /// Defaults to [ZdsTabBarColor.basic]. + final ZdsTabBarColor color; + + /// Typically a list of two or more [ZdsTab] widgets. + /// + /// The length of this list must match the [controller]'s [TabController.length] + /// and the length of the [TabBarView.children] list. + final List tabs; + + /// Optional [TabController] + final TabController? controller; + + /// True if the tab list should be scrollable in the horizontal axis. + /// + /// In many situations it is preferable to use [ZdsResponsiveTabBar]. + /// + /// Defaults to false. + final bool isScrollable; + + /// Padding applied within each tab around the label. + /// + /// Defaults to [kTabLabelPadding]. + final EdgeInsets labelPadding; + + /// Text style for the labels of the tabs. + final TextStyle? labelStyle; + + /// Makes a [TabBar] with Zds styling applied. It's recommended to instead use [ZdsResponsiveTabBar]. + const ZdsTabBar({ + required this.tabs, + super.key, + this.color = ZdsTabBarColor.primary, + this.controller, + this.isScrollable = false, + this.labelPadding = kTabLabelPadding, + this.labelStyle, + }); + + @override + Widget build(BuildContext context) { + final appBar = context.findAncestorWidgetOfExactType(); + + final customThemeContainer = Theme.of(context).zdsTabBarThemeData( + context, + hasIcons: hasIcons(tabs), + )[appBar != null ? appBar.color : color]!; + final customTheme = customThemeContainer.customTheme; + + return Container( + color: (customThemeContainer.customTheme.decoration as BoxDecoration).color, + child: SafeArea( + bottom: false, + child: Container( + height: customTheme.height, + decoration: customTheme.decoration, + child: Theme( + data: customThemeContainer.theme, + child: TabBar( + isScrollable: isScrollable, + controller: controller, + labelPadding: labelPadding, + labelStyle: labelStyle, + tabs: tabs + .map( + (item) => Builder( + builder: (context) => IconTheme( + data: IconTheme.of(context).copyWith( + size: customTheme.iconSize, + ), + child: item, + ), + ), + ) + .toList(), + ), + ), + ), + ), + ); + } + + @override + Size get preferredSize { + return hasIcons(tabs) ? const Size(0, 56) : const Size(0, 48); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('color', color)); + properties.add(DiagnosticsProperty('controller', controller)); + properties.add(DiagnosticsProperty('isScrollable', isScrollable)); + properties.add(DiagnosticsProperty('labelPadding', labelPadding)); + properties.add(DiagnosticsProperty('labelStyle', labelStyle)); + } +} diff --git a/lib/src/components/molecules/tag.dart b/lib/src/components/molecules/tag.dart new file mode 100644 index 0000000..ae7ff6c --- /dev/null +++ b/lib/src/components/molecules/tag.dart @@ -0,0 +1,242 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Tag colors +/// +/// If `filled`, foreground will always be [ZdsColors.white] and background will be the selected color +/// +/// Otherwise, the background will the same color, but with 10% opacity. +enum ZdsTagColor { + /// [ZdsColors.red]. + error, + + /// [ZdsColors.orange]. + alert, + + /// Primary color. + primary, + + /// Secondary color. + secondary, + + /// [ZdsColors.green]. + success, + + /// [ZdsColors.darkGrey] + basic, +} + +/// A tag used to show status information or selected options. +/// +/// The height of the tag will change depending on the parameters selected. The default height is 24 dp. If [rectangular] +/// is true the height is also 24 dp. If [rounded] is true, the height will be 20 dp. If [onClose] is not null, the +/// height will be 32 dp. The height can be unbounded if [unrestrictedSize] is set to true, but the minimum height will +/// always be of 20 dp at least. +/// +/// There are three main styles: +/// +/// * Setting [rounded] as true, typically with [prefix], typically used to indicate priority levels. +/// * Setting [rectangular] as true, typically with [prefix], typically used to indicate status, e.g pending, approved and declined. +/// * Setting [rounded] and [rectangular] as false, typically used to indicate status and to allow actions on the tag with [onClose]. +class ZdsTag extends StatelessWidget { + /// Whether to have completely rounded ends (pill shape) or to have a rectangle shape + /// + /// If [prefix] is not null, this must be true. Defaults to false + final bool rounded; + + /// Whether to have a more rectangular shape, with only 2dp border radius + /// + /// If [prefix] is not null, this must be true. Defaults to false + final bool rectangular; + + /// Whether the [color] or [customColor] will act as the background color (with the foreground color being + /// [ZdsColors.white]), or as the foreground color (with the background color being the color set to 10% opacity). + /// + /// If true, [prefix] must be null. Defaults to false. + final bool filled; + + /// A prefix shown before the child. + /// + /// If this is not null, [rounded] or [rectangular] must be true. If [filled] is true, this must be null. + final Widget? prefix; + + /// The tag's contents. + /// + /// Usually a [Text]. + final Widget? child; + + /// This tag's background color. + /// + /// If [customColor] is not null, this parameter will be ignored. Defaults to [ZdsTagColor.basic]. + final ZdsTagColor color; + + /// This tag's foreground color. This will be used as a foreground color or if [filled] as background color. + /// + /// If no [customBackgroundColor] is provided, this color will be used as background color with 10% opacity. + /// + /// Overrides [color]. + final Color? customColor; + + /// This tag's background color + /// + /// Unused if [filled]. + /// + /// Overrides [color]. + final Color? customBackgroundColor; + + /// A function called whenever the user taps on the closing button of this tag. + /// + /// If not null, a closing button will be added added at the end of the tag. + final VoidCallback? onClose; + + /// Whether the tag's height sizes itself to the child's size. This is useful for bigger fonts or non-text children + /// like images and such. If false, the tag will have a set height. + /// + /// Defaults to false. + final bool unrestrictedSize; + + /// Tag item that can have a close/remove button. + /// + /// When [unrestrictedSize] is true, the tag will size itself to its child's size. This is useful for bigger fonts + /// or non-text children like images and such. + const ZdsTag({ + super.key, + this.rounded = false, + this.rectangular = false, + this.filled = false, + this.prefix, + this.color = ZdsTagColor.basic, + this.customColor, + this.child, + this.onClose, + this.unrestrictedSize = false, + this.customBackgroundColor, + }) : assert( + prefix != null && (rounded || rectangular) || prefix == null, + 'If prefix is not null, rounded must be true', + ), + assert(filled && prefix == null || !filled, 'If filled is true, prefix must be null'); + + @override + Widget build(BuildContext context) { + final colors = _resolveColor(context, color); + + final double height = onClose == null + ? rounded + ? 20 + : 24 + : 32; + + final EdgeInsets padding = + onClose == null ? const EdgeInsets.symmetric(horizontal: 8) : const EdgeInsets.only(left: 14); + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + MergeSemantics( + child: Padding( + padding: const EdgeInsets.all(2), + child: Container( + constraints: BoxConstraints(minHeight: height, maxHeight: unrestrictedSize ? double.infinity : height), + decoration: BoxDecoration( + color: colors.last, + borderRadius: BorderRadius.circular( + rectangular + ? rectangularBorderRadius + : rounded + ? height + : kDefaultBorderRadius, + ), + ), + child: Row( + children: [ + if (prefix != null) + ZdsIndex( + useBoxDecoration: !rectangular, + color: colors.first, + child: prefix, + ), + DefaultTextStyle( + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: colors.first), + child: child ?? const SizedBox(), + ).paddingInsets(padding), + if (onClose != null) + Semantics( + button: true, + onTapHint: ComponentStrings.of(context).get('REMOVE', 'Remove'), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 48, minWidth: 48), + child: InkResponse( + highlightColor: colors.last, + splashColor: colors.last, + radius: height / 1.5, + onTap: onClose, + child: Icon( + ZdsIcons.close, + color: colors.first, + size: 16, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + + List _resolveColor(BuildContext context, ZdsTagColor color) { + final Color foregroundColor = customColor ?? _tagColors(context, color); + + if (filled) { + return [ZdsColors.white, foregroundColor]; + } + + Color background = customBackgroundColor ?? foregroundColor.withLight(0.1); + + if (color == ZdsTagColor.secondary) { + background = ZdsColors.secondarySwatch(context).shade200; + } + + if (color == ZdsTagColor.success) { + background = ZdsColors.greenSwatch['light']!; + } + + return [foregroundColor, background]; + } + + Color _tagColors(BuildContext context, ZdsTagColor tagColor) { + switch (tagColor) { + case ZdsTagColor.error: + return ZdsColors.red; + case ZdsTagColor.alert: + return ZdsColors.orange; + case ZdsTagColor.primary: + return Theme.of(context).colorScheme.primary; + case ZdsTagColor.secondary: + return Theme.of(context).colorScheme.secondary; + case ZdsTagColor.success: + return ZdsColors.green; + case ZdsTagColor.basic: + return ZdsColors.darkGrey; + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('rounded', rounded)); + properties.add(DiagnosticsProperty('rectangular', rectangular)); + properties.add(DiagnosticsProperty('filled', filled)); + properties.add(EnumProperty('color', color)); + properties.add(ColorProperty('customColor', customColor)); + properties.add(ColorProperty('customBackgroundColor', customBackgroundColor)); + properties.add(ObjectFlagProperty.has('onClose', onClose)); + properties.add(DiagnosticsProperty('unrestrictedSize', unrestrictedSize)); + } +} diff --git a/lib/src/components/molecules/toast.dart b/lib/src/components/molecules/toast.dart new file mode 100644 index 0000000..a1f0ffa --- /dev/null +++ b/lib/src/components/molecules/toast.dart @@ -0,0 +1,217 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +// TODO(colors): Add zeta + +/// Determines foreground and background color of toast to comply with design rules. +/// +/// * `success` = green background / grey foreground +/// * `warning` = yellow background /grey foreground +/// * `error` = red background / grey foreground +/// * `primary` = primary color background (with 0.33 lightness for contrast) / grey foreground +/// * `dark` = dark grey background / white foreground +enum ZdsToastColors { + /// * `success` = green background / grey foreground + success, + + /// * `warning` = yellow background /grey foreground + warning, + + /// * `error` = red background / grey foreground + error, + + /// * `primary` = primary color background (with 0.33 lightness for contrast) / grey foreground + primary, + + /// * `dark` = dark grey background / white foreground + dark +} + +/// A container used with [ZdsSnackBarExtension.showZdsToast] to show a toast. +/// +/// Typically used to confirm an action has been completed, or to notify the user of errors. +/// +/// ```dart +/// ScaffoldMessenger.of(context).showZdsToast( +/// ZdsToast( +/// title: Text('Deleted file'), +/// leading: const Icon(ZdsIcons.check_circle), +/// actions: [ +/// IconButton( +/// onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(), +/// icon: const Icon(ZdsIcons.close), +/// ), +/// ], +/// color: ZdsToastColors.success, +/// ), +/// ); +/// ``` +/// +/// See also: +/// +/// * [ZdsSnackBarExtension.showZdsToast], used to display a toast message on screen. +class ZdsToast extends StatelessWidget implements PreferredSizeWidget { + /// An icon that will be shown before the [title]. + /// + /// Typically an [Icon]. + final Widget? leading; + + /// The main content of this toast. + /// + /// Typically a [Text]. + final Widget? title; + + /// The actions that will be shown at the end of this widget. + /// + /// Typically an [IconButton] or a [ZdsButton.text]. + final List? actions; + + /// Determines foreground and background color of toast to comply with design rules. + /// + /// View [ZdsToastColors] for more details. + final ZdsToastColors? color; + + /// Whether this toast will have rounded corners. + /// + /// Defaults to true. + final bool rounded; + + /// Whether to clip this toast to one line or allow multiple lines. + /// + /// Defaults to false. + final bool multiLine; + + /// The contents of a toast, typically used with [ZdsSnackBarExtension.showZdsToast]. + const ZdsToast({ + super.key, + this.leading, + this.title, + this.actions, + this.color = ZdsToastColors.primary, + this.rounded = true, + this.multiLine = false, + }); + + Color _backgroundColor(BuildContext context, ZdsToastColors toastColor) { + switch (toastColor) { + case ZdsToastColors.success: + return ZdsColors.green.withLight(0.15); + case ZdsToastColors.warning: + return ZdsColors.yellow.withLight(0.15); + case ZdsToastColors.error: + return ZdsColors.red.withLight(0.15); + case ZdsToastColors.primary: + return ZdsColors.secondarySwatch(context).shade100; + case ZdsToastColors.dark: + return ZdsColors.greySwatch(context)[1200]!; + } + } + + Color _foregroundColor(ZdsToastColors toastColor) { + if (toastColor == ZdsToastColors.dark) { + return ZdsColors.white; + } + return ZdsColors.darkGrey; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(horizontal: rounded ? 8.0 : 0), + child: Material( + elevation: 2, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + multiLine + ? 6 + : rounded + ? 49 + : 0, + ), + ), + color: _backgroundColor(context, color!), + child: Container( + constraints: const BoxConstraints(minHeight: kToastHeight), + padding: EdgeInsets.only(left: 18, top: multiLine ? 14 : 0, bottom: multiLine ? 14 : 0, right: 10), + child: IconTheme( + data: IconThemeData(color: _foregroundColor(color!)), + child: Row( + children: [ + if (leading != null) leading!, + Expanded( + child: () { + if (title != null) { + return DefaultTextStyle( + style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: _foregroundColor(color!)), + overflow: TextOverflow.ellipsis, + maxLines: multiLine ? 5 : 1, + child: title!, + ); + } + return const SizedBox(); + }(), + ), + if (actions != null) + Row( + mainAxisSize: MainAxisSize.min, + children: actions!.divide(const SizedBox(width: 10)).toList(), + ), + ].divide(const SizedBox(width: 10)).toList(), + ), + ), + ), + ), + ), + ], + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToastHeight); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('color', color)); + properties.add(DiagnosticsProperty('rounded', rounded)); + properties.add(DiagnosticsProperty('multiLine', multiLine)); + } +} + +/// Extension to show a ZdsToast +extension ZdsSnackBarExtension on ScaffoldMessengerState { + /// Shows a [ZdsToast]. + /// + /// * [duration] sets how many seconds the toast is shown on screen, defaulting to 4. + /// * [padding] defines the space to set around the toast, defaulting to EdgeInsets.only(bottom: 8). + /// + /// ```dart + /// ScaffoldMessenger.of(context).showZdsToast(ZdsToast()); + /// ``` + /// + /// See also: + /// + /// * [ZdsToast], used to define the toast's contents + ScaffoldFeatureController showZdsToast( + ZdsToast toast, { + Duration duration = const Duration(seconds: 4), + EdgeInsets padding = const EdgeInsets.only(bottom: 8), + }) { + return showSnackBar( + SnackBar( + content: toast, + backgroundColor: ZdsColors.transparent, + elevation: 0, + behavior: SnackBarBehavior.fixed, + padding: padding, + duration: duration, + ), + ); + } +} diff --git a/lib/src/components/molecules/toolbar.dart b/lib/src/components/molecules/toolbar.dart new file mode 100644 index 0000000..8decba3 --- /dev/null +++ b/lib/src/components/molecules/toolbar.dart @@ -0,0 +1,152 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// A toolbar, used for additional actions that do not fit in the app bar. +/// +/// ```dart +/// ZdsToolbar( +/// title: Text('Title text'), +/// subtitle: Text('Subtitle text'), +/// actions: [ +/// IconButton(onPressed: () {}, icon: Icon(ZdsIcons.filter),), +/// ], +/// ), +/// ``` +/// +/// See also: +/// +/// * [ZdsAppBar], which allows to put a widget below it. +class ZdsToolbar extends StatelessWidget { + /// The toolbar's title or main widget. + /// + /// Typically a [Text]. + final Widget? title; + + /// A widget shown under [title]. + /// + /// Typically a [Text]. + final Widget? subtitle; + + /// Widgets shown at the end of the toolbar for additional actions. + /// + /// Typically a list of [IconButton]. + final List? actions; + + /// A widget that will be shown below the toolbar. + final Widget? child; + + /// The background color for this ToolBar. Defaults to [ColorScheme.primary] + final Color? backgroundColor; + + /// A toolbar which can be used for additional actions that do not fit in the app bar. + const ZdsToolbar({ + super.key, + this.title, + this.subtitle, + this.actions, + this.child, + this.backgroundColor, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return IconTheme( + data: theme.primaryIconTheme, + child: Material( + color: backgroundColor ?? theme.colorScheme.primary, + child: SafeArea( + child: Column( + children: [ + Container( + constraints: const BoxConstraints(minHeight: 56), + alignment: Alignment.center, + // decoration: BoxDecoration( + // border: Border(top: BorderSide(width: 1, color: theme.colorScheme.onPrimary.withOpacity(0.1))), + // ), + child: Row( + crossAxisAlignment: _getCrossAxisAlignment, + textBaseline: TextBaseline.alphabetic, + children: [ + Expanded( + child: Container( + padding: _resolvedContentPadding(context), + alignment: Alignment.center, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (title != null) + Container( + constraints: const BoxConstraints(minHeight: 43), + alignment: Alignment.bottomLeft, + child: DefaultTextStyle( + style: theme.primaryTextTheme.titleLarge!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: title!, + ), + ), + if (subtitle != null) ...[ + const SizedBox(height: 8), + Container( + constraints: const BoxConstraints(minHeight: 43), + alignment: Alignment.topLeft, + child: DefaultTextStyle( + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: () { + return theme.primaryTextTheme.titleSmall!.copyWith( + color: ZetaColors.of(context).onPrimary.withOpacity(0.8), + ); + }(), + child: subtitle!, + ), + ), + ], + ], + ), + ), + ), + ), + if (actions != null) + Row( + mainAxisSize: MainAxisSize.min, + children: actions!, + ), + ], + ), + ), + if (child != null) child!, + ], + ), + ), + ), + ); + } + + CrossAxisAlignment get _getCrossAxisAlignment { + if (_expandedLayout) return CrossAxisAlignment.baseline; + return CrossAxisAlignment.center; + } + + EdgeInsets _resolvedContentPadding(BuildContext context) { + final contentPadding = Theme.of(context).zdsToolbarThemeData.contentPadding; + + return EdgeInsets.only( + left: title is DateRange ? 0 : contentPadding.left, + right: contentPadding.right, + ); + } + + bool get _expandedLayout => subtitle != null && title != null; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/lib/src/components/molecules/vertical_nav.dart b/lib/src/components/molecules/vertical_nav.dart new file mode 100644 index 0000000..42301ef --- /dev/null +++ b/lib/src/components/molecules/vertical_nav.dart @@ -0,0 +1,260 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A [ZdsVerticalNav] used to switch between different views. Should primarily be used for tablet views and larger screens. +/// +/// Actions are shown at the **top**, items are shown at the **bottom**. +/// +/// ```dart +/// Row( +/// children: [ +/// ZdsVerticalNav( +/// currentIndex: index, +/// onTap: (i) => setState(() => index = i), +/// items: [ZdsVerticalNavItem(), ZdsVerticalNavItem()], +/// ), +/// index == 0 ? BodyForFirstItem() : BodyForSecondItem(), +/// ], +/// ) +/// ``` +class ZdsVerticalNav extends StatefulWidget { + /// The [ZdsNavItem] list that will be displayed at the **bottom** of the component. Each item should be linked to a separate view. + final List items; + + /// The currently selected item index from [items]. + /// + /// Must not be greater or equal to [items].length. + final int currentIndex; + + /// The function that will be called whenever the user taps on an item. The parameter is the item index in [items]. + /// + /// Usually used to call setState and change the [currentIndex]'s value. + final void Function(int)? onTap; + + /// Widgets that will be shown at the **top** of the nav bar. Typically a list of [IconButton]. + final List? actions; + + /// Width of the navigation bar. + /// + /// Defaults to 48 + final double barWidth; + + /// Height of the navigation item. + /// + /// Defaults to 53 + final double itemHeight; + + /// Creates a vertical navigation bar + /// + /// [items] can't be null. [currentIndex]'s value must be equal or greater than 0, and smaller than [items].length. + const ZdsVerticalNav({ + required this.items, + required this.currentIndex, + this.actions, + this.barWidth = 48, + this.itemHeight = 53, + super.key, + this.onTap, + }) : assert( + 0 <= currentIndex && currentIndex < items.length, + 'currentIndex must not be greater than the number of items', + ); + + @override + State createState() => _ZdsVerticalNavState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty('items', items)); + properties.add(IntProperty('currentIndex', currentIndex)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DoubleProperty('barWidth', barWidth)); + properties.add(DoubleProperty('itemHeight', itemHeight)); + } +} + +class _ZdsVerticalNavState extends State { + bool isExpanded = false; + + @override + Widget build(BuildContext context) { + final itemsWidget = Stack( + children: [ + if (!isExpanded) + AnimatedPositioned( + curve: Curves.ease, + duration: const Duration(milliseconds: 200), + left: 0, + right: 0, + top: widget.currentIndex * widget.itemHeight, + child: _SelectedBackground(width: widget.barWidth, height: widget.itemHeight), + ), + Column( + children: widget.items.map( + (item) { + final bool selected = widget.currentIndex == widget.items.indexOf(item); + return MergeSemantics( + child: Semantics( + label: '${item.semanticLabel ?? ''} tab ${widget.items.indexOf(item) + 1} of ${widget.items.length}', + child: Semantics( + selected: selected, + container: true, + child: SizedBox( + width: widget.barWidth, + height: widget.itemHeight, + child: Tooltip( + message: item.semanticLabel ?? '', + child: InkWell( + radius: widget.barWidth, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + onTap: () => widget.onTap?.call(widget.items.indexOf(item)), + child: IconTheme( + data: IconThemeData( + color: selected + ? Theme.of(context).colorScheme.secondary + : ZdsColors.greySwatch(context)[1000], + size: 24, + ), + child: item.icon, + ), + ), + ), + ), + ), + ), + ); + }, + ).toList(), + ), + ], + ); + + return LayoutBuilder( + builder: (context, constraints) { + final bool isTooShort = + (((widget.actions?.length ?? 0) + widget.items.length) * widget.itemHeight) + (2 * widget.itemHeight) + 4 >= + (constraints.minHeight != 0 ? constraints.minHeight : constraints.maxHeight); + + final actionsWidget = widget.actions != null + ? IconTheme( + data: IconThemeData(color: Theme.of(context).colorScheme.secondary, size: 24), + child: SingleChildScrollView( + child: Column( + children: widget.actions! + .map( + (action) => Column( + children: [ + action, + if (widget.actions!.indexOf(action) != widget.actions!.length - 1) + Divider(color: ZdsColors.greySwatch(context)[100]), + ], + ), + ) + .toList(), + ), + ), + ) + : const SizedBox.shrink(); + + return Container( + width: widget.barWidth, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.25), + blurRadius: 2, + offset: const Offset(-1, 0), + ), + ], + ), + padding: const EdgeInsets.only(bottom: 4), + child: Material( + child: Stack( + children: [ + if (isTooShort) + Positioned( + top: 0, + child: IconTheme( + data: IconThemeData(color: Theme.of(context).colorScheme.secondary, size: 24), + child: IconButton( + onPressed: () => setState(() => isExpanded = !isExpanded), + icon: Icon(isExpanded ? ZdsIcons.back : ZdsIcons.more_vert), + ), + ), + ), + if (!isTooShort || (isTooShort && isExpanded)) + Positioned( + top: !isTooShort ? 0 : widget.itemHeight, + child: AnimatedSize( + duration: const Duration(milliseconds: 150), + child: SizedBox( + height: !isTooShort + ? constraints.maxHeight - (widget.items.length * widget.itemHeight) + : constraints.maxHeight - widget.itemHeight, + child: actionsWidget, + ), + ), + ), + if (!isTooShort || (isTooShort && !isExpanded)) Positioned(bottom: 0, child: itemsWidget), + ], + ), + ), + ); + }, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('isExpanded', isExpanded)); + } +} + +class _SelectedBackground extends StatelessWidget { + final double width; + final double height; + + const _SelectedBackground({required this.width, required this.height}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + height: height, + child: Container( + margin: const EdgeInsets.fromLTRB(2, 2, 0, 2), + padding: const EdgeInsets.only(left: 1), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Theme.of(context).colorScheme.background, Theme.of(context).colorScheme.surface], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + blurRadius: 2, + offset: const Offset(-2, 1), + ), + ], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DoubleProperty('width', width)); + properties.add(DoubleProperty('height', height)); + } +} diff --git a/lib/src/components/organisms.dart b/lib/src/components/organisms.dart new file mode 100644 index 0000000..84abc9d --- /dev/null +++ b/lib/src/components/organisms.dart @@ -0,0 +1,25 @@ +// Organisms are groups of molecules joined together to form a relatively complex, distinct section of an interface. + +export 'organisms/app_bar.dart'; +export 'organisms/bottom_bar.dart'; +export 'organisms/bottom_tab_bar.dart'; +export 'organisms/calendar.dart'; +export 'organisms/date_range.dart'; +export 'organisms/date_range_picker_tile.dart'; +export 'organisms/day_picker.dart'; +export 'organisms/file_picker/file_picker.dart'; +export 'organisms/file_preview.dart'; +export 'organisms/image_editor.dart'; +export 'organisms/image_picker.dart'; +export 'organisms/infinite_list.dart'; +export 'organisms/list_group.dart'; +export 'organisms/list_tile.dart'; +export 'organisms/modal.dart'; +export 'organisms/navigation_menu.dart'; +export 'organisms/profile.dart'; +export 'organisms/properties_list.dart'; +export 'organisms/radio_list.dart'; +export 'organisms/search_app_bar.dart'; +export 'organisms/tab_scaffold.dart'; +export 'organisms/tags_list.dart'; +export 'organisms/temp_directory/resolver.dart' show clearUiTempDirectory; diff --git a/lib/src/components/organisms/app_bar.dart b/lib/src/components/organisms/app_bar.dart new file mode 100644 index 0000000..acde915 --- /dev/null +++ b/lib/src/components/organisms/app_bar.dart @@ -0,0 +1,176 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; +import 'package:flutter/services.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// An app bar with Zds styling. +/// +/// Typically used in [Scaffold.appBar], placing it at the top of the screen. By using the [bottom] property, another +/// widget can be displayed below this appbar, typically a [ZdsResponsiveTabBar] or a [ZdsTabBar]. +/// +/// ```dart +/// Scaffold( +/// appBar: ZdsAppBar( +/// title: Text("I'm an appbar!") +/// bottom: ZdsTabBar() +/// ), +/// ) +/// ``` +/// +/// By default, if no [leading] widget is provided, the appbar will provide an [IconButton] to return to the previous +/// page in the [Navigator]'s stack. +/// +/// See also: +/// +/// * [ZdsResponsiveTabBar] and [ZdsTabBar], typically used to display tabs below this widget. +/// * [ZdsPopupMenu], typically used in [actions] to display a kebab menu for further actions that would pollute +/// the appbar if they were all shown. +class ZdsAppBar extends StatelessWidget implements PreferredSizeWidget { + /// The widget shown at the start of the appbar. Typically an [IconButton]. + /// + /// If null and the [Navigator]'s stack can pop, a back button will be shown by default. + final Widget? leading; + + /// The appbar's main text. Typically a [Text] widget, it is usually used to show the page's name. + final Widget? title; + + /// The widget that will be shown below the [title]. Typically a [Text] widget, it is usually used to display + /// secondary information about the current page. + final Widget? subtitle; + + /// A widget that will be shown between the [leading] and [title] widgets. + final Widget? icon; + + /// Widgets that will be shown at the end of the appbar. Typically a list of [IconButton]. + /// + /// The recommended length is 3 items or fewer. If you need to display more actions, consider creating a kebab + /// [IconButton] menu with [ZdsPopupMenu]. + final List? actions; + + /// Specifies a preference for the style of the system's overlays when this appbar is used. If null, + /// [ThemeData.appBarTheme] will be used. If null, [SystemUiOverlayStyle.dark] will be used by default. + final SystemUiOverlayStyle? systemUiOverlayStyle; + + /// This widget appears below the app bar. Typically a [ZdsResponsiveTabBar] or a [ZdsTabBar]. + /// + /// See also: + /// + /// * [PreferredSize], which can be used to give an arbitrary widget a preferred size. + final PreferredSizeWidget? bottom; + + /// Color for app bar. Defaults to primary color. + /// + /// See [ZdsTabBarColor]. + final ZdsTabBarColor color; + + /// Creates an appbar that is typically shown at the top of the screen. + const ZdsAppBar({ + super.key, + this.leading, + this.title, + this.actions, + this.subtitle, + this.icon, + this.bottom, + this.systemUiOverlayStyle, + this.color = ZdsTabBarColor.primary, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final ZetaColors colors = ZetaColors.of(context); + final AppBarTheme appBarTheme = Theme.of(context).buildAppBarTheme(colors)[color]!; + + return AnnotatedRegion( + value: systemUiOverlayStyle ?? appBarTheme.systemOverlayStyle ?? SystemUiOverlayStyle.dark, + sized: false, + child: Material( + color: appBarTheme.backgroundColor, + child: SafeArea( + bottom: false, + child: Column( + children: [ + SizedBox( + height: _toolbarHeight, + child: IconTheme( + data: appBarTheme.iconTheme ?? theme.primaryIconTheme, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: icon == null ? 24 : 12), + child: Row( + children: [ + Semantics( + sortKey: const OrdinalSortKey(2), + child: _resolvedLeading(context), + ), + if (icon != null) icon!, + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Semantics( + sortKey: const OrdinalSortKey(1), + child: DefaultTextStyle( + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: theme.textTheme.titleMedium!.copyWith(color: appBarTheme.foregroundColor), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (title != null) title!, + if (title != null && subtitle != null) const SizedBox(height: 4), + if (subtitle != null) + DefaultTextStyle.merge( + child: subtitle!, + style: theme.primaryTextTheme.titleSmall + ?.copyWith(color: appBarTheme.foregroundColor?.withOpacity(0.8)), + ), + ], + ), + ), + ), + ), + ), + if (actions != null) + Row( + mainAxisSize: MainAxisSize.min, + children: actions!, + ), + ], + ), + ), + ), + ), + if (bottom != null) bottom!, + ], + ), + ), + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight(_toolbarHeight + _bottomHeight); + + double get _bottomHeight => bottom?.preferredSize.height ?? 0; + double get _toolbarHeight => kZdsToolbarHeight; + Widget _resolvedLeading(BuildContext context) { + if (leading != null) return leading!; + + final bool canPop = ModalRoute.of(context)?.canPop ?? false; + + if (canPop) return const ZdsBackButton(); + + return const SizedBox(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('systemUiOverlayStyle', systemUiOverlayStyle)); + properties.add(EnumProperty('color', color)); + } +} diff --git a/lib/src/components/organisms/bottom_bar.dart b/lib/src/components/organisms/bottom_bar.dart new file mode 100644 index 0000000..7ff6537 --- /dev/null +++ b/lib/src/components/organisms/bottom_bar.dart @@ -0,0 +1,94 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A Container typically used with [Scaffold.bottomNavigationBar], in [showZdsBottomSheet].bottomBuilder, or as the +/// last child of a [Column] that contains an [Expanded] to ensure [ZdsBottomBar] stays at the bottom. +/// +/// Typically used with a [Scaffold]: +/// ```dart +/// Scaffold( +/// bottomNavigationBar: ZdsBottomBar( +/// child: bottomBarContents, +/// ), +/// ) +/// ``` +/// +/// The following example shows how it can be used in a [ZdsBottomSheet] as a bottom action bar: +/// ```dart +/// showZdsBottomSheet( +/// bottomBuilder: (context) => ZdsBottomBar( +/// child: Row( +/// children: [Spacer(), ZdsButton(), ZdsButton()] +/// ), +/// ), +/// ); +/// ``` +/// +/// Alternatively, it may be used as a [Column]'s last child to support a wider variety of actions. If this is the case, +/// ensure that the ZdsBottomBar covers the entire width of the screen, and that it is not used anywhere but in the +/// bottom of the screen. +class ZdsBottomBar extends StatelessWidget implements PreferredSizeWidget { + /// The widget that will be below this widget in the widget tree, typically a [Row]. + final Widget? child; + + /// The background color of this bottom bar. Defaults to the [ZdsBottomBarTheme] value. + final Color? color; + + /// The shadows behind bottom bar. Defaults to the [ZdsBottomBarTheme] value. + final List? shadows; + + /// The height that this bottom bar will be. Defaults to [kBottomBarHeight]. + final double minHeight; + + /// Empty space to inscribe inside this widget. The [child], if any, is placed inside this padding. + /// Defaults to the [ZdsBottomBarTheme] value. + final EdgeInsets? contentPadding; + + /// Creates a bottom bar that can be used as a bottom application bar, or as a bottom action bar + /// + /// If [color], [shadows], and [contentPadding] are null, their [ZdsBottomBarTheme] values will be used instead. + const ZdsBottomBar({ + super.key, + this.child, + this.color, + this.shadows, + this.minHeight = kBottomBarHeight, + this.contentPadding, + }); + + @override + Widget build(BuildContext context) { + final customTheme = ZdsBottomBarTheme.of(context); + return DecoratedBox( + decoration: BoxDecoration( + color: color ?? customTheme.backgroundColor, + boxShadow: shadows ?? customTheme.shadows, + ), + child: Material( + color: Colors.transparent, + child: SafeArea( + top: false, + child: Container( + height: minHeight, + padding: contentPadding ?? customTheme.contentPadding, + child: child, + ), + ), + ), + ); + } + + @override + Size get preferredSize => Size(0, minHeight); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ColorProperty('color', color)) + ..add(IterableProperty('shadows', shadows)) + ..add(DoubleProperty('minHeight', minHeight)) + ..add(DiagnosticsProperty('contentPadding', contentPadding)); + } +} diff --git a/lib/src/components/organisms/bottom_tab_bar.dart b/lib/src/components/organisms/bottom_tab_bar.dart new file mode 100644 index 0000000..4581a1c --- /dev/null +++ b/lib/src/components/organisms/bottom_tab_bar.dart @@ -0,0 +1,225 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Defines a button for [ZdsBottomTabBar] or [ZdsVerticalNav]. Used in [ZdsBottomTabBar.items]. +class ZdsNavItem { + /// used for automation purpose + final String? id; + + /// The label that will appear below the icon for this item. + final String label; + + /// The icon that will be shown above this item's label. Usually an [Icon] or [IconWithBadge]. + final Widget icon; + + /// Semantic label to wrap the item. + /// + /// Defaults to [label]. + final String? semanticLabel; + + /// Semantic label for state of the item e.g. Selected. + /// + /// if [ZdsNavItem] state is selected then this get appended to the [semanticLabel]. + final String semanticState; + + /// Creates a button to be used in [ZdsBottomTabBar] + const ZdsNavItem({ + required this.label, + required this.icon, + this.semanticLabel, + this.semanticState = '', + this.id, + }); +} + +/// A [ZdsBottomBar] used to switch between different views. Typically used as a [Scaffold.bottomNavigationBar] in a +/// [StatefulWidget]. +/// ```dart +/// Scaffold( +/// bottomNavigationBar: ZdsBottomTabBar( +/// currentIndex: index, +/// onTap: (i) => setState(() => index = i), +/// items: [ZdsNavItem(), ZdsNavItem()], +/// ), +/// body: index == 0 ? BodyForFirstItem() : BodyForSecondItem(), +/// ) +/// ``` +class ZdsBottomTabBar extends StatelessWidget implements PreferredSizeWidget { + /// The [ZdsBottomTabBar] list that will be displayed. Each item should be linked to a separate view. + final List items; + + /// The currently selected item index from [items]. + /// + /// Must not be greater or equal to [items].length. + final int currentIndex; + + /// The function that will be called whenever the user taps on an item. The parameter is the item index in [items]. + /// + /// Usually used to call setState and change the [currentIndex]'s value. + final void Function(int)? onTap; + + /// Empty space to inscribe inside this widget. + /// + /// Defaults to the [ZdsBottomBarTheme] value. + final EdgeInsets? contentPadding; + + /// The height that this bottom bar will be. + /// + /// Defaults to [kBottomBarHeight]. + final double minHeight; + + /// Creates a bottom tab navigation bar + /// + /// [items] can't be null. [currentIndex]'s value must be equal or greater than 0, and smaller than [items].length. + const ZdsBottomTabBar({ + required this.items, + super.key, + this.currentIndex = 0, + this.onTap, + this.contentPadding, + this.minHeight = kBottomBarHeight, + }) : assert( + 0 <= currentIndex && currentIndex < items.length, + 'currentIndex must not be greater than the number of items', + ); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).bottomNavigationBarTheme; + final zdsBottomBarTheme = ZdsBottomBarTheme.of(context); + final tabBarContentPadding = contentPadding ?? + zdsBottomBarTheme.contentPadding.copyWith( + left: 0, + right: 0, + ); + return ZdsBottomBarTheme( + data: zdsBottomBarTheme.copyWith(contentPadding: tabBarContentPadding), + child: ZdsBottomBar( + minHeight: minHeight, + contentPadding: contentPadding, + child: DefaultTextStyle.merge( + overflow: TextOverflow.ellipsis, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + for (var i = 0; i < items.length; i++) + () { + final selected = i == currentIndex; + //keeping key empty for other apps should be replaced with proper translation once added in repo. + final finalSemanticLabel = ComponentStrings.of(context) + .get('', items[i].semanticLabel ?? items[i].label, args: ['${i + 1}', '${items.length}']); + return Expanded( + child: Semantics( + excludeSemantics: true, + onTap: () { + onTap?.call(i); + }, + label: '$finalSemanticLabel${selected ? ', ${items[i].semanticState}' : ''}', + child: _ZdsBottomTabBarTile( + id: items[i].id, + selected: selected, + icon: IconTheme( + data: selected ? theme.selectedIconTheme! : theme.unselectedIconTheme!, + child: items[i].icon, + ), + label: items[i].label, + labelStyle: selected + ? theme.selectedLabelStyle!.copyWith(color: theme.selectedItemColor) + : theme.unselectedLabelStyle!.copyWith(color: theme.unselectedItemColor), + onTap: () { + onTap?.call(i); + }, + ), + ), + ); + }(), + ], + ), + ), + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight(minHeight); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('currentIndex', currentIndex)); + properties.add(IterableProperty('items', items)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DiagnosticsProperty('contentPadding', contentPadding)); + properties.add(DoubleProperty('minHeight', minHeight)); + } +} + +class _ZdsBottomTabBarTile extends StatelessWidget { + final VoidCallback? onTap; + final Widget icon; + final String label; + final bool selected; + final String? id; + final TextStyle labelStyle; + + const _ZdsBottomTabBarTile({ + required this.icon, + required this.label, + required this.selected, + required this.labelStyle, + this.id, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final effectiveTooltip = label; + Widget result = ConstrainedBox( + constraints: const BoxConstraints(minHeight: 48, minWidth: 48), + child: InkResponse( + onTap: onTap, + enableFeedback: false, + child: Column( + key: id != null ? Key(id!) : null, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + const SizedBox(height: 4), + ExcludeSemantics( + child: Text( + label, + style: labelStyle, + textScaleFactor: MediaQuery.of(context).textScaleFactor > 1.35 ? 1.35 : null, + ), + ), + ], + ), + ), + ); + + result = Tooltip( + message: effectiveTooltip, + preferBelow: false, + verticalOffset: 50, + excludeFromSemantics: true, + child: result, + ); + + return Semantics( + selected: selected, + container: true, + child: result, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(StringProperty('label', label)); + properties.add(DiagnosticsProperty('selected', selected)); + properties.add(StringProperty('id', id)); + properties.add(DiagnosticsProperty('labelStyle', labelStyle)); + } +} diff --git a/lib/src/components/organisms/calendar.dart b/lib/src/components/organisms/calendar.dart new file mode 100644 index 0000000..5a1a5a7 --- /dev/null +++ b/lib/src/components/organisms/calendar.dart @@ -0,0 +1,825 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:table_calendar/table_calendar.dart'; + +import '../../../zds_flutter.dart'; + +enum _ZdsCalendarVariant { switchable, monthly, weekly } + +/// An adaptable calendar widget that can be used in a variety of ways, with selectable days and date ranges, as well +/// as event markers and different formats. +/// +/// This calendar has three variants, each called with a different constructor: +/// * [ZdsCalendar], which allows to switch between a monthly and weekly format. It always has a header with the +/// current month and a format switcher. +/// * [ZdsCalendar.monthly], which shows a calendar with a fixed month format. +/// * [ZdsCalendar.weekly], which shows a calendar with a fixed week format. +/// +/// For this widget, the [selectedDay] refers to the day currently selected (with a filled circle surrounding it), +/// while [TableCalendar.focusedDay] refers to the day that is currently in focus (shown on screen). The [TableCalendar.focusedDay] has no +/// special decoration and looks like any other day. The [selectedDay] may not be the [TableCalendar.focusedDay] and may not be +/// displayed on screen (the user selects a day on February and changes the month to March, making [selectedDay] a +/// day in February and [TableCalendar.focusedDay] a day in March). +/// +/// Several callback functions are available to sync other widgets with this one. These are [onDaySelected], +/// [onRangeSelected], and [onPageChanged]. +class ZdsCalendar extends StatefulWidget { + /// The earliest date that will be shown on the calendar. + final DateTime? firstDay; + + /// The last date that will be shown on the calendar. + final DateTime? lastDay; + + /// set initial selected Date on the calendar + final DateTime? initialSelectedDay; + + /// set and manage the selected day on the calendar. Will override intialSelectedDay. + final DateTime? selectedDay; + + /// set Starting Day Of the week on the calendar + final StartingDayOfWeek? startingDayOfWeek; + + /// set initial selected week on the calendar + final DateTime? initialSelectedWeek; + + /// This enables all button to select current week of the calendar, Defaults to false. + final bool showAllButton; + + /// Whether you can only select one unique day or can select a range of days. Defaults to false. + final bool isRangeSelectable; + + /// A list with events. Can't be null. If markers are enabled, these events will be displayed on the calendar. + final List events; + + /// Whether the header should be shown or not. If using the [ZdsCalendar] constructor, the header will contain a + /// format switcher. To not show a format switcher, use [ZdsCalendar.monthly] instead. + final bool hasHeader; + + /// List of icons to be shown at the beginning of selected weeks. + /// + /// Defaults to empty. + /// + /// See also: + /// * [WeekIcon] + final List? weekIcons; + + /// Whether to show a grid. Defaults to false. + final bool isGridShown; + + /// Function called whenever the selected day changes. Takes two arguments, the selectedDay and the focusedDay. + final void Function(DateTime, DateTime)? onDaySelected; + + /// Function called whenever a date range is selected. Takes three arguments, the starting day, the ending day, and + /// the focusedDay. The starting and end day can be null. + final void Function(DateTime?, DateTime?, DateTime)? onRangeSelected; + + /// Called whenever the user changes page. Takes one argument, the focused day. + final void Function(DateTime)? onPageChanged; + + /// Function called whenever a all option is selected. Takes three arguments, the starting day, the ending day, and + /// the focusedDay. The starting and end day can be null. + final void Function(DateTime?, DateTime?, DateTime)? onAllSelected; + + /// Called whenever the format of the calendar is changed. Takes one argument, the new calendar format. + final void Function(CalendarFormat)? onFormatChanged; + + final _ZdsCalendarVariant _variant; + + /// Padding around the header of the calendar + /// + /// Defaults to EdgeInsets.fromLTRB(4, 8, 8, 8) + final EdgeInsets headerPadding; + + /// Function that creates a single event marker for a given `day` + /// + /// If this is null, the default marker is used + final Widget? Function(BuildContext, DateTime, dynamic)? singleMarkerBuilder; + + /// Specifies swipe gestures available to `TableCalendar`. + /// + /// If `AvailableGestures.none` is used, the calendar will only be interactive via buttons. + final AvailableGestures availableGestures; + + /// True if component is enabled and editable, false if read only. + /// + /// Defaults to true. + final bool enabled; + + /// Color of the chevron icons on the calendar header + /// + /// Defaults to [ColorScheme.onSurface]. + final Color? calendarHeaderIconColor; + + /// Color of the text on the calendar header + /// + /// Defaults to [ColorScheme.onBackground]. + final Color? calendarHeaderTextColor; + + /// Custom color override for unselected days. + /// + /// Applies to both weekdays and weekends. + final Color? calendarTextColor; + + /// A list of holiday dates. holiday will be shown on calendar with grey circle. + /// + /// /// Defaults to empty list + final List holidayEvents; + + /// Label will be shown for all button. + /// + /// Defaults to 'All'. + final String? allCustomLabel; + + /// Calendar widget that allows to switch between a monthly and weekly format. As such, the calendar header will + /// always be shown. To not show the calendar header and use a monthly format, use [ZdsCalendar.monthly] instead. + const ZdsCalendar({ + required this.events, + super.key, + this.showAllButton = false, + this.onAllSelected, + this.firstDay, + this.lastDay, + this.initialSelectedDay, + this.selectedDay, + this.startingDayOfWeek, + this.initialSelectedWeek, + this.weekIcons, + this.isRangeSelectable = false, + this.isGridShown = false, + this.onDaySelected, + this.onRangeSelected, + this.onPageChanged, + this.onFormatChanged, + this.headerPadding = const EdgeInsets.fromLTRB(4, 8, 8, 8), + this.singleMarkerBuilder, + this.availableGestures = AvailableGestures.horizontalSwipe, + this.enabled = true, + this.calendarHeaderIconColor, + this.calendarHeaderTextColor, + this.calendarTextColor, + this.holidayEvents = const [], + this.allCustomLabel, + }) : _variant = _ZdsCalendarVariant.switchable, + hasHeader = true; + + /// Shows a calendar in a fixed monthly format. + const ZdsCalendar.monthly({ + required this.events, + super.key, + this.showAllButton = false, + this.onAllSelected, + this.firstDay, + this.lastDay, + this.initialSelectedDay, + this.selectedDay, + this.startingDayOfWeek, + this.initialSelectedWeek, + this.hasHeader = true, + this.weekIcons, + this.isRangeSelectable = false, + this.isGridShown = false, + this.onDaySelected, + this.onRangeSelected, + this.onPageChanged, + this.onFormatChanged, + this.headerPadding = const EdgeInsets.fromLTRB(4, 8, 8, 8), + this.singleMarkerBuilder, + this.availableGestures = AvailableGestures.horizontalSwipe, + this.enabled = true, + this.calendarHeaderIconColor, + this.calendarHeaderTextColor, + this.calendarTextColor, + this.holidayEvents = const [], + this.allCustomLabel, + }) : _variant = _ZdsCalendarVariant.monthly; + + /// Shows a calendar in a fixed weekly format. + const ZdsCalendar.weekly({ + required this.events, + super.key, + this.showAllButton = false, + this.onAllSelected, + this.firstDay, + this.lastDay, + this.initialSelectedDay, + this.selectedDay, + this.startingDayOfWeek, + this.initialSelectedWeek, + this.isRangeSelectable = false, + this.isGridShown = false, + this.weekIcons, + this.onDaySelected, + this.onRangeSelected, + this.onPageChanged, + this.onFormatChanged, + this.headerPadding = const EdgeInsets.fromLTRB(4, 8, 8, 8), + this.singleMarkerBuilder, + this.availableGestures = AvailableGestures.horizontalSwipe, + this.enabled = true, + this.calendarHeaderIconColor, + this.calendarHeaderTextColor, + this.calendarTextColor, + this.holidayEvents = const [], + this.allCustomLabel, + }) : _variant = _ZdsCalendarVariant.weekly, + hasHeader = false; + + @override + State createState() => _ZdsCalendarState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('firstDay', firstDay)); + properties.add(DiagnosticsProperty('lastDay', lastDay)); + properties.add(DiagnosticsProperty('initialSelectedDay', initialSelectedDay)); + properties.add(DiagnosticsProperty('selectedDay', selectedDay)); + properties.add(EnumProperty('startingDayOfWeek', startingDayOfWeek)); + properties.add(DiagnosticsProperty('initialSelectedWeek', initialSelectedWeek)); + properties.add(DiagnosticsProperty('showAllButton', showAllButton)); + properties.add(DiagnosticsProperty('isRangeSelectable', isRangeSelectable)); + properties.add(IterableProperty('events', events)); + properties.add(DiagnosticsProperty('hasHeader', hasHeader)); + properties.add(IterableProperty('weekIcons', weekIcons)); + properties.add(DiagnosticsProperty('isGridShown', isGridShown)); + properties.add(ObjectFlagProperty.has('onDaySelected', onDaySelected)); + properties.add( + ObjectFlagProperty.has( + 'onRangeSelected', + onRangeSelected, + ), + ); + properties.add(ObjectFlagProperty.has('onPageChanged', onPageChanged)); + properties.add( + ObjectFlagProperty.has( + 'onAllSelected', + onAllSelected, + ), + ); + properties.add(ObjectFlagProperty.has('onFormatChanged', onFormatChanged)); + properties.add(DiagnosticsProperty('headerPadding', headerPadding)); + properties.add( + ObjectFlagProperty.has( + 'singleMarkerBuilder', + singleMarkerBuilder, + ), + ); + properties.add(EnumProperty('availableGestures', availableGestures)); + properties.add(DiagnosticsProperty('enabled', enabled)); + properties.add(ColorProperty('calendarHeaderIconColor', calendarHeaderIconColor)); + properties.add(ColorProperty('calendarHeaderTextColor', calendarHeaderTextColor)); + properties.add(ColorProperty('calendarTextColor', calendarTextColor)); + properties.add(IterableProperty('holidayEvents', holidayEvents)); + properties.add(StringProperty('allCustomLabel', allCustomLabel)); + } +} + +class _ZdsCalendarState extends State { + late DateTime _focusedDay; // Used to be a ValueListenable? + DateTime? _selectedDay; + DateTime? _rangeStart; + DateTime? _rangeEnd; + DateTime? startOfweek; + DateTime? endOfweek; + late CalendarFormat _calendarFormat; + + @override + void initState() { + _focusedDay = widget.initialSelectedWeek ?? DateTime.now(); + _selectedDay = _selectedDay ?? widget.selectedDay ?? widget.initialSelectedDay; + _calendarFormat = (widget._variant == _ZdsCalendarVariant.weekly) ? CalendarFormat.week : CalendarFormat.month; + super.initState(); + } + + @override + void didUpdateWidget(covariant ZdsCalendar oldWidget) { + if (widget.selectedDay != null && oldWidget.selectedDay != widget.selectedDay) { + setState(() { + _selectedDay = widget.selectedDay; + }); + widget.onDaySelected?.call(widget.selectedDay!, _focusedDay); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + final languageCode = Localizations.localeOf(context).languageCode; + final textTheme = Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Theme.of(context).colorScheme.secondary, fontWeight: FontWeight.w500); + + final StartingDayOfWeek startingDayOfWeek = widget.startingDayOfWeek ?? StartingDayOfWeek.sunday; + final calendar = TableCalendar( + startingDayOfWeek: startingDayOfWeek, + availableGestures: widget.availableGestures, + rowHeight: calendarRowHeight, + // TODO(calendar): Determine initial and final dates + firstDay: widget.firstDay ?? DateTime.fromMillisecondsSinceEpoch(0), + lastDay: widget.lastDay ?? DateTime(17776), + focusedDay: _focusedDay, + rangeStartDay: _rangeStart, + rangeEndDay: _rangeEnd, + rangeSelectionMode: widget.isRangeSelectable ? RangeSelectionMode.enforced : RangeSelectionMode.disabled, + calendarFormat: _calendarFormat, + headerVisible: false, + + // Use `selectedDayPredicate` to determine which day is currently selected. + // If this returns true, then `day` will be marked as selected. + selectedDayPredicate: (day) { + // Using `isSameDay` is recommended to disregard + // the time-part of compared DateTime objects. + return isSameDay(_selectedDay, day); + }, + onDaySelected: (selectedDay, focusedDay) { + if (!isSameDay(_selectedDay, selectedDay)) { + setState(() { + _selectedDay = selectedDay; + _focusedDay = focusedDay; + }); + } + widget.onDaySelected?.call(selectedDay, focusedDay); + }, + onRangeSelected: (start, end, focusedDay) { + setState(() { + _focusedDay = focusedDay; + _rangeStart = start; + _rangeEnd = end; + }); + widget.onRangeSelected?.call(start, end, focusedDay); + }, + // TODO(calendar): Figure out why this onFormatChanged function doesn't seem to be called when changing the format + // Currently, a workaround is being used. + onFormatChanged: (format) { + if (_calendarFormat != format) { + setState(() { + _calendarFormat = format; + }); + } + }, + onPageChanged: (focusedDay) { + setState(() { + _focusedDay = focusedDay; + if (!widget.isRangeSelectable && !widget.showAllButton) { + _selectedDay = focusedDay; + } + }); + widget.onPageChanged?.call(focusedDay); + }, + locale: _getCurrentLocaleString(context), + eventLoader: _getEventsForDay, + holidayPredicate: _getHoliday, + daysOfWeekHeight: widget._variant == _ZdsCalendarVariant.weekly ? 24 : calendarDaysOfWeekHeight, + calendarBuilders: CalendarBuilders( + dowBuilder: (context, day) { + final text = DateFormat.E(languageCode).format(day)[0]; + return ExcludeSemantics( + child: Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + text, + style: Theme.of(context).textTheme.titleSmall!.copyWith( + fontWeight: FontWeight.w500, + color: ZdsColors.greySwatch( + context, + )[Theme.of(context).colorScheme.brightness == Brightness.dark ? 700 : 900], + ), + textAlign: TextAlign.center, + ), + ), + ); + }, + singleMarkerBuilder: widget.singleMarkerBuilder, + ), + pageAnimationDuration: const Duration(milliseconds: 500), + calendarStyle: CalendarStyle( + tableBorder: widget.isGridShown + ? TableBorder( + borderRadius: BorderRadius.circular(4), + horizontalInside: BorderSide(color: ZdsColors.greyCoolSwatch.shade100), + verticalInside: BorderSide(color: ZdsColors.greyCoolSwatch.shade100), + left: BorderSide(color: ZdsColors.greyCoolSwatch.shade100), + right: BorderSide(color: ZdsColors.greyCoolSwatch.shade100), + ) + : const TableBorder(), + markersMaxCount: 1, // TODO(calendar): Redefine this if we want multiple dots. + markerSize: 5, + markerMargin: EdgeInsets.only( + top: widget._variant == _ZdsCalendarVariant.weekly + ? 8 + : widget.isGridShown + ? 6 + : 8, + ), + cellMargin: EdgeInsets.all(widget.weekIcons != null && widget.weekIcons!.isNotEmpty ? 5 : 8), + todayTextStyle: textTheme, + defaultTextStyle: textTheme.copyWith( + color: widget.calendarTextColor ?? Theme.of(context).colorScheme.onBackground, + ), + weekendTextStyle: textTheme.copyWith( + color: widget.calendarTextColor ?? Theme.of(context).colorScheme.onBackground, + ), + holidayDecoration: BoxDecoration( + color: ZdsColors.greyWarmSwatch[200], + shape: BoxShape.circle, + ), + selectedTextStyle: textTheme.copyWith(color: Theme.of(context).colorScheme.onSecondary), + outsideTextStyle: textTheme.copyWith(color: ZdsColors.blueGrey), + rangeStartTextStyle: textTheme.copyWith(color: ZdsColors.white), + rangeEndTextStyle: textTheme.copyWith(color: ZdsColors.white), + rangeHighlightColor: Theme.of(context).colorScheme.secondary.withOpacity(0.1), + rangeStartDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + shape: BoxShape.circle, + ), + rangeEndDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + shape: BoxShape.circle, + ), + markerDecoration: BoxDecoration( + color: ZdsColors.blueGrey, + shape: BoxShape.circle, + ), + selectedDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + shape: BoxShape.circle, + ), + todayDecoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.fromBorderSide( + BorderSide( + color: Theme.of(context).colorScheme.secondary, + width: context.isSmallScreen() ? 0 : 1.5, + ), + ), + ), + ), + ) + .paddingOnly( + bottom: (widget._variant == _ZdsCalendarVariant.weekly) ? 6 : 10, + top: (widget._variant == _ZdsCalendarVariant.weekly) ? 10 : 0, + ) + .backgroundColor(Theme.of(context).colorScheme.surface); + + final calendarHeader = Container( + color: Theme.of(context).colorScheme.surface, + padding: widget.headerPadding, + child: Material( + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + color: widget.calendarHeaderIconColor ?? Theme.of(context).colorScheme.onSurface, + splashRadius: 24, + tooltip: MaterialLocalizations.of(context).previousMonthTooltip, + onPressed: () => setState( + () => _focusedDay = _focusedDay.startOfMonth.subtract( + const Duration(days: 1), + ), + ), + ), + ZdsPopupMenu( + items: [ + for (int i = 1; i <= 12; i++) + ZdsPopupMenuItem( + value: i, + child: ListTile( + visualDensity: VisualDensity.compact, + // Shows the month's name + title: Text(DateFormat.MMMM(languageCode).format(DateTime(2000, i))), + ), + ), + ], + onSelected: (int monthNumber) => setState(() => _focusedDay = DateTime(_focusedDay.year, monthNumber)), + builder: (_, open) => InkWell( + onTap: open, + borderRadius: BorderRadius.circular(8), + child: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 110, + minHeight: 48, + ), + child: Align( + child: Semantics( + button: true, + child: Text( + _focusedDay.format('MMMM yyyy', languageCode), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: widget.calendarHeaderTextColor ?? Theme.of(context).colorScheme.onBackground, + ), + ), + ), + ), + ), + ), + ), + IconButton( + icon: const Icon(Icons.chevron_right), + color: widget.calendarHeaderIconColor ?? Theme.of(context).colorScheme.onSurface, + splashRadius: 24, + tooltip: MaterialLocalizations.of(context).nextMonthTooltip, + onPressed: () => setState(() => _focusedDay = _focusedDay.endOfMonth.add(const Duration(days: 1))), + ), + const Spacer(), + if (widget._variant == _ZdsCalendarVariant.switchable) + ZdsPopupMenu( + onSelected: (CalendarFormat format) { + if (_calendarFormat != format) { + setState(() { + _calendarFormat = format; + }); + widget.onFormatChanged?.call(format); + } + }, + items: [ + ZdsPopupMenuItem( + value: CalendarFormat.month, + child: ListTile( + visualDensity: VisualDensity.compact, + title: Text(ComponentStrings.of(context).get('MONTH', 'Month')), + ), + ), + ZdsPopupMenuItem( + value: CalendarFormat.week, + child: ListTile( + visualDensity: VisualDensity.compact, + title: Text(ComponentStrings.of(context).get('WEEK', 'Week')), + ), + ), + ], + builder: (_, open) => InkWell( + onTap: open, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8, left: 8, right: 4), + child: Row( + children: [ + Text( + _calendarFormat == CalendarFormat.week + ? ComponentStrings.of(context).get('WEEK', 'Week') + : ComponentStrings.of(context).get('MONTH', 'Month'), + style: Theme.of(context).textTheme.titleSmall!.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Icon( + Icons.arrow_drop_down, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + + final List weekNumbers = _focusedDay.getWeeksNumbersInMonth(startingDayOfWeek, _focusedDay); + final List weekStartDays = () { + DateTime firstDayOfWeeks = _focusedDay.startOfMonth.getFirstDayOfWeek(); + final List startDays = []; + while (firstDayOfWeeks.month == _focusedDay.month || firstDayOfWeeks.month == _focusedDay.month - 1) { + startDays.add(firstDayOfWeeks); + firstDayOfWeeks = DateTime(firstDayOfWeeks.year, firstDayOfWeeks.month, firstDayOfWeeks.day + 7); + } + return startDays; + }(); + + final allButtonBody = [ + Container( + alignment: Alignment.bottomCenter, + height: context.isSmallScreen() ? 20 : calendarDaysOfWeekHeight - 3, + child: Text( + widget.allCustomLabel ?? ComponentStrings.of(context).get('ALL', 'All'), + style: Theme.of(context).textTheme.titleSmall!.copyWith( + fontWeight: FontWeight.w500, + color: ZdsColors.greySwatch( + context, + )[Theme.of(context).colorScheme.brightness == Brightness.dark ? 700 : 900], + ), + ), + ), + SizedBox(height: 2, width: context.isSmallScreen() ? 20 : 2), + GestureDetector( + onTap: () { + setState( + () { + _selectedDay = null; + startOfweek = getDate( + _focusedDay.subtract(Duration(days: _focusedDay.weekday)), + ); + endOfweek = getDate( + _focusedDay.add(Duration(days: DateTime.daysPerWeek - _focusedDay.weekday - 1)), + ); + }, + ); + widget.onAllSelected?.call(startOfweek, endOfweek, _focusedDay); + }, + child: Container( + alignment: Alignment.center, + height: calendarRowHeight, + width: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + _selectedDay == null ? Theme.of(context).colorScheme.secondary : Theme.of(context).colorScheme.surface, + border: Border.fromBorderSide( + BorderSide( + color: _selectedDay == null + ? Theme.of(context).colorScheme.secondary + : ZdsColors.greySwatch(context)[400]!, + width: 1.5, + ), + ), + ), + ), + ), + ]; + + final allButton = Semantics( + selected: _selectedDay == null, + label: ComponentStrings.of(context).get('ALL', 'All'), + excludeSemantics: true, + child: Container( + color: Theme.of(context).colorScheme.surface, + height: context.isSmallScreen() ? null : calendarDaysOfWeekHeight + calendarRowHeight, + width: context.isSmallScreen() ? null : 48, + child: context.isSmallScreen() + ? Row(mainAxisAlignment: MainAxisAlignment.center, children: allButtonBody) + : Column(children: allButtonBody), + ), + ); + + return Material( + color: Theme.of(context).colorScheme.surface, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.hasHeader) calendarHeader, + if (widget.showAllButton && context.isSmallScreen()) Row(children: [Expanded(child: allButton)]), + AbsorbPointer( + absorbing: !widget.enabled, + child: Opacity( + opacity: widget.enabled ? 1.0 : 0.5, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.weekIcons != null) + Container( + color: Theme.of(context).colorScheme.surface, + width: context.isSmallScreen() ? 36 : 48, + child: () { + if (widget.weekIcons != null) { + final bool isWeekNumber = widget.weekIcons!.every( + (weekIcon) => weekIcon.weekNumber != null && weekIcon.year != null, + ); + final bool isFirstDayOfWeek = + widget.weekIcons!.every((weekIcon) => weekIcon.firstDayOfWeek != null); + if (isWeekNumber || isFirstDayOfWeek) { + final List items = isWeekNumber ? weekNumbers : weekStartDays; + return Column( + children: [ + const SizedBox(height: calendarDaysOfWeekHeight), + for (final index in items) + SizedBox( + height: calendarRowHeight, + child: () { + final WeekIcon? week = () { + if (isWeekNumber) { + if (widget.weekIcons!.any( + (weeks) => weeks.year == _focusedDay.year && weeks.weekNumber == index, + )) { + return widget.weekIcons!.firstWhere( + (weeks) => weeks.year == _focusedDay.year && weeks.weekNumber == index, + ); + } + } else { + if (index is DateTime && + widget.weekIcons! + .any((weeks) => weeks.firstDayOfWeek?.isSameDay(index) ?? false)) { + return widget.weekIcons! + .firstWhere((weeks) => weeks.firstDayOfWeek == index); + } + } + }(); + if (week != null) { + return Semantics( + label: week.semanticLabel, + child: IconTheme( + data: IconThemeData( + color: ZdsColors.greySwatch(context)[ + Theme.of(context).colorScheme.brightness == Brightness.dark + ? 700 + : 900], + size: context.isSmallScreen() ? 18 : 24, + ), + child: week.child, + ), + ); + } + }(), + ), + const SizedBox(height: 10), + ], + ); + } + } + return null; + }(), + ), + if (widget.showAllButton && !context.isSmallScreen()) allButton, + Expanded(child: calendar), + ], + ), + ), + ), + ], + ), + ); + } + + String _getCurrentLocaleString(BuildContext context) { + var currentLocale = const Locale('en', 'US'); + try { + currentLocale = Localizations.localeOf(context); + } catch (_) { + debugPrint('Failed to load Localizations.localeOf(context)'); + } + return '${currentLocale.languageCode}_${currentLocale.countryCode}'; + } + + List _getEventsForDay(DateTime day) { + return widget.events.where((event) => isSameDay(event.date, day)).toList(); + } + + bool _getHoliday(DateTime day) { + if (widget.holidayEvents.isEmpty) { + return false; + } else { + final isHoliday = widget.holidayEvents.contains(DateUtils.dateOnly(day)); + return isHoliday; + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('startOfweek', startOfweek)); + properties.add(DiagnosticsProperty('endOfweek', endOfweek)); + } +} + +/// To get Current week of start and end day. +DateTime getDate(DateTime d) => DateTime(d.year, d.month, d.day); + +/// Calendar Event model. +class CalendarEvent { + /// Id of the event. + final String id; + + /// Date / Time of the event. + final DateTime date; + + /// Constructs a [CalendarEvent]. + const CalendarEvent({required this.id, required this.date}); +} + +/// Model for weeks that should have leading icons. +/// +/// Should use either both `year` and `weekNumber` or `firstDayOfWeek`. +class WeekIcon { + /// Year of week. + final int? year; + + /// Week number in year. + final int? weekNumber; + + /// The first day of the week for the icon to show. + final DateTime? firstDayOfWeek; + + /// Widget to be displayed on the calendar. + /// + /// If an [Icon] is passed, the following styles are applied: + /// * color: `ZdsColors.greySwatch(context)[900 (or 700 in dark mode)]` + /// * size: `24` + final Widget child; + + /// Semantic label for icon. + final String? semanticLabel; + + /// Constructs a [WeekIcon]. + WeekIcon({ + required this.child, + this.year, + this.weekNumber, + this.firstDayOfWeek, + this.semanticLabel, + }) : assert( + year != null && weekNumber != null || firstDayOfWeek != null, + 'Should use either both year and weekNumber or firstDayOfWeek', + ); +} diff --git a/lib/src/components/organisms/date_range.dart b/lib/src/components/organisms/date_range.dart new file mode 100644 index 0000000..0ca99a8 --- /dev/null +++ b/lib/src/components/organisms/date_range.dart @@ -0,0 +1,391 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../../zds_flutter.dart'; + +/// A date range selector that also allows to quickly change the range selected by jumping to the next or +/// previous set of dates. +/// +/// This component is typically used as the title of a [ZdsToolbar], allowing to switch the body's content depending +/// on the selected range. +/// +/// ```dart +/// ZdsToolbar( +/// title: DateRange( +/// emptyLabel: 'Select range', +/// actions: [ +/// ZdsButton.text( +/// child: const Text('Fiscal View'), +/// onTap: () {}, +/// ), +/// ], +/// ), +/// ) +/// ``` +/// +/// When no range has been selected, the [emptyLabel] text will be shown. Tapping on this opens a full-screen date +/// range selector. Once the date range has been selected, it will be shown on the component. +/// +/// The user can quickly jump forwards and backwards by pressing on the chevron icon buttons. For example, if the date +/// range is March 1 - March 5, the previous date range will be February 24 - February 28, and the next one will be +/// March 6 - March 10. You can keep track of these changes with [onChange]. +/// +/// See also: +/// +/// * [ZdsDateRangePickerTile], which allows to select a date range's start and end time separately. +/// * [ZdsDateTimePicker], a widget that allow to select a single date, a single time, or both. +class DateRange extends StatefulWidget { + static const _kYearsFromNow = 20; + + /// The earliest date that can be chosen for the range. + /// + /// Defaults to 1/1/1999. + final DateTime? firstDate; + + /// The last date that can be chosen for the range. + /// + /// Defaults to 20 years in the future from now. + final DateTime? lastDate; + + /// The empty label to show when no date has been chosen. + /// + /// Defaults to an empty String. + final String emptyLabel; + + /// Widgets shown when choosing a date range for additional actions other than clearing and applying. + /// + /// Typically a list of [IconButton]. + final List? actions; + + /// The initial date range to show when first building this widget. + /// + /// If null, no range will be shown at first. + final DateTimeRange? initialDateRange; + + /// Callback function called whenever the selected date range changes. + final void Function(DateTimeRange?)? onChange; + + /// The textStyle to use for the button to open the date range selector. + /// + /// Defaults to [TextTheme.headlineMedium]. + final TextStyle? textStyle; + + /// The string to use for the clear range button. + /// + /// Defaults to 'Clear'. + final String? clearButtonString; + + /// The string to use for the apply range button. + /// + /// Defaults to 'Apply'. + final String? applyButtonString; + + /// Used for making the dateRange clickable + /// + /// Defaults to 'True'. + final bool isSelectable; + + /// Used as a date range separator + /// + /// Defaults to '-'. + final String dateRangeSeparator; + + /// Tooltip to go with the previous icon. + final String? previousTooltip; + + /// Tooltip to go with the next icon. + final String? nextTooltip; + + /// If true, user can only select weeks, not days in popup. + /// + /// If being used to show weeks, user should not be able to select a random selection of days. + /// + /// Defaults to false. + final bool isWeekMode; + + /// 0 indexed where Sunday is 0 and Saturday is 6 + /// + /// Defaults to 0. + final int startDayOfWeek; + + /// to change date format of date range + final String? dateFormat; + + /// Creates a date range selector. + const DateRange({ + super.key, + this.firstDate, + this.lastDate, + this.initialDateRange, + this.onChange, + this.actions, + this.emptyLabel = '', + this.textStyle, + this.clearButtonString, + this.applyButtonString, + this.isSelectable = true, + this.dateRangeSeparator = '-', + this.nextTooltip, + this.previousTooltip, + this.isWeekMode = false, + this.startDayOfWeek = 0, + this.dateFormat, + }) : assert( + !isWeekMode || (startDayOfWeek >= 0 && startDayOfWeek <= 6), + 'startingDayOfWeek must be an int between 0 and 6', + ); + + @override + DateRangeState createState() => DateRangeState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('firstDate', firstDate)) + ..add(DiagnosticsProperty('lastDate', lastDate)) + ..add(StringProperty('emptyLabel', emptyLabel)) + ..add(DiagnosticsProperty('initialDateRange', initialDateRange)) + ..add(ObjectFlagProperty.has('onChange', onChange)) + ..add(DiagnosticsProperty('textStyle', textStyle)) + ..add(StringProperty('clearButtonString', clearButtonString)) + ..add(StringProperty('applyButtonString', applyButtonString)) + ..add(DiagnosticsProperty('isSelectable', isSelectable)) + ..add(StringProperty('dateRangeSeparator', dateRangeSeparator)) + ..add(StringProperty('previousTooltip', previousTooltip)) + ..add(StringProperty('nextTooltip', nextTooltip)) + ..add(DiagnosticsProperty('isWeekMode', isWeekMode)) + ..add(IntProperty('startDayOfWeek', startDayOfWeek)); + properties.add(StringProperty('dateFormat', dateFormat)); + } +} + +/// State for [DateRange]. +class DateRangeState extends State { + DateTimeRange? _selectedDateRange; + Duration? _diff; + + @override + void didUpdateWidget(DateRange oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.initialDateRange != null && widget.initialDateRange != oldWidget.initialDateRange) { + _selectedDateRange = DateTimeRange( + start: widget.initialDateRange!.start.toMidnight, + end: widget.initialDateRange!.end.toMidnight, + ); + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + if (widget.initialDateRange != null) { + _selectedDateRange = DateTimeRange( + start: widget.initialDateRange!.start.toMidnight, + end: widget.initialDateRange!.end.toMidnight, + ); + } + } + + Future _showDateSelector(BuildContext context) async { + final range = await showZdsDateRangePicker( + context: context, + actions: widget.actions, + initialDateRange: _selectedDateRange, + useRootNavigator: false, + firstDate: widget.firstDate ?? DateTime(1999), + lastDate: widget.lastDate ?? DateTime(DateTime.now().year + DateRange._kYearsFromNow), + initialEntryMode: DatePickerEntryMode.calendarOnly, + clearButtonString: widget.clearButtonString, + applyButtonString: widget.applyButtonString, + isWeekMode: widget.isWeekMode, + startingDayOfWeek: widget.startDayOfWeek, + ); + if (mounted) { + setState(() { + if (range != null) { + _selectedDateRange = range; + } + }); + + widget.onChange?.call(range); + } + } + + void _nextDateRange() { + if (_selectedDateRange == null) { + return; + } + + if (_diff == null) _setDiff(); + + setState(() { + // If current whole month is selected then select next whole month + if (_selectedDateRange!.isWholeMonth) { + final start = DateTime( + _selectedDateRange!.start.year, + _selectedDateRange!.start.month + 1, + _selectedDateRange!.start.day, + ).startOfMonth; + _selectedDateRange = DateTimeRange(start: start, end: start.endOfMonth); + } else { + _selectedDateRange = DateTimeRange( + start: _selectedDateRange!.start.add(_diff!), + end: _selectedDateRange!.end.add(_diff!), + ); + } + + widget.onChange?.call(_selectedDateRange); + }); + } + + void _prevDateRange() { + if (_selectedDateRange == null) { + return; + } + if (_diff == null) _setDiff(); + setState(() { + // If current whole month is selected then select previous whole month + if (_selectedDateRange!.isWholeMonth) { + final start = DateTime( + _selectedDateRange!.start.year, + _selectedDateRange!.start.month - 1, + _selectedDateRange!.start.day, + ).startOfMonth; + + _selectedDateRange = DateTimeRange(start: start, end: start.endOfMonth); + } else { + _selectedDateRange = DateTimeRange( + start: _selectedDateRange!.start.subtract(_diff!), + end: _selectedDateRange!.end.subtract(_diff!), + ); + } + + widget.onChange?.call(_selectedDateRange); + }); + } + + bool _isBeforeFirstDate() { + if (_selectedDateRange != null && widget.firstDate != null) { + return _selectedDateRange!.start.isBefore(widget.firstDate!); + } + return false; + } + + bool _isAfterLastDate() { + if (_selectedDateRange != null && widget.lastDate != null) { + return _selectedDateRange!.end.isAfter(widget.lastDate!); + } + return false; + } + + void _setDiff() { + if (_selectedDateRange != null) { + setState( + () => _diff = _selectedDateRange!.end.add(const Duration(days: 1)).difference(_selectedDateRange!.start), + ); + } + } + + @override + void initState() { + super.initState(); + if (_selectedDateRange != null) { + _diff = _selectedDateRange!.end.add(const Duration(days: 1)).difference(_selectedDateRange!.start); + } + } + + @override + Widget build(BuildContext context) { + final textStyle = widget.textStyle ?? Theme.of(context).primaryTextTheme.bodyLarge!; + final text = DefaultTextStyle( + style: textStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: Text( + _selectedDateRange == null ? widget.emptyLabel : _formatRange(context, dateFormat: widget.dateFormat), + ), + ); + return Row( + children: [ + IconButton( + padding: EdgeInsets.zero, + onPressed: _isBeforeFirstDate() ? null : _prevDateRange, + tooltip: widget.previousTooltip, + splashRadius: 24, + icon: Icon(ZdsIcons.chevron_left, color: textStyle.color!.withOpacity(_isBeforeFirstDate() ? 0.5 : 1)), + ), + if (!widget.isSelectable) text, + if (widget.isSelectable) + InkWell( + borderRadius: const BorderRadius.all(Radius.circular(71)), + onTap: () async => _showDateSelector(context), + child: text.paddingInsets(const EdgeInsets.symmetric(vertical: 8)), + ), + IconButton( + padding: EdgeInsets.zero, + onPressed: _isAfterLastDate() ? null : _nextDateRange, + splashRadius: 24, + tooltip: widget.nextTooltip, + icon: Icon(ZdsIcons.chevron_right, color: textStyle.color!.withOpacity(_isAfterLastDate() ? 0.5 : 1)), + ), + ], + ); + } + + /// Returns a locale-appropriate string to describe the start of a date range. + String _formatRange(BuildContext context, {String? dateFormat}) { + if (_selectedDateRange == null) return ''; + final localizations = MaterialLocalizations.of(context); + if (_selectedDateRange!.isWholeMonth) { + return localizations.formatMonthYear(_selectedDateRange!.start); + } + final startDateFormatted = dateFormat != null + ? DateFormat(dateFormat).format(_selectedDateRange!.start) + : _formatStartDate( + localizations, + _selectedDateRange?.start, + _selectedDateRange?.end, + ); + + final endDateFormatted = dateFormat != null + ? DateFormat(dateFormat).format(_selectedDateRange!.end) + : _formatEndDate( + localizations, + _selectedDateRange?.start, + _selectedDateRange?.end, + ); + + return '$startDateFormatted ${widget.dateRangeSeparator} $endDateFormatted'; + } + + /// Returns a locale-appropriate string to describe the start of a date range. + /// + /// If `startDate` is null, then it defaults to 'Start Date', otherwise if it + /// is in the same year as the `endDate` then it will use the short month + /// day format (i.e. 'Jan 21'). Otherwise it will return the short date format + /// (i.e. 'Jan 21, 2020'). + String _formatStartDate(MaterialLocalizations localizations, DateTime? startDate, DateTime? endDate) { + return startDate == null + ? '' + : (endDate == null || startDate.year == endDate.year) + ? localizations.formatShortMonthDay(startDate) + : localizations.formatShortDate(startDate); + } + + /// Returns an locale-appropriate string to describe the end of a date range. + /// + /// If `endDate` is null, then it defaults to 'End Date', otherwise if it + /// is in the same year as the `startDate` and the `currentDate` then it will + /// just use the short month day format (i.e. 'Jan 21'), otherwise it will + /// include the year (i.e. 'Jan 21, 2020'). + String _formatEndDate(MaterialLocalizations localizations, DateTime? startDate, DateTime? endDate) { + return endDate == null + ? '' + : (startDate != null && startDate.year == endDate.year && startDate.year == DateTime.now().year) + ? localizations.formatShortMonthDay(endDate) + : localizations.formatShortDate(endDate); + } +} diff --git a/lib/src/components/organisms/date_range_picker_tile.dart b/lib/src/components/organisms/date_range_picker_tile.dart new file mode 100644 index 0000000..dc044c7 --- /dev/null +++ b/lib/src/components/organisms/date_range_picker_tile.dart @@ -0,0 +1,543 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../../zds_flutter.dart'; + +const double _screenColumnBreakpoint = 240; +const double _padding = 20; +const double _fontLineHeight = 26; + +/// Encapsulates a start and end [DateTime] that represent the range of dates. +/// +/// Unlike [DateTimeRange], start can be before end. This allows for validation in [ZdsDateRangePickerTileForm] +/// and allows for more flexibility. +/// +/// See also: +/// * [DateTimeRange] +class ZdsDateTimeRange { + /// The start of the range of dates. + final DateTime? start; + + /// The end of the range of dates. + final DateTime? end; + + /// Constructor for [ZdsDateTimeRange]. + const ZdsDateTimeRange({this.start, this.end}); + + /// Creates a new [ZdsDateTimeRange] from this one by updating individual properties. + ZdsDateTimeRange copyWith({DateTime? start, DateTime? end}) { + return ZdsDateTimeRange(start: start ?? this.start, end: end ?? this.end); + } + + /// Checks validity of DateTimeRange. + bool get isValid => start != null && end != null && start!.compareTo(end!) <= 0; + + /// Checks if either start or end is not set. + bool get isIncomplete => start == null || end == null; + + /// Constructs a [ZdsDateTimeRange] from a [DateTimeRange]. + ZdsDateTimeRange.fromDateTimeRange(DateTimeRange dateTimeRange) + : start = dateTimeRange.start, + end = dateTimeRange.end; + + /// Constructs a [DateTimeRange] from an instance of [ZdsDateTimeRange] only if [isValid]. + DateTimeRange? get toDateTimeRange { + if (isValid) return DateTimeRange(start: start!, end: end!); + return null; + } +} + +/// A DateRange picker tile that allows to select the from and to dates separately with [Form] validation. +/// +/// See also: +/// * [Form] +/// * [FormField] +/// * [ZdsDateTimeRange] +/// * [ZdsDateRangePickerTile] +class ZdsDateRangePickerTileForm extends FormField { + /// Constructor for [ZdsDateRangePickerTileForm]. + /// + /// Default values: + /// * [initialValue] : `const ZdsDateTimeRange()` + /// * [validator] : `d.isValid ? null : ''` + /// * [format] : `dd/MM/yyyy` + /// * [autovalidateMode] : `AutovalidateMode.onUserInteraction` + ZdsDateRangePickerTileForm({ + String? Function(ZdsDateTimeRange)? validator, + ZdsDateTimeRange initialValue = const ZdsDateTimeRange(), + String? initialHelpText, + String? finalHelpText, + DateTime? earliestSelectableDate, + DateTime? latestSelectableDate, + String format = 'dd/MM/yyyy', + AutovalidateMode autovalidateMode = AutovalidateMode.onUserInteraction, + void Function(ZdsDateTimeRange)? onSaved, + super.key, + }) : super( + validator: (ZdsDateTimeRange? d) { + if (d == null) return null; + if (validator != null) return validator(d); + return d.isValid ? null : ''; + }, + initialValue: initialValue, + autovalidateMode: autovalidateMode, + onSaved: (d) { + if (d != null && onSaved != null) onSaved(d); + }, + builder: (FormFieldState state) { + return Builder( + builder: (context) { + return ZdsCard( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final double scale = MediaQuery.of(context).textScaleFactor; + final double width = _calculateWidth(constraints, scale); + final bool isColumn = constraints.maxWidth <= _screenColumnBreakpoint * scale; + final List fields = [ + _DateField( + date: state.value?.start ?? initialValue.start, + format: format, + initialSelectableDate: earliestSelectableDate, + finalSelectableDate: latestSelectableDate, + helpText: initialHelpText, + updateDate: (newValue) { + state.didChange((state.value ?? initialValue).copyWith(start: newValue)); + }, + validator: (value) => state.hasError ? '' : null, + isInitialDate: true, + width: width, + scale: scale, + ), + SizedBox( + width: isColumn ? 0 : (constraints.maxWidth - (width * 2)) / 2, + height: isColumn ? 8 : 0, + ), + _DateField( + date: state.value?.end ?? initialValue.end, + format: format, + initialSelectableDate: earliestSelectableDate, + finalSelectableDate: latestSelectableDate, + validator: (value) => state.hasError ? '' : null, + helpText: finalHelpText, + updateDate: (newValue) { + state.didChange((state.value ?? initialValue).copyWith(end: newValue)); + }, + width: width, + scale: scale, + ), + ]; + DateTime.now().copyWith(); + if (isColumn) { + return Column( + children: fields.divide(const SizedBox(height: 8)).toList(), + ); + } + return Row(children: fields); + }, + ), + ), + ], + ), + if (state.hasError) + Text( + state.errorText ?? '', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: Theme.of(context).colorScheme.error), + ).paddingOnly(top: 4), + ], + ), + ); + }, + ); + }, + ); +} + +/// A DateRange picker tile that allows to select the from and to dates separately. +/// This can be used if we want to allow the user to select all the dates before a certain date, or all the dates after +/// one. +/// +/// This Widget keeps track of the dates selected. If you want to synchronise these dates with the parent's state, use +/// [onInitialDateChanged] and [onFinalDateChanged]. +/// +/// See also: +/// +/// * [ZdsDateTimePicker], which allows to select a day and hour together or separately +/// * [showDatePicker] to show a date picker directly +class ZdsDateRangePickerTile extends StatefulWidget { + /// The DateTime selected in the "From" field. Set this if you want pre-initialized dates. + /// + /// If no date is selected, the field will be blank. + final DateTime? initialDate; + + /// The DateTime selected in the "To" field. Set this if you want pre-initialized dates. + /// + /// If no date is selected, the field will be blank. + final DateTime? finalDate; + + /// The text displayed at the top of the initial date picker window. + final String? initialHelpText; + + /// The text displayed at the top of the final date picker window. + final String? finalHelpText; + + /// A function called whenever the initial date changes. Use this to synchronise the DateRange's date with a date in + /// the parent's state. + final void Function(DateTime?)? onInitialDateChanged; + + /// A function called whenever the final date changes. Use this to synchronise the DateRange's date with a date in + /// the parent's state. + final void Function(DateTime?)? onFinalDateChanged; + + /// The earliest date that can be selected. Must be before the [latestSelectableDate]. + /// + /// Defaults to 10 years in the past. + final DateTime? earliestSelectableDate; + + /// The latest date that can be selected. Must be after the [earliestSelectableDate]. + /// + /// Defaults to 10 years in the future. + final DateTime? latestSelectableDate; + + /// A controller used to keep track of the selected initial date. + final ZdsValueController? initialDateController; + + /// A controller used to keep track of the selected final date. + final ZdsValueController? finalDateController; + + /// The format in which the [DateTime] will be formatted. + /// + /// See [DateFormat] for more details. + final String format; + + /// The error message shown when an invalid date range is entered. + final String errorMessage; + + /// The key attached to the form within the picker that can be used to check the validation of the inputs. + final GlobalKey? formKey; + + /// A DateRangePicker that allows to pick the "From" and "To" dates separately. + /// + /// If both are set, [earliestSelectableDate] must be on or before [latestSelectableDate]. + ZdsDateRangePickerTile({ + super.key, + this.initialDate, + this.finalDate, + this.onInitialDateChanged, + this.onFinalDateChanged, + this.earliestSelectableDate, + this.latestSelectableDate, + this.initialDateController, + this.finalDateController, + this.format = 'dd/MM/yyyy', + this.initialHelpText, + this.finalHelpText, + this.errorMessage = '', + this.formKey, + }) : assert( + (earliestSelectableDate != null && latestSelectableDate != null) + ? earliestSelectableDate.isBefore(latestSelectableDate) + : earliestSelectableDate == null, + 'Earliest selectable date must be before latest selectable date', + ); + + @override + State createState() => _ZdsDateRangePickerTileState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('initialDate', initialDate)); + properties.add(DiagnosticsProperty('finalDate', finalDate)); + properties.add(StringProperty('initialHelpText', initialHelpText)); + properties.add(StringProperty('finalHelpText', finalHelpText)); + properties.add(ObjectFlagProperty.has('onInitialDateChanged', onInitialDateChanged)); + properties.add(ObjectFlagProperty.has('onFinalDateChanged', onFinalDateChanged)); + properties.add(DiagnosticsProperty('earliestSelectableDate', earliestSelectableDate)); + properties.add(DiagnosticsProperty('latestSelectableDate', latestSelectableDate)); + properties.add(DiagnosticsProperty?>('initialDateController', initialDateController)); + properties.add(DiagnosticsProperty?>('finalDateController', finalDateController)); + properties.add(StringProperty('format', format)); + properties.add(StringProperty('errorMessage', errorMessage)); + properties.add(DiagnosticsProperty?>('formKey', formKey)); + } +} + +double _calculateWidth(BoxConstraints constraints, double scale) { + final double maxWidthScale = _screenColumnBreakpoint * scale; + final double maxScale = (_screenColumnBreakpoint / 2) * scale; + if (constraints.maxWidth < maxWidthScale) { + return maxWidthScale.clamp(0, maxScale); + } else { + final double calculatedWidth = ((constraints.maxWidth - _padding) / 2) * scale; + return calculatedWidth.clamp(0, maxScale); + } +} + +class _ZdsDateRangePickerTileState extends State { + DateTime? initialDate; + DateTime? finalDate; + + late final GlobalKey formKey; + + @override + void initState() { + super.initState(); + initialDate = + widget.initialDate ?? (widget.initialDateController != null ? widget.initialDateController!.value : null); + finalDate = widget.finalDate ?? (widget.finalDateController != null ? widget.finalDateController!.value : null); + + widget.initialDateController?.updateListener = (value) { + setState(() { + initialDate = value; + }); + widget.initialDateController?.notifyListeners(value); + }; + widget.finalDateController?.updateListener = (value) { + setState(() { + finalDate = value; + }); + widget.finalDateController?.notifyListeners(value); + }; + + formKey = widget.formKey ?? GlobalKey(); + } + + @override + Widget build(BuildContext context) { + return ZdsCard( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: LayoutBuilder( + builder: (context, constraints) { + final double scale = MediaQuery.of(context).textScaleFactor; + final double width = _calculateWidth(constraints, scale); + final bool isColumn = constraints.maxWidth <= _screenColumnBreakpoint * scale; + final List fields = [ + _DateField( + date: initialDate, + format: widget.format, + onDateChanged: widget.onInitialDateChanged, + initialSelectableDate: widget.earliestSelectableDate, + finalSelectableDate: widget.latestSelectableDate, + helpText: widget.initialHelpText, + updateDate: (newValue) { + setState(() => initialDate = newValue); + widget.initialDateController?.notifyListeners(newValue); + }, + validator: (value) { + if (value != null && (finalDate != null && value.isAfter(finalDate!))) { + return widget.errorMessage; + } + return null; + }, + isInitialDate: true, + width: width, + scale: scale, + ), + SizedBox( + width: isColumn ? 0 : (constraints.maxWidth - (width * 2)) / 2, + height: isColumn ? 8 : 0, + ), + _DateField( + date: finalDate, + format: widget.format, + onDateChanged: widget.onFinalDateChanged, + initialSelectableDate: widget.earliestSelectableDate, + finalSelectableDate: widget.latestSelectableDate, + helpText: widget.finalHelpText, + updateDate: (newValue) { + setState(() => finalDate = newValue); + widget.finalDateController?.notifyListeners(newValue); + }, + validator: (value) { + if (value != null && (initialDate != null && value.isBefore(initialDate!))) { + return widget.errorMessage; + } + return null; + }, + width: width, + scale: scale, + ), + ]; + + if (isColumn) { + return Column( + children: fields.divide(const SizedBox(height: 8)).toList(), + ); + } + return Row(children: fields); + }, + ), + ), + ), + ], + ), + if (initialDate != null && + finalDate != null && + formKey.currentState != null && + !formKey.currentState!.validate()) + Text( + widget.errorMessage, + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.error), + ).paddingOnly(top: 4), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('initialDate', initialDate)); + properties.add(DiagnosticsProperty('finalDate', finalDate)); + properties.add(DiagnosticsProperty>('formKey', formKey)); + } +} + +class _DateField extends StatelessWidget { + final DateTime? date; + final DateTime? initialSelectableDate; + final DateTime? finalSelectableDate; + final bool isInitialDate; + final void Function(DateTime?)? onDateChanged; + final void Function(DateTime?) updateDate; + final String format; + final String? helpText; + final double width; + final double scale; + final String? Function(DateTime?)? validator; + + const _DateField({ + required this.updateDate, + required this.date, + required this.format, + required this.width, + required this.scale, + this.validator, + this.initialSelectableDate, + this.finalSelectableDate, + this.helpText, + this.onDateChanged, + this.isInitialDate = false, + }); + + @override + Widget build(BuildContext context) { + return FormField( + validator: validator, + autovalidateMode: AutovalidateMode.onUserInteraction, + builder: (state) => Container( + width: width, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: state.hasError ? Theme.of(context).colorScheme.error : ZdsColors.lightGrey, + ), + ), + ), + child: MergeSemantics( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isInitialDate + ? ComponentStrings.of(context).get('FROM', 'From') + : ComponentStrings.of(context).get('TO', 'To'), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: ZdsColors.greySwatch(context)[1000]), + ).paddingOnly(left: 2), + InkWell( + onTap: () async { + final DateTime initialPickerDate; + if (date != null) { + initialPickerDate = date!; + } else if (initialSelectableDate != null) { + if (initialSelectableDate!.isAfter(DateTime.now())) { + initialPickerDate = initialSelectableDate!; + } else { + initialPickerDate = DateTime.now(); + } + } else { + initialPickerDate = DateTime.now(); + } + final DateTime? selectedDate = await showDatePicker( + context: context, + initialDate: initialPickerDate, + firstDate: initialSelectableDate ?? DateTime.now().subtract(const Duration(days: 365 * 10)), + lastDate: finalSelectableDate ?? DateTime.now().add(const Duration(days: 365 * 10)), + initialEntryMode: DatePickerEntryMode.calendarOnly, + helpText: helpText, + builder: (context, child) { + return Theme( + data: Theme.of(context).zdsDateTimePickerTheme, + child: child!, + ); + }, + ); + state.didChange(selectedDate); + if (selectedDate != null) updateDate(selectedDate); + if (onDateChanged != null) onDateChanged?.call(selectedDate); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + height: _fontLineHeight * scale, + child: date != null + ? Text( + date!.format(format), + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + height: + (_fontLineHeight / Theme.of(context).textTheme.bodyLarge!.fontSize!) / scale, + ), + ) + : null, + ), + Icon( + ZdsIcons.calendar, + color: ZdsColors.greySwatch(context)[800], + size: 22, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('date', date)); + properties.add(DiagnosticsProperty('initialSelectableDate', initialSelectableDate)); + properties.add(DiagnosticsProperty('finalSelectableDate', finalSelectableDate)); + properties.add(DiagnosticsProperty('isInitialDate', isInitialDate)); + properties.add(ObjectFlagProperty.has('onDateChanged', onDateChanged)); + properties.add(ObjectFlagProperty.has('updateDate', updateDate)); + properties.add(StringProperty('format', format)); + properties.add(StringProperty('helpText', helpText)); + properties.add(DoubleProperty('width', width)); + properties.add(DoubleProperty('scale', scale)); + properties.add(ObjectFlagProperty.has('validator', validator)); + } +} diff --git a/lib/src/components/organisms/day_picker.dart b/lib/src/components/organisms/day_picker.dart new file mode 100644 index 0000000..0f452db --- /dev/null +++ b/lib/src/components/organisms/day_picker.dart @@ -0,0 +1,329 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../../zds_flutter.dart'; + +/// Model for days used in [ZdsDayPicker]. +class DayDetails { + /// Index for the day. + final DateTime date; + + /// Text for displaying day. + final String dayText; + + /// Enable or disable day. + final bool isDisabled; + + /// Selected or unselected. + bool isChecked; + + /// Constructs a [DayDetails]. + DayDetails({ + required this.date, + required this.dayText, + required this.isDisabled, + this.isChecked = false, + }); +} + +/// A widget that allow to select days. +class ZdsDayPicker extends StatefulWidget { + /// Starting date of the week. + final DateTime startingWeekDate; + + /// The default selected dates. + final List? initialSelectedDates; + + /// Text used for showing header at the beginning. + final String? header; + + /// Text for All button. + /// + /// Defaults to 'All'. + final String? allText; + + /// Disable days selection based on the days index, Sunday = 1, Monday = 2, ... + /// + /// Defaults to empty list. + final List disableDaysList; + + /// Method gets triggered on the click of a day. + final void Function(List)? onDaySelected; + + /// Used to enable the selection of multiple days. + /// + /// Defaults to false. + final bool allowMultiSelect; + + /// Used to move the component inside/outside a card; + /// + /// Defaults to 'true'. + final bool showInCard; + + /// Used to enable / disable the component. + /// + /// Defaults to true. + final bool enabled; + + /// Constructs a [ZdsDayPicker]. + const ZdsDayPicker({ + required this.startingWeekDate, + this.initialSelectedDates, + this.header, + this.allText, + this.disableDaysList = const [], + this.onDaySelected, + this.allowMultiSelect = false, + this.showInCard = true, + this.enabled = true, + super.key, + }) : assert( + (initialSelectedDates?.length ?? 0) <= 1 || allowMultiSelect, + 'Wrong configuration allowMultiSelect=false and initialSelectedDates has multiple dates', + ); + + @override + State createState() => _ZdsDayPickerState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('startingWeekDate', startingWeekDate)) + ..add(IterableProperty('initialSelectedDates', initialSelectedDates)) + ..add(StringProperty('header', header)) + ..add(StringProperty('allText', allText)) + ..add(IterableProperty('disableDaysList', disableDaysList)) + ..add(ObjectFlagProperty p1)?>.has('onDaySelected', onDaySelected)) + ..add(DiagnosticsProperty('allowMultiSelect', allowMultiSelect)) + ..add(DiagnosticsProperty('showInCard', showInCard)) + ..add(DiagnosticsProperty('enabled', enabled)); + } +} + +class _ZdsDayPickerState extends State { + List weekDays = []; + int disabledCount = 0; + bool isSelectedAllDay = false; + List selectedDates = []; + final dayPickerController = TextEditingController(); + + @override + void initState() { + super.initState(); + + if (widget.initialSelectedDates != null) { + selectedDates.addAll(getDatesOnly(widget.initialSelectedDates!)); + } + + _computeWeekDaysDetails(); + _updateDayController(selectedDates); + } + + @override + void didUpdateWidget(ZdsDayPicker oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.initialSelectedDates != widget.initialSelectedDates && widget.initialSelectedDates != null) { + selectedDates = getDatesOnly(widget.initialSelectedDates!); + + _computeWeekDaysDetails(); + _updateDayController(selectedDates); + } + } + + void _updateDayController(List selectedDates) { + selectedDates.sort((first, next) => first.compareTo(next)); + final List dayBuffer = []; + for (final date in selectedDates) { + dayBuffer.add(DateFormat('EEE').format(date)); + } + dayPickerController.value = dayPickerController.value.copyWith(text: dayBuffer.join(', ')); + } + + void _computeWeekDaysDetails() { + weekDays = []; + disabledCount = widget.disableDaysList.isEmpty ? 0 : widget.disableDaysList.length; + + for (int index = 0; index < 7; index++) { + final DateTime date = DateUtils.dateOnly(widget.startingWeekDate.add(Duration(days: index))); + weekDays.add( + DayDetails( + date: date, + isChecked: _isDateMatchingForSelectedDate(date), + dayText: DateFormat('EEE').format(date), + isDisabled: widget.disableDaysList.contains(index), + ), + ); + } + } + + void _updateDayDetails(DayDetails selectedDayDetail) { + setState(() { + selectedDayDetail.isChecked = !selectedDayDetail.isChecked; + if (widget.allowMultiSelect) { + selectedDayDetail.isChecked + ? selectedDates.add(DateUtils.dateOnly(selectedDayDetail.date)) + : selectedDates.remove(DateUtils.dateOnly(selectedDayDetail.date)); + } else { + if (selectedDayDetail.isChecked) { + if (selectedDates.length == 1) { + for (final dayDetail in weekDays) { + if (selectedDayDetail.dayText != dayDetail.dayText && dayDetail.isChecked) { + dayDetail.isChecked = false; + selectedDates + ..remove(DateUtils.dateOnly(dayDetail.date)) + ..add(DateUtils.dateOnly(selectedDayDetail.date)); + break; + } + } + } else { + selectedDates.add(DateUtils.dateOnly(selectedDayDetail.date)); + } + } else { + if (selectedDates.length > 1) { + for (final dayDetail in weekDays) { + if (selectedDayDetail.dayText != dayDetail.dayText) { + dayDetail.isChecked = false; + selectedDates.remove(DateUtils.dateOnly(dayDetail.date)); + } + } + selectedDayDetail.isChecked = !selectedDayDetail.isChecked; + } else { + selectedDates.remove(DateUtils.dateOnly(selectedDayDetail.date)); + } + } + } + widget.onDaySelected?.call(selectedDates); + + _updateDayController(selectedDates); + }); + } + + void selectAll() { + setState(() { + if ((selectedDates.length + widget.disableDaysList.length) == 7) { + selectedDates = []; + for (final dayDetail in weekDays) { + if (!dayDetail.isDisabled) { + dayDetail.isChecked = false; + } + } + } else { + selectedDates = []; + for (final dayDetail in weekDays) { + if (!dayDetail.isDisabled) { + dayDetail.isChecked = true; + selectedDates.add(dayDetail.date); + } + } + } + _updateDayController(selectedDates); + }); + widget.onDaySelected?.call(selectedDates); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final double pillWidth = 68 * MediaQuery.of(context).textScaleFactor; + const pillSpacing = 4; + const double horizontalPadding = 12; + + final totalLength = (weekDays.length * (pillWidth + pillSpacing)) + (horizontalPadding * 2); + final isWrapping = totalLength > constraints.maxWidth; + + final body = Row( + children: [ + Expanded( + child: Wrap( + alignment: isWrapping ? WrapAlignment.start : WrapAlignment.center, + children: [ + ...weekDays.map( + (dayDetail) { + return SizedBox( + width: pillWidth, + child: ZdsSelectionPill( + selected: dayDetail.isChecked, + label: dayDetail.dayText, + onTap: dayDetail.isDisabled || !widget.enabled + ? null + : () => setState(() => _updateDayDetails(dayDetail)), + padding: const EdgeInsets.all(4), + ), + ); + }, + ), + ], + ), + ), + ], + ); + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (widget.header != null) + Text( + widget.header!, + style: Theme.of(context).textTheme.titleSmall, + ).paddingOnly(left: 6), + if (widget.allowMultiSelect) + SizedBox( + height: 34, + width: 54, + child: ZdsButton.text( + textPadding: EdgeInsets.zero, + onTap: widget.enabled ? selectAll : null, + child: Text(widget.allText ?? ComponentStrings.of(context).get('ALL', 'All')), + ), + ), + ], + ).paddingInsets( + widget.allowMultiSelect ? const EdgeInsets.only(left: 4) : const EdgeInsets.only(left: 4, bottom: 8), + ), + if (widget.showInCard) + ZdsCard( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: horizontalPadding), + child: body, + ) + else + body, + ], + ); + }, + ); + } + + /// This loop is to check if date falls in selected dates or not. + bool _isDateMatchingForSelectedDate(DateTime date) { + for (final DateTime selectedDate in selectedDates) { + if (selectedDate.isSameDay(date)) { + return true; + } + } + return false; + } + + List getDatesOnly(List dateList) { + final List returnList = []; + for (final completeDate in dateList) { + returnList.add(DateUtils.dateOnly(completeDate)); + } + return returnList; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IterableProperty('weekDays', weekDays)) + ..add(IntProperty('disabledCount', disabledCount)) + ..add(DiagnosticsProperty('isSelectedAllDay', isSelectedAllDay)) + ..add(IterableProperty('selectedDates', selectedDates)) + ..add(DiagnosticsProperty('dayPickerController', dayPickerController)); + } +} diff --git a/lib/src/components/organisms/file_picker/file_annotations.dart b/lib/src/components/organisms/file_picker/file_annotations.dart new file mode 100644 index 0000000..d780093 --- /dev/null +++ b/lib/src/components/organisms/file_picker/file_annotations.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_editor_plus/image_editor_plus.dart'; + +import '../../../../zds_flutter.dart'; + +/// Editors used to edit only image files & to launch other types of files +class ZdsImageAnnotationPostProcessor implements ZdsFilePostProcessor { + ///default constructor + ZdsImageAnnotationPostProcessor( + this.buildContext, { + this.initialCropAspectRatio = ZdsAspectRatio.ratio1x1, + }); + + ///Context used for navigations and toasts + final BuildContextProvider buildContext; + + /// Initial Aspect ratio of crop rect + /// default is [ZdsAspectRatio.original] + /// + /// The argument only affects the initial aspect ratio. + final ZdsAspectRatio initialCropAspectRatio; + + @override + Future process(FilePickerConfig config, FileWrapper file) async { + if (kIsWeb) return file; + + if (file.isImage() && file.content != null) { + final originalFile = File(file.xFilePath); + final result = await _editFile(buildContext.call(), originalFile); + if (result != null) { + return FileWrapper(file.type, result); + } + } + + return file; + } + + Future _editFile(BuildContext context, File originalFile) async { + ImageEditor.i18n(ComponentStrings.of(context).getAll()); + final bytes = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return SingleImageEditor( + image: originalFile.readAsBytesSync(), + ); + }, + ), + ); + + if (bytes != null) { + await originalFile.delete(recursive: true); + final result = File(originalFile.path); + await result.writeAsBytes(bytes); + return ZdsXFile.fromFile(result); + } else { + return null; + } + } +} diff --git a/lib/src/components/organisms/file_picker/file_compress.dart b/lib/src/components/organisms/file_picker/file_compress.dart new file mode 100644 index 0000000..2871a69 --- /dev/null +++ b/lib/src/components/organisms/file_picker/file_compress.dart @@ -0,0 +1,149 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path/path.dart' as path; +import 'package:video_compress/video_compress.dart'; + +import '../../../utils/tools/compression.dart'; +import '../../../utils/tools/utils.dart'; + +import '../temp_directory/resolver.dart'; +import 'file_picker.dart'; + +const _maxImageUploadSize = 256 * 1024; // 256 Kb +const _maxVideoUploadSize = 5 * 1024 * 1024; // 25 Mb + +/// Compressors for mobile devices +@immutable +class ZdsFileCompressPostProcessor implements ZdsFilePostProcessor { + /// Default constructor + const ZdsFileCompressPostProcessor(); + + @override + Future process(FilePickerConfig config, FileWrapper file) async { + if (kIsWeb) { + return file; + } else if (file.isImage()) { + return FileWrapper(file.type, await _compressImage(File(file.xFilePath), config)); + } else if (file.isVideo()) { + return FileWrapper( + file.type, + await _compressVideo(File(file.xFilePath), config), + ); + } else { + return file; + } + } + + Future _compressVideo(File video, FilePickerConfig config) async { + try { + final dir = await zdsTempDirectory(); + final fileExtension = path.extension(video.path).toLowerCase().replaceAll('.', ''); + final targetFile = File('$dir/${path.basenameWithoutExtension(video.path)}.$fileExtension'); + final maxFileSize = config.maxFileSize == 0 ? _maxVideoUploadSize : config.maxFileSize; + + File compressedVideo; + try { + compressedVideo = await ZdsCompressor.compressVideo( + video: video, + target: targetFile, + maxFileSize: maxFileSize, + quality: _videoQuality(config.videoCompressionLevel), + ) ?? + video; + } catch (e) { + compressedVideo = video; + } + + final size = await compressedVideo.length(); + if (size > maxFileSize) { + throw FilePickerException(PickerExceptionType.maxFileSize, args: [fileSizeWithUnit(maxFileSize)]); + } + + return ZdsXFile.fromFile(compressedVideo); + } catch (e) { + rethrow; + } + } + + VideoQuality _videoQuality(int config) { + const qualityMap = { + 1: VideoQuality.Res1280x720Quality, + 2: VideoQuality.Res960x540Quality, + 3: VideoQuality.Res640x480Quality, + 4: VideoQuality.MediumQuality, + 5: VideoQuality.LowQuality, + }; + + return qualityMap[config] ?? VideoQuality.Res640x480Quality; + } + + Future _compressImage(File image, FilePickerConfig config) async { + try { + final fileSize = config.maxFileSize == 0 ? _maxImageUploadSize : config.maxFileSize; + final h = config.maxPixelSize <= 0 ? 1080 : config.maxPixelSize; + + // Resolve compression type + final fileExtension = path.extension(image.path).toLowerCase().replaceAll('.', ''); + final format = _getCompressFormat(fileExtension, config); + final quality = h < 1000 + ? 95 + : h < 1500 + ? 75 + : 50; + + File result = image; + + if (format != null) { + try { + final c = Compression(maxFileSize: fileSize, minHeight: h, minWidth: h, format: format, quality: quality); + var compressed = await ZdsCompressor.compressImage(image: image, compression: c); + if (compressed != null) { + // Rename file back to jpg if picked file was jpg and allowed file types dose not contain jpeg. + // This step is needed as jpg is compressed to jpeg + if (fileExtension == 'jpg' && !config.allowedExtensions.contains('jpeg')) { + compressed = compressed.renameSync(compressed.absolute.path.replaceAll('.jpeg', '.jpg')); + } + + result = compressed; + await image.delete(); + } + } catch (e) { + result = image; + } + } + + final int size = await result.length(); + if (size > fileSize) { + throw FilePickerException(PickerExceptionType.maxFileSize, args: [fileSizeWithUnit(fileSize)]); + } + + return ZdsXFile.fromFile(result); + } catch (e) { + rethrow; + } + } + + CompressFormat? _getCompressFormat(String extension, FilePickerConfig config) { + final allowedExt = config.allowedExtensions.map((e) => e.toLowerCase()); + + // If allowed file extension list is empty then return jpeg + if (allowedExt.isEmpty) return CompressFormat.jpeg; + + // fallback to other extension + switch (extension) { + case 'png': + return CompressFormat.png; + case 'jpeg': + case 'jpg': + return CompressFormat.jpeg; + case 'heic': + return CompressFormat.heic; + case 'webp': + return CompressFormat.webp; + default: + return null; + } + } +} diff --git a/lib/src/components/organisms/file_picker/file_edit.dart b/lib/src/components/organisms/file_picker/file_edit.dart new file mode 100644 index 0000000..32c41b7 --- /dev/null +++ b/lib/src/components/organisms/file_picker/file_edit.dart @@ -0,0 +1,57 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_editor_plus/image_editor_plus.dart'; +import 'package:path/path.dart' as path; + +import '../../../../zds_flutter.dart'; +import '../temp_directory/resolver.dart'; + +/// Editors used to edit only image files & to launch other types of files +class ZdsFileEditPostProcessor implements ZdsFilePostProcessor { + ///default constructor + ZdsFileEditPostProcessor( + this.buildContext, { + this.initialCropAspectRatio = ZdsAspectRatio.ratio1x1, + }); + + ///Context used for navigations and toasts + final BuildContextProvider buildContext; + + /// Initial Aspect ratio of crop rect + /// default is [ZdsAspectRatio.original] + /// + /// The argument only affects the initial aspect ratio. + final ZdsAspectRatio initialCropAspectRatio; + + @override + Future process(FilePickerConfig config, FileWrapper file) async { + if (kIsWeb) return file; + + if (file.isImage() && file.content != null) { + final originalFile = File(file.xFilePath); + ImageEditor.i18n(ComponentStrings.of(buildContext.call()).getAll()); + final bytes = await Navigator.push( + buildContext.call(), + MaterialPageRoute( + builder: (context) { + return SingleImageEditor( + image: originalFile.readAsBytesSync(), + ); + }, + ), + ); + + if (bytes != null) { + final dir = await zdsTempDirectory('edited'); + await originalFile.delete(recursive: true); + final result = File(path.join(dir, path.basename(originalFile.absolute.path))); + await result.writeAsBytes(bytes); + return FileWrapper(file.type, ZdsXFile.fromFile(result)); + } + } + + return file; + } +} diff --git a/lib/src/components/organisms/file_picker/file_picker.dart b/lib/src/components/organisms/file_picker/file_picker.dart new file mode 100644 index 0000000..6338919 --- /dev/null +++ b/lib/src/components/organisms/file_picker/file_picker.dart @@ -0,0 +1,937 @@ +// TODO(thelukewalton): throwing error on mac + +import 'dart:async'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_swipe_action_cell/flutter_swipe_action_cell.dart'; +import 'package:giphy_get/giphy_get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:universal_platform/universal_platform.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../../zds_flutter.dart' hide ImagePicker; +import 'giphy_picker.dart'; + +export 'file_post_processor.dart'; +export 'file_validator.dart'; +export 'file_wrapper.dart'; +export 'xfile.dart'; + +/// File validator +/// +/// This method will be called once the file is selected +typedef ZdsFileValidator = Future Function( + ZdsFilePickerController controller, + FilePickerConfig config, + FileWrapper fileWrapper, +); + +/// The configuration used in a [ZdsFilePicker]. +/// +/// [maxPixelSize] is used to set a maximum side dimension for an image. For example, if the [maxPixelSize] is 500, +/// images can have a maximum resolution of 500x500. Any image above this resolution will be scaled down to this size. +/// +/// See also: +/// +/// * [ZdsFilePicker] +class FilePickerConfig { + /// The maximum number of files allowed. + /// + /// Defaults to 0. + final int maxFilesAllowed; + + /// The allowed file extensions. + /// + /// If empty, all files are allowed. Defaults to empty. + final Set allowedExtensions; + + /// If the file picked is an image, its maximum side dimension once it's been selected. + /// + /// If 0, it will be unlimited. Defaults to unlimited size. + final int maxPixelSize; + + /// The maximum size of a file attachment in bytes. + /// + /// If 0, it will be unlimited. Defaults to unlimited size. + /// + /// Does not apply to images if it's value in more than 250Kb, + /// images will always be compressed with 250Kb as max size. + final int maxFileSize; + + /// The options that will be shown in the file picker. + /// + /// Defaults to all of the options. + final List options; + + /// Video compression quality + /// + /// Defaults to 3. + /// VID_COMPRESSION_LEVEL + /// 1 -> 1280x720 24fps + /// 2 -> 960x540 24fps + /// 3 -> 640x480 24fps + /// 4 -> Medium 24fps + /// 5 -> Lowest 24fps + final int videoCompressionLevel; + + /// API Key, required to use giphy service. + /// + /// See https://developers.giphy.com/ + final String? giphyApiKey; + + /// Creates the configuration to use in the [ZdsFilePicker]. + const FilePickerConfig({ + this.videoCompressionLevel = 3, + this.maxFilesAllowed = 0, + this.maxFileSize = 0, + this.maxPixelSize = 0, + this.allowedExtensions = const {}, + this.giphyApiKey, + this.options = const [ + FilePickerOptions.VIDEO, + FilePickerOptions.FILE, + FilePickerOptions.CAMERA, + FilePickerOptions.GALLERY, + ], + }) : assert(maxPixelSize >= 0, 'maxPixelSize must be greater than or equal to 0'), + assert(maxFileSize >= 0, 'maxFileSize must be greater than or equal to 0'); + + /// Creates a copy of this [FilePickerConfig], but with the given fields replaced wih the new values. + FilePickerConfig copyWith({ + int? videoCompressionLevel, + int? maxFilesAllowed, + int? maxFileSize, + int? maxPixelSize, + Set? allowedExtensions, + List? options, + }) { + return FilePickerConfig( + maxFilesAllowed: maxFilesAllowed ?? this.maxFilesAllowed, + videoCompressionLevel: videoCompressionLevel ?? this.videoCompressionLevel, + maxFileSize: maxFileSize ?? this.maxFileSize, + maxPixelSize: maxPixelSize ?? this.maxPixelSize, + allowedExtensions: allowedExtensions ?? this.allowedExtensions, + options: options ?? this.options, + ); + } + + /// Returns true if any of the [allowedExtensions] is for the video + bool allowVideos() { + if (allowedExtensions.isEmpty) { + return true; + } else { + return UniversalPlatform.isAndroid + ? allowedExtensions.intersection({'mp4'}).isNotEmpty + : allowedExtensions.intersection({'mov'}).isNotEmpty; + } + } + + /// Returns true if any of the [allowedExtensions] is for the image + bool allowImages() { + return allowedExtensions.isEmpty || allowedExtensions.intersection({'jpg', 'jpeg', 'png'}).isNotEmpty; + } +} + +/// Whether to display the [ZdsFilePicker] as a horizontal row or vertical column. +enum ZdsFilePickerDisplayStyle { + ///Displays [ZdsFilePicker] as a horizontal row. + horizontal, + + ///Displays [ZdsFilePicker] as a vertical column. + vertical, +} + +/// UI Variants of the [ZdsFilePicker]. +enum ZdsOptionDisplay { + /// Shows an icon above text for each File option. + standard, + + /// Just shows icon without text for each file option. + /// + /// Useful for scenario's where screen space is limited. + plain, +} + +/// Shows a component that can be used to select files and display their previews. +/// +/// If [showSelected] is true, the attachments will be shown. There are two ways to display attachments, either +/// vertically or horizontally +/// +/// {@image } +/// +/// See also: +/// +/// * [FilePickerConfig], the configuration for this filepicker. +/// * [FilePicker], the interface this widget uses to select a file. +/// * [ImagePicker], a widget used to select a single image and show its preview. +/// * [ZdsFilePreview], which this component uses to show previews of the selected files. +class ZdsFilePicker extends StatefulWidget { + /// Whether to use a default card background. If false, uses a transparent background. + /// + /// Defaults to true. + final bool useCard; + + /// Whether to show the selected files. + /// + /// Defaults to true. + final bool showSelected; + + /// Whether to show the picker option label. + /// + /// Defaults to true. + final ZdsOptionDisplay optionDisplay; + + /// Whether to show the attachments in a horizontal or vertical list. + final ZdsFilePickerDisplayStyle? displayStyle; + + /// The visual density of this card. + /// + /// Defaults to [VisualDensity.standard]. + final VisualDensity? visualDensity; + + /// The configuration for this file picker. + final FilePickerConfig config; + + /// The controller attached to this file picker. + final ZdsFilePickerController controller; + + /// A function called whenever the selected items change, i.e. an item gets removed or added. + final void Function(List items)? onChange; + + /// Whether to allow the user to give a name to the links attached. + /// + /// Defaults to true. + final bool showLinkName; + + /// List of processes a file should undergo post getting picked from file picker + /// + /// Defaults to [zds DefaultPostProcessors] + final List? postProcessors; + + /// Validations that are needed to be performed on a file + /// + /// Defaults to [zdsValidator] + final ZdsFileValidator? validator; + + /// A function called whenever any exception is thrown in selection process + /// + /// Defaults to [zds FileError] + final void Function(BuildContext context, FilePickerConfig config, Exception exception)? onError; + + /// Creates a component that allows to select files and can display a preview of the selected files. + const ZdsFilePicker({ + required this.controller, + super.key, + this.onChange, + this.config = const FilePickerConfig(), + this.displayStyle = ZdsFilePickerDisplayStyle.vertical, + this.validator = zdsValidator, + this.onError = zdsFileError, + this.postProcessors = zdsDefaultPostProcessors, + this.optionDisplay = ZdsOptionDisplay.standard, + this.showLinkName = true, + this.showSelected = true, + this.useCard = true, + this.visualDensity = VisualDensity.standard, + }); + + @override + ZdsFilePickerState createState() => ZdsFilePickerState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('useCard', useCard)); + properties.add(DiagnosticsProperty('showSelected', showSelected)); + properties.add(EnumProperty('optionDisplay', optionDisplay)); + properties.add(EnumProperty('displayStyle', displayStyle)); + properties.add(DiagnosticsProperty('visualDensity', visualDensity)); + properties.add(DiagnosticsProperty('config', config)); + properties.add(DiagnosticsProperty('controller', controller)); + properties.add(ObjectFlagProperty items)?>.has('onChange', onChange)); + properties.add(DiagnosticsProperty('showLinkName', showLinkName)); + properties.add(IterableProperty('postProcessors', postProcessors)); + properties.add(ObjectFlagProperty.has('validator', validator)); + properties.add( + ObjectFlagProperty.has( + 'onError', + onError, + ), + ); + } +} + +///State fot [ZdsFilePicker]. +class ZdsFilePickerState extends State with AutomaticKeepAliveClientMixin { + bool __busy = false; + + bool get _busy => __busy; + + set _busy(bool busy) { + if (!mounted || __busy == busy) return; + setState(() { + __busy = busy; + }); + } + + /// Current [ZdsFilePickerController] for this state + ZdsFilePickerController get controller => widget.controller; + + /// Current [FilePickerConfig] for this state + FilePickerConfig get config => widget.config; + + List get _allowedOptions { + final list = [...config.options]; + if (config.giphyApiKey == null || config.giphyApiKey!.isEmpty) { + list.remove(FilePickerOptions.GIF); + } + + return list; + } + + @override + void initState() { + super.initState(); + controller.addListener(() { + if (mounted) { + widget.onChange?.call(controller.items); + setState(() {}); + } + }); + } + + @override + Widget build(BuildContext context) { + super.build(context); + final maxFiles = config.maxFilesAllowed; + final busy = _busy || controller.busy; + final attachmentList = controller.items.where((element) => !element.isLink).toList(); + final disableWidget = _busy || controller.busy || (maxFiles != 0 && maxFiles <= attachmentList.length); + final content = AnimatedSize( + duration: const Duration(milliseconds: 250), + child: Stack( + alignment: Alignment.center, + children: [ + ZdsAbsorbPointer( + absorbing: busy, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.showSelected && (controller.items.isNotEmpty)) ...[ + _buildAttachments(), + if (widget.displayStyle == ZdsFilePickerDisplayStyle.vertical) const Divider(), + ], + ZdsAbsorbPointer( + absorbing: disableWidget, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: _allowedOptions + .map((option) => _buildOption(context, option)) + .toList() + .divide(_divider) + .toList(), + ), + ), + ], + ), + ), + if (_busy || controller.busy) + const Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))), + ], + ), + ); + + if (widget.useCard) { + return ZdsCard(padding: EdgeInsets.zero, child: content); + } else { + return content; + } + } + + /// Builds a [ZdsFilePicker as a horizontal row. + Widget _buildHorizontalDisplay({double height = 120}) { + if (controller.items.isEmpty) return const SizedBox(); + return SizedBox( + height: height, + child: Builder( + builder: (context) { + return DefaultTextStyle( + style: Theme.of(context).textTheme.bodySmall!, + child: ZdsHorizontalList.builder( + isReducedHeight: true, + itemCount: controller.items.length, + itemBuilder: (context, index) { + final fileWrapper = controller.items[index]; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ZdsFilePreview( + file: fileWrapper, + size: height * 0.7, + onDelete: () => controller.removeFile(fileWrapper), + onTap: () async => controller.openFile(context, config, fileWrapper), + ), + if (fileWrapper.content is XFile) ZdsFileSize(file: fileWrapper.content as XFile), + ], + ); + }, + ), + ); + }, + ), + ); + } + + /// Builds a [ZdsFilePicker] as a vertical column. + Widget _buildVerticalDisplay() { + if (controller.items.isEmpty) return const SizedBox(); + return ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: controller.items.length, + separatorBuilder: (_, __) => const Divider(), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final wrapper = controller.items[index]; + return SwipeActionCell( + key: ObjectKey(wrapper.hashCode), + backgroundColor: Theme.of(context).colorScheme.surface, + trailingActions: [ + SwipeAction( + color: Theme.of(context).colorScheme.error, + onTap: (handler) async => controller.openFile(context, config, wrapper), + content: Semantics( + focused: true, + label: ComponentStrings.of(context).get('DELETE', 'Delete'), + child: IconButton( + icon: Icon(ZdsIcons.delete, color: Theme.of(context).colorScheme.onError), + onPressed: () => controller.removeFile(wrapper), + ), + ), + ), + ], + child: Semantics( + hint: ComponentStrings.of(context).get('SWIPE_TO_REVEAL_SEMANTIC', 'Swipe left to reveal actions'), + child: ZdsListTile( + shrinkWrap: true, + leading: ZdsFilePreview(file: wrapper, size: 50, useCard: false), + onTap: () async => controller.openFile(context, config, wrapper), + title: Text( + wrapper.name ?? ComponentStrings.of(context).get('UNKNOWN', 'Unknown'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: wrapper.content is XFile + ? ZdsFileSize(file: wrapper.content as XFile) + : wrapper.content is XUri + ? Text((wrapper.content as XUri).uri.toString(), maxLines: 1, overflow: TextOverflow.ellipsis) + : null, + trailing: const Icon(ZdsIcons.chevron_right), + ), + ), + ); + }, + ); + } + + Widget _buildAttachments() { + return widget.displayStyle == ZdsFilePickerDisplayStyle.horizontal + ? _buildHorizontalDisplay() + : _buildVerticalDisplay(); + } + + @override + bool get wantKeepAlive => true; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('controller', controller)); + properties.add(DiagnosticsProperty('config', config)); + } +} + +extension _FileWrapperIcon on FilePickerOptions { + IconData get icon { + final Map map = { + FilePickerOptions.FILE: ZdsIcons.upload, + FilePickerOptions.LINK: ZdsIcons.add_link, + FilePickerOptions.GALLERY: ZdsIcons.image, + FilePickerOptions.VIDEO: ZdsIcons.video, + FilePickerOptions.CAMERA: ZdsIcons.camera, + FilePickerOptions.GIF: Icons.gif_box_outlined, + }; + + return map[this] ?? Icons.error; + } + + String getLabel(BuildContext context) { + final Map map = { + FilePickerOptions.FILE: ComponentStrings.of(context).get('FILE', 'File'), + FilePickerOptions.LINK: ComponentStrings.of(context).get('LINK', 'Link'), + FilePickerOptions.GALLERY: ComponentStrings.of(context).get('GALLERY', 'Gallery'), + FilePickerOptions.VIDEO: ComponentStrings.of(context).get('VIDEO', 'Video'), + FilePickerOptions.CAMERA: ComponentStrings.of(context).get('CAMERA', 'Camera'), + FilePickerOptions.GIF: ComponentStrings.of(context).get('GIF', 'Gif'), + }; + + return map[this] ?? ''; + } +} + +extension _Methods on ZdsFilePickerState { + Future handleOptionAction(BuildContext context, FilePickerOptions option) async { + if (option == FilePickerOptions.FILE) { + await _handleFileAction(context); + } else if (option == FilePickerOptions.LINK) { + await _handleLinkAction(context); + } else if (option == FilePickerOptions.GALLERY) { + await _handleGalleryAction(context); + } else if (option == FilePickerOptions.VIDEO) { + await _handleVideoAction(context); + } else if (option == FilePickerOptions.CAMERA) { + await _handleCameraAction(context); + } else if (option == FilePickerOptions.GIF) { + await _handleGifAction(context); + } + } + + Future _handleLinkAction(BuildContext context) async { + String? isValidUrl(Uri? uri) { + final urlExp = RegExp(r'(http|ftp|https)://[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?'); + final url = uri.toString(); + if (uri == null || !uri.isAbsolute || !urlExp.hasMatch(url)) { + return ComponentStrings.of(context).get('URL_INVALID_ER', 'Please enter valid URL.'); + } else { + return null; + } + } + + final result = await showDialog?>( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + final strings = ComponentStrings.of(context); + return _MultiInputDialog( + title: strings.get('ADD_URL', 'Add url'), + textFields: [ + if (widget.showLinkName) + _TextField(id: 'name', hint: strings.get('LINK_NAME', 'Link Name'), autoFocus: true), + _TextField(id: 'url', hint: strings.get('LINK_URL', 'Link URL'), autoFocus: true), + ], + primaryAction: strings.get('SAVE', 'Save'), + secondaryAction: strings.get('CANCEL', 'Cancel'), + onValidate: (field) { + if (field.id == 'url') { + return isValidUrl(Uri.tryParse(field.value)); + } else { + return null; + } + }, + ); + }, + ); + + if (result != null) { + final urlField = result.last; + final nameField = result.length == 2 ? result.first : null; + final uri = Uri.tryParse(urlField.value); + if (uri != null) { + final name = nameField?.value ?? ''; + controller.addFiles([ + FileWrapper(FilePickerOptions.LINK, XUri(uri: uri, name: name.isEmpty ? uri.toString() : name)), + ]); + } + } + } + + Future _handleGifAction(BuildContext context) async { + final GiphyGif? gif = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ZdsGiphyPicker(apiKey: config.giphyApiKey ?? ''), + ), + ); + + try { + if (gif == null) return; + _busy = true; + if (mounted) await onPicked(context, FileWrapper(FilePickerOptions.GIF, gif)); + } on Exception catch (e) { + if (mounted) widget.onError?.call(context, config, e); + } finally { + _busy = false; + } + } + + Future _handleCameraAction(BuildContext context) async { + try { + final ImagePicker picker = ImagePicker(); + final photo = await picker.pickImage(source: ImageSource.camera); + if (photo != null && mounted) { + final FileWrapper file = FileWrapper(FilePickerOptions.CAMERA, photo); + await onPicked(context, file); + } + } on Exception catch (e) { + if (mounted) widget.onError?.call(context, config, e); + } finally { + _busy = false; + } + } + + Future _handleVideoAction(BuildContext context) async { + try { + final ImagePicker picker = ImagePicker(); + final video = await picker.pickVideo(source: ImageSource.camera); + + if (video != null && mounted) { + final FileWrapper file = FileWrapper(FilePickerOptions.VIDEO, video); + await onPicked(context, file); + } + } on Exception catch (e) { + if (mounted) widget.onError?.call(context, config, e); + } finally { + _busy = false; + } + } + + /// Picks media from gallery + /// + /// Allow to pick media if allowed extensions are empty. + /// Else checks based on allowed types. + Future _handleGalleryAction(BuildContext context) async { + var fileType = FileType.media; + + final allowImages = config.allowImages(); + final allowVideos = config.allowVideos(); + + if (allowImages && allowVideos) { + fileType = FileType.media; + } else if (allowImages) { + fileType = FileType.image; + } else if (allowVideos) { + fileType = FileType.video; + } else { + return; + } + + await _handleFileAction(context, type: fileType, option: FilePickerOptions.GALLERY); + } + + Future _handleFileAction( + BuildContext context, { + FileType type = FileType.any, + FilePickerOptions option = FilePickerOptions.FILE, + }) async { + try { + final allowedFileExt = Set.from(config.allowedExtensions); + final maxFilesAllowed = config.maxFilesAllowed; + final allowMultiple = maxFilesAllowed == 0 || maxFilesAllowed > 1; + final mutableType = type == FileType.any + ? allowedFileExt.isNotEmpty + ? FileType.custom + : type + : type; + + _busy = true; + + final result = mutableType == FileType.custom + ? await FilePicker.platform.pickFiles( + type: mutableType, + allowedExtensions: List.from(allowedFileExt), + allowMultiple: allowMultiple, + ) + : await FilePicker.platform.pickFiles( + type: mutableType, + allowMultiple: allowMultiple, + ); + + if (result != null && mounted) { + for (final file in result.files) { + if (maxFilesAllowed != 0 && + controller.items.where((element) => !element.isLink).toList().length >= maxFilesAllowed) break; + if (kIsWeb) { + final mimeType = lookupMimeType(file.name); + final xfile = XFile.fromData(file.bytes!, name: file.name, length: file.size, mimeType: mimeType); + await onPicked(context, FileWrapper(option, xfile)); + } else { + final mimeType = lookupMimeType(file.path ?? ''); + final xfile = XFile(file.path!, name: file.name, length: file.size, mimeType: mimeType); + await onPicked(context, FileWrapper(option, xfile)); + } + } + } + } on Exception catch (e) { + if (mounted) widget.onError?.call(context, config, e); + } finally { + _busy = false; + } + } + + Future onPicked(BuildContext context, FileWrapper file) async { + try { + if (file.content == null) return; + _busy = true; + + final exception = await widget.validator?.call(controller, config, file); + + var input = file; + if (exception == null && widget.postProcessors != null) { + for (final p in widget.postProcessors!) { + input = await p.process(config, input); + } + } + + if (exception != null && mounted) { + widget.onError?.call(context, config, exception); + } else { + controller.addFiles([input]); + } + } on Exception catch (e) { + if (mounted) widget.onError?.call(context, config, e); + } finally { + _busy = false; + } + } +} + +extension on ZdsFilePickerState { + Widget get _divider { + return widget.optionDisplay == ZdsOptionDisplay.standard + ? Container( + height: widget.visualDensity == VisualDensity.standard ? 40 : 26, + decoration: BoxDecoration( + border: Border( + left: BorderSide(color: ZdsColors.greySwatch(context)[400] ?? ZdsColors.lightGrey), + ), + ), + ) + : const SizedBox.shrink(); + } + + Widget _buildOption(BuildContext context, FilePickerOptions option) { + final bool isStandard = widget.visualDensity == VisualDensity.standard; + final style = isStandard + ? Theme.of(context).textTheme.bodyMedium + : Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 12, height: 16 / 12); + final padding = 16 + ((widget.visualDensity?.vertical ?? 0) * 4); + return Expanded( + child: Semantics( + button: true, + enabled: true, + child: InkResponse( + radius: widget.optionDisplay == ZdsOptionDisplay.plain + ? isStandard + ? 24 + : 20 + : isStandard + ? 40 + : 30, + onTap: () async => handleOptionAction(context, option), + child: Column( + children: [ + Icon( + option.icon, + size: 24 + ((widget.visualDensity?.vertical ?? 0) * 4), + color: ZetaColors.of(context).textSubtle, + ), + if (widget.optionDisplay == ZdsOptionDisplay.standard) ...[ + SizedBox(height: 10 + ((widget.visualDensity?.vertical ?? 0) * 4)), + Text( + option.getLabel(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: style?.copyWith(color: ZdsColors.greySwatch(context)[800]), + textScaleFactor: MediaQuery.of(context).textScaleFactor > 2.7 ? 2.7 : null, + ), + ], + ], + ).paddingInsets(EdgeInsets.symmetric(vertical: padding)), + ), + ), + ); + } +} + +/// A controller used with [ZdsFilePicker] to keep track and use the selected files. +/// +/// See also: +/// +/// * [ZdsFilePicker] +class ZdsFilePickerController extends ChangeNotifier { + bool _busy = false; + + /// Busy state indicator + bool get busy => _busy; + + set busy(bool busy) { + if (_busy == busy) return; + _busy = busy; + notifyListeners(); + } + + /// The selected files. + List items = []; + + /// file from server to check itemCount only + List remoteItems = []; + + /// Programmatically adds a list of files to the linked [ZdsFilePicker]. + void addFiles(List files) { + items += files; + notifyListeners(); + } + + /// Programmatically removes files from the linked [ZdsFilePicker]. + int removeFile(FileWrapper file, {bool notify = true}) { + final index = items.indexWhere((element) => element == file); + if (index >= 0) { + items.removeAt(index); + if (notify) notifyListeners(); + } + + return index; + } + + /// Opens the provided file. + /// + /// Opens links in an InAppWebView, otherwise opens the file using the native viewer. + Future openFile(BuildContext context, FilePickerConfig config, FileWrapper file) async { + if (file.content != null && file.content is XFile) { + final fileToOpen = file.content as XFile; + if (file.isImage()) { + // Edit the file + final editPostProcessor = ZdsFileEditPostProcessor(() => context); + final editedFile = await editPostProcessor.process(config, file); + if (editedFile.content != file.content) { + const compressPostProcessor = ZdsFileCompressPostProcessor(); + final compressedFile = await compressPostProcessor.process(config, editedFile); + final index = removeFile(file); + items.insert(index, compressedFile); + notifyListeners(); + } + } else { + await OpenFilex.open(fileToOpen.path); + } + } + } +} + +class _TextField { + _TextField({this.id, this.hint, this.autoFocus = false}); + + final dynamic id; + final String? hint; + bool autoFocus = false; + String value = ''; + String? error; +} + +class _MultiInputDialog extends StatefulWidget { + final String? title; + final String primaryAction; + final String? secondaryAction; + final List<_TextField> textFields; + final String? Function(_TextField)? onValidate; + + const _MultiInputDialog({ + required this.textFields, + required this.primaryAction, + this.title, + this.secondaryAction, + this.onValidate, + }); + + @override + _MultiInputDialogState createState() => _MultiInputDialogState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('title', title)); + properties.add(StringProperty('primaryAction', primaryAction)); + properties.add(StringProperty('secondaryAction', secondaryAction)); + properties.add(IterableProperty<_TextField>('textFields', textFields)); + properties.add(ObjectFlagProperty.has('onValidate', onValidate)); + } +} + +class _MultiInputDialogState extends State<_MultiInputDialog> { + bool get isValid => widget.textFields.fold(true, (p, r) => p && (widget.onValidate?.call(r) == null)); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Dialog( + child: Container( + constraints: const BoxConstraints(maxWidth: 480), + padding: const EdgeInsets.all(24), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.title != null) Text(widget.title!, style: theme.textTheme.displaySmall), + if (widget.textFields.isEmpty) const SizedBox(), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: widget.textFields.length, + itemBuilder: (BuildContext context, int index) { + return TextFormField( + autofocus: widget.textFields[index].autoFocus, + textInputAction: TextInputAction.done, + autovalidateMode: AutovalidateMode.onUserInteraction, + onChanged: (value) { + setState(() { + widget.textFields[index].value = value; + }); + }, + onFieldSubmitted: (value) => widget.textFields[index].value = value, + validator: (value) => widget.onValidate?.call(widget.textFields[index]), + decoration: ZdsInputDecoration( + hintText: widget.textFields[index].hint, + errorText: widget.textFields[index].error, + errorStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(color: ZdsColors.red), + ), + ); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (widget.secondaryAction != null) + ZdsButton.muted( + child: Text(widget.secondaryAction!), + onTap: () async => Navigator.maybePop(context), + ), + if (widget.secondaryAction == null) const Spacer(), + const SizedBox(width: 16), + ZdsButton( + onTap: isValid + ? () { + if (isValid) { + unawaited(Navigator.maybePop(context, widget.textFields)); + } + } + : null, + child: Text(widget.primaryAction), + ), + ], + ), + ], + ), + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('isValid', isValid)); + } +} diff --git a/lib/src/components/organisms/file_picker/file_post_processor.dart b/lib/src/components/organisms/file_picker/file_post_processor.dart new file mode 100644 index 0000000..b147d4b --- /dev/null +++ b/lib/src/components/organisms/file_picker/file_post_processor.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +import 'file_picker.dart'; + +export 'file_compress.dart'; +export 'file_edit.dart'; +export 'file_rename.dart'; + +/// Context used for page navigation +typedef BuildContextProvider = BuildContext Function(); + +/// abstract class to process file actions for all processors. +// ignore: one_member_abstracts +abstract class ZdsFilePostProcessor { + /// method that is to be implemented. + Future process(FilePickerConfig config, FileWrapper wrapper); +} + +/// Default post processors +const zdsDefaultPostProcessors = [ZdsFileCompressPostProcessor(), ZdsFileRenamePostProcessor()]; diff --git a/lib/src/components/organisms/file_picker/file_rename.dart b/lib/src/components/organisms/file_picker/file_rename.dart new file mode 100644 index 0000000..18d5d12 --- /dev/null +++ b/lib/src/components/organisms/file_picker/file_rename.dart @@ -0,0 +1,44 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:path/path.dart' as path; +import 'file_picker.dart'; + +/// used to rename a file before compression. +@immutable +class ZdsFileRenamePostProcessor implements ZdsFilePostProcessor { + /// Default constructor + const ZdsFileRenamePostProcessor(); + + @override + Future process(FilePickerConfig config, FileWrapper file) async { + if (kIsWeb || file.content is! XFile) return file; + final photoDir = path.dirname(file.xFilePath); + final epoch = DateFormat('yyyyMMdd_HHmmssSSS').format(DateTime.now()); + final fileName = '${file.type.toPrefix()}_$epoch${path.extension(file.xFilePath)}'; + final newPath = path.join(photoDir, fileName); + final photoFile = File(file.xFilePath).renameSync(newPath); + return FileWrapper(file.type, ZdsXFile.fromFile(photoFile)); + } +} + +extension on FilePickerOptions { + String toPrefix() { + switch (this) { + case FilePickerOptions.CAMERA: + return 'IMG'; + case FilePickerOptions.FILE: + return 'FIL'; + case FilePickerOptions.GALLERY: + return 'GAL'; + case FilePickerOptions.GIF: + return 'GIF'; + case FilePickerOptions.LINK: + return 'LNK'; + case FilePickerOptions.VIDEO: + return 'VID'; + } + } +} diff --git a/lib/src/components/organisms/file_picker/file_validator.dart b/lib/src/components/organisms/file_picker/file_validator.dart new file mode 100644 index 0000000..027916c --- /dev/null +++ b/lib/src/components/organisms/file_picker/file_validator.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../../../utils/assets/icons.dart'; +import '../../../utils/localizations/translation.dart'; +import '../../../utils/tools/utils.dart'; +import '../../molecules/toast.dart'; +import 'file_picker.dart'; + +/// Class defining different exceptions types with its arguments +class FilePickerException implements Exception { + /// Used to specify different types of errors + PickerExceptionType type; + + /// Used to specify custom error sizes + List args; + + /// Default constructor + FilePickerException(this.type, {this.args = const []}); +} + +/// Types of errors, commonly used to generate error toast messages later +enum PickerExceptionType { + /// This file type is not allowed + unsupportedFile, + + /// File size limit + maxFileSize, + + /// Pixel size limit + maxPixelSize, + + /// Error in file compression + compression, + + /// Error in reading file size + fileSizeRead, + + /// File processing error + processing, + + /// File already selected + duplicateFile, + + /// URL already added + duplicateURL, + + /// Invalid URL added + invalidUrl, + + /// Maximum total attachment size reached + maxLimitReached, + + /// Used for custom error message + custom +} + +/// Default file validator +Future zdsValidator( + ZdsFilePickerController controller, + FilePickerConfig config, + FileWrapper wrapper, +) async { + final file = wrapper.content; + if (file is! XFile) return null; + + // File type check + if (config.allowedExtensions.isNotEmpty && !config.allowedExtensions.contains(wrapper.extension)) { + return FilePickerException(PickerExceptionType.unsupportedFile); + } + + // Check if file is already selected + if (controller.items.contains(wrapper)) { + return FilePickerException(PickerExceptionType.duplicateFile); + } + + // check if the file size is within the limit + if (!(wrapper.isImage() || wrapper.isVideo()) && config.maxFileSize > 0 && config.maxFileSize < await file.length()) { + return FilePickerException(PickerExceptionType.maxFileSize, args: [fileSizeWithUnit(config.maxFileSize)]); + } + + return null; +} + +/// Default error handler +void zdsFileError(BuildContext context, FilePickerConfig config, Exception exception) { + String message(Exception exception) { + if (exception is FilePickerException) { + return exception.type.message(context, args: exception.args); + } else { + return ComponentStrings.of(context).get('FILE_COMPRESSING_ER', 'An error occurred while compressing a file.'); + } + } + + ScaffoldMessenger.of(context).showZdsToast( + padding: const EdgeInsets.all(16).copyWith(top: 0), + ZdsToast( + multiLine: true, + title: Text(message(exception)), + leading: const Icon(ZdsIcons.close_circle), + color: ZdsToastColors.error, + ), + ); +} + +///extension used to return different strings on exceptions. +extension TextMessage on PickerExceptionType { + /// method returns FilePickerException message's. + String message(BuildContext context, {List args = const []}) { + final strings = ComponentStrings.of(context); + switch (this) { + case PickerExceptionType.unsupportedFile: + return strings.get('FILE_UNSUPPORTED', 'This file type is not allowed'); + case PickerExceptionType.maxFileSize: + return strings.get('ATTACH_MAXSIZE_VALIDATE', 'File size must be less than {0}', args: args); + case PickerExceptionType.maxPixelSize: + return strings.get('FILE_PIXEL_SIZE_ER', 'File pixel size should be less than {0}', args: args); + case PickerExceptionType.compression: + return strings.get('FILE_COMPRESSING_ER', 'An error occurred while compressing a file.'); + case PickerExceptionType.fileSizeRead: + return strings.get('FILE_SIZE_CALCULATING_ER', 'An error occurred while calculating a file size.'); + case PickerExceptionType.processing: + return strings.get('FILE_PROCESSING_ER', 'An error occurred while processing a file.'); + case PickerExceptionType.duplicateFile: + return strings.get('FILE_DUPLICATE_ER', 'File is already attached. Please choose different file.'); + case PickerExceptionType.duplicateURL: + return strings.get('URL_DUPLICATE_ER', 'URL is already attached. Please enter different URL.'); + case PickerExceptionType.invalidUrl: + return strings.get('URL_INVALID_ER', 'Please enter valid URL.'); + case PickerExceptionType.maxLimitReached: + return strings.get('MAX_ATTACH_MSG', 'Attachment max limit reached'); + case PickerExceptionType.custom: + return args.join(' '); + } + } +} diff --git a/lib/src/components/organisms/file_picker/file_wrapper.dart b/lib/src/components/organisms/file_picker/file_wrapper.dart new file mode 100644 index 0000000..ba48304 --- /dev/null +++ b/lib/src/components/organisms/file_picker/file_wrapper.dart @@ -0,0 +1,161 @@ +// ignore_for_file: constant_identifier_names + +import 'package:flutter/cupertino.dart'; +import 'package:giphy_get/giphy_get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart' as mime; +import 'package:path/path.dart' as path; + +import '../../../../zds_flutter.dart'; + +/// Extension on FilePickerException to show message + +/// Types of files the [ZdsFilePicker] can be used to pick. +enum FilePickerOptions { + /// Opens native video file picker. + VIDEO, + + /// Opens native file picker. + FILE, + + /// Opens a modal with text fields to enter link name and url. + LINK, + + /// Opens camera. Requires user to give camera permission. + CAMERA, + + /// Opens native image gallery picker. + GALLERY, + + /// Opens Giphy gif picker. + GIF +} + +/// Wrapper around files picked using [ZdsFilePicker]. +@immutable +class FileWrapper { + /// Constructs a [FileWrapper]. + const FileWrapper(this.type, this.content); + + /// The type of file wrapped. + final FilePickerOptions type; + + /// The content of the picked file. + final dynamic content; + + /// Gets the name of the file. + /// + /// For links this will be either the provided name or the url. + String? get name { + if (content is XFile) { + return (content as XFile).name; + } else if (content is XUri) { + return (content as XUri).name; + } else if (content is GiphyGif) { + return (content as GiphyGif).title; + } else { + return null; + } + } + + /// Gets the path of a file, if the file is [XFile]. + String get xFilePath { + if (content is XFile) { + return (content as XFile).path; + } + return ''; + } + + /// Gets the extension of the picked file in lowercase- e.g: jpg, pdf. + /// + /// For links, the url is returned + String? get extension { + if (content is XFile) { + return path.extension((content as XFile).name).toLowerCase().replaceAll('.', ''); + } else if (content is XUri) { + return 'url'; + } else if (content is GiphyGif) { + return 'gif'; + } else { + return null; + } + } + + /// True if the file is a link. + bool get isLink => content != null && content is XUri; + + /// Returns mimeType of the [content] if it is a valid file + String? get mimeType { + if (content is XFile) { + final file = content as XFile; + return file.mimeType ?? mime.lookupMimeType(file.path) ?? mime.lookupMimeType(file.name); + } else { + return null; + } + } + + /// True if the file is an image. + bool isImage() { + if (content is XFile) { + return mimeType?.contains('image') ?? false; + } else { + return false; + } + } + + /// True if the file is a video. + bool isVideo() { + if (content is XFile) { + return mimeType?.contains('video') ?? false; + } else { + return false; + } + } + + @override + int get hashCode => type.hashCode ^ content.hashCode; + + @override + bool operator ==(covariant FileWrapper other) { + // ignore: avoid_dynamic_calls + if (other.content.runtimeType != content.runtimeType) return false; + if (content is XFile && other.content is XFile) { + final f1 = content as XFile; + final f2 = other.content as XFile; + return f1.name == f2.name || f1.path == f2.path; + } else if (content is XUri && other.content is XUri) { + final u1 = content as XUri; + final u2 = other.content as XUri; + return u1.uri == u2.uri; + } else { + return content == other.content; + } + } +} + +/// Uri wrapper +@immutable +class XUri { + /// Uri + final Uri uri; + + /// Name of the uri + final String name; + + /// Const constructor + const XUri({required this.uri, required this.name}); + + /// Creates a deep copy + XUri copyWith({Uri? uri, String? name}) { + return XUri(uri: uri ?? this.uri, name: name ?? this.name); + } + + @override + int get hashCode => uri.hashCode ^ name.hashCode; + + @override + bool operator ==(Object other) { + return identical(this, other) || + other is XUri && runtimeType == other.runtimeType && uri == other.uri && name == other.name; + } +} diff --git a/lib/src/components/organisms/file_picker/giphy_picker.dart b/lib/src/components/organisms/file_picker/giphy_picker.dart new file mode 100644 index 0000000..139bf4c --- /dev/null +++ b/lib/src/components/organisms/file_picker/giphy_picker.dart @@ -0,0 +1,262 @@ +import 'dart:async'; + +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:giphy_get/giphy_get.dart'; + +import '../../../../zds_flutter.dart'; + +/// See [ZdsFilePicker]. +class ZdsGiphyPicker extends StatefulWidget { + /// API Key, required to use giphy service. + /// + /// See https://developers.giphy.com/ + final String apiKey; + + /// See [ZdsFilePicker]. + const ZdsGiphyPicker({super.key, required this.apiKey}); + + @override + State createState() => _ZdsGiphyPickerState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('apiKey', apiKey)); + } +} + +class _ZdsGiphyPickerState extends State { + // is Loading gifs + bool _isLoading = false; + + // Collection + GiphyCollection? _collection; + + String _queryText = ''; + + // List of gifs + final List _list = []; + + // Limit of query + late int _limit; + + // Offset + int offset = 0; + + ScrollController scrollController = ScrollController(); + final TextEditingController _searchController = TextEditingController(); + + bool _hasText = false; + + Timer? _debounce; + late Duration debounceDelay; + + @override + void initState() { + super.initState(); + + _searchController.addListener(() { + setState(() { + _hasText = _searchController.text.isNotEmpty; + }); + }); + + scrollController.addListener(_loadMore); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + const double gifWidth = 80; + + _searchController.addListener(() { + setState(() { + _hasText = _searchController.text.isNotEmpty; + }); + }); + + scrollController.addListener(_loadMore); + + // Set items count responsive + final int crossAxisCount = (MediaQuery.of(context).size.width / gifWidth).round(); + + // Set vertical max items count + final int mainAxisCount = ((MediaQuery.of(context).size.height - 30) / gifWidth).round(); + + _limit = crossAxisCount * mainAxisCount; + if (_limit > 100) _limit = 100; + // Initial offset + offset = 0; + + // Load Initial Data + unawaited(_loadMore()); + } + + @override + void dispose() { + _debounce?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final clearButton = _hasText + ? IconButton( + icon: Icon(ZdsIcons.close_circle, color: ZdsColors.greySwatch(context)[800]), + onPressed: () { + _searchController.clear(); + _queryText = ''; + _listenerQuery(); + }, + ) + : null; + + return Scaffold( + appBar: AppBar(title: Text(ComponentStrings.of(context).get('PICK_GIF', 'Pick a Gif'))), + backgroundColor: Theme.of(context).colorScheme.background, + body: Column( + children: [ + ZdsSearchField( + hintText: ComponentStrings.of(context).get('SEARCH_GIF', 'Search all the GIFs'), + suffixIcon: clearButton, + controller: _searchController, + onChange: (value) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () async { + setState(() { + _queryText = value; + _listenerQuery(); + }); + }); + }, + ), + if (_isLoading && _list.isEmpty) + const Expanded(child: Center(child: CircularProgressIndicator())) + else if (_list.isNotEmpty) + Expanded( + child: GridView.builder( + itemCount: _list.length, + padding: const EdgeInsets.symmetric(horizontal: 12), + controller: scrollController, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + crossAxisSpacing: 2, + mainAxisSpacing: 2, + ), + itemBuilder: (ctx, idx) { + return _item(ctx, _list[idx]); + }, + ), + ) + else + const ZdsEmpty(), + ], + ), + ); + } + + Widget _item(BuildContext context, GiphyGif gif) { + return InkWell( + onTap: () => Navigator.pop(context, gif), + child: gif.images == null || gif.images?.fixedWidth.webp == null + ? Container() + : ExtendedImage.network( + gif.images!.fixedWidth.webp!, + semanticLabel: gif.title, + gaplessPlayback: true, + headers: const {'accept': 'image/*'}, + loadStateChanged: (state) => AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: gif.images == null + ? Container() + : { + LoadState.loading: AspectRatio( + aspectRatio: 1, + child: Container(color: Theme.of(context).cardColor), + ), + LoadState.completed: AspectRatio( + aspectRatio: 1, + child: ExtendedRawImage(fit: BoxFit.contain, image: state.extendedImageInfo?.image), + ), + LoadState.failed: AspectRatio( + aspectRatio: 1, + child: Container(color: Theme.of(context).cardColor), + ), + }.get( + state.extendedImageLoadState, + orDefault: AspectRatio( + aspectRatio: 1, + child: Container( + color: Theme.of(context).cardColor, + ), + ), + ), + ), + ), + ); + } + + Future _loadMore() async { + if (_isLoading || _collection?.pagination?.totalCount == _list.length) { + return; + } + + _isLoading = true; + + // Giphy Client from library + final GiphyClient client = GiphyClient(apiKey: widget.apiKey, randomId: ''); + + // Offset pagination for query + if (_collection == null) { + offset = 0; + } else { + offset = _collection!.pagination!.offset + _collection!.pagination!.count; + } + + // If query text is not null search gif else trending + if (_queryText.isNotEmpty) { + _collection = await client.search( + _queryText, + lang: Localizations.localeOf(context).languageCode, + offset: offset, + limit: _limit, + ); + } else { + _collection = await client.trending( + lang: Localizations.localeOf(context).languageCode, + offset: offset, + limit: _limit, + ); + } + + // Set result to list + if (_collection!.data.isNotEmpty && mounted) { + setState(() { + _list.addAll(_collection!.data); + }); + } + _isLoading = false; + } + + // listener query + void _listenerQuery() { + // Reset pagination + _collection = null; + + // Reset list + _list.clear(); + + // Load data + unawaited(_loadMore()); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IntProperty('offset', offset)); + properties.add(DiagnosticsProperty('scrollController', scrollController)); + properties.add(DiagnosticsProperty('debounceDelay', debounceDelay)); + } +} diff --git a/lib/src/components/organisms/file_picker/xfile.dart b/lib/src/components/organisms/file_picker/xfile.dart new file mode 100644 index 0000000..033d3b0 --- /dev/null +++ b/lib/src/components/organisms/file_picker/xfile.dart @@ -0,0 +1,43 @@ +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mime/mime.dart' as mime; +import 'package:path/path.dart' as path; + +/// Constructors for XFile +extension ZdsXFile on XFile { + /// Creates [XFile] instance from a [File] + static XFile fromFile(File file) { + return XFile( + file.absolute.path, + length: file.lengthSync(), + mimeType: mime.lookupMimeType(file.absolute.path), + name: path.basename(file.absolute.path), + ); + } + + /// Synchronously deletes this [XFile]. + /// + /// If the [XFile] is a directory, and if [recursive] is false, + /// the directory must be empty. Otherwise, if [recursive] is true, the + /// directory and all sub-directories and files in the directories are + /// deleted. Links are not followed when deleting recursively. Only the link + /// is deleted, not its target. + void deleteSync({bool recursive = false}) { + if (kIsWeb || this.path.isEmpty) return; + File(this.path).deleteSync(recursive: recursive); + } + + /// Deletes this [XFile]. + /// + /// If the [XFile] is a directory, and if [recursive] is false, + /// the directory must be empty. Otherwise, if [recursive] is true, the + /// directory and all sub-directories and files in the directories are + /// deleted. Links are not followed when deleting recursively. Only the link + /// is deleted, not its target. + Future delete({bool recursive = false}) async { + if (kIsWeb || this.path.isEmpty) return; + await File(this.path).delete(recursive: recursive); + } +} diff --git a/lib/src/components/organisms/file_preview.dart b/lib/src/components/organisms/file_preview.dart new file mode 100644 index 0000000..69622ae --- /dev/null +++ b/lib/src/components/organisms/file_preview.dart @@ -0,0 +1,199 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:giphy_get/giphy_get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path/path.dart' as path; + +import '../../../zds_flutter.dart'; +import '../atoms/ximage.dart'; + +/// Creates a preview of a file. +/// +/// If the file is an image, a image preview will be shown. If not, an icon representing the filetype will be shown. +/// When [useCard] is true, the extension's name will also be shown if the file is not an image. +/// +/// When [useCard] is true, the user will be able to interact with the preview through [onTap] and [onDelete]. +/// +/// See also: +/// +/// * [ZdsFilePicker], which uses this component to show previews of chosen files. +class ZdsFilePreview extends StatelessWidget { + /// Constructs a [ZdsFilePreview]. + const ZdsFilePreview({ + required this.file, + super.key, + this.useCard = true, + this.size = 80.0, + this.onDelete, + this.onTap, + }); + + /// The file that needs to be previewed. + final FileWrapper file; + + /// The maximum preview size. + /// + /// If [useCard] is true, the size will be respected on both sides. If false, and the file is an image, the image + /// ratio will affect the size, with the longest side taking this size. + /// + /// Defaults to 80. + final double size; + + /// Whether to use a card-like preview that can also be removed and tapped on. If false, + /// + /// Defaults to true. + final bool useCard; + + /// A function called whenever [useCard] is true and the user taps on the remove button. + final VoidCallback? onDelete; + + /// A function called whenever [useCard] is true and the user taps on the preview. + /// + /// Typically used to edit or view the file. + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + SizedBox( + height: size, + width: size, + child: Padding( + padding: const EdgeInsets.all(4), + child: useCard + ? Semantics( + button: true, + label: ComponentStrings.of(context).get('ATTACHED_FILE', 'Attached file'), + onTapHint: ComponentStrings.of(context).get('VIEW', 'View'), + child: ZdsCard(onTap: onTap, padding: EdgeInsets.zero, child: _getPreview(size)), + ) + : Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(2)), + child: Center(child: _getPreview(size)), + ), + ), + ), + if (onDelete != null) + Positioned( + top: -12, + right: -12, + child: SizedBox( + height: 46, + width: 44, + child: IconButton( + tooltip: ComponentStrings.of(context).get('DELETE_ATTACHMENT', 'Delete attachment'), + padding: EdgeInsets.zero, + splashRadius: 24, + visualDensity: VisualDensity.compact, + icon: Icon(ZdsIcons.close_circle, size: 24, color: ZdsColors.greySwatch(context)[900]), + onPressed: onDelete, + ), + ), + ), + ], + ); + } + + Widget _getPreview(double size) { + return file.content is GiphyGif + // ignore: avoid_dynamic_calls + ? _getImage(Uri.parse(file.content.images.previewGif?.url as String), size) + : file.isImage() + ? _getImage(file.content, size) + : _getFile(file); + } + + Widget _getImage(dynamic file, double size) { + return file is Uri + ? Image.network(file.toString(), fit: BoxFit.cover, height: size, width: size) + : file is XFile + ? XImage.file(file, height: size, width: size) + : const SizedBox(); + } + + Widget _getFile(FileWrapper file) { + return Builder( + builder: (context) { + final isUrl = file.type == FilePickerOptions.LINK; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isUrl ? ZdsIcons.sphere : file.name?.fileIcon(), + size: 18, + color: Theme.of(context).colorScheme.secondary, + ), + if (size >= 80) ...[ + const SizedBox(height: 4), + Text( + isUrl ? file.name ?? '' : (path.extension(file.name ?? 'file.file').replaceAll('.', '')), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.secondary), + ), + ], + ], + ); + }, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('file', file)); + properties.add(DoubleProperty('size', size)); + properties.add(DiagnosticsProperty('useCard', useCard)); + properties.add(ObjectFlagProperty.has('onDelete', onDelete)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + } +} + +/// [Text] widget showing size of the [file] +/// +/// The file size is read asynchronously and shown in readable format. +class ZdsFileSize extends StatelessWidget { + /// Default constructor + const ZdsFileSize({super.key, this.file, this.fileSize}) + : assert(file != null || fileSize != null, 'Either file or fileSize is required'); + + /// File input to show file size + final XFile? file; + + /// File size + final int? fileSize; + + Widget _sizeText(BuildContext context, int size) { + return Text(fileSizeWithUnit(size), style: Theme.of(context).textTheme.bodySmall); + } + + @override + Widget build(BuildContext context) { + if (file != null) { + return FutureBuilder( + // ignore: discarded_futures + future: file!.length(), + builder: (context, snapshot) { + if (snapshot.data != null) { + return _sizeText(context, snapshot.data ?? 0); + } else { + return const SizedBox.shrink(); + } + }, + ); + } else { + return _sizeText(context, fileSize ?? 0); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('file', file)); + properties.add(IntProperty('fileSize', fileSize)); + } +} diff --git a/lib/src/components/organisms/image_editor.dart b/lib/src/components/organisms/image_editor.dart new file mode 100644 index 0000000..5a87079 --- /dev/null +++ b/lib/src/components/organisms/image_editor.dart @@ -0,0 +1,403 @@ +import 'dart:io'; + +import 'package:extended_image/extended_image.dart' as image_editor; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_editor_plus/image_editor_plus.dart'; +import 'package:path/path.dart' as path; +import './temp_directory/resolver.dart'; + +import '../../../zds_flutter.dart'; + +/// A full-screen image editor that allows to edit an image, including cropping and orientation. Returns a [Scaffold], +/// so it's highly recommended to push a new page when using this component +/// +/// This widget can be used with [Navigator.push], whose [Future] will be contain the edited [File]: +/// ```dart +/// Navigator.push( +/// context, +/// CupertinoPageRoute(builder: (context) => ImageEditor(image: originalFile)) +/// ).then((result) { +/// if (result != null) { +/// final editedImage = result as File; +/// // Handle new file +/// } +/// }); +/// ``` +class ZdsImageEditor extends StatefulWidget { + /// The image to be edited + final File image; + + /// Initial Aspect ratio of crop rect + /// default is [ZdsAspectRatio.original] + /// + /// The argument only affects the initial aspect ratio. + final ZdsAspectRatio initialCropAspectRatio; + + /// Creates an image editor that allows to crop and rotate an image. + /// Recommended to push a new page as this returns a [Scaffold]. + const ZdsImageEditor({ + required this.image, + this.initialCropAspectRatio = ZdsAspectRatio.original, + super.key, + }); + + @override + ZdsImageEditorState createState() => ZdsImageEditorState(); + + /// Navigates to the `ImageEditor` screen, allowing the user to edit + /// the [originalFile]. Once editing is complete, the edited image data + /// replaces the original file. + /// + /// This method takes the following parameters: + /// - [context]: The BuildContext required for the Navigator and translation + /// purposes. + /// - [originalFile]: The original image File that needs to be edited. + /// + /// The method first retrieves all available translations for the + /// `ImageEditor` component and sets them using `ImageEditor.i18n`. + /// It then pushes the `SingleImageEditor` screen onto the navigation stack, + /// passing the image data from [originalFile] as a parameter. + /// + /// After the `SingleImageEditor` screen is popped, the method checks if + /// there is a non-null value of type Uint8List (edited image data). If + /// there is, the [originalFile] is deleted, and a new File with the same + /// path is created with the edited image data. + static Future editFile(BuildContext context, File originalFile) async { + final translations = ComponentStrings.of(context).getAll(); + ImageEditor.i18n(translations); + final Uint8List? value = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return SingleImageEditor( + image: originalFile.readAsBytesSync(), + ); + }, + ), + ); + if (value != null) { + await originalFile.delete(recursive: true); + await File(originalFile.path).writeAsBytes(value); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('image', image)); + properties.add(EnumProperty('initialCropAspectRatio', initialCropAspectRatio)); + } +} + +/// Aspect ratio for the initial crop +enum ZdsAspectRatio { + /// no aspect ratio for crop + custom, + + /// the same as aspect ratio of image + /// `cropAspectRatio` is not more than 0.0, it's original + original, + + /// ratio of width and height is 1 : 1 + ratio1x1, + + /// ratio of width and height is 3 : 4 + ratio3x4, + + /// ratio of width and height is 4 : 3 + ratio4x3, + + /// ratio of width and height is 9 : 16 + ratio9x16, + + /// ratio of width and height is 16 : 9 + ratio16x9, +} + +extension _ZdsAspectRatioToDouble on ZdsAspectRatio { + double? toValue() { + switch (this) { + case ZdsAspectRatio.custom: + return null; + case ZdsAspectRatio.original: + return 0; + case ZdsAspectRatio.ratio1x1: + return 1; + case ZdsAspectRatio.ratio3x4: + return 3 / 4; + case ZdsAspectRatio.ratio4x3: + return 4 / 3; + case ZdsAspectRatio.ratio9x16: + return 9 / 16; + case ZdsAspectRatio.ratio16x9: + return 16 / 9; + } + } +} + +/// State for [ZdsImageEditor]. +class ZdsImageEditorState extends State with FrameCallbackMixin { + final _editorKey = GlobalKey(); + + late final List<_AspectRatioItem> _aspectRatios = <_AspectRatioItem>[ + _AspectRatioItem( + text: ComponentStrings.of(context).get('DEFAULT', 'Default'), + value: image_editor.CropAspectRatios.original, + ), + _AspectRatioItem(text: ComponentStrings.of(context).get('CUSTOM', 'Custom')), + _AspectRatioItem(text: '1:1', value: image_editor.CropAspectRatios.ratio1_1), + _AspectRatioItem(text: '4:3', value: image_editor.CropAspectRatios.ratio4_3), + _AspectRatioItem(text: '3:4', value: image_editor.CropAspectRatios.ratio3_4), + _AspectRatioItem(text: '16:9', value: image_editor.CropAspectRatios.ratio16_9), + _AspectRatioItem(text: '9:16', value: image_editor.CropAspectRatios.ratio9_16), + ]; + + late Uint8List _imageBytes; + late double? _aspectRatio = widget.initialCropAspectRatio.toValue(); + + bool _saving = false; + + /// True if image is being saved. + bool get saving => _saving; + + set saving(bool saving) { + if (_saving == saving) return; + setState(() { + _saving = saving; + }); + } + + bool _cropping = false; + + /// True if image is being cropped. + bool get cropping => _cropping; + + set cropping(bool cropping) { + if (_cropping == cropping) return; + setState(() { + _cropping = cropping; + }); + } + + @override + void initState() { + _imageBytes = widget.image.readAsBytesSync(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + const color = Colors.white; + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + surfaceTintColor: Colors.white, + title: Text(ComponentStrings.of(context).get('EDIT', 'Edit')), + actions: [ + IconButton( + tooltip: ComponentStrings.of(context).get('COMPLETE_EDITING', 'Complete editing'), + icon: saving + ? const Center( + child: SizedBox( + width: 24, + height: 24, + child: Center( + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + ), + ) + : const Icon(Icons.done), + onPressed: _saveImage, + ), + ], + ), + body: image_editor.ExtendedImage.memory( + _imageBytes, + fit: BoxFit.contain, + cacheRawData: true, + mode: image_editor.ExtendedImageMode.editor, + extendedImageEditorKey: _editorKey, + initEditorConfigHandler: (state) { + return image_editor.EditorConfig( + maxScale: 8, + initialCropAspectRatio: widget.initialCropAspectRatio.toValue(), + cropAspectRatio: _aspectRatio, + cornerColor: Colors.white, + editorMaskColorHandler: (context, editing) { + if (editing) { + return Colors.black26; + } else { + return Colors.black45; + } + }, + ); + }, + ), + bottomNavigationBar: BottomAppBar( + color: Colors.black, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: cropping + ? _croppingOptions() + : Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _iconWithText( + const Icon(Icons.crop), + ComponentStrings.of(context).get('CROP', 'Crop'), + () => cropping = true, + color, + ), + _iconWithText( + const Icon(Icons.flip), + ComponentStrings.of(context).get('FLIP', 'Flip'), + () => _editorKey.currentState?.flip(), + color, + ), + _iconWithText( + const Icon(Icons.rotate_left), + ComponentStrings.of(context).get('ROTATE_LEFT', 'Rotate Left'), + () => _editorKey.currentState?.rotate(right: false), + color, + ), + _iconWithText( + const Icon(Icons.rotate_right), + ComponentStrings.of(context).get('ROTATE_RIGHT', 'Rotate Right'), + () => _editorKey.currentState?.rotate(), + color, + ), + _iconWithText( + const Icon(Icons.restore), + ComponentStrings.of(context).get('RESET', 'Reset'), + () => _editorKey.currentState?.reset(), + color, + ), + ], + ), + ), + ), + ); + } + + Widget _croppingOptions() { + return SizedBox( + height: 66, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: _aspectRatios.length, + itemBuilder: (context, index) { + final _AspectRatioItem item = _aspectRatios[index]; + + void onTap() { + setState(() { + cropping = false; + _aspectRatio = item.value; + }); + } + + if (item.value == _aspectRatio) { + return Builder( + builder: (context) { + atLast(() async => Scrollable.ensureVisible(context, alignment: 0.5)); + return Center( + child: ZdsButton.filled( + onTap: onTap, + child: Text(item.text), + ), + ); + }, + ); + } else { + return Center( + child: ZdsButton.text( + onTap: onTap, + child: Text( + item.text, + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), + ), + ), + ); + } + }, + ), + ); + } + + Future _saveImage() async { + if (saving) return; + + try { + final currentState = _editorKey.currentState; + + if (currentState == null) return; + + saving = true; + + final fileData = await cropImageDataWithNativeLibrary(state: currentState); + + if (fileData == null) return; + + final appDir = await zdsTempDirectory('edited'); + final fileName = path.basename(widget.image.path); + + //save image to tem file + final tempFile = File(path.join(appDir, 'edit.tmp')); + if (tempFile.existsSync()) { + await tempFile.delete(); + } + + await tempFile.writeAsBytes(fileData); + + // delete fileName file if already exist + final editedFile = File(path.join(appDir, fileName)); + if (editedFile.existsSync()) { + await editedFile.delete(); + } + + // rename edited file to expected filename + if (mounted) { + Navigator.pop(context, await tempFile.rename(editedFile.absolute.path)); + } + } catch (e, stack) { + debugPrint('save failed: $e\n $stack'); + } finally { + saving = false; + } + } + + Widget _iconWithText(Icon icon, String text, void Function()? onTap, Color color) { + return MergeSemantics( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton(color: color, icon: icon, onPressed: onTap), + Text(text, style: TextStyle(fontSize: 10, color: color)), + const SizedBox(height: 5), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('saving', saving)); + properties.add(DiagnosticsProperty('cropping', cropping)); + } +} + +class _AspectRatioItem { + /// Constructs an [_AspectRatioItem]. + _AspectRatioItem({required this.text, this.value}); + + final String text; + final double? value; +} diff --git a/lib/src/components/organisms/image_picker.dart b/lib/src/components/organisms/image_picker.dart new file mode 100644 index 0000000..4c5066e --- /dev/null +++ b/lib/src/components/organisms/image_picker.dart @@ -0,0 +1,145 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../zds_flutter.dart'; + +/// A button that can be used to select an image and show its preview. +/// +/// This component shows [icon] when no image has been selected. Once an image is selected, an image preview will be +/// shown instead. It's possible to reselect a different image once an image has been chosen by tapping on the component +/// again. +/// +/// See also: +/// +/// * [ZdsFilePicker], used to select a variety of files. +class ImagePicker extends StatefulWidget { + /// The side dimension of this component. + /// + /// Defaults to 64. + final double size; + + /// Function called whenever the chosen image changes. + /// + /// Use this to synchronize the image chosen in the parent's state. + final void Function(String)? onChange; + + /// A function called whenever the permission to access the gallery is denied. + final void Function()? onPermissionDenied; + + /// Whether to show a border. + /// + /// Defaults to true. + final bool showBorder; + + /// The background color. + final Color backgroundColor; + + /// The icon that will be shown in the center of this image picker. + final Icon icon; + + /// An optional default image to this image picker. + final Image? image; + + /// A button that can be used to select an image and show its preview. + const ImagePicker({ + required this.backgroundColor, + required this.icon, + super.key, + this.size = 64, + this.onChange, + this.showBorder = true, + this.image, + this.onPermissionDenied, + }); + + @override + ImagePickerState createState() => ImagePickerState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DoubleProperty('size', size)) + ..add(ObjectFlagProperty.has('onChange', onChange)) + ..add(ObjectFlagProperty.has('onPermissionDenied', onPermissionDenied)) + ..add(DiagnosticsProperty('showBorder', showBorder)) + ..add(ColorProperty('backgroundColor', backgroundColor)); + } +} + +/// State for [ImagePicker]. +class ImagePickerState extends State { + String _imagePath = ''; + + Future _pickImage() async { + try { + final FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && mounted) { + setState(() { + _imagePath = result.files.single.path!; + }); + widget.onChange?.call(_imagePath); + } + } on PlatformException catch (_) { + if (widget.onPermissionDenied != null) { + widget.onPermissionDenied?.call(); + } + } + } + + @override + Widget build(BuildContext context) { + return Semantics( + label: _imagePath == '' + ? ComponentStrings.of(context).get('IMAGE_PICKER_SEMANTIC_UNSELECTED', 'Image Picker. No image selected') + : ComponentStrings.of(context).get('IMAGE_PICKER_SEMANTIC_SELECTED', 'Image Picker. Image selected'), + onTapHint: _imagePath == '' + ? ComponentStrings.of(context).get('PICK_IMAGE', 'Pick Image') + : ComponentStrings.of(context).get('CHANGE_IMAGE', 'Change Image'), + button: true, + child: Material( + color: widget.backgroundColor, + borderRadius: const BorderRadius.all(Radius.circular(6)), + child: InkWell( + onTap: _pickImage, + child: Container( + width: widget.size, + height: widget.size, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(6)), + border: widget.showBorder ? Border.all(color: Theme.of(context).colorScheme.secondary) : null, + ), + child: !(_imagePath == '') + ? _ImageContainer( + Image.file( + File(_imagePath), + ), + ) + : widget.image != null + ? _ImageContainer( + widget.image!, + ) + : widget.icon, + ), + ), + ), + ); + } +} + +class _ImageContainer extends StatelessWidget { + final Image image; + + const _ImageContainer(this.image); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(5)), + child: FittedBox(fit: BoxFit.cover, child: image), + ); + } +} diff --git a/lib/src/components/organisms/infinite_list.dart b/lib/src/components/organisms/infinite_list.dart new file mode 100644 index 0000000..1e7a87e --- /dev/null +++ b/lib/src/components/organisms/infinite_list.dart @@ -0,0 +1,276 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Wrapper around a ListView.builder with Zds styling and functions for fetching new items when the user scrolls to the bottom of the list. +/// +/// * [hasMore] - Whether the list has more items that have not yet been retrieved. +/// * [onLoadMore] - Function to retrieve more data. +/// +/// Some variables have changed default values compared to ZdsList / ListView. +/// +/// * [padding] - Default value is EdgeInsets.zero. +/// +/// The other parameters act the same way as the ones in [ZdsList], which extends [ListView]. +/// +/// This example is split into state and view code, and shows how the onLoadMore function works with an API call. +/// +/// ```dart +/// // In state +/// List items = await service.apiCall(); +/// +/// // In view +/// ZdsInfiniteListView( +/// itemBuilder: ((_, index) { +/// return ZdsListTileWrapper( +/// top: index == 0, +/// bottom: index == items.length - 1, +/// child: ZdsListTile( +/// title: Text(items[index]), +/// ), +/// ); +/// }), +/// itemCount: items.length, +/// hasMore: true, +/// onLoadMore: () async => items += await service.apiCall(), +/// ) +/// ``` +/// +/// See also: +/// +/// * [ZdsList], A List with Zds styling. +/// * [ListView], A scrollable list of widgets arranged linearly. +class ZdsInfiniteListView extends StatefulWidget { + /// Key passed to [ZdsList.builder]. + final Key? childKey; + + /// True to set the list in reverse. Defaults to false. + final bool reverse; + + /// Direction of scroll. Defaults to [Axis.vertical]. + final Axis scrollDirection; + + /// ScrollController passed to [ZdsList.builder]. + /// + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + final ScrollController? controller; + + /// Primary passed to [ZdsList.builder]. + /// + /// Whether this is the primary scroll view associated with the parent [PrimaryScrollController]. + final bool? primary; + + /// ScrollPhysics passed to passed to [ZdsList.builder]. + /// + /// How the scroll view should respond to user input. + final ScrollPhysics? physics; + + /// ShrinkWrap passed to [ZdsList.builder]. + /// + /// Defaults to false. + final bool shrinkWrap; + + /// Default value is EdgeInsets.zero. + final EdgeInsetsGeometry? padding; + + /// Builds loading widget whilst data is being fetched. + /// + /// Displayed as last item in list whilst data is loading. + final WidgetBuilder? loadingBuilder; + + /// Builds individual items in the list. + final IndexedWidgetBuilder itemBuilder; + + /// Length of all items in the list. + final int itemCount; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// AddAutomaticKeepAlives passed to [ZdsList.builder]. + /// + /// See: + /// * [SliverChildBuilderDelegate.addAutomaticKeepAlives]. + final bool addAutomaticKeepAlives; + + /// AddRepaintBoundaries passed to [ZdsList.builder]. + /// + /// See: + /// * [SliverChildBuilderDelegate.addRepaintBoundaries]. + final bool addRepaintBoundaries; + + /// AddSemanticIndexes passed to [ZdsList.builder]. + ///See: + /// * [SliverChildBuilderDelegate.addSemanticIndexes]. + final bool addSemanticIndexes; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// {@macro ZdsList.showEmpty} + final bool showEmpty; + + /// Callback function that awaits the retrieval of more data items. + /// Typically used for an API call. + /// + /// This must be provided if hasMore is true. + final Future Function()? onLoadMore; + + /// Whether the list has more items that have not yet been retrieved. + /// When true, onLoadMore is called once the user scrolls to the bottom of the list. + /// + /// If true, onLoadMore must not be null + /// + /// Defaults to false. + final bool hasMore; + + /// Whether the list items should be close together, or separated. + final bool compact; + + /// Creates a [ZdsInfiniteListView]. + const ZdsInfiniteListView({ + required this.itemBuilder, + required this.itemCount, + super.key, + this.childKey, + this.reverse = false, + this.scrollDirection = Axis.vertical, + this.controller, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding = EdgeInsets.zero, + this.cacheExtent, + this.restorationId, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.clipBehavior = Clip.hardEdge, + this.showEmpty = false, + this.loadingBuilder, + this.onLoadMore, + this.hasMore = false, + this.compact = false, + }) : assert( + onLoadMore != null ? hasMore : true && (hasMore ? onLoadMore != null : onLoadMore == null), + 'If hasMore is true, onLoadMore must not be null; if hasMore is false, onLoadMore must be null.', + ); + + @override + ZdsInfiniteListViewState createState() => ZdsInfiniteListViewState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('childKey', childKey)) + ..add(DiagnosticsProperty('reverse', reverse)) + ..add(EnumProperty('scrollDirection', scrollDirection)) + ..add(DiagnosticsProperty('controller', controller)) + ..add(DiagnosticsProperty('primary', primary)) + ..add(DiagnosticsProperty('physics', physics)) + ..add(DiagnosticsProperty('shrinkWrap', shrinkWrap)) + ..add(DiagnosticsProperty('padding', padding)) + ..add(ObjectFlagProperty.has('loadingBuilder', loadingBuilder)) + ..add(ObjectFlagProperty.has('itemBuilder', itemBuilder)) + ..add(IntProperty('itemCount', itemCount)) + ..add(DoubleProperty('cacheExtent', cacheExtent)) + ..add(StringProperty('restorationId', restorationId)) + ..add(DiagnosticsProperty('addAutomaticKeepAlives', addAutomaticKeepAlives)) + ..add(DiagnosticsProperty('addRepaintBoundaries', addRepaintBoundaries)) + ..add(DiagnosticsProperty('addSemanticIndexes', addSemanticIndexes)) + ..add(EnumProperty('dragStartBehavior', dragStartBehavior)) + ..add(EnumProperty('keyboardDismissBehavior', keyboardDismissBehavior)) + ..add(EnumProperty('clipBehavior', clipBehavior)) + ..add(DiagnosticsProperty('showEmpty', showEmpty)) + ..add(ObjectFlagProperty Function()?>.has('onLoadMore', onLoadMore)) + ..add(DiagnosticsProperty('hasMore', hasMore)) + ..add(DiagnosticsProperty('compact', compact)); + } +} + +/// State for [ZdsInfiniteListView]. +class ZdsInfiniteListViewState extends State { + bool _loadingMore = false; + + @override + Widget build(BuildContext context) { + final ZdsList list = ZdsList.builder( + key: widget.childKey, + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + controller: widget.controller, + primary: widget.primary, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + padding: widget.padding, + cacheExtent: widget.cacheExtent, + restorationId: widget.restorationId, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + addSemanticIndexes: widget.addSemanticIndexes, + dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + clipBehavior: widget.clipBehavior, + showEmpty: widget.showEmpty, + itemCount: widget.itemCount + (widget.hasMore ? 1 : 0), + itemBuilder: (context, index) { + if (widget.hasMore && index >= widget.itemCount) { + if (widget.loadingBuilder != null) { + return widget.loadingBuilder!.call(context); + } + return const SizedBox( + height: 44, + child: Center( + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(), + ), + ), + ); + } else { + return widget.itemBuilder.call(context, index); + } + }, + ); + return NotificationListener( + onNotification: (scrollInfo) { + if (!widget.hasMore) return true; + if (scrollInfo.metrics.axisDirection != AxisDirection.down) return true; + if (widget.onLoadMore == null || _loadingMore) return true; + if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) { + _loadingMore = true; + unawaited( + widget.onLoadMore + ?.call() + .whenComplete(() => _loadingMore = false) + .onError((error, stackTrace) => _loadingMore = false), + ); + } + return true; + }, + child: widget.compact ? ZdsListGroup(child: list) : list, + ); + } +} diff --git a/lib/src/components/organisms/list_group.dart b/lib/src/components/organisms/list_group.dart new file mode 100644 index 0000000..47dfe70 --- /dev/null +++ b/lib/src/components/organisms/list_group.dart @@ -0,0 +1,142 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A component that groups together items in a ZdsList so that they have no padding between items. +/// +/// Can either be used with a list of items, or with a dynamic builder as child. +/// +/// You can not have both a child and items. +class ZdsListGroup extends StatelessWidget { + /// A label that goes in the header of this component above the list aligned to the start. + final Text? headerLabel; + + /// A list of widgets that are displayed in the header of the list aligned to the end. + final List? headerActions; + + /// Items in the list. + /// + /// When items are provided they are built in a ZdsList. + /// + /// When items are provided, a child can not be provided, as they are alternate ways of implementing the same part of the component. + final List? items; + + /// A [ZdsList] child that can be a ZdsList.builder. + /// + /// When a child is provided, items can not be provided, as they are alternate ways of implementing the same part of the component. + final Widget? child; + + /// Padding around the outside of the list group. + /// + /// Defaults to ```EdgeInsets.only( bottom: tileTheme.tileMargin,top: tileTheme.tileMargin + additionalMargin);``` + final EdgeInsets? padding; + + /// {@macro card-variant} + final ZdsCardVariant cardVariant; + + /// Background color. + /// + /// Defaults to `[ColorScheme.surface]. + final Color? itemsBackgroundColor; + + /// Constructs a [ZdsListGroup]. + const ZdsListGroup({ + super.key, + this.items, + this.itemsBackgroundColor, + this.child, + this.headerLabel, + this.headerActions, + this.cardVariant = ZdsCardVariant.elevated, + this.padding, + }) : assert( + (items != null && child == null) || (items == null && child != null), + 'Provide only 1 of either items or child', + ); + + @override + Widget build(BuildContext context) { + final cardMargin = Theme.of(context).cardTheme.margin as EdgeInsets? ?? EdgeInsets.zero; + final tileTheme = Theme.of(context).zdsListTileThemeData; + final labelDistance = cardMargin.top + tileTheme.tileMargin; + final hasHeader = headerLabel != null; + final additionalMargin = hasHeader ? tileTheme.labelAdditionalMargin : 0; + + return Padding( + padding: padding ?? + EdgeInsets.only( + bottom: tileTheme.tileMargin, + top: tileTheme.tileMargin + additionalMargin, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasHeader) + Padding( + padding: EdgeInsets.only(left: cardMargin.left, right: cardMargin.right), + child: Row( + children: [ + Expanded( + child: DefaultTextStyle( + style: Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.blueGrey), + child: headerLabel != null ? headerLabel! : const SizedBox(), + ), + ), + if (headerActions != null && headerActions!.isNotEmpty) + IconTheme.merge( + data: IconThemeData(color: Theme.of(context).colorScheme.primaryContainer, size: 20), + child: DefaultTextStyle( + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Theme.of(context).colorScheme.primaryContainer), + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: headerActions!, + ), + ), + ), + ), + ], + ), + ), + SizedBox(height: hasHeader ? labelDistance : 0), + if (child != null) + Expanded( + child: ZdsCard( + variant: cardVariant, + backgroundColor: itemsBackgroundColor ?? Theme.of(context).colorScheme.surface, + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + child: child, + ), + ) + else + // ZdsCard( + // variant: cardVariant, + // backgroundColor: itemsBackgroundColor ?? Theme.of(context).colorScheme.surface, + // padding: EdgeInsets.zero, + // child: + Column( + mainAxisSize: MainAxisSize.min, + children: items!.divide(const Divider()).toList(), + // ), + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('padding', padding)) + ..add(EnumProperty('cardVariant', cardVariant)) + ..add(ColorProperty('itemsBackgroundColor', itemsBackgroundColor)); + } +} diff --git a/lib/src/components/organisms/list_tile.dart b/lib/src/components/organisms/list_tile.dart new file mode 100644 index 0000000..183d15b --- /dev/null +++ b/lib/src/components/organisms/list_tile.dart @@ -0,0 +1,250 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; + +/// List tile with Zds styling. +/// +/// Many parameters have similar functionality to those in [ListTile]. +/// +/// See also: +/// +/// * [ListTile] +/// * [ZdsList] +/// * [ZdsListGroup]. +class ZdsListTile extends StatelessWidget { + /// A widget to display before the title. + /// + /// Typically an [Icon] or a [CircleAvatar] widget. + final Widget? leading; + + /// The primary content of the list tile. + /// + /// Typically a [Text] widget. + /// + /// This should not wrap. To enforce the single line limit, use [Text.maxLines]. + final Widget? title; + + /// Additional content displayed below the title. + /// + /// Typically a [Text] widget. + /// + /// The subtitle's default [TextStyle] depends on [TextTheme.bodyMedium] except + /// [TextStyle.color]. The [TextStyle.color] is [ColorScheme.onBackground]. + final Widget? subtitle; + + /// A widget to display after the title. + final Widget? trailing; + + /// A widget to display below the content of the tile. + /// + /// Typically a divider. + final Widget? bottom; + + /// Whether the tiles are closely packed together or separated + /// + /// Defaults to false + /// + /// Is overridden to true if the tile is within a [ZdsListGroup]. + final bool? shrinkWrap; + + /// Called when the user taps this list tile. + final VoidCallback? onTap; + + /// Empty space to inscribe within the tile, surrounding the contents. + /// + /// Defaults to [ZdsListTileTheme.contentPadding]. + final EdgeInsets? contentPadding; + + /// The background color of the tile. + /// + /// Defaults to [ColorScheme.background], or [Colors.transparent] if with a [ZdsListGroup]. + final Color? backgroundColor; + + /// The crossAxisAlignment of the tile's main Row. + /// + /// Defaults to [CrossAxisAlignment.center]. + final CrossAxisAlignment crossAxisAlignment; + + /// The margin for the tile. + final EdgeInsets? margin; + + /// {@macro card-variant} + final ZdsCardVariant? cardVariant; + + /// for semantics of list tile + final String? semanticLabel; + + /// Constructs a [ZdsListTile]. + const ZdsListTile({ + super.key, + this.leading, + this.title, + this.subtitle, + this.trailing, + this.bottom, + this.onTap, + this.shrinkWrap, + this.contentPadding, + this.backgroundColor, + this.margin, + this.crossAxisAlignment = CrossAxisAlignment.center, + this.cardVariant = ZdsCardVariant.elevated, + this.semanticLabel, + }); + + bool _isAction(Widget? widget) => widget is IconButton || widget is Switch; + + EdgeInsets _resolveInsets(EdgeInsets padding) { + return EdgeInsets.only( + left: _isAction(leading) ? 12 : padding.left, + right: trailing == null + ? 0 + : trailing is Text + ? padding.right + : _isAction(trailing) + ? 6 + : 18, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).zdsListTileThemeData; + final padding = contentPadding ?? theme.contentPadding; + + final leadingWrapper = leading != null + ? IconTheme( + data: Theme.of(context) + .iconTheme + .copyWith(size: theme.iconSize, color: Theme.of(context).colorScheme.secondary), + child: leading!, + ) + : null; + final titleStyle = Theme.of(context).textTheme.bodyMedium!; + final subtitleColor = theme.subtitleColor; + final trailingWrapper = trailing != null + ? DefaultTextStyle( + style: titleStyle.copyWith(color: subtitleColor), + child: IconTheme( + data: Theme.of(context).iconTheme.copyWith( + size: theme.iconSize, + color: Theme.of(context).colorScheme.onSurface, + ), + child: trailing!, + ), + ) + : null; + + final insets = _resolveInsets(padding); + + final setBackgroundColor = backgroundColor ?? + (context.findAncestorWidgetOfExactType() != null + ? Colors.transparent + : Theme.of(context).colorScheme.surface); + Widget tile = Container( + padding: insets, + constraints: const BoxConstraints(minHeight: 40), + alignment: Alignment.center, + color: setBackgroundColor, + child: Row( + crossAxisAlignment: crossAxisAlignment, + children: [ + if (leadingWrapper != null) ...[ + leadingWrapper, + SizedBox(width: _isAction(leading) ? 2 : 6), + ], + Expanded( + child: Padding( + padding: EdgeInsets.only(top: padding.top, bottom: padding.bottom), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) DefaultTextStyle(style: titleStyle, child: title!), + if (subtitle != null) ...[ + const SizedBox(height: 5), + DefaultTextStyle( + style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: subtitleColor), + child: subtitle!, + ), + ], + ], + ), + ), + ), + if (trailing != null) + Theme( + data: Theme.of(context).copyWith( + inputDecorationTheme: const InputDecorationTheme( + contentPadding: EdgeInsets.symmetric(horizontal: 4), + border: InputBorder.none, + ), + ), + child: Container( + padding: const EdgeInsets.only(left: 12), + width: + trailing is TextField || trailing is TextFormField ? MediaQuery.of(context).size.width / 2 : null, + child: trailingWrapper, + ), + ), + ], + ), + ); + + tile = InkWell( + onTap: onTap, + splashColor: ZdsColors.splashColor, + hoverColor: ZetaColors.of(context).isDarkMode ? ZetaColors.of(context).warm.shade10 : ZdsColors.hoverColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + tile, + if (bottom != null) bottom!, + ], + ), + ); + + if (semanticLabel != null) { + tile = Semantics( + button: onTap != null, + label: semanticLabel, + excludeSemantics: true, + onTap: onTap, + child: tile, + ); + } + + if (!(shrinkWrap ?? true)) { + tile = Padding( + padding: EdgeInsets.symmetric(vertical: theme.tileMargin), + child: ZdsCard( + variant: cardVariant ?? ZdsCardVariant.elevated, + padding: EdgeInsets.zero, + margin: margin, + child: Padding( + padding: EdgeInsets.symmetric(vertical: theme.tileMargin), + child: tile, + ), + ), + ); + } + return Material( + color: Colors.transparent, + child: tile, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('shrinkWrap', shrinkWrap)); + properties.add(ObjectFlagProperty.has('onTap', onTap)); + properties.add(DiagnosticsProperty('contentPadding', contentPadding)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(EnumProperty('crossAxisAlignment', crossAxisAlignment)); + properties.add(DiagnosticsProperty('margin', margin)); + properties.add(EnumProperty('cardVariant', cardVariant)); + properties.add(StringProperty('semanticLabel', semanticLabel)); + } +} diff --git a/lib/src/components/organisms/modal.dart b/lib/src/components/organisms/modal.dart new file mode 100644 index 0000000..587f6e8 --- /dev/null +++ b/lib/src/components/organisms/modal.dart @@ -0,0 +1,168 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A widget that can be passed to [showDialog] to warn the user of a potentially destructive action, like deleting a +/// file. Can be used to obtain confirmation that they want to perform said action. +/// +/// ```dart +/// showDialog( +/// context: context, +/// builder: (BuildContext localContext) { +/// return ZdsModal( +/// child: Text('Do you want to delete this comment?'), +/// actions: [ +/// ZdsButton.muted( +/// child: Text('Cancel'), +/// onTap: () => Navigator.of(localContext).pop(), +/// ), +/// ZdsButton( +/// child: Text('Delete'), +/// onTap: () => deleteComment(), +/// ), +/// ], +/// ); +/// }, +/// ); +/// ``` +/// +/// See also: +/// +/// * [ZdsInputDialog], used to retrieve 1 String value with built-in validation. +class ZdsModal extends StatelessWidget { + /// The padding that will surround the [child] widget. + /// + /// EdgeInsets.all(24) by default. + final EdgeInsets padding; + + /// The main widget that will be shown within the modal. + final Widget? child; + + /// Will be displayed at the bottom of the modal. + /// + /// Typically is usually a list of [ZdsButton]. + final List actions; + + /// Whether a keyboard will be shown at any point of interaction with this modal, enabling the modal to avoid the + /// keyboard when it shows. + /// + /// Defaults to false. + final bool usesKeyboard; + + /// An optional icon that will be displayed at the top of the modal. Useful for alert modals. + final IconData? icon; + + /// The alignment in which the [child] will be put along the horizontal axis. + /// + /// Defaults to `CrossAxisAlignment.center`. + final CrossAxisAlignment crossAxisAlignment; + + /// Padding around actions at the bottom of the modal. + /// + /// Defaults to `EdgeInsets.all(24)`. + final EdgeInsets actionsPadding; + + /// Creates the contents of a modal. + const ZdsModal({ + super.key, + this.child, + this.actions = const [], + this.padding = const EdgeInsets.all(24), + this.usesKeyboard = false, + this.icon, + this.crossAxisAlignment = CrossAxisAlignment.center, + this.actionsPadding = const EdgeInsets.all(24), + }); + + @override + Widget build(BuildContext context) { + final Widget modal = SafeArea( + child: Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: IntrinsicHeight( + child: Stack( + alignment: Alignment.topCenter, + clipBehavior: Clip.none, + children: [ + ZdsCard( + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: crossAxisAlignment, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: padding, + child: child, + ), + Align( + alignment: Directionality.of(context) == TextDirection.ltr + ? Alignment.centerRight + : Alignment.centerLeft, + // Scrollable to adapt for small screens and users with big font size settings + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: actionsPadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: actions.divide(const SizedBox(width: 16)).toList(), + ), + ), + ), + ), + ], + ), + ), + if (icon != null) + Positioned( + top: -20, + child: Container( + width: 46, + height: 46, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).primaryColor, + ), + child: Icon(icon, color: Theme.of(context).colorScheme.onPrimary), + ), + ), + ], + ), + ), + ), + ), + ); + return usesKeyboard ? _KeyboardAvoider(child: modal) : modal; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('padding', padding)) + ..add(DiagnosticsProperty('usesKeyboard', usesKeyboard)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(EnumProperty('crossAxisAlignment', crossAxisAlignment)) + ..add(DiagnosticsProperty('actionsPadding', actionsPadding)); + } +} + +class _KeyboardAvoider extends StatelessWidget { + final Widget child; + + const _KeyboardAvoider({required this.child}); + + @override + Widget build(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + return Center( + child: AnimatedContainer( + padding: mediaQuery.viewInsets, + duration: const Duration(milliseconds: 300), + child: SingleChildScrollView(child: child), + ), + ); + } +} diff --git a/lib/src/components/organisms/navigation_menu.dart b/lib/src/components/organisms/navigation_menu.dart new file mode 100644 index 0000000..fa61974 --- /dev/null +++ b/lib/src/components/organisms/navigation_menu.dart @@ -0,0 +1,96 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A menu for navigation to other parts of the app or to other apps. +/// +/// This navigation menu is typically included in the drawer to include links to other section of the app (like +/// settings), or links to other apps. +/// +/// See also: +/// +/// * [ZdsMenuItem], typically used as the [children] of this widget. +class ZdsNavigationMenu extends StatelessWidget { + /// An optional label shown at the start of the menu describing its contents. + /// + /// Typically a [Text] + final Widget? label; + + /// The list of actions in this menu. + /// + /// Typically a list of [ZdsMenuItem]. + final List children; + + /// Whether to have dividers between the items + /// + /// Defaults to false. + final bool withDividers; + + /// Whether to have a spacer at the end of the menu. + /// + /// Defaults to true. + final bool withSpacer; + + /// Constructs a [ZdsNavigationMenu]. + const ZdsNavigationMenu({ + required this.children, + super.key, + this.label, + this.withDividers = false, + this.withSpacer = true, + }); + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (label != null) + ZdsNavigationMenuLabel( + child: label!, + ).paddingInsets(const EdgeInsets.symmetric(vertical: 12, horizontal: 24)), + Material( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children + .divide( + withDividers ? const Divider().paddingInsets(kMenuHorizontalPadding) : const SizedBox(height: 6), + ) + .toList(), + ), + ), + if (withSpacer) Container(height: 12, color: Theme.of(context).colorScheme.background), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('withDividers', withDividers)); + properties.add(DiagnosticsProperty('withSpacer', withSpacer)); + } +} + +//// Label used above a [ZdsNavigationMenu]. +class ZdsNavigationMenuLabel extends StatelessWidget { + /// Label to be rendered. + /// + /// Typically a [Text]. + final Widget child; + + /// Constructs a [ZdsNavigationMenuLabel]. + const ZdsNavigationMenuLabel({required this.child, super.key}); + + @override + Widget build(BuildContext context) { + return child.textStyle( + Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.blueGrey), + ); + } +} diff --git a/lib/src/components/organisms/profile.dart b/lib/src/components/organisms/profile.dart new file mode 100644 index 0000000..f0fd208 --- /dev/null +++ b/lib/src/components/organisms/profile.dart @@ -0,0 +1,116 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A component used to show a snapshot of someone's profile. +/// +/// It is possible to allow users to do actions related to the profile, like editing or favoriting, by using the +/// [action] parameter. +/// +/// ```dart +/// ZdsProfile( +/// avatar: ZdsAvatar( +/// image: Image() +/// nameText: const Text('Average Joe'), +/// jobTitleText: const Text('Store Employee'), +/// action: ZdsButton.text( +/// onTap: () => editProfile(), +/// child: Row( +/// children: [ +/// const Icon(ZdsIcons.edit).paddingOnly(right: 4), +/// const Text('Edit'), +/// ], +/// ), +/// ), +/// ), +/// ) +/// ``` +/// +/// See also: +/// +/// [ZdsNetworkAvatar] and [ZdsAvatar], used to show someone's profile picture. +class ZdsProfile extends StatelessWidget { + /// The user's avatar. + /// + /// Typically a [ZdsNetworkAvatar] or a [ZdsAvatar]. + final PreferredSizeWidget avatar; + + /// The user's name. + final Text nameText; + + /// The user's job title or position. + final Text jobTitleText; + + /// A button at the end of the component to perform actions. + /// + /// Typically a [ZdsButton.text] or an [IconButton]. + final Widget? action; + + /// Semantic label title. + final String? semanticLabelTitle; + + /// Semantic label subtitle. + final String? semanticLabelSubTitle; + + /// Creates a snapshot of someone's profile. + const ZdsProfile({ + required this.avatar, + required this.nameText, + required this.jobTitleText, + this.semanticLabelTitle, + this.semanticLabelSubTitle, + super.key, + this.action, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + avatar, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Semantics( + label: semanticLabelTitle, + child: nameText.textStyle( + Theme.of(context).textTheme.displaySmall?.copyWith(height: 1.2, fontSize: 20), + ), + ), + ), + if (action != null) + action!.applyTheme(Theme.of(context).shrunkenButtonsThemeData).frameConstrained( + minWidth: 0, + maxWidth: double.infinity, + minHeight: 0, + maxHeight: kMaxActionHeight, + ), + ], + ), + Semantics( + label: semanticLabelSubTitle, + child: jobTitleText.textStyle( + Theme.of(context).textTheme.titleSmall!.copyWith(color: Theme.of(context).colorScheme.onBackground), + ), + ), + ], + ).paddingOnly(left: 24), + ), + ], + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('semanticLabelTitle', semanticLabelTitle)) + ..add(StringProperty('semanticLabelSubTitle', semanticLabelSubTitle)); + } +} diff --git a/lib/src/components/organisms/properties_list.dart b/lib/src/components/organisms/properties_list.dart new file mode 100644 index 0000000..9f31c30 --- /dev/null +++ b/lib/src/components/organisms/properties_list.dart @@ -0,0 +1,138 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Enum to determine direction used in [ZdsPropertiesList]. +enum ZdsPropertiesListDirection { + /// Display the data in a horizontal row. + horizontal, + + /// Display the data in a vertical column. + vertical, +} + +/// A list of properties with their respective values. +/// +/// This component can be used instead of a table to show a lot of data at once in an easy to scan format. +/// +/// ```dart +/// ZdsPropertiesList( +// title: Text('Walk #4'), +// properties: { +// 'Points Scored': '380', +// 'Total Points': '700', +// 'Score (%)': '42.12', +// 'Passed (%)': '25.00', +// }, +// ), +/// ``` +/// +/// The data can be displayed in two ways. When [direction] is horizontal, the property and its value will be shown on +/// the same line. When it's vertical, the value will be below the property, vertically. +/// +/// See also: +/// +/// * [ZdsFieldsListTile], another way of showing table-like data. +class ZdsPropertiesList extends StatelessWidget { + /// The optional title to show at the top of the list. + /// + /// Typically a [Text]. + final Widget? title; + + /// The properties to be shown. + /// + /// In this map, the keys represent each property's title or short description, and the values represent the detailed + /// information about each. + final Map? properties; + + /// Whether the properties keys and values are shown horizontally (each key with its value in the same row), or + /// vertically (each value is under its key). + /// + /// Defaults to [ZdsPropertiesListDirection.horizontal]. + final ZdsPropertiesListDirection direction; + + /// Shows a list of properties with their respective values. + const ZdsPropertiesList({ + super.key, + this.title, + this.properties, + this.direction = ZdsPropertiesListDirection.horizontal, + }); + + bool get _horizontal => direction == ZdsPropertiesListDirection.horizontal; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) + title! + .textStyle( + Theme.of(context).textTheme.displaySmall!.copyWith( + color: Theme.of(context).colorScheme.primaryContainer, + ), + ) + .space(20), + if (properties != null) + ...properties!.entries + .map( + (set) => _wrap( + context, + Text(set.key), + _horizontal ? Text(set.value, textAlign: TextAlign.end) : Text(set.value), + ), + ) + .toList() + .divide(_divider), + ], + ), + ); + } + + Widget get _divider { + return SizedBox(height: _horizontal ? 14 : 24); + } + + Widget _wrap(BuildContext context, Widget label, Widget value) { + if (_horizontal) { + return MergeSemantics( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + label.textStyle(Theme.of(context).textTheme.bodyLarge), + Flexible(child: value.textStyle(Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.black))), + ], + ), + ); + } + + return MergeSemantics( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + label + .textStyle( + Theme.of(context).textTheme.titleSmall!.copyWith(color: ZdsColors.blueGrey), + ) + .space(8), + value.textStyle( + Theme.of(context).textTheme.bodyLarge!.copyWith(color: Theme.of(context).colorScheme.onBackground), + overflow: TextOverflow.clip, + ), + ], + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty?>('properties', properties as Map?)) + ..add(EnumProperty('direction', direction)); + } +} diff --git a/lib/src/components/organisms/radio_list.dart b/lib/src/components/organisms/radio_list.dart new file mode 100644 index 0000000..e1ae457 --- /dev/null +++ b/lib/src/components/organisms/radio_list.dart @@ -0,0 +1,156 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A class that [ZdsRadioList.items] must extend. +/// +/// View [ZdsRadioList] for more details. +@immutable +abstract class ZdsRadioItem { + /// The label that will be shown for this item in the ZdsRadioList. + final String label; + + /// The associated value to this item. + final T value; + + /// Constructs a [ZdsRadioItem]. + const ZdsRadioItem(this.label, this.value); + + @override + bool operator ==(Object other) { + if (other is ZdsRadioItem) { + return value == other.value; + } else { + return false; + } + } + + @override + int get hashCode => value.hashCode; +} + +/// Creates a list of radio buttons with labels. Tapping anywhere on the list tile selects the item. +/// +/// The items used in the widget must extend [ZdsRadioItem]. +/// +/// ```dart +/// ZdsRadioList( +/// initialValue: DateValue('Current Month', DateRange.currentMonth), +/// items: [ +/// WalkDate('Current Week', DateRange.currentWeek), +/// WalkDate('Last Week', DateRange.lastWeek), +/// WalkDate('Current Month', DateRange.currentMonth), +/// ], +/// ) +/// // Class definitions +/// class DateValue extends ZdsRadioItem { +/// DateValue(String label, DateRange value) : super(label, value); +/// } +/// +/// enum DateRange {currentWeek,lastWeek, currentMonth,} +/// ``` +class ZdsRadioList> extends StatefulWidget { + /// Primary content of the list, typically a [Text] widget. + final Widget? title; + + /// List of items in the radio list. + final List items; + + /// Initial value of the radio button. + final U? initialValue; + + /// Callback to update item state. + final void Function(U? item)? onChange; + + /// Create a list of items with a radio button and a label. + /// + /// [items] can't be null. + const ZdsRadioList({ + super.key, + this.title, + this.items = const [], + this.onChange, + this.initialValue, + }); + + @override + ZdsRadioListState createState() => ZdsRadioListState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IterableProperty('items', items)) + ..add(DiagnosticsProperty('initialValue', initialValue)) + ..add(ObjectFlagProperty.has('onChange', onChange)); + } +} + +/// State for [ZdsRadioList]. +class ZdsRadioListState> extends State> { + U? _groupValue; + + @override + void initState() { + super.initState(); + _groupValue = widget.initialValue; + } + + void _handleChange(U? value) { + setState(() { + _groupValue = value; + }); + widget.onChange?.call(_groupValue); + } + + bool get _isInExpansionTile => context.findAncestorWidgetOfExactType() != null; + + Widget _paddingWrapper({required Widget child}) { + if (_isInExpansionTile) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: child, + ); + } + return child; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: _isInExpansionTile ? const EdgeInsets.only(bottom: 14) : EdgeInsets.zero, + child: Column( + children: widget.items.map((item) { + return Material( + color: ZdsColors.transparent, + // The list tile wraps its children with a MergeSemantics. This attempts to merge its descendant Semantics + // nodes into one node in the semantics tree. + child: MergeSemantics( + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 48), + child: InkWell( + onTap: () => _handleChange(item), + child: _paddingWrapper( + child: IgnorePointer( + child: Row( + children: [ + Radio( + value: item, + groupValue: _groupValue, + // Empty void function to allow touch events to be handled by the larger container + onChanged: (_) {}, + ), + Text(item.label, style: Theme.of(context).textTheme.bodyMedium), + ], + ), + ), + ), + ), + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/src/components/organisms/search_app_bar.dart b/lib/src/components/organisms/search_app_bar.dart new file mode 100644 index 0000000..51e4196 --- /dev/null +++ b/lib/src/components/organisms/search_app_bar.dart @@ -0,0 +1,308 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../zds_flutter.dart'; + +/// An AppBar with an integrated [ZdsSearchField]. +/// +/// This is typically used to switch out [ZdsAppBar] to perform a search action. As both widgets implement +/// [PreferredSizeWidget], the Scaffold's appBar's height stays the same, and the body content is not displaced. +/// +/// ```dart +/// Scaffold( +/// appBar: isSearching +/// ? ZdsSearchAppBar( +/// hintText: 'Search', +/// textEditingController: controller, +/// onSubmit: (text) { +/// controller.text = text; +/// state.searchTerm = text; +/// state.onSearchSubmit(); +/// }, +/// trailing: ZdsButton.text( +// child: const Text('Cancel'), +// onTap: () => (setState() => isSearching = false), +// ), +/// ) +/// : ZdsAppBar( +/// actions: [ +/// IconButton(onPressed: () => (setState() => isSearching = true)) +/// ], +/// ) +/// ) +/// ``` +/// +/// This component can be used to update search results in two ways. [onChange] and [onClear] can be used to update +/// the search results as the user types their query without needing to be submitted through the press of a button. If +/// the search requires a lookup that will take time, you can instead only use [onSubmit] to only query results when +/// the user presses the search button on their keyboard. +/// +/// It is also possible to show an overlay on this appBar, which is typically used to inform the user about validation +/// errors or when no results are found. +/// +/// ```dart +/// Scaffold( +/// appBar: ZdsSearchAppBar( +/// showOverlay: state.showNoResultsBar && state.searchString != '', +/// overlayBuilder: (_) => Column( +/// crossAxisAlignment: CrossAxisAlignment.start, +/// children: [ +/// Text('no results found...').padding(5), +/// ], +/// ).padding(10), +/// // Other code declaring the appBar's content goes here +/// ), +/// ) +/// ``` +/// +/// See also: +/// +/// * [ZdsSearchField], which this component uses to provide a search field. +/// * [ZdsAppBar], a more typical appBar +/// * [ZdsEmpty], which can be used to show a no results message. +class ZdsSearchAppBar extends StatefulWidget implements PreferredSizeWidget { + /// Hint text that will appear when the user hasn't written anything in the search field. + final String? hintText; + + /// Optional pre-filled text. + /// + /// If null, [hintText] will be shown instead. + final String? initValue; + + /// Callback called whenever the search field's text changes. + final void Function(String value)? onChange; + + /// Callback called whenever the user presses the 'Search' button on their keyboard. + final void Function(String value)? onSubmit; + + /// Callback called whenever the text on the search field is cleared + final VoidCallback? onClear; + + /// A controller that can be used to notify listeners when the text changes. + final TextEditingController? textEditingController; + + /// A widget shown before the search field. + /// + /// Do not put actions like cancelling a search here, use [trailing] instead. + final Widget? leading; + + /// A widget shown after the search field. + /// + /// Typically a [ZdsButton.text] used to show a 'cancel' button. + final Widget? trailing; + + /// The focusNode for the search field. + final FocusNode? focusNode; + + /// This appBar's preferred height. + /// + /// Defaults to 80. + final double height; + + /// A builder for an optional overlay that will be shown below the search field. + final WidgetBuilder? overlayBuilder; + + /// Whether to show the overlay created with [overlayBuilder]. + final bool showOverlay; + + /// Set custom background color . + final Color? backgroundColor; + + /// Set custom top padding . + final double? topPadding; + + /// Creates an appBar with a search field. + const ZdsSearchAppBar({ + super.key, + this.hintText, + this.onChange, + this.onSubmit, + this.leading, + this.trailing, + this.focusNode, + this.textEditingController, + this.height = 80.0, + this.overlayBuilder, + this.showOverlay = false, + this.onClear, + this.initValue, + this.backgroundColor, + this.topPadding, + }); + + @override + ZdsSearchAppBarState createState() => ZdsSearchAppBarState(); + + @override + Size get preferredSize => Size.fromHeight(height); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('hintText', hintText)); + properties.add(StringProperty('initValue', initValue)); + properties.add(ObjectFlagProperty.has('onChange', onChange)); + properties.add(ObjectFlagProperty.has('onSubmit', onSubmit)); + properties.add(ObjectFlagProperty.has('onClear', onClear)); + properties.add(DiagnosticsProperty('textEditingController', textEditingController)); + properties.add(DiagnosticsProperty('focusNode', focusNode)); + properties.add(DoubleProperty('height', height)); + properties.add(ObjectFlagProperty.has('overlayBuilder', overlayBuilder)); + properties.add(DiagnosticsProperty('showOverlay', showOverlay)); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + properties.add(DoubleProperty('topPadding', topPadding)); + } +} + +/// State for [ZdsSearchAppBar]. +class ZdsSearchAppBarState extends State { + final GlobalKey _searchKey = GlobalKey(); + + OverlayEntry? _overlayEntry; + + bool _isShowingOverlay() => _overlayEntry != null; + + void _showOverlay() { + _overlayEntry = _createOverlayEntry(); + + if (_overlayEntry != null) { + Overlay.of(context).insert(_overlayEntry!); + } + } + + void _hideOverlay() { + _overlayEntry?.remove(); + _overlayEntry = null; + } + + void _syncWidgetAndOverlay() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + + if (_isShowingOverlay() && !widget.showOverlay) { + _hideOverlay(); + } else if (!_isShowingOverlay() && widget.showOverlay) { + _showOverlay(); + } + }); + } + + OverlayEntry? _createOverlayEntry() { + if (widget.overlayBuilder == null) { + return null; + } + + final Offset offset = _searchKey.currentContext?.widgetGlobalPosition ?? Offset.zero; + final Size size = _searchKey.currentContext?.widgetBounds.size ?? Size.zero; + + const EdgeInsets padding = EdgeInsets.all(20); + + return OverlayEntry( + builder: (context) => Positioned( + top: offset.dy + (size.height * 0.8), + left: offset.dx + padding.left, + width: size.width - padding.left - padding.right, + child: Material( + elevation: 2, + borderRadius: BorderRadius.circular(6), + child: widget.overlayBuilder?.call(context), + ), + ), + ); + } + + bool _showSuffixClearButton = false; + final _searchController = TextEditingController(); + + TextEditingController get _textEditingController { + return widget.textEditingController ?? _searchController; + } + + @override + void initState() { + if (widget.showOverlay) { + WidgetsBinding.instance.addPostFrameCallback((_) => _showOverlay()); + } + + _textEditingController.addListener(_showClearButton); + super.initState(); + } + + @override + void didUpdateWidget(ZdsSearchAppBar oldWidget) { + super.didUpdateWidget(oldWidget); + _syncWidgetAndOverlay(); + } + + @override + void reassemble() { + super.reassemble(); + _syncWidgetAndOverlay(); + } + + @override + void dispose() { + if (_isShowingOverlay()) { + _hideOverlay(); + } + _textEditingController.removeListener(_showClearButton); + super.dispose(); + } + + void _showClearButton() { + setState(() { + _showSuffixClearButton = _textEditingController.text.isNotEmpty; + }); + } + + @override + Widget build(BuildContext context) { + final clearButton = _showSuffixClearButton + ? IconButton( + tooltip: ComponentStrings.of(context).get('CLEAR', 'Clear'), + icon: Icon( + ZdsIcons.close_circle, + color: ZdsColors.greySwatch(context)[800], + ), + onPressed: () { + _textEditingController.clear(); + widget.onChange?.call(_textEditingController.text); + widget.onClear?.call(); + }, + ) + : null; + + final backgroundColor = widget.backgroundColor ?? Theme.of(context).scaffoldBackgroundColor; + return AnnotatedRegion( + value: computeSystemOverlayStyle(backgroundColor), + child: Material( + color: backgroundColor, + child: SafeArea( + child: Row( + children: [ + if (widget.leading != null) widget.leading!, + Expanded( + child: ZdsSearchField( + variant: ZdsSearchFieldVariant.outlined, + textFormFieldKey: _searchKey, + focusNode: widget.focusNode, + controller: _textEditingController, + hintText: widget.hintText, + onSubmit: widget.onSubmit, + onChange: widget.onChange, + initValue: widget.initValue, + suffixIcon: clearButton, + padding: EdgeInsets.zero.copyWith( + left: widget.leading == null ? 19 : 0, + right: widget.trailing == null ? 19 : 0, + ), + ).paddingInsets(const EdgeInsets.symmetric(vertical: 4)), + ), + if (widget.trailing != null) widget.trailing!, + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/components/organisms/tab_scaffold.dart b/lib/src/components/organisms/tab_scaffold.dart new file mode 100644 index 0000000..206575d --- /dev/null +++ b/lib/src/components/organisms/tab_scaffold.dart @@ -0,0 +1,130 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'bottom_tab_bar.dart'; + +/// Scaffold with built in support for bottom tabs. +/// +/// Example using [bodyBuilder]: +/// ```dart +/// ZdsBottomTabScaffold( +/// tabs: [ +/// ZdsNavItem( +/// icon: const Icon(Icons.search), +/// label: 'Search', +/// ), +/// ZdsNavItem( +/// icon: const Icon(Icons.analytics_outlined), +/// label: 'Reports', +/// ), +/// ], +/// bodyBuilder: (context, index) => Center(child: Text(index.toString())), +/// ); +/// ``` +/// +/// Example using [children]: +/// ```dart +/// ZdsBottomTabScaffold( +/// tabs: [ +/// ZdsNavItem( +/// icon: const Icon(Icons.search), +/// label: 'Search', +/// ), +/// ZdsNavItem( +/// icon: const Icon(Icons.analytics_outlined), +/// label: 'Reports', +/// ), +/// ], +/// children: const [Center(child: Text('Search')), Center(child: Text('Reports'))], +/// ); +/// ``` +/// +/// See also: +/// * [ZdsBottomTabBar] - To use a bottom tab bar without a scaffold. +/// * [ZdsNavItem] +class ZdsBottomTabScaffold extends StatefulWidget { + /// List of [ZdsNavItem]. + final List tabs; + + /// List of views. + /// + /// The list index corresponds to the tab. + /// + /// __You can have either children or a bodyBuilder, not both.__ + final List? children; + + /// Builder function to create the body of the page. + /// + /// __You can have either children or a bodyBuilder, not both.__ + /// + /// * `context` is the build context of the view. + /// + /// * `index` is the index of the selected tab. + final Widget Function(BuildContext context, int index)? bodyBuilder; + + /// Callback function for whenever the tab is changed. + /// + /// Returns the index of the selected tab. + final void Function(int)? onTabChange; + + /// Builds a scaffold with built in support for bottom tabs. + const ZdsBottomTabScaffold({ + required this.tabs, + super.key, + this.children, + this.bodyBuilder, + this.onTabChange, + }) : assert( + (children != null && bodyBuilder == null) || (children == null && bodyBuilder != null), + 'You can have either children or a bodyBuilder, not both.', + ), + assert( + children != null && tabs.length == children.length || children == null, + 'The size of children and tabs must be the same', + ); + + @override + ZdsBottomTabScaffoldState createState() => ZdsBottomTabScaffoldState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty('tabs', tabs)); + properties + .add(ObjectFlagProperty.has('bodyBuilder', bodyBuilder)); + properties.add(ObjectFlagProperty.has('onTabChange', onTabChange)); + } +} + +/// State for [ZdsBottomTabScaffold]. +class ZdsBottomTabScaffoldState extends State { + int _currentIndex = 0; + void _handleTap(int index) { + widget.onTabChange?.call(index); + setState(() { + _currentIndex = index; + }); + } + + Widget? _resolveChild() { + if (widget.bodyBuilder != null) { + return widget.bodyBuilder!(context, _currentIndex); + } else { + if (widget.children!.length - 1 >= _currentIndex) { + return widget.children![_currentIndex]; + } + return null; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: _resolveChild(), + bottomNavigationBar: ZdsBottomTabBar( + onTap: _handleTap, + currentIndex: _currentIndex, + items: widget.tabs, + ), + ); + } +} diff --git a/lib/src/components/organisms/tags_list.dart b/lib/src/components/organisms/tags_list.dart new file mode 100644 index 0000000..dc3de05 --- /dev/null +++ b/lib/src/components/organisms/tags_list.dart @@ -0,0 +1,63 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Creates a list to show multiple [ZdsTag]. +/// +/// If [direction] is [Axis.horizontal], the items will wrap around if they don't fit. +class ZdsTagsList extends StatelessWidget { + /// This list's items. + final List? items; + + /// The direction of this list. + /// + /// If [Axis.horizontal], the items will wrap around if they don't fit. + /// + /// Defaults to [Axis.vertical]. + final Axis direction; + + /// Defines the horizontal separation between tags. + /// + /// Defaults to 8. + final double horizontalSpace; + + /// Defines the vertical separation between tags. + /// + /// Defaults to 6. + final double verticalSpace; + + /// Creates a list of [ZdsTag]. + const ZdsTagsList({ + super.key, + this.items, + this.direction = Axis.vertical, + this.horizontalSpace = 8, + this.verticalSpace = 6, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Wrap( + direction: direction, + spacing: direction == Axis.vertical ? verticalSpace : horizontalSpace, + runSpacing: direction == Axis.vertical ? horizontalSpace : verticalSpace, + children: items ?? [], + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('direction', direction)) + ..add(DoubleProperty('horizontalSpace', horizontalSpace)) + ..add(DoubleProperty('verticalSpace', verticalSpace)); + } +} diff --git a/lib/src/components/organisms/temp_directory/io.dart b/lib/src/components/organisms/temp_directory/io.dart new file mode 100644 index 0000000..51979fe --- /dev/null +++ b/lib/src/components/organisms/temp_directory/io.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:video_compress/video_compress.dart'; +import '../../../../zds_flutter.dart'; + +/// Creates a temporary directory for [ZdsFilePicker]. +Future zdsTempDirectory([ + String subdir1 = '', + String subdir2 = '', + String subdir3 = '', + String subdir4 = '', + String subdir5 = '', + String subdir6 = '', +]) async { + final supportDir = await path_provider.getApplicationSupportDirectory(); + final tempDir = Directory( + path.join(supportDir.path, 'Zds-ui', subdir1, subdir2, subdir3, subdir4, subdir5, subdir6), + ); + if (!tempDir.existsSync()) await tempDir.create(recursive: true); + return tempDir.absolute.path; +} + +/// Clears all files and cache from any [ZdsFilePicker]. +Future clearUiTempDirectory() async { + await Directory(await zdsTempDirectory()).delete(recursive: true); + await VideoCompress.deleteAllCache(); +} diff --git a/lib/src/components/organisms/temp_directory/resolver.dart b/lib/src/components/organisms/temp_directory/resolver.dart new file mode 100644 index 0000000..4ef7702 --- /dev/null +++ b/lib/src/components/organisms/temp_directory/resolver.dart @@ -0,0 +1 @@ +export 'web.dart' if (dart.library.io) 'io.dart'; diff --git a/lib/src/components/organisms/temp_directory/web.dart b/lib/src/components/organisms/temp_directory/web.dart new file mode 100644 index 0000000..fa5b24c --- /dev/null +++ b/lib/src/components/organisms/temp_directory/web.dart @@ -0,0 +1,15 @@ +import '../../../../zds_flutter.dart'; + +/// Creates a temporary directory for [ZdsFilePicker]. +Future zdsTempDirectory([ + String subdir1 = '', + String subdir2 = '', + String subdir3 = '', + String subdir4 = '', + String subdir5 = '', + String subdir6 = '', +]) async => + ''; + +/// Clears all files and cache from any [ZdsFilePicker]. +Future clearUiTempDirectory() async {} diff --git a/lib/src/utils/assets.dart b/lib/src/utils/assets.dart new file mode 100644 index 0000000..31eaf18 --- /dev/null +++ b/lib/src/utils/assets.dart @@ -0,0 +1,3 @@ +export 'assets/animations.dart'; +export 'assets/icons.dart'; +export 'assets/images.dart'; diff --git a/lib/src/utils/assets/animations.dart b/lib/src/utils/assets/animations.dart new file mode 100644 index 0000000..db9023e --- /dev/null +++ b/lib/src/utils/assets/animations.dart @@ -0,0 +1,49 @@ +/// A collection of animations to be used in Zds applications. +/// +/// The variables are JSON assets so will need to be used with the Lottie package: https://pub.dev/packages/lottie +/// +/// Usage: +/// ```dart +/// Lottie.asset(ZdsAnimations.taskComplete) +/// ``` +class ZdsAnimations { + // This class is not meant to be instantiated or extended; this constructor + // prevents instantiation and extension. + ZdsAnimations._(); + + /// An animated approval stamp + static const approvalStamped = 'packages/zds_flutter/lib/assets/animations/approval_stamped.json'; + + /// An animated checkmark inside a circle + static const check = 'packages/zds_flutter/lib/assets/animations/check.json'; + + /// An animated checkmark inside an animated circle + static const checkCircle = 'packages/zds_flutter/lib/assets/animations/two_checks.json'; + + /// An animated checkmark with a glimmer around it + static const checkGlimmer = 'packages/zds_flutter/lib/assets/animations/check_glimmer.json'; + + /// An rippling checkmark in a circle + static const checkRipple = 'packages/zds_flutter/lib/assets/animations/check_ripple.json'; + + /// An animated thumbs up + static const thumbsUp = 'packages/zds_flutter/lib/assets/animations/thumbs_up.json'; + + /// An animated thumbs up next to 'approved' text + static const thumbsUpApproved = 'packages/zds_flutter/lib/assets/animations/thumbs_up_approved.json'; + + /// A clock animated followed by an animated check + static const timeApproved = 'packages/zds_flutter/lib/assets/animations/time_approved.json'; + + /// A clock animated followed by an animated thumbs up in a box + static const timeApprovedBox = 'packages/zds_flutter/lib/assets/animations/time_approved_box.json'; + + /// A clock animated followed by an animated thumbs up with a glimmer + static const timeApprovedGlimmer = 'packages/zds_flutter/lib/assets/animations/time_approved_glimmer.json'; + + /// A timecard being approved + static const timecardTapping = 'packages/zds_flutter/lib/assets/animations/timecard_tapping.json'; + + /// Two animated checkmarks on a page + static const twoChecks = 'packages/zds_flutter/lib/assets/animations/check_circle.json'; +} diff --git a/lib/src/utils/assets/icons.dart b/lib/src/utils/assets/icons.dart new file mode 100644 index 0000000..9f6e2ff --- /dev/null +++ b/lib/src/utils/assets/icons.dart @@ -0,0 +1,445 @@ +// ignore_for_file: constant_identifier_names, public_member_api_docs + +// To add new icons, follow the instructions in https://confluence.zebra.com/display/IDD/Internal+developer+resources +import 'package:flutter/widgets.dart'; +import 'package:path/path.dart' as path; +import 'package:zeta_flutter/zeta_flutter.dart'; + +/// Identifiers for the supported icons. +/// +/// Use with the [Icon] class to show specific icons. +/// +/// Icons are identified by their name as listed below, e.g. [ZdsIcons.add_task]. +/// +/// ```dart +/// Icon( +/// ZdsIcons.add_task, +/// color: Colors.green, +/// size: 24.0, +/// semanticLabel: 'Text to announce in accessibility modes', +/// ) +/// ``` +/// +/// See also: +/// +/// * [Icon]. +class ZdsIcons { + ZdsIcons._(); + + static const String _fontFamily = 'zds-icons'; + static const String _fontPackage = 'zds_flutter'; + + static const IconData action = IconData(0xe9ad, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData add = IconData(0xe903, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData add_link = IconData(0xe902, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData add_task = IconData(0xe94c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData add_user = IconData(0xe904, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData attachment = IconData(0xe905, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData audio = IconData(0xe9af, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData audio_file = IconData(0xe906, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData audit_activity = IconData(0xe900, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData back = IconData(0xe907, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData break_ = IconData(0xe9b0, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData broadcast = IconData(0xe9b1, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData building = IconData(0xe9b2, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar = IconData(0xe96f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_3_day = IconData(0xe9b3, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_available = IconData(0xe9b4, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_day = IconData(0xe9b5, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_edit = IconData(0xe9b6, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_month = IconData(0xe9b7, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_rollover = IconData(0xe9b8, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_today = IconData(0xe965, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_trade = IconData(0xe9ba, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_unavailable = IconData(0xe96c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_week = IconData(0xe9bc, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData calendar_year = IconData(0xe9bd, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData call = IconData(0xe9bf, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData camera = IconData(0xe95d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData camera_switch = IconData(0xe95c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData category = IconData(0xe908, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData certified = IconData(0xe9c0, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData channel = IconData(0xe909, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chat = IconData(0xe9c7, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chat_bot = IconData(0xe9c1, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chat_message = IconData(0xe9c5, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chat_message_read = IconData(0xe9c2, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chat_search = IconData(0xe9c6, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chat_unread_active = IconData(0xe90a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData check = IconData(0xe90b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData check_circle = IconData(0xe90c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chevron_down = IconData(0xe90d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chevron_left = IconData(0xe90e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chevron_right = IconData(0xe90f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData chevron_up = IconData(0xe910, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clear = IconData(0xe963, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clipboard = IconData(0xe945, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock = IconData(0xe9d8, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_available = IconData(0xe9c8, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_bid = IconData(0xe9ca, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_edit = IconData(0xe9cc, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_filled = IconData(0xe98b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_flexible = IconData(0xe9cd, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_in_progress = IconData(0xe9ce, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_info = IconData(0xe9cf, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_missed = IconData(0xe9d0, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_on = IconData(0xe9d1, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_rollover = IconData(0xe9d2, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_segment = IconData(0xe9d3, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_start = IconData(0xe9d4, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_stop = IconData(0xe9d5, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_switch = IconData(0xe9d6, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData clock_timer = IconData(0xe9d7, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData close = IconData(0xe911, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData close_circle = IconData(0xe944, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData cloud_download = IconData(0xe9d9, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData cloud_off = IconData(0xe964, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData cloud_upload = IconData(0xe9da, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData complaint = IconData(0xe912, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData copy = IconData(0xe966, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData dashboard = IconData(0xe967, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData delete = IconData(0xe913, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData details = IconData(0xe914, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData dialpad = IconData(0xe9db, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData download = IconData(0xe9dc, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData edit = IconData(0xe916, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData edit_notifications = IconData(0xe94d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData edit_redo = IconData(0xe968, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData edit_undo = IconData(0xe969, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData email = IconData(0xe9e0, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData email_forward = IconData(0xe9dd, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData email_reply = IconData(0xe9df, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData email_reply_all = IconData(0xe9de, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData empty = IconData(0xe99a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData ess = IconData(0xe94f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData expand = IconData(0xe915, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData eye_preview = IconData(0xe96a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData eye_view = IconData(0xe96b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file = IconData(0xf016, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_archive = IconData(0xf1c6, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_audio = IconData(0xf1c7, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_code = IconData(0xf1c9, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_excel = IconData(0xf1c3, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_image = IconData(0xf1c5, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_pdf = IconData(0xf1c1, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_powerpoint = IconData(0xf1c4, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_text = IconData(0xf0f6, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_video = IconData(0xf1c8, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData file_word = IconData(0xf1c2, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData filter = IconData(0xe917, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData flag = IconData(0xe96d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData flip = IconData(0xe96e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData folder = IconData(0xe9e1, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData form = IconData(0xe918, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData format_bold = IconData(0xe923, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData format_italic = IconData(0xe924, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData forward = IconData(0xe970, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData fullscreen = IconData(0xe972, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData fullscreen_exit = IconData(0xe971, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData funnel = IconData(0xe919, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData globe = IconData(0xe9e2, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData glossary = IconData(0xe901, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData grid = IconData(0xe973, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData headset = IconData(0xe9e3, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData history = IconData(0xe91a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData home = IconData(0xe975, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData horn = IconData(0xe91b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData hourglass = IconData(0xe9e4, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData how_do_I = IconData(0xe91c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData image = IconData(0xe976, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData import = IconData(0xe9e5, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData inbox = IconData(0xe9e6, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData indicator_alert = IconData(0xe977, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData indicator_cancel = IconData(0xe978, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData indicator_check = IconData(0xe979, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData indicator_help = IconData(0xe97a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData indicator_info = IconData(0xe97b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData info = IconData(0xe921, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData keyboard = IconData(0xe9e7, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData launch = IconData(0xe922, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData lightbulb = IconData(0xe9e8, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData link = IconData(0xe97d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData link_add = IconData(0xe97c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData list_bullet = IconData(0xe97f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData list_bullet_contained = IconData(0xe97e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData list_bulleted = IconData(0xe91f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData list_details = IconData(0xe980, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData list_feedback = IconData(0xe981, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData list_form = IconData(0xe982, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData list_numbered = IconData(0xe920, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData list_task = IconData(0xe983, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData location = IconData(0xe9ea, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData location_off = IconData(0xe9e9, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData lock = IconData(0xe985, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData lock_undo = IconData(0xe984, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData maintenance = IconData(0xe9eb, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData meal = IconData(0xe9ec, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData menu = IconData(0xe927, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData message_read = IconData(0xe928, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData mic = IconData(0xe9ed, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData minus = IconData(0xe987, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData mode_dark = IconData(0xe988, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData mode_light = IconData(0xe989, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData money = IconData(0xe929, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData more_hori = IconData(0xe98a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData more_vert = IconData(0xe92a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData mywork = IconData(0xe950, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData navigate = IconData(0xe9ee, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData negative = IconData(0xe92b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData new_message = IconData(0xe92c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData note = IconData(0xe98c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData notification = IconData(0xe92d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData notification_silence = IconData(0xe95f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData notifications_off = IconData(0xe948, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData notifications_off_outlined = + IconData(0xe92f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData open_in_small = IconData(0xe95e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData open_new = IconData(0xe9f1, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData open_new_off = IconData(0xe9f0, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData open_shift = IconData(0xe962, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData org = IconData(0xe99d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData palette = IconData(0xe9f2, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData paper = IconData(0xe98d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData partially_approved = IconData(0xe95a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData partner = IconData(0xe9f3, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData pdf = IconData(0xe92e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData person_assign = IconData(0xe9f4, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData person_clock = IconData(0xe9f5, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData person_info = IconData(0xe9f6, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData person_manager = IconData(0xe9f7, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData person_move = IconData(0xe9f8, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData person_swap = IconData(0xe9f9, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData person_walk = IconData(0xe9fa, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData phone = IconData(0xe9fb, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData pin = IconData(0xe931, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData pinboard = IconData(0xe951, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData plus = IconData(0xe98f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData point_gift = IconData(0xe9fc, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData point_heart = IconData(0xe9fd, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData point_money = IconData(0xe9fe, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData point_star = IconData(0xe9ff, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData point_token = IconData(0xea00, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData positive = IconData(0xe932, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData preview = IconData(0xe933, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData print = IconData(0xe990, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData profile = IconData(0xea01, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData project = IconData(0xe934, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData publish = IconData(0xea02, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qchat = IconData(0xe952, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qcheck = IconData(0xe954, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qdocs = IconData(0xe955, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qforms = IconData(0xe956, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qnote = IconData(0xe94b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qnote_expired = IconData(0xe94e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qnotes = IconData(0xe957, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qr = IconData(0xea03, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qrcode = IconData(0xe94a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qstar = IconData(0xe958, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData queue = IconData(0xe991, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData qvisual = IconData(0xe959, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData radio_off = IconData(0xe992, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData radio_on = IconData(0xe993, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData rar = IconData(0xe953, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData react_add = IconData(0xea04, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData react_angry = IconData(0xea05, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData react_thumbsdown = IconData(0xea06, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData react_thumbsup = IconData(0xea07, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData reassign = IconData(0xe947, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData recall = IconData(0xe994, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData redo_variant = IconData(0xe925, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData refresh = IconData(0xe995, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData reorder = IconData(0xe996, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData repeat = IconData(0xe997, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData report = IconData(0xe935, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData reset = IconData(0xe998, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData review = IconData(0xe936, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData sales = IconData(0xe937, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData save = IconData(0xea08, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData schedule = IconData(0xe938, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData scheduling = IconData(0xe99e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData search = IconData(0xe939, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData search_advance = IconData(0xe949, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData security = IconData(0xe9a4, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData send = IconData(0xe93a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData settings_gear = IconData(0xe99b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData settings_tune = IconData(0xe99c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData share = IconData(0xea0b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData share_file = IconData(0xea0a, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData sign_out = IconData(0xe93b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData sm = IconData(0xe95b, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData sort = IconData(0xe93c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData sphere = IconData(0xe9c9, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData star = IconData(0xe93d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData start_outlined = IconData(0xe946, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData store = IconData(0xea0d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData store_swap = IconData(0xea0c, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData strikethrough = IconData(0xe91e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData subscription = IconData(0xe9a5, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData summary = IconData(0xe93e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData support = IconData(0xe93f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData surveys = IconData(0xe99f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData switch_ = IconData(0xe9a0, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData sync = IconData(0xe9a1, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData synchronize = IconData(0xe940, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData tag = IconData(0xe941, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData task = IconData(0xe942, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData taskmanager = IconData(0xe9a2, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData tiers = IconData(0xe98e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData timecard = IconData(0xe999, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData timecard_approve = IconData(0xea0e, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData timecard_warning = IconData(0xea0f, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData translation = IconData(0xe9a3, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData trash = IconData(0xe9a8, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData unclaim = IconData(0xe943, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData underlined = IconData(0xe91d, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData undo_variant = IconData(0xe926, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData unsubscribe = IconData(0xe9a7, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData update = IconData(0xe9aa, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData upload = IconData(0xea11, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData user = IconData(0xea15, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData user_add = IconData(0xea12, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData user_group = IconData(0xea13, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData user_multiple = IconData(0xea14, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData vacation = IconData(0xea16, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData verified = IconData(0xea17, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData video = IconData(0xe974, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData walk = IconData(0xe986, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData wifi = IconData(0xea18, fontFamily: _fontFamily, fontPackage: _fontPackage); + static const IconData write = IconData(0xe9ac, fontFamily: _fontFamily, fontPackage: _fontPackage); + + static const _extensions = { + '.au': ZdsIcons.file_audio, + '.avi': ZdsIcons.file_video, + '.csv': ZdsIcons.file_text, + '.doc': ZdsIcons.file_word, + '.docx': ZdsIcons.file_word, + '.flv': ZdsIcons.file_video, + '.gif': ZdsIcons.file_image, + '.htm': ZdsIcons.file, + '.ico': ZdsIcons.file_image, + '.jpg': ZdsIcons.file_image, + '.jpeg': ZdsIcons.file_image, + '.m4v': ZdsIcons.file_video, + '.midi': ZdsIcons.file_audio, + '.mov': ZdsIcons.file_video, + '.mp3': ZdsIcons.file_audio, + '.mp4': ZdsIcons.file_video, + '.mpg': ZdsIcons.file_video, + '.mpeg': ZdsIcons.file_video, + '.pdf': ZdsIcons.file_pdf, + '.png': ZdsIcons.file_image, + '.bmp': ZdsIcons.file_image, + '.ppt': ZdsIcons.file_powerpoint, + '.pptx': ZdsIcons.file_powerpoint, + '.qt': ZdsIcons.file, + '.rar': ZdsIcons.file_archive, + '.rtf': ZdsIcons.file_text, + '.tif': ZdsIcons.file_image, + '.tiff': ZdsIcons.file_image, + '.ttf': ZdsIcons.file, + '.txt': ZdsIcons.file_text, + '.wav': ZdsIcons.file_audio, + '.xls': ZdsIcons.file_excel, + '.xlsx': ZdsIcons.file_excel, + '.xml': ZdsIcons.file_excel, + '.zip': ZdsIcons.file_archive, + '.url': ZdsIcons.file, + }; + + // Text Documents: #376FC9 +// PDF & PPT (distinguished as they are commonly used): #DB0D00 +// Images: #F56200 +// Video: #6F00C6 +// Audio: #70A300 +// Spreadsheets: #1F802E +// Misc: #888888 + static Color iconColor(String ext) { + switch (ext) { + case '.doc': + case '.docx': + case '.rtf': + case '.ttf': + case '.txt': + return ZetaColorBase.blue.shade60; + + case '.pdf': + case '.ppt': + case '.pptx': + return ZetaColorBase.red.shade80; + + case '.gif': + case '.ico': + case '.jpg': + case '.jpeg': + case '.png': + case '.tif': + case '.tiff': + return ZetaColorBase.orange.shade50; + + case '.flv': + case '.m4v': + case '.mov': + case '.mpeg': + case '.mpg': + case '.qt': + return ZetaColorBase.purple.shade70; + + case '.au': + case '.avi': + case '.midi': + case '.mp3': + case '.mp4': + case '.wav': + return ZetaColorBase.green.shade40; + + case '.csv': + case '.xml': + case '.xls': + case '.xlsx': + return ZetaColorBase.green.shade80; + + case '.htm': + case '.rar': + case '.url': + case '.zip': + default: + return ZetaColorBase.greyWarm.shade50; + } + } + + static IconData _resolveFileIcon(String? name) { + if (name == null) return ZdsIcons.file; + final ext = path.extension(name); + return _extensions[ext] ?? ZdsIcons.file; + } + + static Color? _resolveFileColor(String? name) { + if (name == null) return null; + final ext = path.extension(name).toLowerCase(); + return iconColor(ext); + } +} + +extension IconDataFromExt on String { + /// Assuming the string is a file name, this function returns the corresponding icon for the filetype. + /// + /// Here the icon for an image will be returned + /// + /// ```dart + /// Icon( + /// 'example.jpg'.fileIcon(), + /// color: green, + /// size: 24.0, + /// semanticLabel: 'Text to announce in accessibility modes', + /// ) + /// ``` + /// + /// If the string does not contain a file type, or contains an unrecognized filetype, [ZdsIcons.file] will be returned. + IconData fileIcon() { + return ZdsIcons._resolveFileIcon(this); + } + + Icon coloredFileIcon() { + return Icon(ZdsIcons._resolveFileIcon(this), color: ZdsIcons._resolveFileColor(this)); + } +} diff --git a/lib/src/utils/assets/images.dart b/lib/src/utils/assets/images.dart new file mode 100644 index 0000000..30493e4 --- /dev/null +++ b/lib/src/utils/assets/images.dart @@ -0,0 +1,61 @@ +import 'package:flutter_svg/flutter_svg.dart'; + +/// A collection of Images and SvgPictures to be used in Zds applications. +/// +/// These images are all static and so do not need to have any instantiation. +/// +/// For example: +/// ```dart +/// SizedBox( +/// child: ZdsImages.notifications, +/// height: 300, +/// width: 300, +/// ), +/// ``` +class ZdsImages { + // This class is not meant to be instantiated or extended; this constructor + // prevents instantiation and extension. + ZdsImages._(); + + /// ![Notifications graphic](https://Zds-components.web.app/assets/png/notifications.png) + static SvgPicture notifications = SvgPicture.asset('packages/zds_flutter/lib/assets/images/notifications.svg'); + + /// ![Chat graphic](https://Zds-components.web.app/assets/png/chat.png) + static SvgPicture chat = SvgPicture.asset('packages/zds_flutter/lib/assets/images/chat.svg'); + + /// ![Notes graphic](https://Zds-components.web.app/assets/png/notes.png) + static SvgPicture notes = SvgPicture.asset('packages/zds_flutter/lib/assets/images/notes.svg'); + + /// ![Fail load graphic](https://Zds-components.web.app/assets/png/load_fail.png) + static SvgPicture loadFail = SvgPicture.asset('packages/zds_flutter/lib/assets/images/load_fail.svg'); + + /// ![Fail load graphic](https://Zds-components.web.app/assets/png/cloud_fail.png) + static SvgPicture cloudFail = SvgPicture.asset('packages/zds_flutter/lib/assets/images/cloud_fail.svg'); + + /// ![Fail load graphicZ](https://Zds-components.web.app/assets/png/server_fail.png) + static SvgPicture serverFail = SvgPicture.asset('packages/zds_flutter/lib/assets/images/server_fail.svg'); + + /// ![Calendar illustration](https://Zds-components.web.app/assets/png/calendar.png) + static SvgPicture calendar = SvgPicture.asset('packages/zds_flutter/lib/assets/images/calendar.svg'); + + /// ![Completed Tasks illustration](https://Zds-components.web.app/assets/png/completedTasks.png) + static SvgPicture completedTasks = SvgPicture.asset('packages/zds_flutter/lib/assets/images/completed_tasks.svg'); + + /// ![Empty Box illustration](https://Zds-components.web.app/assets/png/emptyBox.png) + static SvgPicture emptyBox = SvgPicture.asset('packages/zds_flutter/lib/assets/images/empty_box.svg'); + + /// ![Sad Zebra illustration] (https://Zds-components.web.app/assets/png/sadZebra.png) + static SvgPicture sadZebra = SvgPicture.asset('packages/zds_flutter/lib/assets/images/sad_zebra.svg'); + + /// ![Sleeping Zebra illustration] (https://Zds-components.web.app/assets/png/sleepingZebra.png) + static SvgPicture sleepingZebra = SvgPicture.asset('packages/zds_flutter/lib/assets/images/sleeping_zebra.svg'); + + /// ![Search illustration](https://Zds-components.web.app/assets/png/search.png) + static SvgPicture search = SvgPicture.asset('packages/zds_flutter/lib/assets/images/search.svg'); + + /// ![Punch Illustration] + static SvgPicture punch = SvgPicture.asset('packages/zds_flutter/lib/assets/images/punch.svg'); + + /// ![Map illustration](https://Zds-components.web.app/assets/png/map.png) + static SvgPicture map = SvgPicture.asset('packages/zds_flutter/lib/assets/images/map.svg'); +} diff --git a/lib/src/utils/localizations.dart b/lib/src/utils/localizations.dart new file mode 100644 index 0000000..8a6e88e --- /dev/null +++ b/lib/src/utils/localizations.dart @@ -0,0 +1,2 @@ +export 'localizations/localization.dart'; +export 'localizations/translation.dart'; diff --git a/lib/src/utils/localizations/localization.dart b/lib/src/utils/localizations/localization.dart new file mode 100644 index 0000000..2c3683b --- /dev/null +++ b/lib/src/utils/localizations/localization.dart @@ -0,0 +1,74 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +import '../../../zds_flutter.dart'; + +/// Provides localizations for this library. +/// +/// Provided used within [ZdsApp], so if [ZdsApp] is used, this should not be added to application. +class ComponentLocalisation extends StatelessWidget { + /// Creates a [ComponentLocalisation]. + const ComponentLocalisation({ + required this.child, + super.key, + this.delegates, + this.localeOverride, + }); + + /// The widget to contain translations. + /// + /// Typically the entire application, as is the case for any app using [ZdsApp], as it contains [ComponentLocalisation] + /// + /// ```dart + ///class App extends StatelessWidget { + /// const App({Key? key}) : super(key: key); + /// + /// @override + /// Widget build(BuildContext context) { + /// return ComponentLocalisation( + /// localeOverride: Locale('de'), + /// child: MaterialApp( + /// body: + /// builder: (context, child) { + /// return Text(ComponentStrings.of(context).get('VIEW','View')); + /// }, + /// ), + /// ); + /// } + /// } + /// ``` + final Widget child; + + ///The delegates for this app's [Localizations] widget. + /// + ///The delegates collectively define all of the localized resources for this application's [Localizations] widget. + /// + ///Internationalized apps that require translations for one of the locales listed in [GlobalMaterialLocalizations] should specify this parameter and list the [MaterialApp.supportedLocales] that the application can handle. + final List>? delegates; + + /// Overrides the locale that would otherwise be set. + final Locale? localeOverride; + + @override + Widget build(BuildContext context) { + return Localizations( + delegates: delegates ?? + [ + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ComponentStrings.delegate, + ], + locale: localeOverride ?? ComponentStrings.defaultLocale, + child: child, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(IterableProperty>('delegates', delegates)); + properties.add(DiagnosticsProperty('localeOverride', localeOverride)); + } +} diff --git a/lib/src/utils/localizations/translation.dart b/lib/src/utils/localizations/translation.dart new file mode 100644 index 0000000..bc3ab49 --- /dev/null +++ b/lib/src/utils/localizations/translation.dart @@ -0,0 +1,278 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:flutter_localizations/flutter_localizations.dart'; + +/// Translated Strings file +class ComponentStrings { + /// Default locale of the components is English. + static const defaultLocale = Locale('en'); + + /// Locale set to get translated strings of. + /// + /// If the locale is not supported, will fallback to using [defaultLocale]. + final Locale locale; + + /// debug strings flag. + /// Default value of [debugStrings] is false. + /// If true then [get] method returns key instead of value + final bool debugStrings; + + /// Delegate used to get the translated strings. + static LocalizationsDelegate delegate = ComponentDelegate(); + + Map _strings = {}; + + /// Constructs a [ComponentStrings]. + /// default value for [debugStrings] is false. + ComponentStrings(this.locale, {this.debugStrings = false}); + + /// Returns the localized resources object of the given `ComponentStrings` for the widget + /// tree that corresponds to the given `context`. + static ComponentStrings of(BuildContext context) { + return Localizations.of(context, ComponentStrings)!; + } + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('ar'), + Locale('bg'), + Locale('bs'), + Locale('cs'), + Locale('de'), + Locale('de', 'AT'), + Locale('de', 'BE'), + Locale('el'), + Locale('en'), + Locale('en', 'CA'), + Locale('en', 'GB'), + Locale('en', 'US'), + Locale('es'), + Locale('es', 'CL'), + Locale('es', 'EC'), + Locale('es', 'ES'), + Locale('es', 'PE'), + Locale('es', 'PR'), + Locale('es', 'UY'), + Locale('fi'), + Locale('fr'), + Locale('fr', 'BE'), + Locale('fr', 'CA'), + Locale('hr'), + Locale('hu'), + Locale('id'), + Locale('it'), + Locale('ja'), + Locale('ka'), + Locale('ko'), + Locale('lt'), + Locale('lv'), + Locale('nb'), + Locale('nl'), + Locale('pl'), + Locale('pt'), + Locale('pt', 'BR'), + Locale('ro'), + Locale('ru'), + Locale('sk'), + Locale('sl'), + Locale('sr'), + Locale('sv'), + Locale('th'), + Locale('tl'), + Locale('tr'), + Locale('uk'), + Locale('vi'), + Locale('zh'), + Locale('zh', 'HK'), + ]; + + /// Gets a translated string from [key], otherwise uses [fallback]. + /// + /// Optional args can be added to place hardcoded values (such as numbers) within the translated text. + /// if the [debugStrings] is true then it always return key. + String get(String key, String fallback, {List? args}) { + if (debugStrings) return '#$key'; + + final str = _strings[key] ?? ''; + final string = str.isEmpty ? fallback : str; + if (args != null && args.isNotEmpty) { + final mapping = List>.generate(args.length, (index) => MapEntry('$index', args[index])); + return string.format(Map.fromEntries(mapping)); + } else { + return string; + } + } + + /// Gets all translated strings + Map getAll() { + return _strings; + } + + /// Update existing strings with [delta]. + void update(Map delta) { + for (final entry in delta.entries) { + if (entry.value.isNotEmpty) { + _strings[entry.key] = entry.value; + } + } + } + + /// Loads strings from file and sets [_strings]. + Future load() async { + // Loading strings from assets + _strings = await _loadStrings(); + return this; + } + + Future> _loadStrings() async { + const assetPath = 'packages/zds_flutter/lib/assets/strings/'; + + String strings = ''; + + if (locale.countryCode != null) { + try { + // Check if strings with lang-code and country code are present + strings = await rootBundle.loadString('$assetPath${locale.languageCode}_${locale.countryCode}.json'); + } catch (e) { + debugPrint(e.toString()); + try { + strings = await rootBundle.loadString('$assetPath${locale.languageCode}.json'); + } catch (e2) { + debugPrint(e2.toString()); + } + } + } + + // Fallback on langCode if it's not the english language + if (strings.isEmpty) { + try { + // Check if strings with lang-code and country code are present + strings = await rootBundle.loadString('$assetPath${locale.languageCode}.json'); + } catch (e) { + debugPrint(e.toString()); + } + } + + if (strings.isEmpty) { + return {}; + } else { + try { + return Map.from(jsonDecode(strings) as Map); + } catch (e) { + return {}; + } + } + } +} + +/// Abstract class for the component delta strings +// ignore: one_member_abstracts +abstract class ComponentDeltaProvider { + ///Load delta string of the locale. + Future> loadDelta(Locale locale); +} + +/// Delegate to get translations for these components. +class ComponentDelegate extends LocalizationsDelegate { + /// Custom string delta + final ComponentDeltaProvider? deltaProvider; + + ///debug strings + final bool debugStrings; + + /// Constructs a [ComponentDelegate]. + ComponentDelegate({this.deltaProvider, this.debugStrings = false}); + + @override + Future load(Locale locale) async { + final strings = await ComponentStrings(locale).load(); + if (deltaProvider != null) { + final delta = await deltaProvider!.loadDelta(locale); + if (delta.isNotEmpty) { + strings.update(delta); + } + } + return strings; + } + + @override + bool isSupported(Locale locale) => [ + 'ar', + 'bg', + 'bs', + 'cs', + 'de', + 'el', + 'en', + 'es', + 'fi', + 'fr', + 'hr', + 'hu', + 'id', + 'it', + 'ja', + 'ka', + 'ko', + 'lt', + 'lv', + 'nb', + 'nl', + 'pl', + 'pt', + 'ro', + 'ru', + 'sk', + 'sl', + 'sr', + 'sv', + 'th', + 'tl', + 'tr', + 'uk', + 'vi', + 'zh', + ].contains(locale.languageCode); + + @override + bool shouldReload(ComponentDelegate old) => true; +} + +/// Formats the strings. +extension StringFormatter on String { + /// Formats the strings. + /// + /// Replaces {placeholder} in strings with { "placeholder" : "some value"} map. + /// Example + /// print("Hello {user}! You have {count} new messages.".format({ "user" : "John", "count" : "10"})) + /// prints -> "Hello John! You have 10 new messages." + String format(Map args) { + var test = this; + for (final entry in args.entries) { + test = test.replaceAll('{${entry.key}}', entry.value); + } + return test; + } +} diff --git a/lib/src/utils/localizations/untranslated.json b/lib/src/utils/localizations/untranslated.json new file mode 100644 index 0000000..1d3fe40 --- /dev/null +++ b/lib/src/utils/localizations/untranslated.json @@ -0,0 +1,5 @@ +{ + "WEEK": "Week", + "FROM": "From", + "ALL": "All" +} \ No newline at end of file diff --git a/lib/src/utils/theme.dart b/lib/src/utils/theme.dart new file mode 100644 index 0000000..fb68529 --- /dev/null +++ b/lib/src/utils/theme.dart @@ -0,0 +1,5 @@ +export 'theme/colors.dart'; +export 'theme/input_border.dart'; +export 'theme/theme.dart'; +export 'theme/theme_constants.dart'; +export 'theme/theme_provider.dart'; diff --git a/lib/src/utils/theme/colors.dart b/lib/src/utils/theme/colors.dart new file mode 100644 index 0000000..959522f --- /dev/null +++ b/lib/src/utils/theme/colors.dart @@ -0,0 +1,401 @@ +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; +import '../tools/app.dart'; +import 'theme_loader.dart'; + +ZetaColors get _zetaColors => appZetaColors ?? ZetaColors(); +late ColorScheme? _colorScheme; + +/// Utility extensions on Color. +extension ColorUtils on Color { + /// This function calculates the luminance of the given color and returns either false or true based + /// on whether the luminance is greater than 0.5 or not. The higher the luminance, the lighter the color. + /// A luminance of 0.5 is considered the threshold to determine if the color is light or dark. + bool get isDark => computeLuminance() <= 0.5; + + /// Determines if a color is dark taking into account its opacity. + bool isDarkWithOpacity({Color? background}) => Color.alphaBlend(this, background ?? _colorScheme!.background).isDark; + + /// This getter on the Color class that determines if the color is warm or not. + /// We can use the Hue-Saturation-Value (HSV) representation of the color. + /// A color is generally considered warm if its hue lies between 0 and 180 degrees. + bool get isWarm { + final hsvColor = HSVColor.fromColor(this); + return hsvColor.hue >= 0 && hsvColor.hue <= 180; + } +} + +/// Class that defines all colors to be used in the ZdsComponents +/// +/// +/// [greySwatch], [primarySwatch] and [secondarySwatch] all require a context be passed in that is a child of [ZdsApp], +/// like so: +/// ```dart +/// Container(color: ZdsColors.greySwatch(context)[500]) +/// ``` +/// +/// All the other colors can be called directly without the context: +/// ```dart +/// Container(color: ZdsColors.darkGrey) +/// ``` +class ZdsColors { + ZdsColors._(); + + /// Green color. + /// + /// Typically used to show success. + static final green = _zetaColors.green.primary; + + /// Yellow color. + /// + /// Typically used to show warning. + static final yellow = _zetaColors.yellow.primary; + + ///Orange color. + static final orange = _zetaColors.orange.primary; + + /// Red color. + /// + /// Typically used to show an error. + static final red = _zetaColors.red.primary; + + /// Purple color. + static final purple = _zetaColors.purple.primary; + + /// Blue color. + static final blue = _zetaColors.blue.primary; + + /// Teal color. + static final teal = _zetaColors.teal.primary; + + /// Black color. + /// + /// Default text color. + static final black = _zetaColors.textDefault; + + ///Dark-grey color + /// + /// Subtle text color. + static final darkGrey = _zetaColors.textSubtle; + + /// Blue-grey color + static final blueGrey = _zetaColors.cool.shade60; + + /// Light-grey color + static final lightGrey = _zetaColors.cool.shade40; + + /// White color + /// + /// #FFFFFF + static final white = _zetaColors.white; + + /// Transparent color + /// + /// #FFFFFF with 0% opacity + static const Color transparent = Color(0x00000000); + + /// Splash color. + /// + /// Typically used for inkwell splash color + /// + /// #171717 with 10% opacity + static final splashColor = _zetaColors.black.withOpacity(0.1); + + /// Shadow color. + /// + /// Used on cards and tiles to create the shadow. + static final shadowColor = _zetaColors.shadow; + + /// Barrier color. + /// + /// Used as a background scrim for modals. + static const barrierColor = Color(0x80000000); + + /// Shadow color with 100% opacity. + /// + /// Typically used with a level of opacity, i.e. `shadowColor100.withOpacity(0.4` + /// + /// Used on cards and tiles to create the shadow. + static const shadowColor100 = Color(0xFF49505E); + + /// Hover color + /// + /// Typically used to show what the user has hovered on. + static final hoverColor = _zetaColors.isDarkMode ? _zetaColors.black : _zetaColors.surfaceHovered; + + /// Swatch of green colors. + + static final ColorSwatch greenSwatch = ColorSwatch(_zetaColors.green.shade60.value, { + 'light': _zetaColors.green.shade30, + 'medium': _zetaColors.green.shade60, + 'dark': _zetaColors.green.shade80, + }); + + /// Swatch of red colors. + static final ColorSwatch redSwatch = ColorSwatch(_zetaColors.red.shade60.value, { + 'fair': _zetaColors.red.shade10, + 'light': _zetaColors.red.shade30, + 'medium': _zetaColors.red.shade60, + 'dark': _zetaColors.red.shade80, + }); + + /// Requires that a [BuildContext] that is a child of [ZdsApp] is passed. Can be called using: + /// ```dart + /// Container(color: ZdsColors.greySwatch(context)[500]) + /// ``` + static MaterialColor greySwatch(BuildContext context) { + final bool isWarm = Theme.of(context).colorScheme.background != greyCoolSwatch[50]; + return isWarm ? greyWarmSwatch : greyCoolSwatch; + } + + /// Gets MaterialColor swatch for shades of the primary color. + /// Requires that a [BuildContext] that is a child of [ZdsApp] is passed. Can be called using: + /// ```dart + /// Container(color: ZdsColors.primarySwatch(context)[500]) + /// ``` + static MaterialColor primarySwatch(BuildContext context) { + final Color primary = Theme.of(context).colorScheme.primary; + return MaterialColor(primary.value, { + 50: getShadedColor(primary, 0.05), + 100: getShadedColor(primary, 0.1), + 200: getShadedColor(primary, 0.2), + 300: getShadedColor(primary, 0.3), + 400: getShadedColor(primary, 0.4), + 500: getShadedColor(primary, 0.5), + 600: getShadedColor(primary, 0.6), + 700: getShadedColor(primary, 0.7), + 800: getShadedColor(primary, 0.8), + 900: getShadedColor(primary, 0.9), + 1000: getShadedColor(primary, 1), + }); + } + + /// Gets MaterialColor swatch for shades of the secondary color + /// Requires that a [BuildContext] that is a child of [ZdsApp] is passed. Can be called using: + /// ```dart + /// Container(color: ZdsColors.secondarySwatch(context)[500]) + /// ``` + static MaterialColor secondarySwatch(BuildContext context) { + final Color secondary = Theme.of(context).colorScheme.secondary; + return MaterialColor(secondary.value, { + 50: getShadedColor(secondary, 0.05), + 100: getShadedColor(secondary, 0.1), + 200: getShadedColor(secondary, 0.2), + 300: getShadedColor(secondary, 0.3), + 400: getShadedColor(secondary, 0.4), + 500: getShadedColor(secondary, 0.5), + 600: getShadedColor(secondary, 0.6), + 700: getShadedColor(secondary, 0.7), + 800: getShadedColor(secondary, 0.8), + 900: getShadedColor(secondary, 0.9), + }); + } + + /// Swatch of cool grey colors to be used with a cool theme. + static final MaterialColor greyCoolSwatch = MaterialColor(_zetaColors.cool.shade60.value, { + 50: _zetaColors.cool.shade10, + 100: _zetaColors.cool.shade10, + 200: _zetaColors.cool.shade20, + 300: _zetaColors.cool.shade30, + 400: _zetaColors.cool.shade40, + 500: _zetaColors.cool.shade50, + 600: _zetaColors.cool.shade60, + 700: _zetaColors.cool.shade70, + 800: _zetaColors.cool.shade80, + 900: _zetaColors.cool.shade90, + 1000: _zetaColors.cool.shade100, + 1100: _zetaColors.cool.shade100, + 1200: _zetaColors.cool.shade100, + }); + + /// Swatch of warm grey colors to be used with a warm theme. + /// + /// NOTE: Colors come from zeta, and so 50,1100 and 1200 no longer exist and will be their nearest values. + static final MaterialColor greyWarmSwatch = MaterialColor(_zetaColors.warm.shade60.value, { + 50: _zetaColors.warm.shade10, + 100: _zetaColors.warm.shade10, + 200: _zetaColors.warm.shade20, + 300: _zetaColors.warm.shade30, + 400: _zetaColors.warm.shade40, + 500: _zetaColors.warm.shade50, + 600: _zetaColors.warm.shade60, + 700: _zetaColors.warm.shade70, + 800: _zetaColors.warm.shade80, + 900: _zetaColors.warm.shade90, + 1000: _zetaColors.warm.shade100, + 1100: _zetaColors.warm.shade100, + 1200: _zetaColors.warm.shade100, + }); + + /// Const definition needed for input border. + // TODO(colors): replace this with reference to Zeta. + static const Color inputBorderColor = Color(0xFFBDBDBD); + + static const Color _defaultPrimary = Color(0xFF1C3760); + static const Color _defaultPrimaryContainer = Color(0xFF2A526F); + static const Color _defaultSecondary = Color(0xFF007ABA); + static const Color _defaultSecondaryContainer = Color(0xFF2B54A3); +} + +/// Class to allow custom color definitions in dark and light modes +/// +/// See also: +/// * [ZdsColorScheme] +@immutable +class BrandColors { + /// Constructs a [BrandColors] + const BrandColors({ + required this.light, + required this.dark, + }); + + /// Color Scheme to use in light mode. + final ColorScheme light; + + ///Color scheme to use in dark mode. + final ColorScheme dark; + + /// Default color scheme containing Zds theme colors. + BrandColors.zdsDefault() + : light = ZdsColorScheme.light(), + dark = ZdsColorScheme.dark(); + + /// Creates a BrandColors from a json object passed in. + factory BrandColors.fromJson(Map json) { + return BrandColors( + light: ZdsColorScheme.lightFromJson(json['light'] as Map), + dark: ZdsColorScheme.darkFromJson(json['dark'] as Map), + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BrandColors && runtimeType == other.runtimeType && light == other.light && dark == other.dark; + + @override + int get hashCode => light.hashCode ^ dark.hashCode; +} + +/// Builds color schemes that are supported in the Zds Library +/// [ZdsColorScheme.light] and [ZdsColorScheme.dark] optional parameters have the following defaults: +/// * primary: #1C3760 +/// * primaryContainer: #2A526F +/// * secondary: #007ABA +/// * secondaryContainer: #2B54A3 +/// * error: #ED1C24 +class ZdsColorScheme extends ColorScheme { + /// Constructs a light theme. + /// + /// All parameters are optional, and if not provided the fallback will be the Zds default colors. + ZdsColorScheme.light({ + Color? primary, + Color? primaryContainer, + Color? secondary, + Color? secondaryContainer, + Color? error, + bool isWarm = true, + }) : super.light( + brightness: Brightness.light, + primary: primary ?? ZdsColors._defaultPrimary, + onPrimary: computeForeground(primary ?? ZdsColors._defaultPrimary), + primaryContainer: primaryContainer ?? ZdsColors._defaultPrimaryContainer, + onPrimaryContainer: computeForeground(primaryContainer ?? ZdsColors._defaultPrimaryContainer), + secondary: secondary ?? ZdsColors._defaultSecondary, + onSecondary: computeForeground(secondary ?? ZdsColors._defaultSecondary), + secondaryContainer: secondaryContainer ?? ZdsColors._defaultSecondaryContainer, + onSecondaryContainer: computeForeground(secondary ?? ZdsColors._defaultSecondaryContainer), + surface: ZdsColors.white, + onSurface: ZdsColors.black, + background: isWarm ? ZdsColors.greyWarmSwatch[50]! : ZdsColors.greyCoolSwatch[50]!, + onBackground: isWarm ? ZdsColors.greyWarmSwatch[1200]! : ZdsColors.greyCoolSwatch[1200]!, + error: error ?? ZdsColors.red, + onError: computeForeground(error ?? ZdsColors.red), + ); + + /// Constructs a dark theme. + /// + /// All parameters are optional, and if not provided the fallback will be the Zds default colors: + ZdsColorScheme.dark({ + Color? primary, + Color? primaryContainer, + Color? secondary, + Color? secondaryContainer, + Color? error, + bool isWarm = true, + }) : super.dark( + brightness: Brightness.dark, + primary: primary ?? ZdsColors._defaultPrimary, + onPrimary: computeForeground(primary ?? ZdsColors._defaultPrimary), + primaryContainer: primaryContainer ?? ZdsColors._defaultPrimaryContainer, + onPrimaryContainer: computeForeground(primaryContainer ?? ZdsColors._defaultPrimaryContainer), + secondary: secondary ?? ZdsColors._defaultSecondary, + onSecondary: computeForeground(secondary ?? ZdsColors._defaultSecondary), + secondaryContainer: secondaryContainer ?? ZdsColors._defaultSecondaryContainer, + onSecondaryContainer: computeForeground(secondary ?? ZdsColors._defaultSecondaryContainer), + surface: ZdsColors.black, + onSurface: ZdsColors.white, + background: isWarm ? ZdsColors.greyWarmSwatch[1200]! : ZdsColors.greyCoolSwatch[1200]!, + onBackground: isWarm ? ZdsColors.greyWarmSwatch[50]! : ZdsColors.greyCoolSwatch[50]!, + error: error ?? ZdsColors.red, + onError: computeForeground(error ?? ZdsColors.red), + ); + + /// Constructs a light theme from a provided json object. + /// + /// Colors should be as hexadecimal colors, and the naming conventions should match [ColorScheme] + /// + /// The json object should contain the keys: + /// * primary + /// * primaryVariant + /// * secondary + /// * secondaryVariant + /// * error + factory ZdsColorScheme.lightFromJson(Map? json) { + if (json != null) { + return ZdsColorScheme.light( + primary: _toColor(json['primary']), + primaryContainer: _toColor(json['primaryVariant']), + secondary: _toColor(json['secondary']), + secondaryContainer: _toColor(json['secondaryVariant']), + error: _toColor(json['error']), + ); + } else { + return ZdsColorScheme.light(); + } + } + + /// Constructs a dark theme from a provided json object. + /// + /// Colors should be as hexadecimal colors, and the naming conventions should match [ColorScheme] + /// + /// The json object should contain the keys: + /// * primary + /// * primaryVariant + /// * secondary + /// * secondaryVariant + /// * error + factory ZdsColorScheme.darkFromJson(Map? json) { + if (json != null) { + return ZdsColorScheme.dark( + primary: _toColor(json['primary']), + primaryContainer: _toColor(json['primaryVariant']), + secondary: _toColor(json['secondary']), + secondaryContainer: _toColor(json['secondaryVariant']), + error: _toColor(json['error']), + ); + } else { + return ZdsColorScheme.dark(); + } + } + + static Color? _toColor(dynamic color) { + if (color != null && color is String && color.isNotEmpty) { + return color.toColor(); + } else { + return null; + } + } +} diff --git a/lib/src/utils/theme/input_border.dart b/lib/src/utils/theme/input_border.dart new file mode 100644 index 0000000..fe6a9cc --- /dev/null +++ b/lib/src/utils/theme/input_border.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// An extension of [InputDecoration] that applies Zds styling. +/// +/// This can be passed to [TextField] and [TextFormField] to apply Zds styling to these widgets. +/// +/// See also: +/// +/// * [InputDecoration], which ZdsInputDecoration extends. +/// * [ZdsDateTimePicker], which should use ZdsInputDecoration. +class ZdsInputDecoration extends InputDecoration { + /// An extension of [InputDecoration] that applies Zds styling. + ZdsInputDecoration({ + super.icon, + String? labelText, + super.labelStyle, + super.helperText, + super.helperStyle, + super.helperMaxLines, + super.hintText, + super.hintStyle, + super.hintTextDirection, + super.hintMaxLines, + super.errorText, + super.errorStyle, + super.errorMaxLines, + super.floatingLabelBehavior, + super.isCollapsed, + super.isDense, + super.contentPadding, + super.prefixIcon, + super.prefixIconConstraints, + Widget? prefix, + super.prefixText, + super.prefixStyle, + Widget? suffixIcon, + Widget? suffix, + super.suffixText, + super.suffixStyle, + super.suffixIconConstraints, + super.counter, + super.counterText, + super.counterStyle, + super.filled, + super.fillColor, + super.focusColor, + super.hoverColor, + super.errorBorder, + super.focusedBorder, + super.focusedErrorBorder, + super.disabledBorder, + super.enabledBorder, + super.border, + super.enabled, + super.semanticCounterText, + super.alignLabelWithHint, + bool? mandatory, + String? semanticsLabel, + EdgeInsets suffixPadding = const EdgeInsets.only(left: 16), + EdgeInsets prefixPadding = const EdgeInsets.only(right: 16), + }) : super( + label: (labelText?.isNotEmpty ?? false) + ? (mandatory ?? false) + ? Row( + children: [ + Text( + labelText ?? '', + style: labelStyle, + semanticsLabel: semanticsLabel, + ), + Text( + ' *', + style: TextStyle(color: ZdsColors.red), + ).excludeSemantics(), + ], + ) + : Text( + labelText ?? '', + style: labelStyle, + semanticsLabel: semanticsLabel, + ) + : null, + prefix: prefixText == null && prefixIcon == null ? Padding(padding: prefixPadding, child: prefix) : null, + suffixIcon: suffixIcon != null ? Padding(padding: suffixPadding, child: suffixIcon) : null, + suffix: suffixText == null && suffixIcon == null ? Padding(padding: suffixPadding, child: suffix) : null, + ); + + /// An extension of [InputDecoration] that applies Zds styling with no label. + factory ZdsInputDecoration.withNoLabel({ + Widget? icon, + String? helperText, + TextStyle? helperStyle, + int? helperMaxLines, + String? hintText, + TextStyle? hintStyle, + TextDirection? hintTextDirection, + int? hintMaxLines, + String? errorText, + TextStyle? errorStyle, + int? errorMaxLines, + FloatingLabelBehavior? floatingLabelBehavior, + bool isCollapsed = false, + bool? isDense, + Widget? prefixIcon, + BoxConstraints? prefixIconConstraints, + Widget? prefix, + String? prefixText, + TextStyle? prefixStyle, + Widget? suffixIcon, + Widget? suffix, + String? suffixText, + TextStyle? suffixStyle, + BoxConstraints? suffixIconConstraints, + Widget? counter, + String? counterText, + TextStyle? counterStyle, + bool? filled, + Color? fillColor, + Color? focusColor, + Color? hoverColor, + InputBorder? errorBorder, + InputBorder? focusedBorder, + InputBorder? focusedErrorBorder, + InputBorder? disabledBorder, + InputBorder? enabledBorder, + InputBorder? border, + bool enabled = true, + String? semanticCounterText, + bool? alignLabelWithHint, + EdgeInsetsGeometry? contentPadding, + EdgeInsets suffixPadding = const EdgeInsets.only(left: 16), + EdgeInsets prefixPadding = const EdgeInsets.only(right: 16), + }) { + return ZdsInputDecoration( + icon: icon, + helperText: helperText, + helperStyle: helperStyle, + helperMaxLines: helperMaxLines, + hintText: hintText, + hintStyle: hintStyle, + hintTextDirection: hintTextDirection, + hintMaxLines: hintMaxLines, + errorText: errorText, + errorStyle: errorText != null && errorText.isEmpty ? const TextStyle(fontSize: 0, height: 0) : errorStyle, + errorMaxLines: errorMaxLines, + floatingLabelBehavior: floatingLabelBehavior, + isCollapsed: isCollapsed, + isDense: isDense, + prefixIcon: prefixIcon, + prefixIconConstraints: prefixIconConstraints, + prefix: prefix, + prefixText: prefixText, + prefixStyle: prefixStyle, + prefixPadding: prefixPadding, + suffixIcon: suffixIcon, + suffix: suffix, + suffixText: suffixText, + suffixStyle: suffixStyle, + suffixPadding: suffixPadding, + suffixIconConstraints: suffixIconConstraints, + counter: counter, + counterText: counterText, + counterStyle: counterStyle, + filled: filled, + fillColor: fillColor, + focusColor: focusColor, + hoverColor: hoverColor, + enabled: enabled, + semanticCounterText: semanticCounterText, + alignLabelWithHint: alignLabelWithHint, + disabledBorder: disabledBorder ?? + const ZdsInputBorder( + space: 2, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + border: border ?? + const ZdsInputBorder( + space: 2, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + focusedBorder: focusedBorder ?? + const ZdsInputBorder( + space: 2, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + enabledBorder: enabledBorder ?? + const ZdsInputBorder( + space: 2, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + errorBorder: errorBorder ?? + ZdsInputBorder( + space: 2, + borderSide: BorderSide(color: ZdsColors.red), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + focusedErrorBorder: focusedErrorBorder ?? + ZdsInputBorder( + space: 2, + borderSide: BorderSide(color: ZdsColors.red), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + contentPadding: contentPadding ?? const EdgeInsets.symmetric(vertical: 19), + ); + } +} diff --git a/lib/src/utils/theme/text.dart b/lib/src/utils/theme/text.dart new file mode 100644 index 0000000..f99d2e3 --- /dev/null +++ b/lib/src/utils/theme/text.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +// ignore: public_member_api_docs +TextTheme buildZdsTextTheme(TextTheme base) { + return base.copyWith( + displayLarge: TextStyle( + fontSize: 30, + fontWeight: FontWeight.w400, + color: base.displayLarge?.color, + height: 36 / 30, + ), + displayMedium: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w400, + color: base.displayMedium?.color, + height: 24 / 20, + ), + displaySmall: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: base.displaySmall?.color, + height: 22 / 18, + ), + headlineLarge: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: base.headlineMedium?.color, + height: 22 / 18, + ), + headlineMedium: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: base.headlineMedium?.color, + height: 20 / 16, + ), + headlineSmall: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: base.headlineSmall?.color, + height: 18 / 14, + ), + titleLarge: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: base.titleLarge?.color, + height: 16 / 12, + ), + titleMedium: TextStyle( + // TextField uses this as input color + // But be careful as ListTile also does + fontSize: 16, + fontWeight: FontWeight.w400, + color: base.titleMedium?.color, + height: 20 / 16, + ), + titleSmall: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: base.titleSmall?.color, + height: 20 / 16, + ), + bodyLarge: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: base.bodyLarge?.color, + height: 20 / 16, + ), + bodyMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: base.bodyMedium?.color, + height: 18 / 14, + ), + bodySmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: base.bodySmall?.color, + ), + labelLarge: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: base.bodyMedium?.color, + height: 18 / 14, + ), + labelMedium: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: base.bodyMedium?.color, + height: 16 / 12, + ), + labelSmall: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w400, + color: base.bodySmall?.color, + ), + ); +} diff --git a/lib/src/utils/theme/theme.dart b/lib/src/utils/theme/theme.dart new file mode 100644 index 0000000..b860e72 --- /dev/null +++ b/lib/src/utils/theme/theme.dart @@ -0,0 +1,790 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../zds_flutter.dart'; +import 'text.dart'; + +/// Builds a theme that can be consumed by Zds components +/// +/// [bt](Base Theme) is optional ThemeData that acts as the base of the theme. +/// +/// [colorScheme] sets all colors within the library. +ThemeData buildTheme(ThemeData baseTheme, ColorScheme colorScheme) { + // Primary text theme. Used for texts drawn on primary color. e.g app-bar title + final primaryTextTheme = buildZdsTextTheme( + baseTheme.primaryTextTheme.apply( + bodyColor: colorScheme.onPrimary, + displayColor: colorScheme.onPrimary, + ), + ); + + final bodyTextTheme = buildZdsTextTheme( + baseTheme.textTheme.apply( + bodyColor: colorScheme.onSurface, + displayColor: colorScheme.onSurface, + ), + ); + + final baseButtonStyle = _buildBaseButtonStyle(primaryTextTheme, colorScheme); + + final bool isWarm = colorScheme.background != ZdsColors.greyCoolSwatch[50]; + + return baseTheme.copyWith( + colorScheme: colorScheme, + canvasColor: colorScheme.surface, + brightness: colorScheme.brightness, + textTheme: bodyTextTheme, + primaryTextTheme: primaryTextTheme, + primaryColor: colorScheme.primary, + scaffoldBackgroundColor: colorScheme.background, + indicatorColor: colorScheme.secondary, + appBarTheme: _buildAppBarTheme(primaryTextTheme, colorScheme), + elevatedButtonTheme: _buildElevatedButtonTheme(baseButtonStyle), + textButtonTheme: _buildTextButtonTheme(baseButtonStyle, bodyTextTheme, colorScheme), + bottomSheetTheme: _buildBottomSheetTheme(), + iconTheme: _buildZdsIconTheme(baseTheme.iconTheme.copyWith(color: ZdsColors.white)), + primaryIconTheme: _buildZdsIconTheme(baseTheme.primaryIconTheme.copyWith(color: colorScheme.onPrimary)), + dividerTheme: _buildDividerTheme(baseTheme.dividerTheme), + inputDecorationTheme: _buildInputDecorationTheme(bodyTextTheme, colorScheme, isWarm), + bottomAppBarTheme: _buildBottomAppBarTheme(baseTheme.bottomAppBarTheme, colorScheme), + bottomNavigationBarTheme: _buildBottomNavigationBarTheme( + baseTheme.bottomNavigationBarTheme, + bodyTextTheme, + colorScheme, + ), + progressIndicatorTheme: ProgressIndicatorThemeData(color: colorScheme.secondary), + cardTheme: CardTheme( + margin: const EdgeInsets.all(3), + shadowColor: ZdsColors.shadowColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), + ), + popupMenuTheme: PopupMenuThemeData( + textStyle: bodyTextTheme.bodyMedium, + elevation: 5, + color: colorScheme.surface, + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: colorScheme.secondary, + selectionColor: colorScheme.secondary.withOpacity(0.4), + selectionHandleColor: colorScheme.secondary, + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: baseButtonStyle.copyWith(textStyle: MaterialStateProperty.resolveWith((_) => bodyTextTheme.titleSmall)), + ), + checkboxTheme: baseTheme.checkboxTheme.copyWith( + fillColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return colorScheme.secondary; + } + return ZdsColors.blueGrey; + }), + ), + radioTheme: baseTheme.radioTheme.copyWith( + fillColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return colorScheme.secondary; + } + return ZdsColors.blueGrey; + }), + ), + switchTheme: _buildSwitchTheme(baseTheme.switchTheme, colorScheme, isWarm).copyWith( + thumbColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return colorScheme.secondary; + } + return ZdsColors.blueGrey; + }), + ), + ); +} + +/// Returns a [ZdsBottomBarThemeData]. +ZdsBottomBarThemeData buildZdsBottomBarThemeData(BuildContext context) { + return ZdsBottomBarThemeData( + height: kBottomBarHeight, + shadows: [ + BoxShadow( + offset: const Offset(0, -1), + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1), + blurRadius: 2, + ), + ], + backgroundColor: Theme.of(context).colorScheme.surface, + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + ); +} + +/// Theme for a ZdsListTile using Zds style. +/// +/// See [ZdsListTile]. +class ZdsListTileTheme { + /// Interior padding on the tile. + final EdgeInsets contentPadding; + + /// Size of any icons used in the leading or trailing widget. + final double iconSize; + + /// Color of the subtitle text. + final Color subtitleColor; + + /// Exterior margin of the tile. + /// + /// Only used if the tile is not shrink wrapped. + final double tileMargin; + + /// Additional margin to be added to the tile after the leading widget but before the rest of the content. + final double labelAdditionalMargin; + + /// Constructs a [ZdsListTileTheme] + const ZdsListTileTheme({ + required this.iconSize, + required this.contentPadding, + required this.tileMargin, + required this.labelAdditionalMargin, + required this.subtitleColor, + }); +} + +/// Applies style to any child of type [ZdsTabBar]. +class ZdsTabBarStyleContainer { + ///Base theme. + final ThemeData theme; + + /// Custom theme for tab bar specifically. + final ZdsTabBarThemeData customTheme; + + /// Constructs a [ZdsTabBarStyleContainer] + ZdsTabBarStyleContainer({required this.theme, required this.customTheme}); +} + +/// Applies style to any child of type [ZdsBottomBar]. +class ZdsBottomBarThemeData { + /// Height of the bottom bar. + final double height; + + /// Box shadows to be applied to the bottom bar. + final List shadows; + + /// Background color of the bottom bar. + final Color backgroundColor; + + /// Interior content padding of the bottom bar. + final EdgeInsets contentPadding; + + /// Constructs a [ZdsBottomBarThemeData]. + ZdsBottomBarThemeData({ + required this.height, + required this.shadows, + required this.backgroundColor, + required this.contentPadding, + }); + + /// Creates a copy of this ZdsBottomBarThemeData, but with the given fields replaced wih the new values. + ZdsBottomBarThemeData copyWith({ + double? height, + List? shadows, + Color? backgroundColor, + EdgeInsets? contentPadding, + }) { + return ZdsBottomBarThemeData( + height: height ?? this.height, + shadows: shadows ?? this.shadows, + backgroundColor: backgroundColor ?? this.backgroundColor, + contentPadding: contentPadding ?? this.contentPadding, + ); + } +} + +/// Theme for ZdsBottomBar. +class ZdsBottomBarTheme extends InheritedWidget { + /// Theme data to be applied. + final ZdsBottomBarThemeData data; + + /// Constructs a [ZdsBottomBarTheme]. + const ZdsBottomBarTheme({ + required super.child, + required this.data, + super.key, + }); + + /// Returns the [ZdsBottomBarThemeData] object of the given type for the widget tree that corresponds to the given context. + static ZdsBottomBarThemeData of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType()!.data; + } + + @override + bool updateShouldNotify(ZdsBottomBarTheme oldWidget) { + return true; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('data', data)); + } +} + +/// Theme container for [ZdsTabBar]. +class ZdsTabBarThemeData { + /// Decoration for [ZdsTabBar]. + /// + /// Typically [BoxDecoration]. + final Decoration decoration; + + /// Height of [ZdsTabBar]. + final double height; + + /// Size of icon. Defaults to 16. + final double iconSize; + + /// Constructor for [ZdsTabBarThemeData]. + const ZdsTabBarThemeData({required this.decoration, required this.height, this.iconSize = 16}); +} + +/// Theme data for [ZdsToolbar]. +class ZdsToolbarThemeData { + /// Interior content padding for the toolbar. + final EdgeInsets contentPadding; + + /// Constructs a [ZdsToolbarThemeData]. + const ZdsToolbarThemeData({required this.contentPadding}); +} + +/// Border used for input components, such as [TextField] to apply a Zds style. +class ZdsInputBorder extends InputBorder { + /// The space between the border and the interior content. + /// + /// Defaults to 6 + final double space; + + /// The border radius used for the border. + /// + /// Defaults to `BorderRadius.all(Radius.circular(12))`. + final BorderRadius borderRadius; + + /// Constructs a [ZdsInputBorder]. + const ZdsInputBorder({ + super.borderSide = const BorderSide(color: ZdsColors.inputBorderColor), + this.space = kSpace, + this.borderRadius = const BorderRadius.all(Radius.circular(12)), + }); + + static bool _cornersAreCircular(BorderRadius borderRadius) { + return borderRadius.topLeft.x == borderRadius.topLeft.y && + borderRadius.bottomLeft.x == borderRadius.bottomLeft.y && + borderRadius.topRight.x == borderRadius.topRight.y && + borderRadius.bottomRight.x == borderRadius.bottomRight.y; + } + + @override + bool get isOutline => true; + + @override + ZdsInputBorder copyWith({ + BorderSide? borderSide, + BorderRadius? borderRadius, + double? gapPadding, + }) { + return ZdsInputBorder( + borderSide: borderSide ?? this.borderSide, + borderRadius: borderRadius ?? this.borderRadius, + ); + } + + @override + EdgeInsetsGeometry get dimensions { + return EdgeInsets.all(borderSide.width); + } + + @override + ZdsInputBorder scale(double t) { + return ZdsInputBorder( + borderSide: borderSide.scale(t), + borderRadius: borderRadius * t, + ); + } + + @override + ShapeBorder? lerpFrom(ShapeBorder? a, double t) { + if (a is ZdsInputBorder) { + final ZdsInputBorder outline = a; + return outline; + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder? lerpTo(ShapeBorder? b, double t) { + if (b is ZdsInputBorder) { + final ZdsInputBorder outline = b; + return outline; + } + return super.lerpTo(b, t); + } + + @override + Path getInnerPath(Rect rect, {TextDirection? textDirection}) { + final Rect setRect = Rect.fromLTWH( + rect.left - (space - borderSide.width), + rect.top + space, + rect.width + ((space - borderSide.width) * 2), + rect.height - (space * 2), + ); + + return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(setRect).deflate(borderSide.width)); + } + + @override + Path getOuterPath(Rect rect, {TextDirection? textDirection}) { + final Rect setRect = Rect.fromLTWH( + rect.left - (space - borderSide.width), + rect.top + space, + rect.width + ((space - borderSide.width) * 2), + rect.height - (space * 2), + ); + return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(setRect).deflate(space)); + } + + @override + void paint( + Canvas canvas, + Rect rect, { + double? gapStart, + double gapExtent = 0.0, + double gapPercentage = 0.0, + TextDirection? textDirection, + }) { + assert(gapPercentage >= 0.0 && gapPercentage <= 1.0, 'gapPercentage must be between 0 and 1'); + assert(_cornersAreCircular(borderRadius), 'Corners must be circular'); + + final Rect setRect = Rect.fromLTWH( + rect.left - (space - borderSide.width), + rect.top + space, + rect.width + ((space - borderSide.width) * 2), + rect.height - (space * 2), + ); + + final Paint paint = borderSide.toPaint(); + final RRect outer = borderRadius.toRRect(setRect); + final RRect center = outer.deflate(space); + canvas.drawRRect(center, paint); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is ZdsInputBorder && other.borderSide == borderSide && other.borderRadius == borderRadius; + } + + @override + int get hashCode => borderSide.hashCode ^ borderRadius.hashCode; +} + +/// Extension of ThemeData. +extension ThemeExtension on ThemeData { + /// Gets prefix icon used for [ZdsSearchField]. + Icon get prefixIcon => Icon(ZdsIcons.search, color: greySwatch[800]); + + /// Gets default theme data for [ZdsListTile]. + ZdsListTileTheme get zdsListTileThemeData => ZdsListTileTheme( + contentPadding: const EdgeInsets.only(left: 24, top: 18, bottom: 18, right: 24), + iconSize: 24, + subtitleColor: greySwatch.shade900, + tileMargin: 6, + labelAdditionalMargin: 10, + ); + + ///Gets default theme data for [ZdsToolbar]. + ZdsToolbarThemeData get zdsToolbarThemeData => const ZdsToolbarThemeData( + contentPadding: EdgeInsets.all(24), + ); + + /// Gets default theme data for [ZdsSearchField] + Map get zdsSearchThemeData { + final border = OutlineInputBorder( + borderRadius: BorderRadius.circular(kSearchBorderRadius), + borderSide: const BorderSide( + style: BorderStyle.none, + ), + ); + + InputDecorationTheme inputDecorationTheme(OutlineInputBorder border) => InputDecorationTheme( + border: border, + focusedBorder: border, + errorBorder: border, + enabledBorder: border, + disabledBorder: border, + focusedErrorBorder: border, + contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + ); + + final cardTheme = this.cardTheme.copyWith( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(kSearchBorderRadius), + ), + shadowColor: ZdsColors.blueGrey.withOpacity(0.1), + ); + + return Map.from({ + ZdsSearchFieldVariant.outlined: ThemeData( + inputDecorationTheme: inputDecorationTheme( + border.copyWith( + borderSide: BorderSide( + color: greySwatch[colorScheme.brightness == Brightness.dark ? 1000 : 500]!, + ), + ), + ), + textSelectionTheme: TextSelectionThemeData(cursorColor: colorScheme.onSurface), + hintColor: colorScheme.onSurface.withOpacity(0.5), + cardTheme: cardTheme.copyWith(shadowColor: ZdsColors.transparent), + ), + ZdsSearchFieldVariant.elevated: ThemeData( + inputDecorationTheme: inputDecorationTheme(border), + textSelectionTheme: TextSelectionThemeData( + cursorColor: colorScheme.onSurface, + ), + cardTheme: cardTheme, + hintColor: colorScheme.onSurface.withOpacity(0.5), + ), + }); + } + + /// Custom theme for [ZdsDateTimePicker]. + ThemeData get zdsDateTimePickerTheme { + return copyWith( + dialogBackgroundColor: colorScheme.brightness == Brightness.dark ? colorScheme.background : null, + colorScheme: colorScheme.copyWith( + primary: colorScheme.secondary.withLight(colorScheme.brightness == Brightness.dark ? 0.75 : 1), + onPrimary: colorScheme.onSecondary, + ), + ); + } + + /// Builds [ZdsTabBarStyleContainer]. Defaults to primary color. + ZdsTabBarStyleContainer _tabbarStyle( + BuildContext context, + bool hasIcons, { + Color? selectedText, + Color? background, + Color? unselectedText, + Color? indicator, + }) { + final height = hasIcons ? 56.0 : 48.0; + final ThemeData theme = Theme.of(context); + final ZetaColors colors = ZetaColors.of(context); + final tabBarTheme = theme.tabBarTheme.copyWith(indicatorSize: TabBarIndicatorSize.tab); + final labelStyle = hasIcons ? theme.textTheme.bodyXSmall : theme.textTheme.bodyLarge; + if (colors.isDarkMode) return _tabBarDark(context, hasIcons); + + return ZdsTabBarStyleContainer( + customTheme: ZdsTabBarThemeData( + decoration: BoxDecoration(color: background ?? colors.primary.primary), + height: height, + ), + theme: theme.copyWith( + tabBarTheme: tabBarTheme.copyWith( + labelStyle: labelStyle, + unselectedLabelStyle: labelStyle, + unselectedLabelColor: unselectedText ?? colors.cool.shade40, + labelColor: selectedText ?? colors.onPrimary, + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + width: 5, + color: indicator ?? colors.primary.shade20, + ), + ), + ), + ), + ); + } + + ZdsTabBarStyleContainer _tabBarDark(BuildContext context, bool hasIcons) { + final height = hasIcons ? 56.0 : 48.0; + final ThemeData theme = Theme.of(context); + final ZetaColors colors = ZetaColors.of(context); + final tabBarTheme = theme.tabBarTheme.copyWith(indicatorSize: TabBarIndicatorSize.tab); + final labelStyle = hasIcons ? theme.textTheme.bodySmall : theme.textTheme.bodyLarge; + return ZdsTabBarStyleContainer( + customTheme: ZdsTabBarThemeData( + decoration: BoxDecoration(color: colors.cool.shade10), + height: height, + ), + theme: theme.copyWith( + tabBarTheme: tabBarTheme.copyWith( + labelStyle: labelStyle, + unselectedLabelStyle: labelStyle, + unselectedLabelColor: colors.textDefault, + labelColor: colors.textSubtle, + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + width: 5, + color: colors.primary, + ), + ), + ), + ), + ); + } + + /// Generates theme for [ZdsTabBar]. + Map zdsTabBarThemeData( + BuildContext context, { + required bool hasIcons, + Color? indicatorColor, + }) { + final ZetaColors colors = ZetaColors.of(context); + return { + ZdsTabBarColor.primary: _tabbarStyle(context, hasIcons), + ZdsTabBarColor.basic: _tabbarStyle( + context, + hasIcons, + background: colors.cool.shade90, + selectedText: colors.cool.shade20, + indicator: colors.primary.primary, + ), + ZdsTabBarColor.surface: _tabbarStyle( + context, + hasIcons, + background: colors.surface, + selectedText: colors.onSurface, + indicator: colors.primary.primary, + unselectedText: colors.cool.shade70, + ), + }; + } + + /// Builds theme variants for [ZdsAppBar]. + /// + /// See also + /// * [ZdsTabBarColor]. + Map buildAppBarTheme(ZetaColors colors) { + final Map foreground = {}; + final Map background = {}; + + foreground[ZdsTabBarColor.basic] = colors.isDarkMode ? colors.cool.shade90 : colors.cool.shade10; + foreground[ZdsTabBarColor.primary] = colors.isDarkMode ? colors.cool.shade90 : colors.onPrimary; + foreground[ZdsTabBarColor.surface] = colors.isDarkMode ? colors.cool.shade90 : colors.onSurface; + + background[ZdsTabBarColor.basic] = colors.isDarkMode ? colors.cool.shade10 : colors.cool.shade90; + background[ZdsTabBarColor.primary] = colors.isDarkMode ? colors.cool.shade10 : colors.primary; + background[ZdsTabBarColor.surface] = colors.isDarkMode ? colors.cool.shade10 : colors.surface; + + return { + ZdsTabBarColor.primary: AppBarTheme( + systemOverlayStyle: computeSystemOverlayStyle(background[ZdsTabBarColor.primary]!), + backgroundColor: background[ZdsTabBarColor.primary], + foregroundColor: foreground[ZdsTabBarColor.primary], + centerTitle: false, + titleSpacing: 0, + elevation: 0.5, + iconTheme: IconThemeData(color: foreground[ZdsTabBarColor.primary]), + actionsIconTheme: IconThemeData(color: foreground[ZdsTabBarColor.primary]), + ), + ZdsTabBarColor.basic: AppBarTheme( + systemOverlayStyle: computeSystemOverlayStyle(background[ZdsTabBarColor.basic]!), + backgroundColor: background[ZdsTabBarColor.basic], + foregroundColor: foreground[ZdsTabBarColor.basic], + centerTitle: false, + titleSpacing: 0, + elevation: 0.5, + iconTheme: IconThemeData(color: foreground[ZdsTabBarColor.basic]), + actionsIconTheme: IconThemeData(color: foreground[ZdsTabBarColor.basic]), + ), + ZdsTabBarColor.surface: AppBarTheme( + systemOverlayStyle: computeSystemOverlayStyle(background[ZdsTabBarColor.surface]!), + backgroundColor: background[ZdsTabBarColor.surface], + foregroundColor: foreground[ZdsTabBarColor.surface], + centerTitle: false, + titleSpacing: 0, + elevation: 0.5, + iconTheme: IconThemeData(color: foreground[ZdsTabBarColor.surface]), + actionsIconTheme: IconThemeData(color: foreground[ZdsTabBarColor.surface]), + ), + }; + } + + /// Theme data used to create compact buttons. + /// + /// Should not be used often as buttons typically should not be too small. + ThemeData get shrunkenButtonsThemeData { + MaterialStateProperty buildShrunkenButtonPadding() { + return MaterialStateProperty.all(EdgeInsets.zero); + } + + return copyWith( + textButtonTheme: TextButtonThemeData( + style: textButtonTheme.style?.copyWith( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: buildShrunkenButtonPadding(), + foregroundColor: MaterialStateProperty.all(colorScheme.primary), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: elevatedButtonTheme.style?.copyWith( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: buildShrunkenButtonPadding(), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: outlinedButtonTheme.style?.copyWith( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: buildShrunkenButtonPadding(), + ), + ), + ); + } +} + +/// Private functions +DividerThemeData _buildDividerTheme(DividerThemeData base) { + return base.copyWith(thickness: 1, space: 1, color: ZdsColors.lightGrey.withOpacity(0.5)); +} + +IconThemeData _buildZdsIconTheme(IconThemeData base) { + return base.copyWith(size: 30); +} + +BottomNavigationBarThemeData _buildBottomNavigationBarTheme( + BottomNavigationBarThemeData base, + TextTheme textTheme, + ColorScheme colorScheme, +) { + final Color unselectedColor = + colorScheme.brightness == Brightness.dark ? ZdsColors.greyWarmSwatch.shade400 : ZdsColors.greyWarmSwatch.shade900; + + return base.copyWith( + type: BottomNavigationBarType.fixed, + selectedLabelStyle: textTheme.bodySmall, + unselectedLabelStyle: textTheme.bodySmall, + unselectedItemColor: unselectedColor, + selectedItemColor: colorScheme.secondary, + selectedIconTheme: IconThemeData(size: 24, color: colorScheme.secondary), + unselectedIconTheme: IconThemeData(size: 24, color: unselectedColor), + elevation: 8, + ); +} + +BottomAppBarTheme _buildBottomAppBarTheme(BottomAppBarTheme base, ColorScheme colorScheme) { + return base.copyWith( + color: colorScheme.surface, + surfaceTintColor: colorScheme.onSurface, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: kIsWeb ? 8 : 4), + ); +} + +SwitchThemeData _buildSwitchTheme(SwitchThemeData base, ColorScheme colorScheme, bool isWarm) { + Color getColor(Set states) { + Color color = isWarm ? ZdsColors.greyWarmSwatch[900]! : ZdsColors.greyCoolSwatch[900]!; + if (states.contains(MaterialState.selected)) { + color = colorScheme.secondary; + } + if (states.contains(MaterialState.disabled)) { + color = color.withOpacity(0.5); + } + + return color; + } + + return base.copyWith( + thumbColor: MaterialStateProperty.resolveWith(getColor), + trackColor: MaterialStateProperty.resolveWith((states) => getColor(states).withOpacity(0.2)), + ); +} + +BottomSheetThemeData _buildBottomSheetTheme() { + return BottomSheetThemeData( + shape: _buildBottomSheetShapeBorder(), + ); +} + +ShapeBorder _buildBottomSheetShapeBorder() { + return const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(14), + topRight: Radius.circular(14), + ), + ); +} + +MaterialStateProperty _buildCircularShapeBorder() { + return MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: _buildButtonBorderRadius(), + ), + ); +} + +ButtonStyle _buildBaseButtonStyle(TextTheme textTheme, ColorScheme colorScheme) { + return ButtonStyle( + backgroundColor: MaterialStateProperty.all(colorScheme.secondary), + textStyle: MaterialStateProperty.all(textTheme.headlineSmall?.copyWith(color: colorScheme.onSecondary)), + padding: MaterialStateProperty.all(_buildButtonPadding()), + shape: _buildCircularShapeBorder(), + side: _buildBaseButtonBorderSide(), + elevation: MaterialStateProperty.all(0), + visualDensity: VisualDensity.standard, + ); +} + +MaterialStateProperty _buildBaseButtonBorderSide() { + return MaterialStateProperty.all(BorderSide.none); +} + +ElevatedButtonThemeData _buildElevatedButtonTheme(ButtonStyle base) { + return ElevatedButtonThemeData(style: base); +} + +TextButtonThemeData _buildTextButtonTheme(ButtonStyle baseButtonStyle, TextTheme textTheme, ColorScheme colorScheme) { + return TextButtonThemeData( + style: baseButtonStyle.copyWith( + textStyle: MaterialStateProperty.all(textTheme.titleMedium?.copyWith(color: colorScheme.secondary)), + backgroundColor: MaterialStateProperty.all(ZdsColors.transparent), + ), + ); +} + +EdgeInsets _buildButtonPadding() { + return const EdgeInsets.symmetric(horizontal: 24, vertical: 10); +} + +BorderRadius _buildButtonBorderRadius() { + return const BorderRadius.all(Radius.circular(71)); +} + +AppBarTheme _buildAppBarTheme(TextTheme textTheme, ColorScheme colorScheme) { + return AppBarTheme( + systemOverlayStyle: computeSystemOverlayStyle(colorScheme.primary), + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + centerTitle: false, + titleSpacing: 0, + elevation: 0.5, + titleTextStyle: textTheme.headlineLarge, + ); +} + +InputDecorationTheme _buildInputDecorationTheme(TextTheme textTheme, ColorScheme colorScheme, bool isWarm) { + return InputDecorationTheme( + contentPadding: const EdgeInsets.symmetric(vertical: 27), + floatingLabelBehavior: FloatingLabelBehavior.always, + border: const ZdsInputBorder(), + focusedBorder: const ZdsInputBorder(), + enabledBorder: const ZdsInputBorder(), + disabledBorder: const ZdsInputBorder(), + errorBorder: ZdsInputBorder(borderSide: BorderSide(color: colorScheme.error)), + focusedErrorBorder: ZdsInputBorder(borderSide: BorderSide(color: colorScheme.error)), + focusColor: isWarm ? ZdsColors.greyWarmSwatch[600] : ZdsColors.greyCoolSwatch[600], + labelStyle: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w500, + color: ZdsColors.blueGrey, + height: 0, + ), + counterStyle: textTheme.bodyMedium?.copyWith( + height: 0.9, + color: ZdsColors.darkGrey, + ), + ); +} diff --git a/lib/src/utils/theme/theme_constants.dart b/lib/src/utils/theme/theme_constants.dart new file mode 100644 index 0000000..86edc22 --- /dev/null +++ b/lib/src/utils/theme/theme_constants.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import '../../../zds_flutter.dart'; + +/// Default bottom bar height. +const kBottomBarHeight = 62.0; + +/// Default radius for search bar. +const kSearchBorderRadius = 53.0; + +/// Default toolbar height. +const kZdsToolbarHeight = 64.0; + +/// Default horizontal menu padding. +const kMenuHorizontalPadding = EdgeInsets.symmetric(horizontal: 24); + +/// Default vertical menu padding. +const kMenuVerticalPadding = EdgeInsets.symmetric(vertical: 24 / 2); + +/// Default height of toast. +const kToastHeight = 48.0; + +/// Default header border size. +const kHeaderBoarderSize = 1.0; + +/// Default max action height. +const kMaxActionHeight = 48.0; + +/// Height for[ZdsToggleButton]. +const double kBigToggleHeight = 40; + +/// Default padding for [ZdsToggleButton]. +const double kBigTogglePadding = 18; + +/// Default border radius for [ZdsTag]. +const double kDefaultBorderRadius = 6; + +/// Rectangular border radius for [ZdsTag]. +const double rectangularBorderRadius = 2; + +/// Default horizontal padding for [ZdsList]. +const kDefaultHorizontalPadding = 14.0; + +/// Default vertical padding for [ZdsList]. +const kDefaultVerticalPadding = 8.0; + +/// Space used for [ZdsInputBorder]. +const double kSpace = 6; + +/// Border radius used for [ZdsSelectableListTile]. +const double kZdsSelectableListTileBorderRadius = 30; + +/// Padding used for [ZdsSelectableListTile]. +const EdgeInsets kZdsSelectableListTilePadding = EdgeInsets.all(2); + +/// Width of [ZdsVerticalNav] +const double kVerticalNavWidth = 48; + +/// Height of row in [ZdsCalendar] +const double calendarRowHeight = 60; + +/// Height of days of week in [ZdsCalendar] +const double calendarDaysOfWeekHeight = 36; + +/// Width / height of circular buttons in [ZdsCheckableButton]. +const double checkableButtonSize = 48; diff --git a/lib/src/utils/theme/theme_loader.dart b/lib/src/utils/theme/theme_loader.dart new file mode 100644 index 0000000..298577b --- /dev/null +++ b/lib/src/utils/theme/theme_loader.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; + +import '../../../zds_flutter.dart'; + +/// Loads themes from json file +class ThemeLoader { + /// Loads a [BrandColors] from a json file from it's provided path. + /// + /// See + /// * [BrandColors] + static Future fromAssets(String path) async { + try { + final jsonString = await rootBundle.loadString(path); + + final json = jsonDecode(jsonString); + + return BrandColors.fromJson(json as Map); + } catch (e) { + return BrandColors.zdsDefault(); + } + } +} + +/// Extension to parse color from string +extension ColorParser on String { + /// Returns a Color parsed from the contents of the String. + /// + /// Supports Hex colors with or without a leading # + Color? toColor() { + try { + if (isEmpty) return null; + if (startsWith('#')) { + return Color(int.parse(substring(1, 7), radix: 16) + 0xFF000000); + } + return Color(int.parse(this, radix: 16) + 0xFF000000); + } catch (_) { + return null; + } + } +} diff --git a/lib/src/utils/theme/theme_provider.dart b/lib/src/utils/theme/theme_provider.dart new file mode 100644 index 0000000..26046f0 --- /dev/null +++ b/lib/src/utils/theme/theme_provider.dart @@ -0,0 +1,101 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// Provides the Zds Theme to all children in the app +class ThemeProvider extends StatefulWidget { + /// Builds the child. + final Widget Function( + BuildContext context, + BrandColors colors, + // ignore: avoid_positional_boolean_parameters + bool isDarkMode, + ) builder; + + /// Set of colors to theme the components within the app. + final BrandColors colors; + + /// True if the app should run in dark mode. + /// + /// Not yet supported. + final bool isDarkMode; + + /// Constructs a ThemeProvider + const ThemeProvider({ + required this.builder, + required this.colors, + super.key, + this.isDarkMode = false, + }); + + @override + ThemeProviderState createState() => ThemeProviderState(); + + /// Returns an ancestor ThemeProvider from context. + /// + /// If no theme provider is found this will throw an error. + static ThemeProviderState? of(BuildContext context) { + return context.findAncestorStateOfType(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + ObjectFlagProperty.has( + 'builder', + builder, + ), + ); + properties.add(DiagnosticsProperty('colors', colors)); + properties.add(DiagnosticsProperty('isDarkMode', isDarkMode)); + } +} + +/// State for [ThemeProvider]. +class ThemeProviderState extends State { + /// Custom brand colors provided to the theme. + late BrandColors colors; + + /// True if dark mode is to be used. + late bool isDarkMode; + + /// Sets [colors]. + void setColors(BrandColors brandColors) { + if (colors == brandColors) return; + setState(() { + colors = brandColors; + }); + } + + /// Sets [isDarkMode]. + void setDarkMode({bool isDark = false}) { + if (isDark == isDarkMode) return; + setState(() { + isDarkMode = isDark; + }); + } + + /// Toggles [isDarkMode] boolean value. + void toggleDarkMode() => setState(() => isDarkMode = !isDarkMode); + + @override + void initState() { + colors = widget.colors; + isDarkMode = widget.isDarkMode; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return widget.builder.call(context, colors, isDarkMode); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('colors', colors)); + properties.add(DiagnosticsProperty('isDarkMode', isDarkMode)); + } +} diff --git a/lib/src/utils/tools.dart b/lib/src/utils/tools.dart new file mode 100644 index 0000000..ef2e4b2 --- /dev/null +++ b/lib/src/utils/tools.dart @@ -0,0 +1,10 @@ +export 'tools/app.dart' hide appZetaColors; +export 'tools/compression.dart'; +export 'tools/controller.dart'; +export 'tools/crop_image_helper.dart'; +export 'tools/frame_mixin.dart'; +export 'tools/keyboard_dismiss.dart'; +export 'tools/modifiers.dart'; +export 'tools/nested_flow.dart'; +export 'tools/tab_navigator.dart'; +export 'tools/utils.dart'; diff --git a/lib/src/utils/tools/app.dart b/lib/src/utils/tools/app.dart new file mode 100644 index 0000000..f7f2841 --- /dev/null +++ b/lib/src/utils/tools/app.dart @@ -0,0 +1,271 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; +import '../../../zds_flutter.dart'; + +/// App wide zeta colors object. +ZetaColors? appZetaColors; + +/// A wrapper around MaterialApp that adds Zds styling and other properties. +/// +/// Should be used in the same way as MaterialApp. +/// +/// ```dart +/// class App extends StatelessWidget { +/// const App({Key? key}) : super(key: key); +/// +/// @override +/// Widget build(BuildContext context) { +/// return ZdsApp( +/// title: 'App', +/// routes: [...], +/// ); +/// } +/// } +/// ``` +class ZdsApp extends StatelessWidget { + /// A one line description of the application. + /// {@macro flutter.widgets.widgetsApp.title} + final String title; + + /// {@macro flutter.widgets.widgetsApp.home} + final Widget? home; + + /// Default theme used, to which Zds styles are applied. + /// + /// Default visual properties, like colors fonts and shapes, for this app's material widgets. + /// + /// The default value of this property is the value of [ThemeData.light()]. + final ThemeData? theme; + + /// {@macro flutter.widgets.widgetsApp.routes} + final Map routes; + + /// {@macro flutter.widgets.widgetsApp.localizationsDelegates} + final List>? localizationsDelegates; + + /// The initial locale for this app's [Localizations] widget is based on this value. + /// + /// If the 'locale' is null then the system's locale value is used. + /// + /// The value of [Localizations.locale] will equal this locale if it matches one of the [MaterialApp.supportedLocales]. Otherwise it will be `en`. + final Locale? localeOverride; + + /// Optional parameter to add custom branded colors to the application. + /// + /// See also: + /// + /// * [BrandColors] + final BrandColors? colors; + + /// {@macro flutter.widgets.widgetsApp.onGenerateRoute} + final Route? Function(RouteSettings)? onGenerateRoute; + + /// {@macro flutter.widgets.widgetsApp.initialRoute} + final String? initialRoute; + + /// {@macro flutter.widgets.widgetsApp.onGenerateInitialRoutes} + final List> Function(String)? onGenerateInitialRoutes; + + /// {@macro flutter.widgets.widgetsApp.onUnknownRoute} + final Route? Function(RouteSettings)? onUnknownRoute; + + /// {@macro flutter.widgets.widgetsApp.navigatorKey} + final GlobalKey? navigatorKey; + + /// ZetaColors object for app color theme. + final ZetaColors? zetaColors; + + /// {@macro flutter.widgets.widgetsApp.debugShowCheckedModeBanner} + final bool? debugShowCheckedModeBanner; + + /// Creates a ZdsApp + const ZdsApp({ + required this.title, + super.key, + this.home, + this.theme, + this.routes = const {}, + this.localizationsDelegates, + this.localeOverride, + this.colors, + this.zetaColors, + this.onGenerateRoute, + this.initialRoute, + this.onGenerateInitialRoutes, + this.onUnknownRoute, + this.navigatorKey, + this.debugShowCheckedModeBanner, + }); + + @override + Widget build(BuildContext context) { + appZetaColors = zetaColors ?? ZetaColors(); + return Zeta( + colors: zetaColors, + builder: (BuildContext context, ThemeData zetaThemeData, ZetaColors zetaColors) { + return ComponentLocalisation( + child: Builder( + builder: (context) { + if (colors != null) { + /// Use BrandColors + return ThemeProvider( + colors: colors!, + builder: (BuildContext context, BrandColors brandColors, bool isDarkTheme) => _AppWrapper( + title, + localeOverride, + localizationsDelegates, + home, + routes, + onGenerateRoute, + onGenerateInitialRoutes, + onUnknownRoute, + initialRoute, + navigatorKey, + isDarkTheme ? ThemeData.dark() : ThemeData.light(), + isDarkTheme ? brandColors.dark : brandColors.light, + debugShowCheckedModeBanner ?? false, + ), + ); + } + + /// Use ZetaColors + return _AppWrapper( + title, + localeOverride, + localizationsDelegates, + home, + routes, + onGenerateRoute, + onGenerateInitialRoutes, + onUnknownRoute, + initialRoute, + navigatorKey, + zetaThemeData, + zetaColors.toColorScheme, + debugShowCheckedModeBanner ?? false, + ); + }, + ), + ); + }, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('title', title)); + properties.add(DiagnosticsProperty('theme', theme)); + properties.add(DiagnosticsProperty>('routes', routes)); + properties.add(IterableProperty>('localizationsDelegates', localizationsDelegates)); + properties.add(DiagnosticsProperty('localeOverride', localeOverride)); + properties.add(DiagnosticsProperty('colors', colors)); + properties + .add(ObjectFlagProperty? Function(RouteSettings p1)?>.has('onGenerateRoute', onGenerateRoute)); + properties.add(StringProperty('initialRoute', initialRoute)); + properties.add( + ObjectFlagProperty> Function(String p1)?>.has( + 'onGenerateInitialRoutes', + onGenerateInitialRoutes, + ), + ); + properties + .add(ObjectFlagProperty? Function(RouteSettings p1)?>.has('onUnknownRoute', onUnknownRoute)); + properties.add(DiagnosticsProperty?>('navigatorKey', navigatorKey)); + properties.add(DiagnosticsProperty('zetaColors', zetaColors)); + properties.add(DiagnosticsProperty('debugShowCheckedModeBanner', debugShowCheckedModeBanner)); + } +} + +class _AppWrapper extends StatelessWidget { + const _AppWrapper( + this.title, + this.localeOverride, + this.localizationsDelegates, + this.home, + this.routes, + this.onGenerateRoute, + this.onGenerateInitialRoutes, + this.onUnknownRoute, + this.initialRoute, + this.navigatorKey, + this.theme, + this.colorScheme, + this.debugShowCheckedModeBanner, + ); + + final String title; + final Locale? localeOverride; + final List>? localizationsDelegates; + final Widget? home; + final Map routes; + final Route? Function(RouteSettings p1)? onGenerateRoute; + final List> Function(String p1)? onGenerateInitialRoutes; + final Route? Function(RouteSettings p1)? onUnknownRoute; + final String? initialRoute; + final GlobalKey? navigatorKey; + final ThemeData theme; + final ColorScheme colorScheme; + final bool debugShowCheckedModeBanner; + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: debugShowCheckedModeBanner, + title: title, + localeResolutionCallback: (deviceLocale, supportedLocales) { + final locale = localeOverride ?? deviceLocale; + return ComponentStrings.delegate.isSupported(locale!) ? locale : ComponentStrings.defaultLocale; + }, + locale: localeOverride, + builder: (context, child) { + return ZdsBottomBarTheme( + data: buildZdsBottomBarThemeData(context), + child: child ?? const SizedBox(), + ); + }, + localizationsDelegates: localizationsDelegates ?? + [ + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ComponentStrings.delegate, + ], + home: home, + routes: routes, + theme: buildTheme(theme, colorScheme), + onGenerateRoute: onGenerateRoute, + onGenerateInitialRoutes: onGenerateInitialRoutes, + onUnknownRoute: onUnknownRoute, + initialRoute: initialRoute, + navigatorKey: navigatorKey, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(StringProperty('title', title)); + properties.add(DiagnosticsProperty('localeOverride', localeOverride)); + properties.add(IterableProperty>('localizationsDelegates', localizationsDelegates)); + properties.add(DiagnosticsProperty>('routes', routes)); + properties + .add(ObjectFlagProperty? Function(RouteSettings p1)?>.has('onGenerateRoute', onGenerateRoute)); + properties.add( + ObjectFlagProperty> Function(String p1)?>.has( + 'onGenerateInitialRoutes', + onGenerateInitialRoutes, + ), + ); + properties + .add(ObjectFlagProperty? Function(RouteSettings p1)?>.has('onUnknownRoute', onUnknownRoute)); + properties.add(StringProperty('initialRoute', initialRoute)); + properties.add(DiagnosticsProperty?>('navigatorKey', navigatorKey)); + properties.add(DiagnosticsProperty('theme', theme)); + properties.add(DiagnosticsProperty('colorScheme', colorScheme)); + properties.add(DiagnosticsProperty('debugShowCheckedModeBanner', debugShowCheckedModeBanner)); + } +} diff --git a/lib/src/utils/tools/compression.dart b/lib/src/utils/tools/compression.dart new file mode 100644 index 0000000..981512a --- /dev/null +++ b/lib/src/utils/tools/compression.dart @@ -0,0 +1,301 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:path/path.dart' as path; +import 'package:video_compress/video_compress.dart'; + +import '../../components/organisms/temp_directory/resolver.dart'; + +/// Compression Profile +/// +/// This provides information on compression parameters +/// +@immutable +class Compression { + /// Default constructor + const Compression({ + required this.maxFileSize, + required this.minWidth, + required this.minHeight, + required this.quality, + this.inSampleSize = 1, + this.rotate = 0, + this.autoCorrectionAngle = true, + this.keepExif = false, + this.numberOfRetries = 5, + this.format = CompressFormat.jpeg, + }) : assert(1 <= quality && quality <= 100, 'quality must be between 1 and 100.'), + assert(minWidth > 0, 'minWidth must be greater than 0.'), + assert(inSampleSize > 0, 'inSampleSize must be greater than 0.'), + assert(minHeight > 0, 'minHeight must be greater than 0.'); + + /// Compression quality. Ranges from 1-100 + /// + /// If [format] is png, the param will be ignored in iOS + final int quality; + + /// Minimum width of the image. Compression will be made around this width. + final int minWidth; + + /// Minimum width of the image. Compression will be made around this height. + final int minHeight; + + /// Maximum byte-size of the image. Default to 250Kb + final int maxFileSize; + + /// Compress format, only used in case of images + final CompressFormat format; + + /// The param is only support android. + /// + /// If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save + /// memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the + /// decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, + /// and 1/16 the number of pixels. Any value <= 1 is treated the same as 1. Note: the decoder uses a final value + /// based on powers of 2, any other value will be rounded down to the nearest power of 2. + final int inSampleSize; + + /// If you need to rotate the picture, use this parameter. + final int rotate; + + /// Modify rotate to 0 or autoCorrectionAngle to false. + final bool autoCorrectionAngle; + + /// If this parameter is true, EXIF information is saved in the compressed result. + /// Default value is false. + final bool keepExif; + + /// No of retries before giving up on error. + final int numberOfRetries; + + /// Efficient compression with minimum values + const Compression.standard() + : quality = 10, + minHeight = 1920, + minWidth = 1080, + maxFileSize = 0, + inSampleSize = 1, + rotate = 0, + autoCorrectionAngle = true, + keepExif = false, + numberOfRetries = 5, + format = CompressFormat.jpeg; + + /// Creates compression from [quality]. + const Compression.withQuality( + this.quality, { + this.maxFileSize = 0, + this.minWidth = 1920, + this.minHeight = 1080, + }) : inSampleSize = 1, + rotate = 0, + autoCorrectionAngle = true, + keepExif = false, + numberOfRetries = 5, + format = CompressFormat.jpeg, + assert(1 <= quality && quality <= 100, 'quality must be between 1 and 100.'), + assert(minWidth > 0, 'minWidth must be greater than 0.'), + assert(minHeight > 0, 'minHeight must be greater than 0.'); + + /// Creates compression from [minWidth] & [minHeight]. + const Compression.withSize({ + required this.maxFileSize, + this.minWidth = 1920, + this.minHeight = 1080, + }) : quality = 10, + inSampleSize = 1, + rotate = 0, + autoCorrectionAngle = true, + keepExif = false, + numberOfRetries = 5, + format = CompressFormat.jpeg, + assert(minWidth > 0, 'minWidth must be greater than 0'), + assert(minHeight > 0, 'minHeight must be greater than 0'); + + /// Creates a copy of this compression, but with the given fields replaced wih the new values. + Compression copyWith({ + int? quality, + int? minWidth, + int? minHeight, + int? maxFileSize, + CompressFormat? format, + int? inSampleSize, + int? rotate, + bool? autoCorrectionAngle, + bool? keepExif, + int? numberOfRetries, + }) { + return Compression( + maxFileSize: maxFileSize ?? this.maxFileSize, + quality: quality ?? this.quality, + minWidth: minWidth ?? this.minWidth, + minHeight: minHeight ?? this.minHeight, + format: format ?? this.format, + inSampleSize: inSampleSize ?? this.inSampleSize, + rotate: rotate ?? this.rotate, + autoCorrectionAngle: autoCorrectionAngle ?? this.autoCorrectionAngle, + keepExif: keepExif ?? this.keepExif, + numberOfRetries: numberOfRetries ?? this.numberOfRetries, + ); + } +} + +/// Compresses the files based on [Compression] provided. +class ZdsCompressor { + /// Private constructor + ZdsCompressor._(); + + /// Compresses [image] and stores the image at `target`. + /// + /// This method compresses the image at-least once and then keeps it compressing until [Compression.maxFileSize] + /// is reached. + /// + /// See also + /// * [Compression] + static Future compressImage({required File image, Compression? compression}) async { + File? compressedImage; + + try { + compression ??= const Compression.standard(); + + final int maxFileSize = compression.maxFileSize; + + final originalSize = await image.length(); + var quality = compression.quality; + + if (kDebugMode) print('Size before compression : $originalSize'); + + compressedImage = image; + + //tempFile for more compression if needed + final dir = await zdsTempDirectory('compressed'); + final imageExt = compression.format == CompressFormat.jpeg + ? 'jpeg' + : compression.format == CompressFormat.png + ? 'png' + : path.extension(compressedImage.absolute.path).toLowerCase(); + + final tmpName = 'TMP_${DateTime.now().microsecondsSinceEpoch}.$imageExt'; + final tempFile = File(path.join(dir, tmpName)); + + //targetFile + final targetFile = File('$dir/${path.basenameWithoutExtension(image.absolute.path)}.$imageExt'); + final decodeImage = await decodeImageFromList(image.readAsBytesSync()); + final scaleFactor = decodeImage.height > decodeImage.width + ? compression.minHeight / decodeImage.height + : compression.minWidth / decodeImage.width; + + final int newHeight = (decodeImage.height * scaleFactor).toInt(); + final int newWidth = (decodeImage.width * scaleFactor).toInt(); + + int compressedSize; + + /// Compress at-least once + do { + if (targetFile.existsSync()) await targetFile.delete(); + final processedFile = await FlutterImageCompress.compressAndGetFile( + compressedImage?.absolute.path ?? '', + autoCorrectionAngle: compression.autoCorrectionAngle, + format: compression.format, + inSampleSize: compression.inSampleSize, + keepExif: compression.keepExif, + minHeight: newHeight, + minWidth: newWidth, + numberOfRetries: compression.numberOfRetries, + quality: quality, + rotate: compression.rotate, + targetFile.absolute.path, + ); + + // Return if compression failed + if (processedFile == null) throw PlatformException(code: 'PROCESS_ERR'); + + //delete existing compressed file + if (tempFile.existsSync()) await tempFile.delete(); + compressedImage = await File(processedFile.path).copy(tempFile.path); + compressedSize = await compressedImage.length(); + + if (compressedSize > originalSize) throw PlatformException(code: 'COMPRESS_ERR'); + + if (kDebugMode) print('quality after compression $quality : $compressedSize'); + + quality = quality - 5; + } while (quality >= 50 && compressedSize >= maxFileSize); + + return targetFile; + } catch (e) { + if (kDebugMode) print(e); + rethrow; + } finally { + await compressedImage?.delete(); + } + } + + /// Compresses video + /// + /// This method compresses the video at-least once and then keeps it compressing until [Compression.maxFileSize] + /// is reached. + /// + /// See also + /// * [Compression] + static Future compressVideo({ + required File video, + required int maxFileSize, + File? target, + VideoQuality? quality, + }) async { + try { + quality ??= VideoQuality.Res640x480Quality; + + var compressedVideo = video; + final originalSize = await compressedVideo.length(); + var iteration = 1; + if (kDebugMode) print('Size before compression : $originalSize'); + + int compressedSize; + do { + final MediaInfo? info = await VideoCompress.compressVideo( + compressedVideo.path, + quality: quality, + includeAudio: true, + frameRate: 24, + ); + + if (info == null || info.file == null) throw PlatformException(code: 'PROCESS_ERR'); + compressedVideo = info.file!; + compressedSize = await compressedVideo.length(); + if (compressedSize > originalSize) throw PlatformException(code: 'COMPRESS_ERR'); + + if (kDebugMode) print('Size after compression $iteration : $compressedSize'); + + ++iteration; + } while (compressedSize >= maxFileSize && iteration < 4); + + /// Create target file + var newTarget = target; + if (newTarget == null) { + final fileExtension = path.extension(compressedVideo.absolute.path).toLowerCase(); + final newName = 'VID_${DateTime.now().microsecondsSinceEpoch}$fileExtension'; + final String dir = path.dirname(compressedVideo.absolute.path); + newTarget = File(path.join(dir, newName)); + } + + /// Delete target if exists + if (newTarget.existsSync()) await newTarget.delete(); + + /// move file to target + final compressed = await compressedVideo.copy(newTarget.absolute.path); + + /// Delete cached file + if (compressedVideo.absolute.path != compressed.absolute.path) await compressedVideo.delete(); + + return compressed; + } catch (e) { + if (kDebugMode) print(e); + rethrow; + } + } +} diff --git a/lib/src/utils/tools/controller.dart b/lib/src/utils/tools/controller.dart new file mode 100644 index 0000000..c422529 --- /dev/null +++ b/lib/src/utils/tools/controller.dart @@ -0,0 +1,52 @@ +import '../../../zds_flutter.dart'; + +/// Generic controller that can control a value that can be updated over time. +/// +/// See the implementation of [ZdsDateTimePicker] to see how this can be used. +class ZdsValueController { + /// Creates a controller for T. + ZdsValueController({T? value}) : _value = value; + + T? _value; + + /// Gets the current value of T. + T? get value => _value; + + /// Sets the current value of T and calls updateListener. + set value(T? newValue) { + _value = newValue; + updateListener?.call(_value); + } + + List? _listeners = []; + + /// Should be used by the state to listen for updates from the calling widget. + void Function(T? newValue)? updateListener; + + /// Should be used by the state to send updates to the calling widget. + void notifyListeners(T? value) { + _value = value; + if (_listeners == null) return; + for (final void Function(T? newValue) listener in _listeners!) { + listener.call(value); + } + } + + /// Adds a listener to a list of listeners. + void addListener(void Function(T? newValue) listener) { + if (_listeners?.contains(listener) ?? true) return; + _listeners?.add(listener); + } + + /// Removes a single listener. + void removeListener(void Function(T? newValue) listener) { + _listeners?.remove(listener); + } + + /// Discards any active listeners. + /// After this is called the object will not be in a usable state. + void dispose() { + _listeners?.forEach(removeListener); + _listeners = null; + } +} diff --git a/lib/src/utils/tools/crop_image_helper.dart b/lib/src/utils/tools/crop_image_helper.dart new file mode 100644 index 0000000..92d72c4 --- /dev/null +++ b/lib/src/utils/tools/crop_image_helper.dart @@ -0,0 +1,211 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:isolate'; +import 'dart:ui'; + +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http_client_helper/http_client_helper.dart'; +import 'package:image/image.dart'; +import 'package:image_editor/image_editor.dart'; + +/// Crop image using dart library. +Future cropImageDataWithDartLibrary({required ExtendedImageEditorState state}) async { + debugPrint('dart library start cropping'); + + ///crop rect base on raw image + final Rect cropRect = state.getCropRect()!; + + debugPrint('getCropRect : $cropRect'); + + // in web, we can't get rawImageData due to . + // using following code to get imageCodec without download it. + // final Uri resolved = Uri.base.resolve(key.url); + // // This API only exists in the web engine implementation and is not + // // contained in the analyzer summary for Flutter. + // return ui.webOnlyInstantiateImageCodecFromUrl( + // resolved); // + + final Uint8List data = kIsWeb && state.widget.extendedImageState.imageWidget.image is ExtendedNetworkImageProvider + ? await _loadNetwork(state.widget.extendedImageState.imageWidget.image as ExtendedNetworkImageProvider) + + ///toByteData is not work on web + ///https://github.com/flutter/flutter/issues/44908 + // (await state.image.toByteData(format: ui.ImageByteFormat.png)) + // .buffer + // .asUint8List() + : state.rawImageData; + + final EditActionDetails editAction = state.editAction!; + + final DateTime time1 = DateTime.now(); + + //Decode source to Animation. It can holds multi frame. + Image? src; + //LoadBalancer lb; + if (kIsWeb) { + src = decodeImage(data); + } else { + src = await compute(decodeImage, data); + } + if (src != null) { + //handle every frame. + src.frames = src.frames.map((Image image) { + final DateTime time2 = DateTime.now(); + //clear orientation + image = bakeOrientation(image); + + if (editAction.needCrop) { + image = copyCrop( + image, + x: cropRect.left.toInt(), + y: cropRect.top.toInt(), + width: cropRect.width.toInt(), + height: cropRect.height.toInt(), + ); + } + + if (editAction.needFlip) { + late FlipDirection mode; + if (editAction.flipY && editAction.flipX) { + mode = FlipDirection.both; + } else if (editAction.flipY) { + mode = FlipDirection.horizontal; + } else if (editAction.flipX) { + mode = FlipDirection.vertical; + } + image = flip(image, direction: mode); + } + + if (editAction.hasRotateAngle) { + image = copyRotate(image, angle: editAction.rotateAngle); + } + final DateTime time3 = DateTime.now(); + debugPrint('${time3.difference(time2)} : crop/flip/rotate'); + return image; + }).toList(); + } + + /// you can encode your image + /// + /// it costs much time and blocks ui. + //var fileData = encodeJpg(src); + + /// it will not block ui with using isolate. + //var fileData = await compute(encodeJpg, src); + //var fileData = await isolateEncodeImage(src); + List? fileData; + debugPrint('start encode'); + final DateTime time4 = DateTime.now(); + if (src != null) { + final bool onlyOneFrame = src.numFrames == 1; + //If there's only one frame, encode it to jpg. + if (kIsWeb) { + fileData = onlyOneFrame ? encodeJpg(Image.from(src.frames.first)) : encodeGif(src); + } else { + //fileData = await lb.run, Image>(encodeJpg, src); + fileData = + (onlyOneFrame ? await compute(encodeJpg, Image.from(src.frames.first)) : await compute(encodeGif, src)); + } + } + final DateTime time5 = DateTime.now(); + debugPrint('${time5.difference(time4)} : encode'); + debugPrint('${time5.difference(time1)} : total time'); + return Uint8List.fromList(fileData!); +} + +/// Crop image using device native library +Future cropImageDataWithNativeLibrary({required ExtendedImageEditorState state}) async { + debugPrint('native library start cropping'); + + final Rect? cropRect = state.getCropRect(); + final EditActionDetails action = state.editAction!; + + final int rotateAngle = action.rotateAngle.toInt(); + final bool flipHorizontal = action.flipY; + final bool flipVertical = action.flipX; + final Uint8List img = state.rawImageData; + + final ImageEditorOption option = ImageEditorOption(); + + if (action.needCrop) { + option.addOption(ClipOption.fromRect(cropRect!)); + } + + if (action.needFlip) { + option.addOption(FlipOption(horizontal: flipHorizontal, vertical: flipVertical)); + } + + if (action.hasRotateAngle) { + option.addOption(RotateOption(rotateAngle)); + } + + final DateTime start = DateTime.now(); + final Uint8List? result = await ImageEditor.editImage( + image: img, + imageEditorOption: option, + ); + + debugPrint('${DateTime.now().difference(start)} :total time'); + return result; +} + +/// Decodes image using [Isolate] to avoid blocking main thread. +Future isolateDecodeImage(List data) async { + final ReceivePort response = ReceivePort(); + await Isolate.spawn(_isolateDecodeImage, response.sendPort); + final dynamic sendPort = await response.first; + final ReceivePort answer = ReceivePort(); + sendPort.send([answer.sendPort, data]); + return answer.first; +} + +/// Encodes image using [Isolate] to avoid blocking main thread. +Future isolateEncodeImage(Image src) async { + final ReceivePort response = ReceivePort(); + await Isolate.spawn(_isolateEncodeImage, response.sendPort); + final dynamic sendPort = await response.first; + final ReceivePort answer = ReceivePort(); + sendPort.send([answer.sendPort, src]); + return answer.first; +} + +void _isolateDecodeImage(SendPort port) { + final ReceivePort rPort = ReceivePort(); + port.send(rPort.sendPort); + rPort.listen((dynamic message) { + final SendPort send = message[0] as SendPort; + final List data = message[1] as List; + send.send(decodeImage(Uint8List.fromList(data))); + }); +} + +void _isolateEncodeImage(SendPort port) { + final ReceivePort rPort = ReceivePort(); + port.send(rPort.sendPort); + rPort.listen((dynamic message) { + final SendPort send = message[0] as SendPort; + final Image src = message[1] as Image; + send.send(encodeJpg(src)); + }); +} + +/// it may be failed, due to Cross-domain +Future _loadNetwork(ExtendedNetworkImageProvider key) async { + try { + final Response? response = await HttpClientHelper.get( + Uri.parse(key.url), + headers: key.headers, + timeLimit: key.timeLimit, + timeRetry: key.timeRetry, + retries: key.retries, + cancelToken: key.cancelToken, + ); + return response!.bodyBytes; + } on OperationCanceledError catch (_) { + debugPrint('User cancel request ${key.url}.'); + return Future.error(StateError('User cancel request ${key.url}.')); + } catch (e) { + return Future.error(StateError('failed load ${key.url}. \n $e')); + } +} diff --git a/lib/src/utils/tools/frame_mixin.dart b/lib/src/utils/tools/frame_mixin.dart new file mode 100644 index 0000000..448dbfa --- /dev/null +++ b/lib/src/utils/tools/frame_mixin.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +/// This mixin could be used in the methods where you want to do some work after you are +/// +/// This mixin is supposed to be a replacement for the use of `WidgetsBinding.instance?.addPostFrameCallback()`. +abstract mixin class FrameCallbackMixin { + /// Post-frame callbacks cannot be unregistered. They are called exactly once. + /// + /// This is a wrapper around `WidgetsBinding.instance?.addPostFrameCallback`, which is commonly used. + /// + /// This mixin should be used instead as it is easier to read and understand. + void atLast(VoidCallback callback) { + WidgetsBinding.instance.addPostFrameCallback((_) => callback.call()); + } +} diff --git a/lib/src/utils/tools/keyboard_dismiss.dart b/lib/src/utils/tools/keyboard_dismiss.dart new file mode 100644 index 0000000..6fcfc73 --- /dev/null +++ b/lib/src/utils/tools/keyboard_dismiss.dart @@ -0,0 +1,64 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Provides functionality so that by tapping on descendant widgets (except textfields) the keyboard is dismissed. +/// +/// This functionality replicates that of Android apps, where the keyboard can be dismissed by either tapping outside +/// of the keyboard or sometimes on another interactable element, like a button. +/// +/// However, on iOS this behaviour is not common. Typically, the keyboard is dismissed when: +/// * The page is scrolled up/down (like in Android, typically) +/// * The user interacts with another interactable element (e.g., a toggle button) +/// * The user slides down on the text input on a messaging app (e.g., iMessage) +/// +/// Tapping on non-interactable elements does not bring the keyboard down on iOS. As such, when implementing this +/// functionality, you must take care that the methods used to dismiss the keyboard are familiar to each platforms' +/// users if possible. +class KeyboardDismiss extends StatelessWidget { + /// The widget that will have this functionality implemented. + final Widget child; + + /// A function called whenever the keyboard is dismissed. + final VoidCallback? onDismissed; + + /// Whether the keyboard should be dismissed. + /// + /// Defaults to true. + final bool shouldDismiss; + + /// Provides functionality so that by tapping on descendant widgets (except textfields) the keyboard is dismissed. + const KeyboardDismiss({ + required this.child, + super.key, + this.onDismissed, + this.shouldDismiss = true, + }); + + @override + Widget build(BuildContext context) { + void dismiss() { + _maybeDismissKeyboard(context); + onDismissed?.call(); + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: shouldDismiss ? dismiss : null, + child: child, + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('onDismissed', onDismissed)); + properties.add(DiagnosticsProperty('shouldDismiss', shouldDismiss)); + } +} + +void _maybeDismissKeyboard(BuildContext context) { + final currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) { + currentFocus.focusedChild!.unfocus(); + } +} diff --git a/lib/src/utils/tools/measure.dart b/lib/src/utils/tools/measure.dart new file mode 100644 index 0000000..4f0a164 --- /dev/null +++ b/lib/src/utils/tools/measure.dart @@ -0,0 +1,94 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +class _MeasureSizeRenderObject extends RenderProxyBox { + Size oldSize = Size.zero; + + final void Function(Size size) onChange; + + _MeasureSizeRenderObject(this.onChange); + + @override + void performLayout() { + super.performLayout(); + + final Size newSize = child!.size; + if (oldSize == newSize) return; + + oldSize = newSize; + WidgetsBinding.instance.addPostFrameCallback((_) { + onChange(newSize); + }); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('oldSize', oldSize)) + ..add(ObjectFlagProperty.has('onChange', onChange)); + } +} + +/// Widget to measure size of rendered object. +class MeasureSizeWidget extends SingleChildRenderObjectWidget { + /// Callback function called whenever the size of the rendered child changes. + final void Function(Size size) onChange; + + /// Constructs a [MeasureSizeWidget]. + const MeasureSizeWidget({ + required this.onChange, + required Widget super.child, + super.key, + }); + + @override + RenderObject createRenderObject(BuildContext context) { + return _MeasureSizeRenderObject(onChange); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('onChange', onChange)); + } +} + +/// Widget to measure size of rendered object. +/// +/// Measurement is performed after first build, and this value is returned using the builder callback function. +class MeasureSize extends StatefulWidget { + /// Child to be measured. + final Widget? child; + + /// Builder function with size to be rendered. + final Widget Function(BuildContext, Size size) builder; + + /// Constructs a [MeasureSize]. + const MeasureSize({required this.builder, super.key, this.child}); + + @override + MeasureSizeState createState() => MeasureSizeState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ObjectFlagProperty.has('builder', builder)); + } +} + +/// State for [MeasureSize]. +class MeasureSizeState extends State { + Size _size = Size.zero; + + @override + Widget build(BuildContext context) { + if (_size != Size.zero) { + return widget.builder(context, _size); + } + return MeasureSizeWidget( + onChange: (newSize) => setState(() => _size = newSize), + child: widget.child!, + ); + } +} diff --git a/lib/src/utils/tools/modifiers.dart b/lib/src/utils/tools/modifiers.dart new file mode 100644 index 0000000..83be5d3 --- /dev/null +++ b/lib/src/utils/tools/modifiers.dart @@ -0,0 +1,611 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; +import 'package:flutter/services.dart'; + +import '../../../zds_flutter.dart'; + +/// A selection of modifiers on [Widget] for padding. +extension PaddingModifers on Widget { + /// Applies [EdgeInsets.all] padding on this widget. + Widget padding(double all) { + return Padding( + padding: EdgeInsets.all(all), + child: this, + ); + } + + /// Applies [EdgeInsets.only] padding on this widget. + Widget paddingOnly({double left = 0, double right = 0, double top = 0, double bottom = 0}) { + return Padding( + padding: EdgeInsets.only( + left: left, + right: right, + top: top, + bottom: bottom, + ), + child: this, + ); + } + + /// Applies padding to this widget using [EdgeInsets]. + Widget paddingInsets(EdgeInsets insets) { + return Padding( + padding: insets, + child: this, + ); + } +} + +/// A selection of modifiers on [Widget] for colors +extension ColorModifers on Widget { + /// Applies a decoration to the widget with a background color. + Widget backgroundColor(Color color) { + return DecoratedBox( + decoration: BoxDecoration(color: color), + child: this, + ); + } + + /// Changes the widget's textStyle to use this color. + Widget foregroundColor(Color color) { + return DefaultTextStyle( + style: TextStyle(color: color), + child: this, + ); + } + + /// Creates a [Stack] where this overlay will be shown on top of this widget. + Widget overlay( + Widget overlay, [ + Alignment alignment = Alignment.center, + ]) { + return Stack( + alignment: alignment, + children: [ + this, + overlay, + ], + ); + } +} + +/// A selection of modifiers on [Widget] for layouts +extension LayoutModifiers on Widget { + /// Frames this widget position. + /// + /// For example, this, in conjunction with other extensions, can be used to create an icon with a circle background. + /// + /// ```dart + /// Icon(ZdsIcons.walk) + /// .frame(width: 30, height: 30, alignment: Alignment.center) + /// .backgroundColor(Theme.of(context).colorScheme.primary.withLight(0.1)) + /// .circle(30), + /// ``` + Widget frame({ + double? width = double.infinity, + double? height = double.infinity, + Alignment? alignment, + }) { + Widget content = this; + if (alignment != null) { + content = Align( + alignment: alignment, + child: this, + ); + } + return SizedBox( + width: width, + height: height, + child: content, + ); + } + + /// A similar extension to [frame], but that uses [BoxConstraints] instead of hardcoded size values. + Widget frameConstrained({ + required double maxWidth, + required double minHeight, + required double minWidth, + required double maxHeight, + double? idealHeight, + Alignment? alignment, + double? idealWidth, + }) { + Widget content = this; + if (alignment != null) { + content = Align( + alignment: alignment, + child: this, + ); + } + final box = ConstrainedBox( + constraints: BoxConstraints( + minWidth: minWidth, + maxWidth: maxWidth, + minHeight: minHeight, + maxHeight: maxHeight, + ), + child: content, + ); + + if (idealWidth != null || idealHeight != null) { + return SizedBox( + width: idealWidth, + height: idealHeight, + child: box, + ); + } + + return box; + } + + /// Offsets the widget using an [Offset]. + Widget offset(Offset offset) { + return Transform.translate( + offset: offset, + child: this, + ); + } + + /// Offsets the widget using offset coordinates. + Widget offsetOnly({double x = 0.0, double y = 0.0}) { + return Transform.translate( + offset: Offset(x, y), + child: this, + ); + } +} + +/// Applies a decoration to the widget with a [Border]. +extension BorderModifiers on Widget { + /// Applies a decoration to the widget with a [Border]. + Widget border(Border style) { + return DecoratedBox( + decoration: BoxDecoration(border: style), + child: this, + ); + } +} + +/// A selection of modifiers on [Widget] for rendering +extension RenderingModifiers on Widget { + /// Applies a [Clip] to the widget. + Widget clipped(Clip clipBehavior) { + return ClipRect( + clipBehavior: clipBehavior, + child: this, + ); + } + + /// Applies a circular clip with a given radius. + Widget circle(double radius) { + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: this, + ); + } + + /// Applies a custom clip shape. + Widget clipShape>( + T clipper, + Clip clipBehavior, + ) { + return ClipRect( + clipper: clipper, + clipBehavior: clipBehavior, + child: this, + ); + } + + /// Applies a decoration to the widget with a [BorderRadius]. + Widget cornerRadius(double radius) { + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(radius)), + ), + child: this, + ); + } + + /// Applies a [ShaderMask] to the widget. + Widget mask(Shader Function(Rect bounds) mask) { + return ShaderMask( + shaderCallback: (Rect bounds) => mask(bounds), + child: this, + ); + } + + /// Changes the widget's opacity. + Widget opacity(double opacity) { + return Opacity(opacity: opacity, child: this); + } + + /// Applies a decoration to the widget with a [BoxShadow]. + Widget shadow( + Color color, + double radius, { + required double blur, + double x = 0.0, + double y = 0.0, + }) { + return DecoratedBox( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: blur, + spreadRadius: radius, + color: color, + offset: Offset(x, y), + ), + ], + ), + child: this, + ); + } +} + +/// Changes this widget's [TextStyle]. +extension DefaultTextStylesModifiers on Widget { + /// Changes this widget's [TextStyle]. + Widget textStyle(TextStyle? style, {TextOverflow? overflow, TextAlign? textAlign}) { + return DefaultTextStyle( + style: style ?? const TextStyle(), + overflow: overflow ?? TextOverflow.ellipsis, + textAlign: textAlign, + child: this, + ); + } +} + +/// Applies a [Theme] with custom [ThemeData] to the widget. +extension ThemeModifiers on Widget { + /// Applies a [Theme] with custom [ThemeData] to the widget. + Widget applyTheme(ThemeData themeData) { + return Theme( + data: themeData, + child: this, + ); + } +} + +/// Changes this widget's [TextStyle]. +extension TextModifiers on Text { + /// Changes this widget's [TextStyle]. + Widget font({ + double? size, + String? family, + FontWeight? weight, + FontStyle? style, + }) { + return DefaultTextStyle( + style: TextStyle( + fontFamily: family, + fontSize: size, + fontWeight: weight, + fontStyle: style, + ), + child: this, + ); + } + + /// Changes the [TextStyle.height] for this widget. + Widget lineSpacing(double height) { + return DefaultTextStyle( + style: TextStyle( + height: height, + ), + child: this, + ); + } + + /// Changes the [DefaultTextStyle.maxLines] for this widget. + Widget lineLimit(int limit) { + return DefaultTextStyle( + maxLines: limit, + style: style!, + child: this, + ); + } + + /// Changes the [DefaultTextStyle.textAlign] for this widget. + Widget multilineTextAlignment(TextAlign textAlign) { + return DefaultTextStyle( + textAlign: textAlign, + style: style!, + child: this, + ); + } + + /// Changes the [DefaultTextStyle.overflow] for this widget. + Widget truncationMode(TextOverflow overflow) { + return DefaultTextStyle( + overflow: overflow, + style: style!, + child: this, + ); + } + + /// Changes the [TextStyle.backgroundColor] for this widget. + Widget backgroundColor(Color color) { + return DefaultTextStyle( + style: TextStyle(backgroundColor: color), + child: this, + ); + } + + /// Changes the [TextStyle.color] for this widget + Widget foregroundColor(Color color) { + return DefaultTextStyle( + style: TextStyle(color: color), + child: this, + ); + } + + /// Changes the [TextStyle.shadows] for this widget, using a singular shadow. + Widget shadow( + Color color, + double radius, { + required double blur, + double x = 0.0, + double y = 0.0, + }) { + return DefaultTextStyle( + style: TextStyle( + shadows: [ + BoxShadow( + blurRadius: blur, + spreadRadius: radius, + color: color, + offset: Offset(x, y), + ), + ], + ), + child: this, + ); + } +} + +/// Changes this Scaffold's [SystemUiOverlayStyle]. +extension StatusBar on Scaffold { + /// Changes this Scaffold's [SystemUiOverlayStyle]. + Widget statusBar(SystemUiOverlayStyle style) { + return AnnotatedRegion(value: style, child: this); + } +} + +/// Applies Zds styling to [Widget] +extension Zds on Widget { + /// Applies [EdgeInsets.all] with value 16 to this widget. + Widget content() { + return padding(16); + } + + /// Applies bottom widget to this widget. + /// + /// Defaults to 22. + Widget space([double size = 22.0]) { + return paddingOnly(bottom: size); + } +} + +/// Makes this widget a [ZdsPopOverIconButton]. + +extension PopOver on Icon { + /// Makes this widget a [ZdsPopOverIconButton]. + Widget withPopOver(WidgetBuilder popOverBuilder) { + return ZdsPopOverIconButton( + icon: this, + popOverBuilder: popOverBuilder, + iconSize: size ?? 24, + color: color, + ); + } +} + +/// Center aligning row's children perfectly on the horizontal AND on the vertical axis +extension RowLayout on Row { + /// Center aligning row's children perfectly on the horizontal AND on the vertical axis + /// when used in a vertical MultichildRenderObject. + /// + /// ``` + /// e.g. normal Column or ListView with rows inside + /// 750000 187 + /// 245000 14501 + /// 5000 0.5 + /// 10 1 + /// + /// e.g. using this modifier method to create a grid where row's children + /// are centered vertically inside the Column or ListView + /// 750000 187 + /// 245000 14501 + /// 5000 0.5 + /// 10 1 + /// ``` + Row get gridCenteredChildren { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: mainAxisSize, + crossAxisAlignment: crossAxisAlignment, + textDirection: textDirection, + verticalDirection: verticalDirection, + textBaseline: textBaseline, + children: children.map((child) { + return Expanded( + child: Row( + children: [ + const Spacer(), + child, + const Spacer(), + ], + ), + ); + }).toList(), + ); + } +} + +/// A selection of modifiers on TextTheme for [tabItem1], [tabItem2]. +extension TextThemeExtension on TextTheme { + /// TextStyle for tab item 1 + TextStyle get tabItem1 { + return bodyLarge!.copyWith(letterSpacing: 0.2); + } + + /// TextStyle tabitem2 + + TextStyle get tabItem2 { + return bodyMedium!; + } +} + +/// Applies Semanctics to [Widget] +extension SemanticsModifier on Widget { + /// Include Semantics with label this widget. + Widget semantics({ + Key? key, + bool container = false, + bool explicitChildNodes = false, + bool excludeSemantics = false, + bool? enabled, + bool? checked, + bool? selected, + bool? toggled, + bool? button, + bool? slider, + bool? keyboardKey, + bool? link, + bool? header, + bool? textField, + bool? readOnly, + bool? focusable, + bool? focused, + bool? inMutuallyExclusiveGroup, + bool? obscured, + bool? multiline, + bool? scopesRoute, + bool? namesRoute, + bool? hidden, + bool? image, + bool? liveRegion, + int? maxValueLength, + int? currentValueLength, + String? label, + AttributedString? attributedLabel, + String? value, + AttributedString? attributedValue, + String? increasedValue, + AttributedString? attributedIncreasedValue, + String? decreasedValue, + AttributedString? attributedDecreasedValue, + String? hint, + AttributedString? attributedHint, + String? onTapHint, + String? onLongPressHint, + TextDirection? textDirection, + SemanticsSortKey? sortKey, + SemanticsTag? tagForChildren, + VoidCallback? onTap, + VoidCallback? onLongPress, + VoidCallback? onScrollLeft, + VoidCallback? onScrollRight, + VoidCallback? onScrollUp, + VoidCallback? onScrollDown, + VoidCallback? onIncrease, + VoidCallback? onDecrease, + VoidCallback? onCopy, + VoidCallback? onCut, + VoidCallback? onPaste, + VoidCallback? onDismiss, + MoveCursorHandler? onMoveCursorForwardByCharacter, + MoveCursorHandler? onMoveCursorBackwardByCharacter, + SetSelectionHandler? onSetSelection, + SetTextHandler? onSetText, + VoidCallback? onDidGainAccessibilityFocus, + VoidCallback? onDidLoseAccessibilityFocus, + Map? customSemanticsActions, + }) { + return Semantics.fromProperties( + key: key, + container: container, + explicitChildNodes: explicitChildNodes, + excludeSemantics: excludeSemantics, + properties: SemanticsProperties( + enabled: enabled, + checked: checked, + toggled: toggled, + selected: selected, + button: button, + slider: slider, + keyboardKey: keyboardKey, + link: link, + header: header, + textField: textField, + readOnly: readOnly, + focusable: focusable, + focused: focused, + inMutuallyExclusiveGroup: inMutuallyExclusiveGroup, + obscured: obscured, + multiline: multiline, + scopesRoute: scopesRoute, + namesRoute: namesRoute, + hidden: hidden, + image: image, + liveRegion: liveRegion, + maxValueLength: maxValueLength, + currentValueLength: currentValueLength, + label: label, + attributedLabel: attributedLabel, + value: value, + attributedValue: attributedValue, + increasedValue: increasedValue, + attributedIncreasedValue: attributedIncreasedValue, + decreasedValue: decreasedValue, + attributedDecreasedValue: attributedDecreasedValue, + hint: hint, + attributedHint: attributedHint, + textDirection: textDirection, + sortKey: sortKey, + tagForChildren: tagForChildren, + onTap: onTap, + onLongPress: onLongPress, + onScrollLeft: onScrollLeft, + onScrollRight: onScrollRight, + onScrollUp: onScrollUp, + onScrollDown: onScrollDown, + onIncrease: onIncrease, + onDecrease: onDecrease, + onCopy: onCopy, + onCut: onCut, + onPaste: onPaste, + onMoveCursorForwardByCharacter: onMoveCursorForwardByCharacter, + onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter, + onDidGainAccessibilityFocus: onDidGainAccessibilityFocus, + onDidLoseAccessibilityFocus: onDidLoseAccessibilityFocus, + onDismiss: onDismiss, + onSetSelection: onSetSelection, + onSetText: onSetText, + customSemanticsActions: customSemanticsActions, + hintOverrides: onTapHint != null || onLongPressHint != null + ? SemanticsHintOverrides( + onTapHint: onTapHint, + onLongPressHint: onLongPressHint, + ) + : null, + ), + child: this, + ); + } + + /// Exclude Semantics for this widget. + Widget excludeSemantics() { + return ExcludeSemantics( + child: this, + ); + } + + /// Merge Semantics for this widget. + Widget mergeSemantics() { + return MergeSemantics( + child: this, + ); + } +} diff --git a/lib/src/utils/tools/nested_flow.dart b/lib/src/utils/tools/nested_flow.dart new file mode 100644 index 0000000..507a1c6 --- /dev/null +++ b/lib/src/utils/tools/nested_flow.dart @@ -0,0 +1,88 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// This widget can be used for nested flows where some parent widget has to be made available to all of +/// its descendants. For example, a provider that needs to be made available to all routes below it. +/// +/// ```dart +/// SomeProvider( +/// child: ZdsNestedFlow( +/// rootPage: MaterialPage(child: SomeWidgetPage()), +/// onGenerateRoute: _generateRoute, +/// ), +/// ) +/// ``` +class ZdsNestedFlow extends StatefulWidget { + /// Constructs a [ZdsNestedFlow]. + const ZdsNestedFlow({ + required this.rootPage, + super.key, + this.onGenerateRoute, + this.shouldClose = true, + }); + + /// Root page for the navigator. + final Page rootPage; + + /// Should page be closed when just root page remains on the stack, useful when added as root widget in TabBar. + /// + /// Defaults to true. + final bool shouldClose; + + /// Route factory for page based navigator 1.0. + final RouteFactory? onGenerateRoute; + + @override + ZdsNestedFlowState createState() => ZdsNestedFlowState(); + + /// Return the [ZdsNestedFlowState] of the current [ZdsNestedFlow] + static ZdsNestedFlowState of(BuildContext context) { + final stateOfType = context.findAncestorStateOfType(); + if (stateOfType == null) throw FlutterError('Ancestor state of type ZdsNestedFlowState not found'); + return stateOfType; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty>('rootPage', rootPage)) + ..add(DiagnosticsProperty('shouldClose', shouldClose)) + ..add(ObjectFlagProperty.has('onGenerateRoute', onGenerateRoute)); + } +} + +/// State for [ZdsNestedFlowState]. +class ZdsNestedFlowState extends State { + late final _navigator = GlobalKey(); + + /// Dismisses the nested navigation flow + void pop([T? result]) { + Navigator.of(context).pop(result); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + if (_navigator.currentState?.canPop() ?? false) { + await _navigator.currentState?.maybePop(); + return false; + } + return widget.shouldClose; + }, + child: Navigator( + key: _navigator, + pages: [widget.rootPage], + onGenerateRoute: widget.onGenerateRoute, + onPopPage: (route, result) { + if (!(_navigator.currentState?.canPop() ?? false)) { + if (widget.shouldClose) Navigator.of(context).pop(result); + return false; + } + return route.didPop(result); + }, /**/ + ), + ); + } +} diff --git a/lib/src/utils/tools/tab_navigator.dart b/lib/src/utils/tools/tab_navigator.dart new file mode 100644 index 0000000..ba932fc --- /dev/null +++ b/lib/src/utils/tools/tab_navigator.dart @@ -0,0 +1,537 @@ +import 'dart:math' show min; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../../../zds_flutter.dart'; + +/// A [ZdsSplitNavigator] used to navigate the screen for the split view for tablet. +/// +/// By default the primary widget will be visible in the tablet portrait mode and in the tablet landscape mode screen will be +/// split into two part and secondary widget will be displayed on the right side of of the the screen. +/// The push operation will push the page only on the right side of the part. +/// + +class ZdsSplitNavigator extends StatefulWidget { + /// String used for empty detail routes. + static const emptyRoute = 'Zds-empty-route'; + + /// Flag used to decide whether split navigator should be used or not. + /// + /// Typical use ```shouldSplit: kIsWeb || context.isTablet()``` + /// + /// Making this mandatory so that existing implementations do not miss this change. + /// + /// If the value is set to true, then a nested navigator will be injected otherwise, the nearest navigator will be used. + final bool shouldSplit; + + /// Widget that will be shown at the left side in the landscape mode. and full screen in the portrait mode. Typically [Scaffold]. + final Widget primaryWidget; + + /// Called to generate a route for a given [RouteSettings]. + final RouteFactory? onGenerateRoute; + + /// Initial page that will be visible on the right side in the landscape mode. Typically [Scaffold]. + final WidgetBuilder emptyBuilder; + + /// Primary to secondary width ratio + /// + /// Default is 0.4 + /// Meaning, primary widget will have 40% of available space and secondary will get 60%. + /// If the [maxPrimaryWidth] parameter is not null, then the minimum of the width after calculating the ratio + /// and maxPrimaryWidth will be used to set the widgets' width in split mode. + final double widthRatio; + + /// Whether to divide the views using a shadow, rather than a line. + /// + /// Defaults to true. + final bool boxShadowDivider; + + /// To split the screen respective of orientation + /// Note: In QChat we are showing split view in portrait also + /// Defaults to false. + final bool alwaysSplit; + + /// Max width for primary widget when it's being displayed in split mode. + /// + /// Default is null, and widthRatio will have control on width. + final double? maxPrimaryWidth; + + /// Navigator key used in split mode. + /// + /// This key will be used on only in split mode. + final GlobalKey? splitNavigatorKey; + + /// Creates a split navigator. + const ZdsSplitNavigator({ + required this.primaryWidget, + required this.emptyBuilder, + required this.shouldSplit, + this.widthRatio = 0.4, + this.onGenerateRoute, + this.maxPrimaryWidth, + this.splitNavigatorKey, + this.boxShadowDivider = true, + this.alwaysSplit = false, + super.key, + }); + + /// Retrieves the nearest [ZdsSplitNavigatorState] ancestor from the given [BuildContext]. + /// + /// This method searches up the widget tree starting from the given [BuildContext], + /// and returns the nearest ancestor of type [ZdsSplitNavigatorState]. + /// + /// If no [ZdsSplitNavigatorState] ancestor is found, a [FlutterError] is thrown. + /// + /// Usage: + /// ``` + /// final ZdsSplitNavigatorState = ZdsSplitNavigatorState.of(context); + /// ``` + /// + /// [context]: The [BuildContext] to start the search from. + /// + /// Returns: The nearest [ZdsSplitNavigatorState] ancestor found. + static ZdsSplitNavigatorState of(BuildContext context) { + final state = context.findAncestorStateOfType(); + if (state == null) throw FlutterError('Ancestor state of type ZdsSplitNavigatorState not found'); + return state; + } + + /// Returns the ZdsSplitNavigatorState if it exists, or null if it throws an exception. + static ZdsSplitNavigatorState? _safeState(BuildContext context) { + try { + return ZdsSplitNavigator.of(context); + } catch (e) { + return null; + } + } + + /// Handles the navigator logic based on the provided [rootNavigator] and [shouldSplit] conditions. + /// + /// [context] is the BuildContext of the current widget tree. + /// [rootNavigator] is the flag to force using the root navigator. + /// [shouldSplit] is the flag to indicate if the state widget should split. + /// [splitNavigatorAction] is the action to be performed on the split navigator. + /// [regularNavigatorAction] is the action to be performed on the regular navigator. + static Future _handleNavigation({ + required BuildContext context, + required bool rootNavigator, + required Future Function(NavigatorState state) splitNavigatorAction, + required Future Function(NavigatorState state) regularNavigatorAction, + }) { + final state = _safeState(context); + return !rootNavigator && + state != null && + state.mounted && + state.widget.shouldSplit && + state.navigatorKey.currentState != null + ? splitNavigatorAction(state.navigatorKey.currentState!) + : regularNavigatorAction(Navigator.of(context, rootNavigator: rootNavigator)); + } + + /// Pushes a new route, either onto the split navigator if available or onto the regular navigator. + /// + /// [context] is the BuildContext of the current widget tree. + /// [route] is the route to be pushed. + /// [rootNavigator] is an optional flag to force using the root navigator. + static Future push( + BuildContext context, + Route route, { + bool rootNavigator = false, + }) async { + return _handleNavigation( + context: context, + rootNavigator: rootNavigator, + regularNavigatorAction: (state) { + return state.push(route); + }, + splitNavigatorAction: (state) { + return state.push(route); + }, + ); + } + + /// Pushes a details route, either onto the split navigator if available or onto the regular navigator. + /// + /// [context] is the BuildContext of the current widget tree. + /// [route] is the route to be pushed. + /// [rootNavigator] is an optional flag to force using the root navigator. + static Future pushDetails( + BuildContext context, + Route route, { + bool rootNavigator = false, + }) { + return _handleNavigation( + context: context, + rootNavigator: rootNavigator, + regularNavigatorAction: (state) { + return state.push(route); + }, + splitNavigatorAction: (state) { + return state.pushAndRemoveUntil(route, ModalRoute.withName(ZdsSplitNavigator.emptyRoute)); + }, + ); + } + + /// Pushes a named details route, either onto the split navigator if available or onto the regular navigator. + /// + /// [context] is the BuildContext of the current widget tree. + /// [routeName] is the name of the route to be pushed. + /// [arguments] are the optional arguments to be passed to the route. + /// [rootNavigator] is an optional flag to force using the root navigator. + static Future pushNamedDetails( + BuildContext context, + String routeName, { + Object? arguments, + bool rootNavigator = false, + }) { + return _handleNavigation( + context: context, + rootNavigator: rootNavigator, + regularNavigatorAction: (state) { + return state.pushNamed(routeName, arguments: arguments); + }, + splitNavigatorAction: (state) { + return state.pushNamedAndRemoveUntil( + routeName, + ModalRoute.withName(ZdsSplitNavigator.emptyRoute), + arguments: arguments, + ); + }, + ); + } + + /// Pops all routes from the split navigator until the root route. + /// + /// [context] is the BuildContext of the current widget tree. + static void popUntilRoot(BuildContext context) { + final state = _safeState(context); + if (state != null && state.mounted && state.widget.shouldSplit) { + state.navigatorKey.currentState?.popUntil(ModalRoute.withName(ZdsSplitNavigator.emptyRoute)); + } + } + + @override + State createState() => ZdsSplitNavigatorState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('shouldSplit', shouldSplit)) + ..add(ObjectFlagProperty.has('onGenerateRoute', onGenerateRoute)) + ..add(ObjectFlagProperty.has('emptyBuilder', emptyBuilder)) + ..add(DoubleProperty('widthRatio', widthRatio)) + ..add(DiagnosticsProperty('boxShadowDivider', boxShadowDivider)) + ..add(DiagnosticsProperty('alwaysSplit', alwaysSplit)) + ..add(DoubleProperty('maxPrimaryWidth', maxPrimaryWidth)) + ..add(DiagnosticsProperty?>('splitNavigatorKey', splitNavigatorKey)); + } +} + +/// ZdsSplitNavigatorState is a state class for the ZdsSplitNavigator StatefulWidget. +/// It extends the State class with NavigatorObserver and FrameCallbackMixin mixins. +/// +/// It is responsible for managing the nested navigator and keeping track of the routes count +/// as well as the current route in the navigation stack. +class ZdsSplitNavigatorState extends State with FrameCallbackMixin implements NavigatorObserver { + /// An integer value to keep track of the number of routes in the nested navigator. + int routesCount = 0; + + /// A GlobalKey used to access the nested navigator's state. + late GlobalKey navigatorKey; + + /// A ValueNotifier that holds the current route's name. By default, it is initialized + /// with ZdsSplitNavigator.emptyRoute. + final currentRoute = ValueNotifier(ZdsSplitNavigator.emptyRoute); + + void _setCurrentRoute() { + String? newCurrentRoute; + navigatorKey.currentState?.popUntil((route) { + newCurrentRoute = route.settings.name; + return true; + }); + currentRoute.value = newCurrentRoute ?? ZdsSplitNavigator.emptyRoute; + } + + @override + void initState() { + navigatorKey = widget.splitNavigatorKey ?? GlobalKey(); + super.initState(); + } + + @override + void didStartUserGesture(Route route, Route? previousRoute) {} + + @override + void didStopUserGesture() {} + + @override + NavigatorState? get navigator => null; + + @override + void didPush(Route route, Route? previousRoute) { + atLast(() { + setState(() => routesCount++); + _setCurrentRoute(); + }); + } + + @override + void didPop(Route route, Route? previousRoute) { + atLast(() { + setState(() => routesCount--); + _setCurrentRoute(); + }); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + atLast(_setCurrentRoute); + } + + @override + void didRemove(Route route, Route? previousRoute) { + atLast(() { + setState(() => routesCount--); + _setCurrentRoute(); + }); + } + + @override + Widget build(BuildContext context) { + if (widget.shouldSplit) { + final bool shouldSplit = widget.alwaysSplit || (context.isTablet() && context.isLandscape()); + return LayoutBuilder( + builder: (context, layout) { + double maxWidth = layout.maxWidth; + + maxWidth = shouldSplit + ? widget.maxPrimaryWidth != null + ? min(layout.maxWidth * widget.widthRatio, widget.maxPrimaryWidth!) + : layout.maxWidth * widget.widthRatio + : routesCount > 1 + ? 0 + : layout.maxWidth; + + final splitContent = _SplitContent( + navigatorKey: navigatorKey, + observer: this, + boxShadowDivider: widget.boxShadowDivider, + onGenerateRoute: widget.onGenerateRoute, + emptyBuilder: widget.emptyBuilder, + ); + + return Stack( + children: [ + SizedBox( + width: shouldSplit ? maxWidth : layout.maxWidth, + child: widget.primaryWidget, + ), + Positioned.fill( + left: maxWidth, + child: splitContent, + ), + ], + ); + }, + ); + } else { + return widget.primaryWidget; + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('routesCount', routesCount)) + ..add(DiagnosticsProperty>('navigatorKey', navigatorKey)) + ..add(DiagnosticsProperty>('currentRoute', currentRoute)); + } +} + +class _SplitContent extends StatelessWidget { + const _SplitContent({ + required this.navigatorKey, + required this.observer, + required this.boxShadowDivider, + required this.onGenerateRoute, + required this.emptyBuilder, + }); + + final bool boxShadowDivider; + final GlobalKey navigatorKey; + final NavigatorObserver observer; + final Route? Function(RouteSettings)? onGenerateRoute; + final WidgetBuilder emptyBuilder; + + Route _initialRoute() { + return PageRouteBuilder( + settings: const RouteSettings(name: ZdsSplitNavigator.emptyRoute), + pageBuilder: (context, animation, secondaryAnimation) => emptyBuilder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: Tween(begin: 0, end: 1).animate(animation), + child: child, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + + final boxShadow = boxShadowDivider + ? [ + BoxShadow( + color: themeData.colorScheme.onSurface.withOpacity(0.1), + blurRadius: 2, + offset: const Offset(-1, 0), + ), + ] + : null; + + final borderSide = boxShadowDivider ? null : Border(left: BorderSide(color: themeData.dividerColor)); + + return DecoratedBox( + decoration: BoxDecoration( + boxShadow: boxShadow, + border: borderSide, + ), + child: ClipRRect( + child: Navigator( + key: navigatorKey, + observers: [observer], + initialRoute: ZdsSplitNavigator.emptyRoute, + onGenerateRoute: onGenerateRoute, + onGenerateInitialRoutes: (_, __) { + return [_initialRoute()]; + }, + onPopPage: (route, result) { + if (!Navigator.of(context).canPop()) { + Navigator.of(context).pop(result); + return false; + } + return route.didPop(result); + }, + ), + ), + ); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('boxShadowDivider', boxShadowDivider)) + ..add(DiagnosticsProperty>('navigatorKey', navigatorKey)) + ..add(DiagnosticsProperty('observer', observer)) + ..add(ObjectFlagProperty? Function(RouteSettings p1)?>.has('onGenerateRoute', onGenerateRoute)) + ..add(ObjectFlagProperty.has('emptyBuilder', emptyBuilder)); + } +} + +/// A custom `PageRouteBuilder` class that creates a new route with a specified builder function. +/// +/// The `ZdsSplitPageRouteBuilder` provides a smooth slide transition animation for the new route. +class ZdsSplitPageRouteBuilder extends PageRouteBuilder { + /// The builder function that returns the widget to display for the new route. + final Widget Function(BuildContext) builder; + + /// Constructs a `ZdsSplitPageRouteBuilder` with the given `builder` function and optional `settings`. + /// + /// The `settings` parameter can be used to provide route settings such as route name and arguments. + ZdsSplitPageRouteBuilder({required this.builder, super.settings}) + : super( + pageBuilder: (context, _, __) => builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1, 0); + const end = Offset.zero; + const curve = Curves.ease; + final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + // Slide transition is used for the new route. + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ); +} + +/// A custom `PageRouteBuilder` class that creates a new route with a specified builder function. +/// +/// The `ZdsFadePageRouteBuilder` provides a fade transition animation for the new route. +class ZdsFadePageRouteBuilder extends PageRouteBuilder { + /// The builder function that returns the widget to display for the new route. + final Widget Function(BuildContext) builder; + + /// Constructs a `ZdsFadePageRouteBuilder` with the given `builder` function and optional `settings`. + /// + /// The `settings` parameter can be used to provide route settings such as route name and arguments. + ZdsFadePageRouteBuilder({required this.builder, super.settings}) + : super( + pageBuilder: (context, _, __) => builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + // Fade transition is used for the new route. + return FadeTransition( + opacity: Tween(begin: 0, end: 1).animate(animation), + child: child, + ); + }, + ); +} + +/// A custom `PageRouteBuilder` class that creates a new route with a specified builder function. +/// +/// The `ZdsNoAnimationPageRouteBuilder` provides no animation for the new route. +class ZdsNoAnimationPageRouteBuilder extends PageRouteBuilder { + /// The builder function that returns the widget to display for the new route. + final Widget Function(BuildContext) builder; + + /// Constructs a `ZdsNoAnimationPageRouteBuilder` with the given `builder` function and optional `settings`. + /// + /// The `settings` parameter can be used to provide route settings such as route name and arguments. + ZdsNoAnimationPageRouteBuilder({required this.builder, super.settings}) + : super( + pageBuilder: (context, _, __) => builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + // No animation is used for the new route. + return child; + }, + ); +} + +/// [ZdsAdaptiveTransitionPageRouteBuilder] is a custom route builder that +/// adapts the transition animation based on the device's orientation. +/// +/// In landscape mode, it doesn't apply any transition. In other orientations, +/// it applies a slide transition for the new route. +class ZdsAdaptiveTransitionPageRouteBuilder extends PageRouteBuilder { + /// The builder function for creating the widget. + final Widget Function(BuildContext) builder; + + /// Flag to enable for disable the animation. + final bool animate; + + /// Constructs a [ZdsAdaptiveTransitionPageRouteBuilder] instance. + /// + /// [builder] is the required function that takes a [BuildContext] and returns a [Widget]. + /// [settings] is an optional [RouteSettings] object to configure the route. + ZdsAdaptiveTransitionPageRouteBuilder({required this.builder, required this.animate, super.settings}) + : super( + pageBuilder: (context, _, __) => builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return animate + ? CupertinoPageTransition( + primaryRouteAnimation: animation, + secondaryRouteAnimation: secondaryAnimation, + linearTransition: true, + child: child, + ) + : child; + }, + ); +} diff --git a/lib/src/utils/tools/utils.dart b/lib/src/utils/tools/utils.dart new file mode 100644 index 0000000..b36cf5a --- /dev/null +++ b/lib/src/utils/tools/utils.dart @@ -0,0 +1,527 @@ +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../zds_flutter.dart'; + +/// Extension to add dividers to any view that can take an iterable. +extension ListDivider on List { + /// Divides a list of widgets with separators. + Iterable divide(Widget separator) sync* { + if (isEmpty) return; + + final Iterator iterator = this.iterator..moveNext(); + yield iterator.current; + while (iterator.moveNext()) { + yield separator; + yield iterator.current; + } + } +} + +/// Extension on [DateTimeRange]. +extension DateTimeRangeUtils on DateTimeRange { + /// Returns true if this [DateTimeRange] ranges a full natural month (e.g. January 1 to January 31). + bool get isWholeMonth { + return start.year == end.year && start.month == end.month && start.day == 1 && start.endOfMonth.day == end.day; + } +} + +/// Gets the first day of the first week of a month. +/// Commonly this is not the first of the month, unless that happens to fall on the start day of the week. +/// +/// Optional `startingDayOfWeek` defaults to sunday (1). See `startingDayOfWeekToInt` for more information +DateTime startMonthDay(DateTime date, {int startingDayOfWeek = 1}) { + final d = date._firstDayOfWeek(startingDayOfWeek); + return DateTime(d.year, d.month); +} + +/// Extension on [DateTime]. +extension DateTimeFormatter on DateTime { + /// Applies a [DateFormat] to this [DateTime]. + String format([String template = 'MM/dd/yyyy KK:mm a', String locale = 'en_US']) { + return DateFormat(template, locale).format(this); + } + + /// Returns date at 00:00:00 + DateTime get toMidnight { + return DateTime(year, month, day); + } + + /// Gets weekday from DateTime where sunday is 0 and saturday is 6 + int get getWeekday => weekday % 7; + + /// Returns the first day of the month of this [DateTime]. + DateTime get startOfMonth => DateTime(year, month); + + /// Returns the last day of the month in this [DateTime]. + DateTime get endOfMonth => DateTime(year, month + 1, 0); + + /// Gets the week number within a year. + int get weekNumberOfYear { + final int dayOfYear = int.parse(DateFormat('D').format(this)); + final myWeekday = weekday == 7 ? 1 : weekday + 1; + int woy = ((dayOfYear - myWeekday + 10) / 7).floor(); + if (woy < 1) { + woy = numberOfWeeksInYear(year - 1); + } else if (woy > numberOfWeeksInYear(year)) { + woy = 1; + } + return woy + 1; + } + + int _getWeekNumber(DateTime date, int startingDayOfWeek) { + final int dayOfYear = int.parse(DateFormat('D').format(date)); + int woy = ((dayOfYear - startingDayOfWeek + 10) / 7).floor(); + if (woy < 1) { + woy = numberOfWeeksInYear(year - 1); + } else if (woy > numberOfWeeksInYear(year)) { + woy = 1; + } + return woy; + } + + /// Returns true if the input DateTimes are on the same day, ignoring the time. + bool isSameDay(DateTime input) { + return input.year == year && input.month == month && input.day == day; + } + + /// returns first date of the week + DateTime _firstDayOfWeek(int startingDayOfWeekInt) { + final date = this; + final int weekNumber = _getWeekDayNumber(DateFormat('EEEE').format(date)); + final int difference = weekNumber - startingDayOfWeekInt; + return date.subtract(Duration(days: difference)); + } + + /// Gets the week numbers for a month + List getWeeksNumbersInMonth(StartingDayOfWeek startingDayOfWeek, DateTime focusedDay) { + final int startingDayOfWeekInt = startingDayOfWeekToInt(startingDayOfWeek); + + final DateTime startOfMonthDate = DateTime(year, month); + final DateTime endOfMonthDate = + DateTime(year, month).endOfMonth._firstDayOfWeek(startingDayOfWeekInt).add(const Duration(days: 6)); + final monthTotal = endOfMonthDate.difference(startOfMonthDate).inDays; + + final List weekNumbers = []; + for (int i = 0; i < monthTotal; i = i + 7) { + weekNumbers.add(startOfMonthDate.add(Duration(days: i))._firstDayOfWeek(startingDayOfWeekInt)); + } + + final List weeks = []; + for (final week in weekNumbers) { + final weekNum = _getWeekNumber(week, startingDayOfWeekInt); + weeks.add(weekNum); + } + return weeks; + } + + /// Gets first day of week. + /// * `weekStartDay` is 0 indexed where Sunday is 0 and Saturday is 6 + /// Defaults to 0. + DateTime getFirstDayOfWeek({int weekStartDay = 0}) { + return DateTime( + year, + month, + day - (getWeekday < weekStartDay ? 7 - weekStartDay + getWeekday : getWeekday - weekStartDay), + hour, + minute, + second, + ); + } + + /// Gets last day of week. + /// * `weekStartDay` is 0 indexed where Sunday is 0 and Saturday is 6 + /// Defaults to 0. + DateTime getLastDayOfWeek({int weekStartDay = 0}) { + final first = getFirstDayOfWeek(weekStartDay: weekStartDay); + return DateTime( + first.year, + first.month, + first.day + 6, + first.hour, + first.minute, + first.second, + ); + } + + int _getWeekDayNumber(String weekDay) { + int dayOfWeekNumber = -1; + switch (weekDay) { + case 'Sunday': + dayOfWeekNumber = 1; + case 'Monday': + dayOfWeekNumber = 2; + case 'Tuesday': + dayOfWeekNumber = 3; + case 'Wednesday': + dayOfWeekNumber = 4; + case 'Thursday': + dayOfWeekNumber = 5; + case 'Friday': + dayOfWeekNumber = 6; + case 'Saturday': + dayOfWeekNumber = 7; + } + return dayOfWeekNumber; + } +} + +/// Returns an int representing the starting day of week. +/// +/// 1 indexed starting at sunday +int startingDayOfWeekToInt(StartingDayOfWeek startingDayOfWeek) { + switch (startingDayOfWeek) { + case StartingDayOfWeek.sunday: + return 1; + case StartingDayOfWeek.monday: + return 2; + case StartingDayOfWeek.tuesday: + return 3; + case StartingDayOfWeek.wednesday: + return 4; + case StartingDayOfWeek.thursday: + return 5; + case StartingDayOfWeek.friday: + return 6; + case StartingDayOfWeek.saturday: + return 7; + } +} + +/// Gets the date of the sunday of a week in a year. +DateTime firstSundayFromWeekInYear(int weekNumber, int year) { + final weekDay = DateTime(2022).add(Duration(days: (weekNumber - 2) * 7)); + return weekDay.add(Duration(days: (DateTime.sunday - weekDay.weekday) % DateTime.daysPerWeek)); +} + +/// Gets the number of weeks in a year +int numberOfWeeksInYear(int year) { + final DateTime dec28 = DateTime(year, 12, 28); + final int dayOfDec28 = int.parse(DateFormat('D').format(dec28)); + return ((dayOfDec28 - dec28.weekday + 10) / 7).floor(); +} + +/// DateTime extension on [String]. +extension DateTimeParser on String { + /// Creates a [DateTime] from this [DateFormat].template string. + DateTime? parseDate([String template = 'MM/dd/yyyy KK:mm a', String locale = 'en_US']) { + try { + return DateFormat(template, locale).parse(this); + } catch (e) { + return null; + } + } +} + +/// Selection of extensions to [Color]. +extension LightHexColor on Color { + /// Lightens this [Color]. + Color withLight(double opacity, {Color? background}) { + return Color.fromRGBO( + _transform(opacity, red, (background ?? ZdsColors.white).red), + _transform(opacity, green, (background ?? ZdsColors.white).green), + _transform(opacity, blue, (background ?? ZdsColors.white).blue), + 1, + ); + } + + /// Creates a [Color] from a String. + /// + /// The String follows either of the "RRGGBB" or "AARRGGBB" formats, with an optional leading "#". + static Color fromHex(String hexString) { + final buffer = StringBuffer(); + if (hexString.length == 6 || hexString.length == 7) buffer.write('ff'); + buffer.write(hexString.replaceFirst('#', '')); + return Color(int.parse(buffer.toString(), radix: 16)); + } + + /// Returns this Color's hexcode. + /// + /// Prefixes a hash sign if [leadingHashSign] is set to true (defaults to true). + String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}' + '${alpha.toRadixString(16).padLeft(2, '0')}' + '${red.toRadixString(16).padLeft(2, '0')}' + '${green.toRadixString(16).padLeft(2, '0')}' + '${blue.toRadixString(16).padLeft(2, '0')}'; + + /// Returns this Color's hexcode without the alpha channel. + /// + /// Prefixes a hash sign if [leadingHashSign] is set to true (defaults to true). + String toHexNoAlpha({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}' + '${red.toRadixString(16).padLeft(2, '0')}' + '${green.toRadixString(16).padLeft(2, '0')}' + '${blue.toRadixString(16).padLeft(2, '0')}'; +} + +int _transform(double p, int t, int b) { + return (p * t + (1 - p) * b).round(); +} + +/// Render extensions on [BuildContext]. +extension BuildContextRenderBox on BuildContext { + /// Returns a [Rect] representing this widget's bounds. + Rect get widgetBounds { + return (owner != null && widgetRenderBox != null) ? widgetRenderBox!.semanticBounds : Rect.zero; + } + + /// Returns an [Offset] representing this widget's screen position, with 0,0 being the top left of the screen. + Offset get widgetGlobalPosition { + return (owner != null && widgetRenderBox != null) ? widgetRenderBox!.localToGlobal(Offset.zero) : Offset.zero; + } + + /// Returns this widget's [RenderBox]. + RenderBox? get widgetRenderBox { + return findRenderObject() as RenderBox?; + } +} + +/// Shows a dialog with a [CircularProgressIndicator], used to indicate loading. +extension LoaderDialog on CircularProgressIndicator { + /// Shows a dialog with a progress indicator, used to indicate loading while blocking the user's interaction with the + /// app. + /// + /// Typically used where the completion of this action is necessary and should not be interrupted by the user (like a + /// transaction, or saving an important and required file). + static Future show(BuildContext context, {required GlobalKey keyLoader}) async { + await showDialog( + barrierDismissible: false, + context: context, + useRootNavigator: false, + builder: (BuildContext context) { + return Dialog( + backgroundColor: ZdsColors.transparent, + elevation: 0, + key: keyLoader, + insetPadding: EdgeInsets.zero, + clipBehavior: Clip.antiAliasWithSaveLayer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: const Center( + child: SizedBox( + height: 80, + width: 80, + child: ZdsCard( + padding: EdgeInsets.all(10), + child: Center( + child: SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator(strokeWidth: 3), + ), + ), + ), + ), + ), + ); + }, + ); + } +} + +/// Attempts to launch this urlString on a web view. +extension LaunchUrlInWebView on Uri { + /// Attempts to launch this urlString on a web view. + Future launch() => launchUrl(this); +} + +/// Converts an input color into its shade with lightness +/// [input] Color to be converted +/// [shade] Shade from 0-1 +Color getShadedColor(Color input, double shade) { + return Color.fromARGB( + input.alpha, + changeShade(input.red, 1 - shade), + changeShade(input.green, 1 - shade), + changeShade(input.blue, 1 - shade), + ); +} + +/// Converts individual red, green, or blue of an input color to match a specific lightness shade +/// [rgb] Red, green or blue color value of the initial color +/// [shade] Shade from 0-1 +int changeShade(int rgb, double shade) => rgb + ((255 - rgb) * shade).floor(); + +/// Calculates a foreground color based on the input background color +/// [color] background color +Color computeForeground(Color color) { + return color.isDark ? ZdsColors.white : ZdsColors.darkGrey; +} + +/// System overlay should be opposite to the brightness of the background color. +SystemUiOverlayStyle computeSystemOverlayStyle(Color color) { + return color.isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark; +} + +/// Checks if the color scheme is using warm or cool greys and returns the correct swatch. +extension ZdsCheckIsWarm on ThemeData { + /// Checks if the color scheme is using warm or cool greys. + bool get isWarm => colorScheme.background != ZdsColors.greyCoolSwatch[50]; + + /// Gets correct [ZdsColors.greySwatch] based on if color scheme is warm or cool. + MaterialColor get greySwatch => isWarm ? ZdsColors.greyWarmSwatch : ZdsColors.greyCoolSwatch; +} + +/// Converts byte length to human readable format. +String fileSizeWithUnit(int bytes) { + if (bytes <= 0) return '0 B'; + const suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + final i = (log(bytes) / log(1024)).floor(); + return '${(bytes / pow(1024, i)).toStringAsFixed(0)} ${suffixes[i]}'; +} + +/// Returns a color from a set of chosen theme colors. +/// +/// Passing the same seed will yield the same color. Any object can be used as a seed. +Color getRandomColorFromTheme(Object? seed, {List? colors}) { + final List setColors; + Object? s = seed; + + if (colors == null || colors.isEmpty) { + setColors = [ + ZdsColors.redSwatch['dark']!, + ZdsColors.blueGrey, + ZdsColors.blue, + ZdsColors.teal, + ZdsColors.purple, + ZdsColors.greenSwatch['dark']!, + ]; + } else { + setColors = colors; + } + + // If the Object is null (e.g. a user whose ID is null), then we always give it a set seed so the color returned is + // the same each time + s ??= 123; + + return setColors[Random(s.hashCode).nextInt(setColors.length)]; +} + +/// Checks if the ZdsTab contains an icon +bool hasIcons(List items) { + return items.any((element) => element.icon != null); +} + +/// Enum to define device types. +enum DeviceType { + /// A device with a shortest side lower than 550dp that is in landscape mode. + phoneLandscape, + + /// A device with a shortest side larger than 550dp that is in landscape mode + tabletLandscape, + + /// A device with a shortest side lower than 550dp that is in portrait mode. + phonePortrait, + + /// A device with a shortest side larger than 550dp that is in portrait mode + tabletPortrait, +} + +/// Utils to determine the [DeviceType] from the current context. +extension DeviceTypeFromContext on BuildContext { + /// Determines the [DeviceType] from the current context. + DeviceType getDeviceType() { + final Orientation orientation = MediaQuery.of(this).orientation; + if (MediaQuery.of(this).size.shortestSide < 550) { + if (orientation == Orientation.landscape) { + return DeviceType.phoneLandscape; + } else { + return DeviceType.phonePortrait; + } + } else if (orientation == Orientation.landscape) { + return DeviceType.tabletLandscape; + } else { + return DeviceType.tabletPortrait; + } + } + + /// True if current device is a tablet. + bool isTablet() { + final deviceType = getDeviceType(); + return deviceType == DeviceType.tabletLandscape || deviceType == DeviceType.tabletPortrait; + } + + /// True if current device is a phone. + bool isPhone() { + final deviceType = getDeviceType(); + return deviceType == DeviceType.phoneLandscape || deviceType == DeviceType.phonePortrait; + } + + /// True if device is a tablet and landscape. + bool isTabletLandscape() => getDeviceType() == DeviceType.tabletLandscape; + + /// Max width of small screen. + static const int smallScreenBreakpoint = 330; + + /// True if the device screen is rather small + bool isSmallScreen() => MediaQuery.of(this).size.width < smallScreenBreakpoint; + + /// True if the device screen height is rather small (please rename this) + bool isShortScreen() => MediaQuery.of(this).size.height < smallScreenBreakpoint; + + /// True is device orientation is portrait. + bool isPortrait() => MediaQuery.of(this).orientation == Orientation.portrait; + + /// True if device orientation is landscape. + bool isLandscape() => MediaQuery.of(this).orientation == Orientation.landscape; +} + +TextPainter _textPainter(String text, TextStyle style, int maxLines, [double maxWidth = double.infinity]) { + return TextPainter( + text: TextSpan(text: text, style: style), + maxLines: maxLines, + textDirection: ui.TextDirection.ltr, + )..layout(maxWidth: maxWidth); +} + +/// Determines if [text] will overflow in a given [width]. +/// +/// Optionally [maxLines] can be provided to check overflow on text with more than one line, otherwise it defaults to 1. +/// +/// See also: +/// * [TextPainter] +bool hasTextOverflow(String text, TextStyle style, double width, {int maxLines = 1}) { + final TextPainter textPainter = _textPainter(text, style, maxLines, width); + return textPainter.didExceedMaxLines; +} + +/// Gets the width of a [text] with a given [style]. +/// +/// Optionally [maxLines] can be provided to check overflow on text with more than one line, otherwise it defaults to 1. +/// +/// See also: +/// * [TextPainter] +double textWidth(String text, TextStyle style, {int maxLines = 1}) => _textPainter(text, style, maxLines).width; + +/// Returns the corresponding value for a given `selectedOption` from the `branches` map. +extension MapExtensions on Map { + /// Returns the corresponding value for a given [key]. + /// If the [key] is not found within the map, or if the value for that key is `null`, + /// the [orDefault] value is returned. + /// + /// Parameters: + /// - `key`: The key to be looked up in the map. + /// - `orDefault` (optional): The default value to be returned if the [key] is not found or if its value is `null`. + /// If not provided, it defaults to `null`. + /// + /// Example: + /// + /// ```dart + /// var fruitColors = { + /// 'apple': 'red', + /// 'banana': 'yellow', + /// 'grape': 'purple' + /// }; + /// print(fruitColors.get('apple', orDefault: 'unknown')); // Outputs: red + /// print(fruitColors.get('orange', orDefault: 'unknown')); // Outputs: unknown + /// ``` + Value? get(Option key, {Value? orDefault}) { + return this[key] ?? orDefault; + } +} diff --git a/lib/zds_flutter.dart b/lib/zds_flutter.dart new file mode 100644 index 0000000..83427fd --- /dev/null +++ b/lib/zds_flutter.dart @@ -0,0 +1,10 @@ +/// A library of Flutter components made by Zebra Technologies based on the Zebra Design System, or ZDS. +library zds_flutter; + +export 'src/components/atoms.dart'; +export 'src/components/molecules.dart'; +export 'src/components/organisms.dart'; +export 'src/utils/assets.dart'; +export 'src/utils/localizations.dart'; +export 'src/utils/theme.dart'; +export 'src/utils/tools.dart'; diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..1c19abe --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,74 @@ +name: zds_flutter +description: Zebra Design System components for Flutter. A selection of UI components from Zebra Technologies. +version: 0.2.0 +homepage: https://github.com/zebradevs/zds_flutter +repository: https://github.com/zebradevs/zds_flutter +issue_tracker: https://github.com/zebradevs/zds_flutter/issues + +platforms: + web: + android: + ios: + macos: + windows: + linux: + +environment: + sdk: ">=3.0.1 <4.0.0" + flutter: ">=2.10.0" + +dependencies: + cached_network_image: ^3.2.1 + collection: ^1.16.0 + cross_file: ^0.3.3+4 + dropdown_button2: ^2.3.8 + expand_tap_area: ^1.1.0 + extended_image: ^8.0.2 + file_picker: ^5.1.0 + flutter: + sdk: flutter + flutter_image_compress: ^2.0.0 + flutter_localizations: + sdk: flutter + flutter_slidable: ^3.0.0 + flutter_svg: ^2.0.6 + flutter_swipe_action_cell: ^3.0.2 + giphy_get: ^3.5.0 + http_client_helper: ^3.0.0 + image: ^4.0.15 + image_editor: ^1.3.0 + image_editor_plus: ^1.0.0 + image_picker: ^1.0.0 + interval_time_picker: ^2.0.0+5 + intl: ^0.18.0 + linked_scroll_controller: ^0.2.0 + mime: ^1.0.0 + modal_bottom_sheet: ^3.0.0-pre + open_filex: ^4.3.2 + path: ^1.8.0 + path_provider: ^2.0.2 + popover: ^0.2.6+2 + table_calendar: ^3.0.3 + universal_platform: ^1.0.0+1 + url_launcher: ^6.0.10 + video_compress: ^3.1.0 + webview_flutter: ^4.2.4 + zeta_flutter: ^0.0.1+12 + +dev_dependencies: + flutter_lints: ^2.0.1 + flutter_test: + sdk: flutter + lottie: ^2.1.0 + +flutter: + assets: + - lib/assets/images/ + - lib/assets/animations/ + - lib/assets/fonts/selection.json + - lib/assets/strings/ + + fonts: + - family: zds-icons + fonts: + - asset: lib/assets/fonts/zds.ttf diff --git a/test/day_picker_test.dart b/test/day_picker_test.dart new file mode 100644 index 0000000..f8a1f9f --- /dev/null +++ b/test/day_picker_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zds_flutter/zds_flutter.dart'; + +void main() { + testWidgets('ZdsDayPicker without any parameters', (WidgetTester tester) async { + // Build our app and trigger a frame. + final Widget testWidget = getTestWidget( + ZdsDayPicker( + startingWeekDate: DateTime.now(), + ), + ); + await tester.pumpWidget(testWidget); + }); + + testWidgets('Select individual with deprecated initialSelectedDate', (WidgetTester tester) async { + // Build our app and trigger a frame. + final Widget testWidget = getTestWidget( + ZdsDayPicker( + startingWeekDate: DateTime.now().add(const Duration(days: -2)), + header: 'Days', + ), + ); + await tester.pumpWidget(testWidget); + }); + + testWidgets('Select individual with initialSelectedDates', (WidgetTester tester) async { + // Build our app and trigger a frame. + final Widget testWidget = getTestWidget( + ZdsDayPicker( + startingWeekDate: DateTime.now().add(const Duration(days: -2)), + initialSelectedDates: [DateTime.now().add(const Duration(days: 2))], + header: 'Days', + ), + ); + await tester.pumpWidget(testWidget); + expect(find.text('Days'), findsOneWidget); + }); + + testWidgets('ZdsDayPicker assertion check', (WidgetTester tester) async { + expect( + () => ZdsDayPicker( + startingWeekDate: DateTime.now(), + initialSelectedDates: [DateTime.now(), DateTime.now().add(const Duration(days: 2))], + ), + throwsAssertionError, + ); + }); +} + +Widget getTestWidget(Widget widget) { + return MediaQuery( + data: const MediaQueryData(), + child: MaterialApp(home: widget), + ); +}